4

Superfluous comment: I cannot believe I couldn't find a clear answer for this anywhere yet!

Using ASP.NET MVC model binding requires use of dot notation (variableName.propertyName) when using a query string. However, jQuery will use bracket notation when using a GET request, such as variableName[propertyName]=value&. ASP.NET MVC cannot understand this notation.

If I issued a POST request ASP.NET is able to properly bind the model because it uses dot notation in the posted body.

Is there any way to force ASP.NET to bind to a model that is a complex object when bracketed notation is used within a query string?

0

1 Answer 1

2

I'm not sure if this is the ideal solution, but I solved this using some reflection magic by implementing a generic implementation of IModelBinder. The stipulations on this implementation is that it assumes the elements from JavaScript in the query string are in camelCase and the class in C# is in PascalCase per standard styles. Additionally, it only functions on public [set-able] properties. Here's my implementation below:

public class BracketedQueryStringModelBinder<T> : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(p => p.CanWrite);
        Dictionary<string, object> values = new Dictionary<string, object>();
        foreach (var p in properties)
        {
            if (!IsNullable(p.PropertyType))
            {
                object val = TryGetValueType(p.PropertyType, bindingContext, p.Name);
                if (val != null)
                {
                    values.Add(p.Name, val);
                }
            }
            else
            {
                object val = GetRefernceType(p.PropertyType, bindingContext, p.Name);
                values.Add(p.Name, val);
            }
        }

        if (values.Any())
        {
            object boundModel = Activator.CreateInstance<T>();
            foreach (var p in properties.Where(i => values.ContainsKey(i.Name)))
            {
                p.SetValue(boundModel, values[p.Name]);
            }

            return boundModel;
        }

        return null;
    }

    private static bool IsNullable(Type t)
    {
        if (t == null)
            throw new ArgumentNullException("t");

        if (!t.IsValueType)
            return true;

        return Nullable.GetUnderlyingType(t) != null;
    }

    private static object TryGetValueType(Type type, ModelBindingContext ctx, string key)
    {
        if (string.IsNullOrEmpty(key))
            throw new ArgumentNullException("key");

        key = ConvertToPascalCase(key);

        ValueProviderResult result = ctx.ValueProvider.GetValue(string.Concat(ctx.ModelName, "[", key, "]"));
        if (result == null && ctx.FallbackToEmptyPrefix)
            result = ctx.ValueProvider.GetValue(key);

        if (result == null)
            return null;

        try
        {
            object returnVal = result.ConvertTo(type);
            ctx.ModelState.SetModelValue(key, result);
            return returnVal;
        }
        catch (Exception ex)
        {
            ctx.ModelState.AddModelError(ctx.ModelName, ex);
            return null;
        }
    }

    private static object GetRefernceType(Type type, ModelBindingContext ctx, string key)
    {
        if (string.IsNullOrEmpty(key))
            throw new ArgumentNullException("key");

        key = ConvertToPascalCase(key);

        ValueProviderResult result = ctx.ValueProvider.GetValue(string.Concat(ctx.ModelName, "[", key, "]"));
        if (result == null && ctx.FallbackToEmptyPrefix)
            result = ctx.ValueProvider.GetValue(key);

        if (result == null)
            return null;

        try
        {
            object returnVal = result.ConvertTo(type);
            ctx.ModelState.SetModelValue(key, result);
            return returnVal;
        }
        catch (Exception ex)
        {
            ctx.ModelState.AddModelError(ctx.ModelName, ex);
            return null;
        }
    }

    private static string ConvertToPascalCase(string str)
    {
        char firstChar = str[0];
        if (char.IsUpper(firstChar))
            return char.ToLower(firstChar) + str.Substring(1);

        return str;
    }
}

Then in your controller you can use it like this:

[HttpGet]
public ActionResult myAction([ModelBinder(typeof(BracketedQueryStringModelBinder<MyClass>))] MyClass mc = null)
{
    ...
}

The main downfall to this method is that if you do get a query string in dot notation this binding will fail since it doesn't revert back to the standard model binder.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.