Thursday, September 10, 2009

ListView control powered by ProcessBatchData

In my last posting I demonstrated how it is possible to render list view HTML using SPWeb.ProcessBatchData with the DisplayPost method – I strongly recommend that you check it first before continuing with this one. While I tested the method I created a small web control that renders list view HTML much like the standard ListView web part or the SPView.RenderAsHtml method, … with several differences though. Basically this is experimental stuff, more like a proof of concept which should be thoroughly tested before used for more serious purposes.

The control is really simple and the code is merely two hundred or so lines – many important production features like appropriate exception handling, parameters and variables checks and logging are just missing. The public interface offers just four public properties which you can use to set up the control – and these should be set before the OnPreRender event:

public Guid ListID { get; set; }

public Guid ViewID { get; set; }

public SPWeb Web { get; set; }

public string ViewHtmlSchema { get; set; }

So, you need to set the ListID and the ViewID properties with the ID-s of the SPList and SPView that you want to render – note that the control uses just the ID-s, so if you have them cached you can pass them directly without opening the corresponding SPList and SPView instances – the control doesn’t use the SharePoint object model to open the view and list objects either – it just puts the ID-s in the ProcessBatchData batch string. For the Web property you should provide a SPWeb instance for the SharePoint site containing the source list – the ProcessBatchData method will be called on it. If you don’t set this property the current web will be used, but if it is not the web containing the list to be rendered, the rendering will fail. The ViewHtmlSchema property is optional in that if you skip it the ViewID property will be used to select the view of the list to be rendered. If you choose to use it however it will override the ViewID property and you will need to pass a valid View CAML definition to it – this is what you basically see in a View element in a schema.xml file of a list template or in the SPView.HtmlSchemaXml property. Several common scenarios of customized View schema that I can think of are for example a standard View schema with dynamically modified Query element for achieving custom filtering or sorting based on certain conditions or a standard View schema with slight modifications of the ViewBody, ViewHeader, ViewFooter and ViewEmpty elements to achieve different look and feel in the rendering. On the other hand you can construct a CAML View definition for some really customized rendering that doesn’t look anything like the standard ListView table like HTML presentation.

And now, let’s have a look at some portions of the code:

        private const string _displayPostXml = @"<?xml version=""1.0"" encoding=""UTF-8""?>

<ows:Batch OnError=""Continue"">

<Method ID=""0"">

  <SetVar Name=""Cmd"">DisplayPost</SetVar>

    {0}

  <SetVar Name=""PostBody"">{1}</SetVar>

</Method>

</ows:Batch>";

 

        private const string _postBody = @"<ows:XML>

    <SetList Scope=""Request"">{0}</SetList>

    {1}

</ows:XML>";

these two constant strings are used as templates for constructing the batch XML for the DisplayPost method. And this is the actual method that does this job:

        protected string GetViewHtml()

        {

            if (this.CurrentWeb == null) return string.Empty;

            bool allowUpdates = this.CurrentWeb.AllowUnsafeUpdates;

            try

            {

                // this should be set - otherwise the call to ProcessBatchData will throw

                this.CurrentWeb.AllowUnsafeUpdates = true;

 

                // format the saved in the ViewState query parameters as SetVar-s

                string parms = this.FormatParams();

 

                // if the ViewHtmlSchema property is not set format an empty View element with Name attribute

                string viewSchema = !string.IsNullOrEmpty(this.ViewHtmlSchema) ? this.ViewHtmlSchema : string.Format(@"<View Name=""{0}"" />", this.ViewID.ToString("B").ToUpper());

 

                // format the post body XML

                string postBody = string.Format(_postBody, this.ListID, viewSchema);

                // format the DisplayPost method XML

                string methodXml = string.Format(_displayPostXml, parms, EscapeForXml(postBody));

                // call the ProcessBatchData method

                string result = this.CurrentWeb.ProcessBatchData(methodXml);

 

                // obviously not the fastest way to get the result

                XmlDocument doc = new XmlDocument();

                doc.LoadXml(result);

                XmlElement el = doc.DocumentElement.SelectSingleNode("./Result") as XmlElement;

                return el == null ? string.Empty : el.InnerText;

            }

            finally

            {

                this.CurrentWeb.AllowUnsafeUpdates = allowUpdates;

            }

        }

