A custom model binder for XML content that produces IEnumerable<T>'s

Using this custom model binder to extract data from the XML generated by a client side grid allowed me to remove reams of boilerplate code from controller after controller after contoller

In a previous post (A custom model binder for XML content) I showed code for a custom MVC Model Binder that takes an XML filled string passed to an action and transforms it to an XDocument. The next step from here is to take the XML and build out an IEnumerable<T> of the items within the XML. This is a chunk of code that I've used in the past with a JavaScript framework to read data from grids that it displays and pass it to a controller action for processing.

As before, this means having a custom model binder and accompanying attribute that's used to decorate the parameter on the action method, just with a bit more code to work the magic of constructing and populating types. The ultimate objective of this code is to be able to have an MVC action method with a signature that looks like this:

public ActionResult UpdateExternalItems(int updateId, int typeId, 
    [TypedModelBinder]IEnumerable<ExternalItem> externalItems, bool performOtherAction)
{
}

Getting the standard MVC model binder to take a blob of XML from the UI and process this served to be something it didn't seem to want to do, so the original version of this code had externalItems declared a a string with the first chunk of this method (which ended up being most of the method!) being responsible for taking the string, generating instances of ExternalItem and pumping them into a list ready for processing. This pattern was repeated throughout the code in every action method of every controller where there was a grid of data to be sent to the server for processing. That ended up being hundreds of methods so a more re-usable, and thus less prone to errors, method was very much desired. 

Building out support for the typed model binder

The first chunk of code, to follow the order of events in my original post, is the attribute to assign to the parameters of the action method which I've named TypedModelBinderAttribute, though in another project a more specific name may have been desirable:

public sealed class TypedModelBinderAttribute : CustomModelBinderAttribute
{
    public override IModelBinder GetBinder()
    {
        return new TypedModelBinder(string.IsNullOrWhiteSpace(BindingPath) ? "undefinedList/undefined" : BindingPath);
    }

    public string BindingPath { get; set; }
}

The main work of this class is in the GetBinder method which is responsible for returning an instance of the, yet to be defined, TypedModelBinder class which does the real work in this solution. The presence of the BindingPath property is to allow for a custom/non-standard path to the list of items to transform within the XML where the data from the page doesn't drop it into the standard path for whatever reason. This property can be set when decorating a method with the attribute like this:

public ActionResult UpdateItemList([TypedModelBinder(BindingPath = "/items/item")] IEnumerable<ItemViewModel> items)

That customisation point aside, the attribute doesn't do anything very interesting or exciting other than spin up an instance of TypedModelBinder ready for it to parse the content of the data sent to the controller. Before digging into that code it's time to have a look at a view model class and see what that looks like:

public sealed class ItemViewModel 
{
    [XmlFieldMapping(XmlName = "id")]
    public int? ResourceId { get; set; }
    [XmlFieldMapping(XmlName = "priority")]
    public int Priority { get; set; }
    [XmlFieldMapping(XmlName = "resourceConfig")]
    public string ResourceConfig { get; set; }
}

This shows that there's another attribute in play in solving this problem, XmlFieldMapping which looks like this:

/// <summary>
/// Maps a value in XML to a property in a model
/// </summary>
public class XmlFieldMappingAttribute : Attribute
{
    /// <summary>
    /// The name of the property in uploaded XML
    /// </summary>
    public string XmlName { get; set; }
}

I've left the doc comments, sparse though they are, in for this class as they pretty much cover its purpose for me. To summarise what you can see from the view model and attribute, and can probably guess from the rest, the XmlFieldMappingAttribute is used by the custom model binder to determine which fields in the XML map to which properties of the model. There are plenty of other ways to achieve this such as mapping by convention, or mapping by specific name, but when this was implemented there were hundreds of methods taking XML input which meant that there were quite a few non-standard ways of naming data points in the XML as compared to their counterparts in the view model.

Building the typed model binder

Without further ado, here's the code for the model binder:

public class TypedModelBinder : IModelBinder
{
    string BindingPath { get; set; }

    public TypedModelBinder(string bindingPath)
    {
        BindingPath = bindingPath;
    }

