Disclaimer

The views expressed on this weblog are mine alone and do not necessarily reflect the views of my employer, Avanade.

Search
Recomends...
  • Code Complete, Second Edition
    Code Complete, Second Edition
    by Steve McConnell
Login
« Announcing Snip-It Pro | Main | Adding Back Lookup Columns in the Provisioning Handler »
Tuesday
Oct162007

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.

PrintView Printer Friendly Version

EmailEmail Article to Friend

Reader Comments (9)

Hmm... I wonder if there would be any difference if you replaced this line:
foreach (SPListItemVersion version in item.Versions)
with this:
SPListItemVersionCollection versions = item.Versions;
foreach (SPListItemVersion version in versions)
(http://msdn2.microsoft.com/en-us/library/microsoft.sharepoint.splistitemversioncollection.aspx)

Or, I wonder if you need to check if item.Versions.Count > 0 first? I.e., is item.Versions null for some items?

I am intrigued; I'd love to know the results if you have a chance to test my theories.
October 17, 2007 | Unregistered CommenterSherman Woo
item.Versions isn'nt null, it throws a system argument exception when you try to access it period.

When I was debugging the code, I couldn't even get to item.Version.count. I would see the system Argument exception if I tried to go item.Versions,

What's throws me for a loop is the fact that looping up to the web and back down to the same exact item corrects the issue.

The only thing I can think of, is that both the web part and this code are in seperate dll's (which are both gacced) and when passing the SPListItem around, the internal references get screwey.

Really weird.
October 17, 2007 | Registered CommenterDavid San Filippo
I actually think I know why this was the case in the first place. I failed to mention that I got the SPListItem object from an SPQuery. I suppose the SPListItem in the SPListItemCollction that was returned wasn't instatiated properly.
November 13, 2007 | Registered CommenterDavid San Filippo
I was able to get this to work just fine:


Dim v As SPListItemVersion
For Each v In invoiceItem.Versions
lblNotes.Text += v.Item("Notes") + "<br>"
Next
February 13, 2009 | Unregistered CommenterStan Johnston
it only is a problem if you get the SPListItem as a result of an SPQuery.
February 13, 2009 | Registered CommenterDavid San Filippo
The Code works Magic..Thanks David !!!
March 9, 2009 | Unregistered CommenterSudheer
Hello.

I can easily explain this problem. The thing is that not all SPListItem objects with the same ID are the same. There are several ways to get an item:

SPList.GetItemById(), SPList.GetItemByUniqueId() - return all properties including versions.
SPList.Items - very slow but should return all properties.
SPList.GetItems(SPQuery q) - this is the fastest and returns listitem objects containing a minimum of data, which is configurable. The SPListItem objects return will have the following properties based on the SPQuery object:

SPQuery.ViewFields - add the field data you want to get (SPListItem[string fieldname]). This defaults to the columns selected in the view for the query.
IncludeAllUserPermissions Gets or sets a Boolean value that specifies whether the query returns security information, including access control lists and scopes.
IncludeAttachmentUrls Gets or sets a Boolean value that specifies whether to include the encoded full URLs, delimited by ;#, of attachments to each item.
IncludeAttachmentVersion Gets or sets a Boolean value that specifies whether to include the unique identifier and version number, delimited by ;#, of attachments to each item.
IncludeMandatoryColumns Gets or sets a Boolean value that specifies whether fields that are required for calculated fields are returned in the query. Mandatory columns include, for example, owsHiddenVersion, dependent fields, and required fields.
IncludePermissions Gets or sets a Boolean value that specifies whether the query returns security information.
IndividualProperties Gets or sets a Boolean value that specifies whether to include properties in the query.

I don't think these return versions though.. so best to go by the GetItemById method or GetItemByUniqueId these are quite fast for just a few items...
June 16, 2009 | Unregistered Commenter=8)-DX
Thank you very much! I am so glad there are people out there like you, kind enough to share your hard earned knowledge with others. You saved me days and my sanity!
July 29, 2009 | Unregistered Commenterdanny
Thanks you very much!!! I try solve this per hours...
May 17, 2010 | Unregistered CommenterErick Souza

PostPost a New Comment

Enter your information below to add a new comment.

My response is on my own website »
Author Email (optional):
Author URL (optional):
Post:
 
All HTML will be escaped. Hyperlinks will be created for URLs automatically.