CodeGrue
CodeGrue

Reputation: 5933

MVC Model Binding subclass object property filled with a string array

I have a base view model with an Id property of type object (so I can have it be an int or a Guid) like so:

public abstract class BaseViewModel
{
    public virtual object Id { get; set; }
}

And the view models thus derive from this

public class UserViewModel : BaseViewModel
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

My HTML then is rendered as:

<input id="Id" name="Id" type="hidden" value="240" />
<input id="FirstName" name="FirstName" type="text" value="John" /> 
<input id="LastName " name="LastName " type="text" value="Smith" /> 

And when submitted to the MVC action:

    [HttpPost]
    public ActionResult EditUser(UserViewModel model)
    {
       ...code omitted...
    }

The values for the model properties are:

Id: string[0] = "240"
FirstName: string = "John"
LastName: string = "Smith"

My question is, why am I getting a one item string array as the value for Id, rather than just a string? And is there a way to change this behavior? It causes problems when I try to parse it into the expected type.

Upvotes: 3

Views: 2376

Answers (2)

Faust
Faust

Reputation: 15404

The issue is with typing your id property as object -- not sure how the default binding is supposed to work here, but since an object is potentially anything -- like a complex object with multiple properties itself -- perhaps it attempts to dump all of the properties it finds there into an array?

If the Id is not always going to be an integer, I'd suggest typing this as string, since the model-binding mechanism should have no problem mapping virtually anything sent over HTTP as string, so:

public abstract class BaseViewModel
{
    public virtual string Id { get; set; }
}

Upvotes: 1

CodeGrue
CodeGrue

Reputation: 5933

I ended up solving this with a custom model binder that handles the "Id" object property as a special case:

public class CustomModelBinder : DefaultModelBinder
{
    protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
    {
        // apply the default model binding first to leverage the build in mapping logic
        base.BindProperty(controllerContext, bindingContext, propertyDescriptor);

        // since "Id" is a special property on BaseViewModel of type object, 
        // we need to figure out what it should be and parse it appropriately
        if (propertyDescriptor.Name == "Id" && propertyDescriptor.PropertyType == typeof(object))
        {
            // get the value that the default binder applied
            var defaultValue = propertyDescriptor.GetValue(bindingContext.Model);

            // this should be a one element string array
            if (defaultValue is string[])
            {
                var defaultArray = defaultValue as string[];

                // extract the first element of the array (the actual value of "Id")
                var propertyString = defaultArray[0];
                object value = propertyString;

                // try to convert the ID value to an integer (the most common scenario)
                int intResult;
                if (int.TryParse(propertyString, out intResult))
                {
                    value = intResult;
                }
                else
                {
                    // try to convert the ID value to an Guid
                    Guid guidResult;
                    if (Guid.TryParse(propertyString, out guidResult)) value = guidResult;
                }

                // set the model value
                propertyDescriptor.SetValue(bindingContext.Model, value);
            }

        }

    }

}

Upvotes: 2

Related Questions