As you see this method uses the two XML templates and the values of the control’s public properties from above to construct the DisplayPost batch XML. You can see the difference in the two modes of operation of the Control – when using just the ViewID property without specifying a ViewHtmlSchema – this results in a batch string with just an empty View element in it, and when you specify a View CAML definition in the ViewHtmlSchema property – then the full schema is inserted into the PostBody parameter of the DisplayPost method. An important note here: if you use a custom view definition and want to use the standard ViewHeader that displays context menus in the header cells with sorting and filtering options the root View element of the definition should have a Name attribute containing the ID of an existing view of the source list – otherwise the rendering of the context menus will fail.

The GetViewHtml method of the control is called normally from the control’s Render method, though there is one other usage of it that I will explain a little later. Let's first see another important piece of code in the control – the setting of the URL query parameters as SetVar parameters in the DisplayPost method XML body. You know that when you filer and sort a standard ListView web part clicking its header cells or the associated context menus certain parameters appear in the current page URL’s query part – parameters like View, RootFolder, SortField, SortDir, FilterField1, FilterValue1, FilterField2, FilterValue2. So the thing is that if we want the rendered view to be fully interactive these parameters should be somehow added to the XML of the DisplayPost method. And well, this turns out to be easy – using CAML SetVar parameters with the same names and values just does the trick. This is the method that saves the URL query parameters to the control’s ViewState:

        protected void GetSetVarParamsFromQuery()

        {

            // don't save the query params on event callback

            if (this.Page != null && this.Page.IsCallback) return;

 

            string viewParam = HttpContext.Current.Request.QueryString["View"];

 

            if (!string.IsNullOrEmpty(viewParam))

            {

                try

                {

                    Guid viewIDParam = new Guid(viewParam);

                    // check the value of the View query param - if it's not our viewID - don't save the query params - they should be used for another ListView

                    if (!this.ViewID.Equals(viewIDParam)) return;

                }

                // catch the Guid constructor exception

                catch { return; }

            }

            // else - if a view query param is not present - just proceed with saving the query params to the ViewState

 

            if (this.SetVarParams == null) this.SetVarParams = new NameValueCollection();

            else this.SetVarParams.Clear();

 

            // save all query params to the ViewState

            foreach (string par in HttpContext.Current.Request.QueryString.AllKeys)

            {

                this.SetVarParams[par] = HttpContext.Current.Request.QueryString[par];

            }

        }

you may ask why I save the URL query parameters to the ViewState and not use them directly in the DisplayPost XML – the answer is simple – these are not always guaranteed to be present in the page’s URL – especially in cases when you have two or more ListView web parts or ListView controls on the page – when you filter or sort one of the ListView-s it puts the parameters for its own filtering and sorting in the query string and in the meantime the other controls should be able to preserve their state. The ListView for which the query parameters should be applied is determined by the View query parameter containing the source view’s ID for that control (this is why the standard ListView web part creates always a hidden view which is a copy of the view that you select to be displayed in it – this guarantees that view that it renders is always unique and not used by another ListView). And the above method does exactly the same – it checks the View query parameter and saves the other query parameters only if it matches its ViewID property.

The method that formats the saved URL query parameters as SetVar CAML parameters is this one:

        protected string FormatParams()

        {

            NameValueCollection parms;

            if (this.SetVarParams == null) parms = new NameValueCollection();

            else parms = new NameValueCollection(this.SetVarParams);

 

            // add the group SetVar-s - the groupArgument will be set when the GetViewHtml method is called from the client callback handler

            if (!string.IsNullOrEmpty(this.groupArgument))

            {

                parms["GroupString"] = this.groupArgument;

                parms["ClientCallback"] = "1";

            }

 

            // this SetVar should be set for the group client java script to work

            if (!string.IsNullOrEmpty(this.ID)) parms["WebPartID"] = this.ID;

            StringBuilder sb = new StringBuilder();

            // output the SetVar-s

            foreach (string par in parms.AllKeys)

            {

                sb.AppendFormat("<SetVar Name=\"{0}\">{1}</SetVar>\r\n", EscapeForXml(par), EscapeForXml(parms[par]));

            }

            return sb.ToString();

        }

