Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Underscore string model binder

I was under the impression that when binding to a complex model, all public properties were processed and a match binding attempted for each.

I'm trying to resolve a variable naming problem so that a model

class Model {
      public string Foo {get;set;}
      public string FooBar {get;set;}
}

works nicely with a query string like

?foo=foo&foo_bar=foo_bar

Is there a better way than with a custom model binder? In any case, mine doesn't work. FooBar is simply skipped.

public class StringModelBinder : DefaultModelBinder
    {
        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            var model = base.BindModel(controllerContext, bindingContext);

            if (model != null)
                return model;

            var modelName = Regex.Replace(bindingContext.ModelName, "([a-z])([A-Z])", "$1_$2").ToLowerInvariant();

            var value = bindingContext.ValueProvider.GetValue(modelName);

            return value;
        }

    }

Registered with

ModelBinders.Binders.Add(typeof(string), new StringModelBinder());
like image 741
Martin Avatar asked Dec 06 '22 10:12

Martin


1 Answers

I was under the impression that when binding to a complex model, all public properties were processed and a match binding attempted for each.

No, that's a wrong impression. The default model binder will attempt to bind only the properties for which you have a corresponding value in the Request. In your case you do not have a corresponding value for the FooBar property so it won't be bound.

Actually it would be nice if we could write:

public class Model
{
    public string Foo { get; set; }

    [ParameterName("foo_bar")]
    public string FooBar { get; set; }
}

So let's implement this. We start by writing a base attribute:

[AttributeUsageAttribute(AttributeTargets.Property)]
public abstract class PropertyBinderAttribute : Attribute, IModelBinder
{
    public abstract object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext);
}

and a custom model binder:

public class CustomModelBinder : DefaultModelBinder
{
    protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
    {
        var propertyBinderAttribute = propertyDescriptor
            .Attributes
            .OfType<PropertyBinderAttribute>()
            .FirstOrDefault();

        if (propertyBinderAttribute != null)
        {
            var value = propertyBinderAttribute.BindModel(controllerContext, bindingContext);
            propertyDescriptor.SetValue(bindingContext.Model, value);
        }
        else
        {
            base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
        }
    }
}

As you can see this custom model analyzes the metadata of the model and if a property is decorated with an instance of the PropertyBinderAttribute it will use it.

We will then replace the default model binder with our custom one in Application_Start:

ModelBinders.Binders.DefaultBinder = new CustomModelBinder();

and all that's left now is to implement the ParameterNameAttribute binder that we used to decorate our model property with:

public class ParameterNameAttribute : PropertyBinderAttribute
{
    private readonly string parameterName;
    public ParameterNameAttribute(string parameterName)
    {
        this.parameterName = parameterName;
    }

    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var value = bindingContext.ValueProvider.GetValue(this.parameterName);
        if (value != null)
        {
            return value.AttemptedValue;
        }
        return null;
    }
}
like image 100
Darin Dimitrov Avatar answered Dec 28 '22 10:12

Darin Dimitrov