Node demotion does not work with blank (empty) values

While working with node promotion/demotion with xml files I noticed a problem (probably a bug) that occurred when a value was emptied (blanked) in the EditForm.
Here´s a rundown of the problem:

I start with uploading a new “book” to my newly created Books document library. Filling in all the values.

Then I decide I no longer want a “book title” and I will empty (blank) that value as you can see above.

The old value, however, is “restored” when the DisplayForm is once again shown.

Ok, so I did a little experiment. I checked out the xml file and edited that in notepad and did a check in:

That worked!
This led me to think that something is wrong with the “synchronization” between the xml file and the columns on the SPListItem when using the EditForm.

So, adding an event receiver and checking the AfterProperties revealed that the “Book Title” actually is empty (when entering) but checking the xml file in the item updating event reveals that the values are not “transferred” back into the xml which then of course leads to SharePoint “syncing” them right back with the SPListItem as the “old” value.

The idea (once again originally thought of by Thomas Persson who also found the problem) is to use the updating and updated events to first check for values that are “empty”, in updating, and to “transfer” them to the updated event to re-write the xml file with empty values to “force” SharePoint to not sync the old values back.

So I´ll start of by adding the two event receivers (this extension method has been mentioned before):

instance.EventReceivers.Add<ItemEventReceiver>(SPEventReceiverType.ItemUpdated);
instance.EventReceivers.Add<ItemEventReceiver>(SPEventReceiverType.ItemUpdating);

So let´s look at the code for ItemUpdating first. I´ll try to explain it afterwards:

public override void ItemUpdating(SPItemEventProperties properties)
{              
  var Web = properties.OpenWeb();
  var FieldNames = new List<string>();
  var Key = properties.ListItem.UniqueId.ToString("B");
  var Fields = properties.ListItem.Fields;
  var Xml = Encoding.Default.GetString(properties.ListItem.File.OpenBinary());
  var File = XDocument.Parse(Xml);
 

  foreach (DictionaryEntry entry in properties.AfterProperties)
  {
    var name = entry.Key.ToString();
    var value = entry.Value == null ? string.Empty : entry.Value.ToString();

    if (value == string.Empty)
    {
     if (Fields.ContainsField(name))
     {
       SPField field = Fields.GetFieldByInternalName(name);
       if (string.IsNullOrEmpty(field.XPath) == false)
       {
         var node = File.XPathSelectElement(field.XPath, File.Root.CreateNavigator());
         if (node != null)
         {
           if (node.IsEmpty == false)
           {
             FieldNames.Add(name);
           }
         }
        }
      }
    }
   }
    
  if (FieldNames.Count >= 1)
  {
    Web.Properties.Add(Key, string.Join(separator, FieldNames.ToArray()));
    Web.Properties.Update();
  }
}

There´s a lot of IF statements there I know but basicly what happens is I will loop through all the entries in the AfterProperties dictionary and if the value is an emtpy string and if the key is the internal name of a field on the list item I will then start to check if the field has an XPath expression (only my fields with nodes will have that).
Next I will use the XPathSelectElement method together with the XPath on the field to find the actual value from the xml file. (btw, this is why I use a XDocument and not a XElement…to get to use the CreateNavigator).
Finally if the value on that node isn´t empty I will add the internal name of the field to a collection of strings (FieldNames).

The last thing that happens is that I will “serialize” (not really) the FieldNames into a string (separated by colon as my separator) and add it to the web.Properties using the SPListItems guid as a key.

Now, let´s look at ItemUpdated (explanation afterwards):