The interesting thing to note here is that several extra SetVar parameters can be optionally added here – these are used when the ListView uses grouping and dynamically loads the items for an expanded group. The control handles this by implementing the ICallbackEventHandler interface for ajax-like loading of data without page reloads – this is how the standard ListView web part does this as well. And as I mentioned before this is the second usage of the GetViewHtml control’s method – this time for rendering just the part that renders the items below the expanded group. To implement this the control renders some auxiliary java script snippets that the java script from the standard ViewHeader view element calls so that it can interact with the rendering control. Note also that the control’s ID property is used here so if you want to use the grouping functionality you should set it explicitly.

You can download the full code from here.

Wednesday, September 9, 2009

SPWeb ProcessBatchData – DisplayPost method

The DisplayPost WSS RPC method is sorts of analogue of the SPWeb.ProcessBatchData method in that you can use it to execute batches of the other RPC methods. It can be invoked using an HTML Post request against _vti_bin/owssvr.dll – in fact this is the testing setup for calling RPC methods as prescribed in MSDN – check here. This article describes how to create a small HTML page that sets several HTML form inputs, most notably a PostBody parameter that is intended to contain an XML with ows:Batch root and Method elements for various RPC methods.

So, at first glance using this method with ProcessBatchData doesn’t seem very reasonable – after all why would you want to add another level of indirection and call batches through the DisplayPost method instead of directly with ProcessBatchData. But still there’re two usages of the DisplayPost method that may be useful and I will describe them briefly.

The first usage is almost identical to the Display method (check my previous posting on that) – you specify the XMLDATA SetVar parameter and the method returns the list data in XML format:

<ows:Batch OnError="Continue">

  <Method ID="0">

    <SetList>702f059d-71f2-4f78-a41a-48978d381948</SetList>

    <SetVar Name="View">{CE0FFB35-F6A9-4F57-B06C-374B2AA4571B}</SetVar>

    <SetVar Name="XMLDATA">TRUE</SetVar>

    <SetVar Name="Cmd">DisplayPost</SetVar>

  </Method>

</ows:Batch>

Note that the PostBody parameter is not used in this case. The View SetVar parameter is optional – if omitted the default view of the list is used. Similarly to the Display method SetVar parameters like SortField, SortDir, FilterField1, FilterValue1, FilterField2, FilterValue2, RootFolder can be used for simple filtering and sorting. The Query SetVar parameter however doesn’t work with DisplayPost.

And the second usage of the DisplayPost method which is much more interesting:

<ows:Batch OnError="Continue">

  <Method ID="0">

    <SetVar Name="Cmd">DisplayPost</SetVar>

    <SetVar Name="PostBody">

      &lt;ows:XML&gt;

        &lt;SetList&gt;702f059d-71f2-4f78-a41a-48978d381948&lt;/SetList&gt;

        &lt;View&gt;

          &lt;ViewFields&gt;&lt;FieldRef Name='ID' /&gt;&lt;/ViewFields&gt;

          &lt;ViewBody&gt;&lt;Column Name='ID'/&gt;&lt;HTML&gt;,&lt;/HTML&gt;&lt;/ViewBody&gt;

        &lt;/View&gt;

      &lt;/ows:XML&gt;

    </SetVar>

  </Method>

</ows:Batch>

And this is the unescaped XML fragment passed to the PostBody SetVar parameter:

<ows:XML>

  <SetList>702f059d-71f2-4f78-a41a-48978d381948</SetList>

  <View>

    <ViewFields><FieldRef Name='ID' /></ViewFields>

    <ViewBody><Column Name='ID'/><HTML>,</HTML></ViewBody>

  </View>

</ows:XML>

