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.

No comments:

Post a Comment