public override void ItemUpdated(SPItemEventProperties properties)
{               
               
 var Web = properties.OpenWeb();
 var Key = properties.ListItem.UniqueId.ToString("B");
 var Fields = properties.ListItem.Fields;
 var HasChanges = false;
 var Xml = Encoding.Default.GetString(properties.ListItem.File.OpenBinary());
 var File = XDocument.Parse(Xml);
 

 if (Web.Properties.ContainsKey(Key))
 {
   try
   {
     var FieldNames = Web.Properties[Key].Split(separator.ToCharArray());

     foreach (string name in FieldNames)
     {
       if (Fields.ContainsField(name))
       {
         SPField field = Fields.GetFieldByInternalName(name);
         if (string.IsNullOrEmpty(field.XPath) == false)
         {
           var node = File.XPathSelectElement(field.XPath, File.Root.CreateNavigator());
           if (node != null)
           {
             if (node.IsEmpty == false)
             {
                node.Value = string.Empty;
                HasChanges = true;
             }
           }
         }
      }
    }

    if (HasChanges)
    {
     DisableEventFiring();

     var bytes = Encoding.Default.GetBytes(File.ToString());
     properties.ListItem.File.SaveBinary(bytes);
    }
  }
  finally
  {
     EnableEventFiring();
     Web.Properties[Key] = null;
     Web.Properties.Update();
  }
 }
}

Ok, so here I´m pulling up the string of fieldnames and “de-serializing” it into a list of strings again. Using the same method as in updating I will pull out the value from the xml file and check if it is empty. If it is not I will update it with an empty string and finally set a flag to indicate that I have changes to take care of.
If there are changes, I will “re-write” the xml back into SharePoint forcing the values to be left blank and then lastly I will remove the string of fieldnames from the web.Properties. 
And now, emptying values works as I expect them to!

Sadly, the story doesn´t end here. There´s another issue with the “published” column. That type of column (DateTime) seems to have some other problems. Let´s see an example:

I`ll empty out both the “published” and “book title” and let´s see what happens:

Ok, so the “book title” behaves like expected (after the event receiver has kicked in) but the “published” column is reverted back to the original value.

I´m going to add some debugging info to my event receiver to see the values of the keys in the AfterProperties dictionary. My event receiver should work, I suspect that the problem lies within the AfterProperties dictionary.

The “published” column isn´t even in the AfterProperties dictionary (you can´t see it here but I promise). This has got to be a bug (I have found this issue both with SPFieldDateTime and SPFieldUser).

Ok, so I need to take care of this issue, here´s some code to handle that:

/* HANDLE BUG IN AFTERPROPERTIES */
 

var emptyFields = (from SPField field in Fields
                   where field.Type == SPFieldType.DateTime &&
                         properties.AfterProperties.ContainsKey(field.InternalName) == false &&
                         field.IsBuiltIn() == false
                   select field).ToList();
 

emptyFields.ForEach(f =>
{
   properties.AfterProperties[f.InternalName] = string.Empty;
}); 

foreach (DictionaryEntry entry in properties.AfterProperties)
......

 

Note: I have inserted this code before I start enumerating all the entries in the AfterProperties dictionary.

What I´m doing here is to find all the fields that are of type DateTime and that aren´t in the AfterProperties dictionary and that are custom fields (otherwise the created and modified will be added).
Then I will manually add them to the AfterProperties dictionary before I start looping through that collection.

 

The code for the two extension methods IsBuiltIn and ContainsKey look like this:

public static bool IsBuiltIn(this SPField instance)
{ 
  return SPBuiltInFieldId.Contains(instance.Id);

}

public static bool ContainsKey(this SPItemEventDataCollection instance, string key)
{  
  return instance[key] != null;

}

 

 

And now…

And also as said before, the same goes for SPFieldType.User so you might wan´t to extend it to include SPFieldType.User like shown here:

, , , ,

  1. #1 by Steffo on September 21, 2009 - 10:08

    Got nothing to do about this particular post but since our last talk I just thought I should let you know about this video series about integrating Sharepoint with EPiServer. Keep up the great work Johan!

    http://world.episerver.com/Blogs/Dan-Matthews/Dates/2009/6/EPiServerSharePoint-Videos-Part-1/

    • #2 by Johan Leino on September 21, 2009 - 12:32

      Hi Steffo,

      great videos…looks like excellent stuff (gotta start learning some EpiServer too I think).
      Cya…

Leave a comment