So, in the outer method definition we have just the method type – Cmd parameter and the PostBody one. The actual stuff is the XML contained in the PostBody SetVar parameter. As you see it contains an <ows:XML> root element – this is an ancient element from the times of the STS used for rendering. So instead of the familiar <ows:Batch> element we see that there is (still) support for other STS CAML “container” elements. At this point you may ask yourself – can the <ows:XML> element be put directly into the batch string of the ProcessBatchData. Unfortunately this doesn’t work, so the indirection of the DisplayPost method is required here. And let’s have a look at the result that we have when executing this batch:

<Results>

  <Result ID="0" Code="0">

    1,2,3,391,392,444,445,446,447,448,449,450,451,452,453,454,

  </Result>

</Results>

So, what I actually did was defining a custom list view (using standard View CAML) within the <ows:XML> element and managed to retrieve some list data with it. The View element placed in an <ows:XML> effectively forces the rendering of the view definition provided in it. You can also provide a fully blown view definition copied from a SharePoint list schema file and the method will return HTML that you see normally in ListView web parts. And you can construct a view definition with custom ViewBody, ViewFields and Query elements that can be used either for rendering purposes or for data retrieval. For data retrieval you will create perhaps a smaller definition with Query part and ViewBody enumerating the fields probably using some unique separator for which you know that it’s not contained in some of the values. For rendering purposes you can specify also custom ViewHeader, ViewFooter and ViewEmpty elements. If you want to render a standard view, you can provide an empty View element with just a Name attribute like this:

<View Name="{CE0FFB35-F6A9-4F57-B06C-374B2AA4571B}" />

Note the upper cased, curly braced view ID in the Name attribute.

And the conclusion about this method is – well … if you have a knack for CAML (may be gone soon – beware of SP 2010) you can use it for both rendering and data retrieving purposes. For the former – you can start with a standard view definition and introduce minor changes without the need of creating custom list schemas (meaning creating custom list templates). For the latter you can take advantage of the fact that you can get many result sets with one call using batches (either placing several DisplayPost methods in the ProcessBatchData batch or several View elements inside the <ows:XML> element in the PostBody) and that the result data will be just as small in size as you specify for its constructing in the ViewBody element as opposed to the produced XML-s or SPListItemCollection’s data if you use the trivial methods for list item data retrieval.

Sunday, September 6, 2009

SPWeb ProcessBatchData – Display method

Basically all methods of the deprecated WSS RPC Protocol (MSDN) can be invoked via the SPWeb’s ProcessBatchData method. Since there’s little to no documentation regarding most of the RPC methods as to their usage directly with ProcessBatchData we can suspect that their support may be dropped in the new SharePoint 2010 (we still have to see how much of the CAML will be thrown out of it, and we’re talking about some really ancient stuff which’s been around since the times of the SharePoint Team Services). Most of the RPC methods were designed to serve specific tasks in the old STS but with the developing of the SharePoint object model most of these could now be much easier carried out with the latter. But … the thing is that even now in SharePoint 2007 the RPC methods are left intact and the ProcessBatchData is advertised as a faster method for inserting, updating and deleting (at least for that, but this may be true for other usages as well) of multiple list items than the standard SPListItem object model implementation.

So, let’s have a look at the Display method. A sample XML for calling it may look like this:

<?xml version="1.0" encoding="UTF-8"?>

<ows:Batch OnError="Continue">

  <Method ID="0">

    <SetList Scope="Request">702f059d-71f2-4f78-a41a-48978d381948</SetList>

    <SetVar Name="Cmd">Display</SetVar>

    <SetVar Name="XMLDATA">TRUE</SetVar>

    <SetVar Name="View">{FF964526-BBFA-4500-A43C-F4B7785E5F71}</SetVar>

  </Method>

</ows:Batch>

The SetList element contains the SPList ID, the Cmd SetVar – the method’s name. The XMLDATA SetVar parameter is actually mandatory but even if its value is FALSE the output of the method will be in XML format, not an HTML presentation of the list data. The View SetVar parameter is not mandatory – if it is not present, the default view of the list will be used, but the GUID format is important here – it should be with curly braces and uppercase. The result XML from the ProcessBatchData method will look something like this:

