Question

I am using a custom model binder to bind my complex type.

Here's the model:

[ModelBinder(typeof(SupplierModelBinder))]
public class SupplierModel
{
    public string VendorId { get; set; }

    public string VendorName { get; set; }

    public override string ToString()
    {
        return VendorId;
    }
}

Here's the binder:

public class SupplierModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        string key = bindingContext.ModelName;
        ValueProviderResult val = bindingContext.ValueProvider.GetValue(key);
        if (val != null)
        {
            string s = val.AttemptedValue as string;
            if (s != null)
            {
                return new SupplierModel() { VendorId = s };
            }
        }

        return null;
    }
}

I'm rendering the model using Html.ActionLink calls the UserModel's ToString method. When that GETs to the server it uses the result of that to bind the model.

This works great (I'm not bothered about the VendorName property here), however it does rely on overriding ToString in the model class so I can use that value in the model binder.

How can I separate the binding/unbinding of my complex type from the ToString method?

I don't want to have to override ToString just so my model gets rendered correctly for my binder to interpret. For other types I'll have to (de)serialise to JSON or simiar, which I don't want to be in ToString.

Was it helpful?

Solution

I've managed to figure out how to do this without using a custom binding. The trick is realising that if I have things like Supplier.VendorId="XXXX" as part of the query string of the action link that gets rendered then it gets mapped correctly.

I used reflection to see what HtmlHelper.ActionLink() does when it's passed an object, which is that it creates an instance of RouteValueDictionary<string, object>, which creates a key for each property of the object.

This default implementation is close to what I want, but I need it to deal with properties of properties.

Luckly there's an overload of ActionLink() that takes a RouteValueDictionary<string, object> directly, so that left me with the problem of constructing one with the Property.SubProperty type keys correctly.

I ended up with the code below, which firstly uses the RouteValueDictonary's constructor to get a key for each property.
Then it removes any that won't get bound according to the Bind attribute (if the class has one), which tidies up the resulting querystring quite a bit.

The main part it does though is to look for any properties of type ...Model (the type name ending with "Model") and add that object's properties to the dictionary. I needed to use some rule for whether to recurse or not otherwise it would try and walk the properties of things like lists of objects etc. I figure that I'm already using a convention for my model classes, so I could stick to it.


public static RouteValueDictionary ToRouteValueDictionary(this object obj)
{
    var Result = new RouteValueDictionary(obj);

    // Find any ignored properties
    var BindAttribute = (BindAttribute)obj.GetType().GetCustomAttributes(typeof(BindAttribute), true).SingleOrDefault();

    var ExcludedProperties = new List<string>();

    if (BindAttribute != null)
    {
        ExcludedProperties.AddRange(BindAttribute.Exclude.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries));
    }

    // Remove any ignored properties from the dictionary
    foreach (var ExcludedProperty in ExcludedProperties)
    {
        Result.Remove(ExcludedProperty);
    }

    // Loop through each property, recursively adding sub-properties that end with "Model" to the dictionary
    foreach (var Property in obj.GetType().GetProperties())
    {
        if (ExcludedProperties.Contains(Property.Name))
        {
            continue;
        }

        if (Property.PropertyType.Name.EndsWith("Model"))
        {
            Result.Remove(Property.Name);

            var PropertyValue = Property.GetValue(obj, null);

            if (PropertyValue != null)
            {
                var PropertyDictionary = PropertyValue.ToRouteValueDictionary();

                foreach (var Key in PropertyDictionary.Keys)
                {
                    Result.Add(string.Format("{0}.{1}", Property.Name, Key), PropertyDictionary[Key]);
                }
            }
        }
    }

    return Result;
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top