The Insanity of getting Versions of a MultiLineText box set to Append Changes
I hate thrashing. I know I'm pretty new to SharePoint 2007 development, but I hated spending an entire day trying to figure out some weird behavior of SharePoint. What made yesterday of particular note is that the solution I found to the problem makes absolutely no sense.
Last week, I wrote a nice little post that shared some of the functions in a nice little library I am building to extract the values out of columns of a SPListItem. Two of the functions I mentioned extracted both plain text and HTML versions out of normal Mutli Line Text boxes. More recently, I needed to extract data from a Multi Line Text when the column was configured to "append changes to existing text."
My normal functions didn't work, they just returned empty strings. So I went about about debugging the code, introspecting all the properties of the SPFieldMultiLineText, but wasn't able to find anything that had the text that I needed. The closest I found was a property called "AppendOnly," but that was a bool. When google proved no help in solving this problem, I opened up Reflector and started to search through the dll that is Microsoft.SharePoint.
A search of the word "Append" lead to only one class, "AppendOnlyHistory," which is a webcontrol. I assume this is the control that is used to display the contents of this column when viewing the item detail. So I opened up the reflected code, and tried to understand how it worked. The bulk of the work appeared in the "Render" method. It was composed of cryptic reflected variables and goto's but it basically looked like it was iterating through instances of SPListItemVersion objects that it got by enuerating the list item's version property.
So I wrote a function as follows:
public static string GetVersionedMultiLineTextAsHTML(SPListItem item, string key)
{
StringBuilder sb = new StringBuilder();
foreach (SPListItemVersion version in item.Versions)
{
SPFieldMultiLineText field = version.Fields[key] as SPFieldMultiLineText;
if (field != null)
{
string comment = field.GetFieldValueAsHtml(version[key]);
if (comment != null && comment.Trim() != string.Empty && comment != "<div></div>")
{
sb.Append("<br>\n\r");
sb.Append(version.CreatedBy.User.Name).Append(" (");
sb.Append(version.Created.ToString("MM/dd/yyyy hh:mm tt"));
sb.Append(")");
sb.Append(comment);
}
}
}
return sb.ToString();
}
But when the code hit "item.Versions" at the begining of the for loop it threw a System.Argument Exception. I couldn't understand it, I wasn't passing any arguments to "item.Version." It was a property, not a method, after all. I thought maybe I misread the reflected code, but after staring at it for hours, digging from one inherited class to another I couldn't figure it out.
From reflecting I did notice the web control had a member variable to hold a reference to an instance of a SPContext object. Just for "grins and chuckles" I decided to navigate from "SPContext.Current" to the list item instance while in the debugger. Sure enough, I could finally access the "item.Versions" property without any errors.
Taking it a step further I decided to replace "item.Versions" with this:
item.Web.Lists[item.ParentList.ID].Items[item.UniqueId].Versions
That's right, I made a complete circle and ended up somewhere else. I navigated up to the web, down to the list, into the list item itself. And it worked. I can not fathom why this worked, but it did. So here are the two functions for getting the history of comments from a SPFieldMultiLineText field, when "Append Changes" is turned on:
public static string GetVersionedMultiLineTextAsHTML(SPListItem item, string key)
{
StringBuilder sb = new StringBuilder();
foreach (SPListItemVersion version in item.Web.Lists[item.ParentList.ID].Items[item.UniqueId].Versions)
{
SPFieldMultiLineText field = version.Fields[key] as SPFieldMultiLineText;
if (field != null)
{
string comment = field.GetFieldValueAsHtml(version[key]);
if (comment != null && comment.Trim() != string.Empty && comment != "<div></div>")
{
sb.Append("<br>\n\r");
sb.Append(version.CreatedBy.User.Name).Append(" (");
sb.Append(version.Created.ToString("MM/dd/yyyy hh:mm tt"));
sb.Append(")");
sb.Append(comment);
}
}
}
return sb.ToString();
}public static string GetVersionedMultiLineTextAsPlainText(SPListItem item, string key)
{
StringBuilder sb = new StringBuilder();
foreach (SPListItemVersion version in item.Web.Lists[item.ParentList.ID].Items[item.UniqueId].Versions)
{
SPFieldMultiLineText field = version.Fields[key] as SPFieldMultiLineText;
if (field != null)
{
string comment = field.GetFieldValueAsText(version[key]);
if (comment != null && comment.Trim() != string.Empty)
{
sb.Append("\n\r");
sb.Append(version.CreatedBy.User.Name).Append(" (");
sb.Append(version.Created.ToString("MM/dd/yyyy hh:mm tt"));
sb.Append(")");
sb.Append(comment);
}
}
}
return sb.ToString();
}
In case you think it might have something to do with how I get a reference to the SPListItem in the first place before calling these functions, I am just calling them from a web part that is iterating through items in a list. It starts by getting "SPContext.Current.Web" and works its way to the list and its items. I can only assume the reference is lost or broken somewhere under the covers.
If you have any idea why it worked that way, I'd love to hear it.