1

Can someone shed some light of how to implement model binder for complex types? I'd like all string properties to be trimmed. I tried the following but "SetProperty" method is never called.

public class TrimmingModelBinder : ComplexTypeModelBinder  
{
    public TrimmingModelBinder(IDictionary propertyBinders) : base(propertyBinders)
    {
    }

    protected override void SetProperty(ModelBindingContext bindingContext, string modelName, ModelMetadata propertyMetadata, ModelBindingResult result)
    {
        if(result.Model is string)
        {
            string resultStr = (result.Model as string).Trim();
            result = ModelBindingResult.Success(resultStr);
        }

        base.SetProperty(bindingContext, modelName, propertyMetadata, result);
    }
}

public class TrimmingModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (context.Metadata.IsComplexType && !context.Metadata.IsCollectionType)
        {
            var propertyBinders = context.Metadata.Properties.ToDictionary(p => p, context.CreateBinder);
            return new TrimmingModelBinder(propertyBinders);
        }

        return null;
    }
}
1

1 Answer 1

4

Here's how we do it.

A simple helper extension method:

static class NormalizeString
{
    public static string TrimAndNullIfWhiteSpace(this string text) =>
       string.IsNullOrWhiteSpace(text)
       ? null
       : text.Trim();
}

Create a custom ModelBinder for strings:

public class StringModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var modelName = bindingContext.ModelName;
        if (modelName == NullOrWhiteSpace)
            return Task.CompletedTask;

        var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
        if(valueProviderResult == ValueProviderResult.None)
            return Task.CompletedTask;

        bindingContext.Result = ModelBindingResult.Success(
            valueProviderResult.FirstValue.TrimAndNullIfWhiteSpace());

        return Task.CompletedTask;
    }
}

Create a custom ModelBinderProvider that references the custom ModelBinder:

public class ModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
            throw new ArgumentNullException(nameof(context));

        if (context.Metadata.ModelType == typeof(string))
            return new BinderTypeModelBinder(typeof(StringModelBinder));

        return null;
    }
}

And finally, register the customer ModelBinderProvider in Startup's ConfigureServices:

    services
        .AddMvc(o =>
        {
            o.ModelBinderProviders.Insert(0, new ModelBinderProvider());
        })
Sign up to request clarification or add additional context in comments.

2 Comments

I have implemented this and it seems to have the desired result so far, so thanks, but in your ModelBinderProvider class why not just return new StringModelBinder() instead of wrapping it in the BinderTypeModelBinder class; is there any advantage of doing so?
@ChristopherKing It's been a few years, so I'm sorry I can't remember. What happens when you try without BinderTypeModelBinder?

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.