    object IModelBinder.BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var incomingData = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).AttemptedValue;
        var doc = new XmlDocument();
        doc.LoadXml(incomingData);

        Type type = bindingContext.ModelType.GetGenericArguments()[0];
        Type listType = typeof(List<>).MakeGenericType(new[] { type });
        IList list = (IList)Activator.CreateInstance(listType);

        foreach (XmlNode item in doc.SelectNodes(BindingPath))
        {
            var newItem = Activator.CreateInstance(type);
            foreach (XmlNode value in item.ChildNodes)
            {
var property = GetPropertyForPropertyName(type, value.Name); if (property != null) { var typedValue = ChangeType(property.PropertyType, value.InnerText); property.SetValue(newItem, typedValue, null); } } list.Add(newItem); } return list; }
private PropertyInfo GetPropertyForPropertyName(Type type, string propertyName) { var property = type.GetProperty(propertyName); if (property == null) { // Attempt to retrieve a property based on an XmlFieldMappingAttribute var propertiesWithMappingAttribute = type.GetProperties().Where(p => p.GetCustomAttributes(typeof(XmlFieldMappingAttribute), true).Length > 0); if (propertiesWithMappingAttribute != null) { foreach (var propertyWithMappingAttribute in propertiesWithMappingAttribute) { var xmlFieldMappingAttribute = propertyWithMappingAttribute.GetCustomAttributes(typeof(XmlFieldMappingAttribute), true)[0] as XmlFieldMappingAttribute; if (xmlFieldMappingAttribute.XmlName.Equals(propertyName, StringComparison.OrdinalIgnoreCase)) { property = propertyWithMappingAttribute; break; } } } } return property; } }

This class relies on another method, ChangeType on the line highlighted in bold, which I've dropped down to the bottom of this post as it's purely a helper method and doesn't add anything to discussing how the model binder itself works.

Essentially what this code does (with little to no error checking for brevity) is:

  1. IModelBinder.BindModel: Retrieves the data it needs to process from the passed in bindingContext
  2. Transforms the data (which will have come from the items argument on the example controller action up above) into an XmlDocument
  3. Creates an instance of List<T>, where the type of is taken from the first generic type for the model, so from IEnumerable<ItemViewModel> it takes ItemViewModel
  4. Iterates over each of the nodes in the XmlDocument (the items), as defined by the BindingPath
    1. Creates an instance of ItemViewModel
    2. Iterates over each of the child elements of the item picked out of the node from the XML (the properties of the item)
      1. GetPropertyForPropertyName: Looks for an exact match by name, i.e. ItemViewModel has a property whose name exactly matches that of the name of the child element, or
      2. Uses the XmlFieldMappingAttributes that are associated with the model type to find the matching property on IndexViewModel
    3. If a match is found, uses ChangeType to transform the received data into the type of the property and assigns it to the property

It's a bit of a lengthy explanation there, but, here's a bit of XML and an example of what the resultant POCOs will look like:

<items>
  <item>
    <id>1</id>
    <priority>2</priority>
    <resourceConfig>ABC123</resourceConfig>
  </item>
  <item>
    <priority>1</priority>
    <resourceConfig>DEF456</resourceConfig>
  </item>
</items>
ResourceIdPriorityResourceConfig
1 2 ABC123
null 1 DEF456

That's pretty much all there is to it, the code worked pretty well in production use for a couple of years though I have no idea whether it's still in use or not and allowed us to massively simplify large chunks of our controllers to take out what was effectively boilerplate code with all the brittleness to change that comes with that.

The 'ChangeType' helper method

This helper method is one that I can take no credit for, I acquired it from somewhere many years back and it seems to generally work for converting from one type to another!

private static object ChangeType(Type conversionType, object value)
{
    if (conversionType.IsGenericType && conversionType.GetGenericTypeDefinition().Equals(typeof(Nullable<>)))
    {
        if (value == null || ((value as string != null) && string.IsNullOrWhiteSpace(value as string)))
        {
            if (conversionType.IsValueType)
            {
                return Activator.CreateInstance(conversionType);
            }
            else
            {
                return null;
            }
        }
        else
        {
            NullableConverter nullableConverter = new NullableConverter(conversionType);
            conversionType = nullableConverter.UnderlyingType;
        }
    }
    else
    {
        if (value == null || ((value as string != null) && string.IsNullOrWhiteSpace(value as string)))
        {
            if (conversionType.IsValueType)
            {
                return Activator.CreateInstance(conversionType);
            }
            else
            {
                return null;
            }
        }
    }

    return Convert.ChangeType(value, conversionType);
}

About Rob

I've been interested in computing since the day my Dad purchased his first business PC (an Amstrad PC 1640 for anyone interested) which introduced me to MS-DOS batch programming and BASIC.

My skillset has matured somewhat since then, which you'll probably see from the posts here. You can read a bit more about me on the about page of the site, or check out some of the other posts on my areas of interest.

No Comments

Add a Comment