JakeJ
JakeJ

Reputation: 1381

Custom 'Field' Binder

By using the answer from this question I have managed to get a comma separated list binding to a simple IEnumerable type in my model

However, rather than having to declare the model binder in the controller itself (or globally for that matter), I would much prefer to be able to declare an attribute of some sort on just one of my model's fields which declares that it is the field that requires this edge-case binding method.

I imagine something that looks like this:

[CommaSeparated]
public IEnumerable<int> ListIDs {get;set;}

Any help will be much appreciated

Upvotes: 0

Views: 129

Answers (1)

Darin Dimitrov
Darin Dimitrov

Reputation: 1039298

Assuming that you have declared a marker attribute:

[AttributeUsage(AttributeTargets.Property)]
public class CommaSeparatedAttribute: Attribute
{

}

it's trivially easy to adapt the implementation of a model binder you saw in the linked post to apply only if the property is decorated with this attribute:

public class CommaSeparatedValuesModelBinder : DefaultModelBinder
{
    private static readonly MethodInfo ToArrayMethod = typeof(Enumerable).GetMethod("ToArray");

    protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
    {
        if (propertyDescriptor.PropertyType.GetInterface(typeof(IEnumerable).Name) != null)
        {
            var actualValue = bindingContext.ValueProvider.GetValue(propertyDescriptor.Name);

            if (actualValue != null && !String.IsNullOrWhiteSpace(actualValue.AttemptedValue) && actualValue.AttemptedValue.Contains(","))
            {
                var valueType = propertyDescriptor.PropertyType.GetElementType() ?? propertyDescriptor.PropertyType.GetGenericArguments().FirstOrDefault();
                bool isCommaSeparated = propertyDescriptor.Attributes.OfType<CommaSeparatedAttribute>().Any();

                if (isCommaSeparated && valueType != null && valueType.GetInterface(typeof(IConvertible).Name) != null)
                {
                    var list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(valueType));

                    foreach (var splitValue in actualValue.AttemptedValue.Split(new[] { ',' }))
                    {
                        list.Add(Convert.ChangeType(splitValue, valueType));
                    }

                    if (propertyDescriptor.PropertyType.IsArray)
                    {
                        return ToArrayMethod.MakeGenericMethod(valueType).Invoke(this, new[] { list });
                    }
                    else
                    {
                        return list;
                    }
                }
            }
        }

        return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
    }
}

Basically that's what you need to determine if the property is decorated with the marked attribute in order to apply the custom comma separation logic or fallback to the default behavior:

bool isCommaSeparated = propertyDescriptor.Attributes.OfType<CommaSeparatedAttribute>().Any();

Upvotes: 2

Related Questions