Download the source code for this project here

Over the last 2 posts in this series we’ve looked at creating a new field type to handle Xml data. We’ve created a basic implementation and added a custom user interface to make editing the data a bit more friendly. In this article we’ll look at using XSD and XSLT data to provide a higher level of data validation and to give a greater degree of control over how our data is displayed within SharePoint.

Previously most of the code that we’ve added has been to the BaseXmlField and SPFieldXmlBase classes. These classes encapsulate the functionality required to capture and display XML data but don’t actually make use of the data format in any meaningful way. You’ll remember that the reason that we defined a base class was so that we could add XML based parameters to our public implementation of an XML field. We added the properties ValidationSchemaXml and RenderXml that can be defined when adding our column to a list or library. This post will make use of these properties and will leave us with a more complete (and hopefully useful!) implementation of an XML field.

Step 1 – Add XSD validation

In part 2 we covered validation and the steps required to implement it in both our rendering class and our field type class. This time round we’ll override the base functionality in order to make use of our ValidationSchemaXml property.

In the SPFieldXml class we’ll add the following code:

       public override string GetValidatedString(object value)
        {
            string validated=base.GetValidatedString(value);
 
            if (!string.IsNullOrEmpty(this.ValidationSchemaXml))
            {
                try
                {
                    StringReader rdr = new StringReader(this.ValidationSchemaXml);
 
                    XmlSchema schema = XmlSchema.Read(rdr, null);
 
                    XmlReaderSettings settings = new XmlReaderSettings()
                    {
                        ValidationType = ValidationType.Schema
                    };
 
                    settings.Schemas.Add(schema);
 
                    StringReader xmlRdr = new StringReader(value.ToString());
                    XmlReader xmlReader = XmlReader.Create(xmlRdr, settings);
 
 
                    using (xmlReader)
                    {
 
                        while (xmlReader.Read())
                        {
                        }
                    }
 
                    return validated;
                }
                catch (XmlException ex)
                {
                    //Validation failed
                    throw new SPFieldValidationException(string.Format("{0} has an invalid value. {1}", this.Title, ex.Message), ex);
                }
            }
            else
            {
                return validated;
            }
 
        }

This code will load any schema information that’s present and use it to validate the entered xml. We’ve called into the base implementation at the top of the method so that we can confirm first of all, that the data entered is at least valid xml.

We haven’t bothered to override the Validate method of the XmlField class since the implementation would be no different from that in GetValidatedString.

Step 2 – Add XSLT rendering capabilities

Since our control has a number of different modes, we need to come up with a mechanism to add a template for each mode or omit certain modes, leaving them to be rendered using the base implementation. One way of doing this would be to add a template parameter for each control mode, meaning that we’d have a separate Xslt for each. However, I’ve opted for a simpler approach, using a single Xslt parameter with multiple <xsl:Template> nodes on the grounds that there will probably be some commonality between the rendering of each control mode and a single template allows for better code reuse.

The control modes supported by a field renderer control are defined by the SPControlMode enumeration and are accessible via the controls ControlMode property. I’ve made this value available in the Xslt by passing it as a parameter.

A very basic example of a template could be:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
  <xsl:output method="html" indent="yes"/>
  <xsl:param name="ControlMode"></xsl:param>
  <xsl:template match="/">
    <xsl:choose>
      <xsl:when test="$ControlMode='Display'">
        <xsl:call-template name="Display"/>
      </xsl:when>
      <xsl:when test="$ControlMode='Edit'">
        <xsl:call-template name="Edit"/>
      </xsl:when>
    </xsl:choose>
  </xsl:template>  
  <xsl:template name="Display" >
    <xsl:text>Display Template</xsl:text>
      <xsl:for-each select="//Item">
        <div>
          <xsl:value-of select="@text"/>
        </div>
      </xsl:for-each> 
  </xsl:template>
  <xsl:template name="Edit">
    <xsl:text>Edit Template</xsl:text>
    <xsl:for-each select="//Item">
      <div>
        <xsl:value-of select="@text"/>
      </div>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

Since we need the facility to revert to the base implementation of the rendering control, we need to determine whether the Xslt contains a template that matches the current mode before deciding to make use of Xslt to generate the output. To do this I’ve added the following function to the XmlField class:

 
        private bool IsTranformTemplateDefined(string controlMode)
        {
            //Load up the transform, make sure we have a template for the control mode
 
            XmlDocument transform = new XmlDocument();
 
            transform.LoadXml(this.field.RenderXml);
 
            XmlNamespaceManager mgr = new XmlNamespaceManager(transform.NameTable);
 
            mgr.AddNamespace("xsl", "http://www.w3.org/1999/XSL/Transform");
 
            string query = string.Format("//xsl:template[@name='{0}']", controlMode);
 
            if (transform.SelectSingleNode(query, mgr) == null)
            {
                return false;
            }
            else
            {
                return true;
            }
        }

Once we have determined whether we should render using Xslt, the next step is to perform the actual transform. To do this I’ve added the following function to the XmlField class:

        private void RenderUsingXslt(HtmlTextWriter output)
        {
 
            XslCompiledTransform transform = new XslCompiledTransform();
            StringReader sr = new StringReader(this.field.RenderXml);
            XmlReader rdr = XmlReader.Create(sr);
            transform.Load(rdr);
 
            XPathNavigator inputNav = this.field.GetFieldXml(this.ItemFieldValue.ToString()).CreateNavigator();
            XsltArgumentList args = new XsltArgumentList();
            args.AddParam("ControlMode", string.Empty, this.ControlMode.ToString());
            transform.Transform(inputNav, args, output);
 
        }

Now that we have all the code in place to generate the desired output, the next step is to override the Render method with code that will call our private functions. In the XmlField class I’ve added:

     protected override void Render(HtmlTextWriter output)
        {
            
            if (string.IsNullOrEmpty(this.field.RenderXml))
            {
                base.Render(output);
            }
            else
            {
                //Don't use standard rendering templates
                if (IsTranformTemplateDefined(ControlMode.ToString()))
                {
                    //Use the attached xslt to render our xml
                    RenderUsingXslt(output);
                }
                else
                {
                    base.Render(output);
                }
 
            }
        }

Conclusion

We now have a useful implementation of an XmlField. By allowing the list creator to define a schema and a rendering template, we’ve made it possible to store practically any type of data in a single field while at the same time providing complete control over how the data is presented in the user interface, all without requiring the user to write a single line of code. This functionality can eliminate the need to create multiple different custom fields and offers a few interesting possibilities for content management applications. (For example some clever coding in InfoPath would allow users to generate a fields value using form. The Xslt could then be used to create a read only version of the data for use on a front end web site. Just a thought!)