Working with the SharePoint DataFormWebPart in Custom Application Pages: Part II

In the first post in this series, we setup a DataFormWebPart on our custom application page, using markup autogenerated by SharePoint. If everything went right, you should have a page with a DFW loading up and letting you do basic CRUD operations. Now for the harder stuff.

Trials and Tribulations

The first issue you’ll probably notice as soon as you save an item. Immediately after save, you’ll probably be redirected to the SharePoint List where the item is stored. Now maybe this isn’t an issue for you, but I suspect most of the time, you’d rather control where the user ends up.

While this seems like it should be easy, the challenge is that all this redirect logic is bundled with all the save logic up in a sharepoint WebControl, aptly labeled “SaveButton.” If you want to control redirect, you have to handle saving the item yourself. To do so, get a reference to the DataFormWebPart that’s on the page. Once you’ve got the reference, you’ll want to retrieve the ItemContext.ListItem property. This gives you access to the SharePoint List Item that stores whatever data the user has entered. Call ListItem.Update() and you’ve got the save handled. You can wrap that up inside the Click handler for a normal asp:Button and remove the SaveButton control from the form completely.

So now you’ve got a simple form which let’s you view and save your data. But what if you want something more complicated? What if you want to put custom server controls or access to page variables inside the xsl? Well then things start to get very tough. By default, the DFW will only allow custom server controls from Microsoft.SharePoint.WebControls. Adding any other controls will result in an “Unknown Server Tag.” Fortunately, another SharePoint blogger, Charlie Holland already dug into this issue and wrote a custom webpart to resolve it. His ExtendedDataFormWebPart allows you to specify a set of additional assemblies to allow controls from.

So that handles server controls, but what about page variables or inline script? Again, by default the DFW doesn’t allow any sort of inline code. However, we can take a similar approach to Charlie and get the ability to use PageVariables in our xsl, even if we can’t do full online inline code.

The best place to start looking for how to pull data into the XSL is the ParameterBindings List we looked at earlier for our QueryStrings. MSDN blogger Josh Gaffey has a good overview on these ParameterBindings. Basically each binding ends up corresponding to an XSL parameter that you can use inside of the XSL by using  <xsl:value-of select=”{$param-name}”>. However, out of the box, you can only pull these parameter values from a limited number of locations (QueryStrings, CAMLVariables, Server Variables, Control Values,  etc.) and Page Variables isn’t one of them.

Poking around in the properties of the dataform webpart, we can see there’s a property named ParameterValues. Looking at this property in debug mode, we see that it is a hashtable that holds all the values of parameter bindings. So what we need to do is inject our own values based on page variables, into the hashtable. Below is the code for a modified ExtendedDataFormWebPart class that incorporates both Charlie Holland’s additional assembly code, and our code to use page Variables.