<Results>

  <Result ID="0" Code="0">

    <xml xmlns:s='uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882'

         xmlns:dt='uuid:C2F41010-65B3-11d1-A29F-00AA00C14882'

         xmlns:rs='urn:schemas-microsoft-com:rowset'

         xmlns:z='#RowsetSchema'>

      <s:Schema id='RowsetSchema'>

          <!-- schema data removed for brevity -->

      </s:Schema>

      <rs:data>

        <z:row ows_LinkTitle='some item' ows_fdate='2009-06-08 04:15:00' ows_flookup='1;#first' ows__UIVersionString='46.0' ows__ModerationStatus='2' ows_Editor='1;#Stefan Stanev' ows__Level='2' ows_ID='1' ows_owshiddenversion='104' ows_UniqueId='1;#{D2D82ED2-CBE7-4A56-84EC-3502FB8C2611}' ows_FSObjType='1;#0' ows_Created_x0020_Date='1;#2009-06-07 11:03:04' ows_Created='2009-06-07 11:03:04' ows_FileLeafRef='1;#1_.000' ows_FileRef='1;#sites/1/Lists/somelist/1_.000' />

        <z:row ows_LinkTitle='another item' ows_flookup='4;#one;more' ows__UIVersionString='22.0' ows__ModerationStatus='2' ows_Editor='1;#Stefan Stanev' ows__Level='2' ows_ID='2' ows_owshiddenversion='56' ows_UniqueId='2;#{9C1D1419-742A-41DC-85E1-1B1E7A50A2C8}' ows_FSObjType='2;#0' ows_Created_x0020_Date='2;#2009-06-07 11:03:27' ows_Created='2009-06-07 11:03:27' ows_FileLeafRef='2;#2_.000' ows_FileRef='2;#sites/1/Lists/somelist/2_.000' />

      </rs:data>

    </xml>

  </Result>

</Results>

The XML schema of the result (the part within the corresponding Result element) is actually identical to the XML schema of the result of the GetListItems method of the standard Lists web service (though the former is generated in the COM owssvr.dll library and the latter in the Microsoft.SharePoint.dll assembly with managed code – in the getter of the SPListItemCollection.Xml property). The result XML contains the data of all fields in the specified view plus several system fields and the returned items are sorted and filtered in accordance with the view’s query settings.

Several additional SetVar parameters can be used for simple filtering and sorting of the returned result set – basically these are the same that appear as query parameters when you sort or filter list views in the SharePoint UI – e.g. SortField, SortDir, FilterField1, FilterValue1, FilterField2, FilterValue2, RootFolder:

    <SetVar Name="SortField">Title</SetVar>

    <SetVar Name="SortDir">Desc</SetVar>

    <SetVar Name="FilterField1">Title</SetVar>

    <SetVar Name="FilterValue1">another</SetVar>

    <SetVar Name="RootFolder">/sites/1/Lists/somelist/fldr1/f1</SetVar>

If you use the RootFolder parameter with * as value you will get all items in the list recursively.

There is yet another optional SetVar parameter that can be used with the Display method – the Query parameter. Despite its name, you cannot specify a view CAML query in it, but just a list of field names, separated with spaces – basically with it you can specify the view fields that will appear in the result (the extra system fields will appear too). An unpleasant side effect of using it is that it effectively overrides the specified view’s query settings – so you end up with an unfiltered and unsorted result set – the effect of the SortField, FilterField1, etc SetVar-s (if present) is not affected though. A sample usage of the Query parameter (the asterisk – * – value can be used here as well):

    <SetVar Name="Query">ID Title</SetVar>

So, the conclusion about the Display ProcessBatchData method is that it is yet another way to retrieve SharePoint list item data, though it is not as flexible as the other mechanisms for list item data fetching – at least in respect to the query options that can be used for filtering and sorting the result set. One advantage though may be that like the other ProcessBatchData methods it can be executed in batches – so with one call of the ProcessBatchData method you will be able to retrieve the data from several lists.