<pre>    [ToolboxItemAttribute(false)]
    public class ExtendedDataFormWebPart : DataFormWebPart
    {
        public ExtendedDataFormWebPart()
            : base()
        {
        }

        [Browsable(false), WebPartStorage(Storage.None), PersistenceMode(PersistenceMode.InnerProperty)]
        public string AssemblyReferences
        {
            get
            {
                List<AssemblyReference> response = new List<AssemblyReference>();
                foreach (string reference in _assemblyReferences)
                {

                    AssemblyReference ar = new AssemblyReference(reference);
                    response.Add(ar);
                }

                return response.ToString();
            }
            set
            {
                XDocument doc = XDocument.Parse("<root>" + value + "</root>");
                var refs = from r in doc.Descendants("AssemblyReference")
                           select new AssemblyReference
                           {
                               Prefix = r.Attribute("Prefix").Value,
                               Namespace = r.Attribute("Namespace").Value,
                               Assembly = r.Attribute("Assembly").Value
                           };

                _assemblyReferences = new string[refs.Count()];

                int i = 0;
                foreach (var ar in refs)
                {
                    _assemblyReferences[i] = ar.ToString();
                    i++;
                }
            }
        }

        public override void DataBind()
        {
            BindPageVariablesToParameterBindings();
            base.DataBind();
        }

        private void BindPageVariablesToParameterBindings()
        {
            var listOfParametersAndVariableNames = BuildListOfPageParameters();
            SetParameterCollectionValues(listOfParametersAndVariableNames);
        }

        private void SetParameterCollectionValues(IEnumerable<KeyValuePair<string, string>> listOfParametersAndVariableNames)
        {
            foreach (var param in listOfParametersAndVariableNames)
            {
                object valueOfVariable;
                FieldInfo field = Page.GetType().GetField(param.Value);
                PropertyInfo prop = Page.GetType().GetProperty(param.Value);

                if(field != null)
                {
                    valueOfVariable = field.GetValue(Page);
                }
                else if(prop != null)
                {
                    valueOfVariable = prop.GetValue(Page, null);
                }
                else
                {
                    throw new InvalidOperationException(string.Format( "There is no member with the name {0} on {1}. Check your parameter binding to ensure the Location attribute is correct", param.Value, Page.ToString()));
                }

                valueOfVariable = (valueOfVariable != null) ? valueOfVariable.ToString() : "";
                ParameterValues.Set(param.Key, valueOfVariable as string);
            }
        }

        private IEnumerable<KeyValuePair<string, string>> BuildListOfPageParameters()
        {
            XDocument parametersXml = XDocument.Parse("<parameters>" + ParameterBindings +"</parameters>");
            XName location = XName.Get("Location", "");
            var parameters = parametersXml.Root.Elements().Where(e => e.Name.LocalName == "ParameterBinding" && e.Attribute(location).Value.Contains("PageVariable") );
            return parameters.Select(p => BuildKeyValuePairFromParameter(p));
        }

        private KeyValuePair<string, string> BuildKeyValuePairFromParameter(XElement parameter)
        {
            string key = parameter.Attribute(XName.Get("Name", "")).Value;
            string value = parameter.Attribute(XName.Get("Location", "")).Value.Replace("PageVariable(", "").Replace(")", "");
            return new KeyValuePair<string, string>(key, value);
        }

    }

    public sealed class AssemblyReference
    {
        public AssemblyReference()
        {
        }

        public override string ToString()
        {
            return string.Format("<%@ Register TagPrefix=\"{0}\" Namespace=\"{1}\" Assembly=\"{2}\" %>", Prefix, Namespace, Assembly);
        }

        public AssemblyReference(string reference)
        {

            Match m = Regex.Match(reference, "TagPrefix=\"(\\S*)\" Namespace=\"(\\S*)\" Assembly=\"(.*)\"");

            Prefix = m.Groups[1].Value;
            Namespace = m.Groups[2].Value;
            Assembly = m.Groups[3].Value;
        }

        public string Prefix;
        public string Namespace;
        public string Assembly;
    }

What we’re doing here is overriding the DataBind method and calling a private method that uses reflection to insert our page variables into the ParameterValues collection. If we want to use a PageVariable, we just add ParameterBinding of the form <ParameterBinding Name=”<xsl param name>” Location=”PageVariable(<variable name>)”/>

The only gotcha is that our variable has to be either a public property or field, not private or protected, since reflection doesn’t seem to pick those up, even when we pass a BindingFlags.NonPublic into the GetFields and GetProperties methods.

Conclusion

SharePoint’s DataFormWebPart is a really neat concept, but the execution leaves a little to be desired when you’re heavily customizing things. Fortunately, most of those short comings can be overcome by extending the webpart with the code samples here and elsewhere. Be warned though, that XSL is a pain to work with, and very unforgiving when it comes to typos. Even with all the improvements offered by the ExtendedDataFormWebPart, it might be easier to build the data manipulation plumbing yourself.

Advertisements

One thought on “Working with the SharePoint DataFormWebPart in Custom Application Pages: Part II

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s