数据输入后修剪字符串的最佳方法.我应该创建自定义模型绑定器吗?

Joh*_*ika 169 asp.net asp.net-mvc model-binders

我正在使用ASP.NET MVC,我希望所有用户输入的字符串字段在插入数据库之前进行修剪.由于我有很多数据输入表单,我正在寻找一种优雅的方法来修剪所有字符串,而不是明确地修剪每个用户提供的字符串值.我很想知道人们如何以及何时修剪弦乐.

我想也许创建一个自定义模型绑定器并修剪那里的任何字符串值......这样,我所有的修剪逻辑都包含在一个地方.这是一个好方法吗?是否有任何代码示例执行此操作?

小智 210

  public class TrimModelBinder : DefaultModelBinder
  {
    protected override void SetProperty(ControllerContext controllerContext, 
      ModelBindingContext bindingContext, 
      System.ComponentModel.PropertyDescriptor propertyDescriptor, object value)
    {
      if (propertyDescriptor.PropertyType == typeof(string))
      {
        var stringValue = (string)value;
        if (!string.IsNullOrWhiteSpace(stringValue))
        {
          value = stringValue.Trim();
        }
        else
        {
          value = null;
        }
      }

      base.SetProperty(controllerContext, bindingContext, 
                          propertyDescriptor, value);
    }
  }
Run Code Online (Sandbox Code Playgroud)

这段代码怎么样?

ModelBinders.Binders.DefaultBinder = new TrimModelBinder();
Run Code Online (Sandbox Code Playgroud)

设置global.asax Application_Start事件.

  • 这值得更多的赞成.我真的很惊讶MVC团队没有选择在默认模型绑定器中实现它... (4认同)
  • 为简洁起见,我只需将内部最多{}中的代码替换为:string stringValue =(string)value; value = string.IsNullOrEmpty(stringValue)?stringValue:stringValue.Trim(); (3认同)
  • 对于像我这样的人在DefaultModelBinder上得到歧义,正确的是使用System.Web.Mvc. (3认同)
  • 你怎么修改这个来保持`type ="password"`输入不变? (3认同)

Kor*_*yem 76

这是@takepara相同的分辨率,但作为IModelBinder而不是DefaultModelBinder,以便在global.asax中添加modelbinder是通过

ModelBinders.Binders.Add(typeof(string),new TrimModelBinder());
Run Code Online (Sandbox Code Playgroud)

班级:

public class TrimModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext,
    ModelBindingContext bindingContext)
    {
        ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        if (valueResult== null || valueResult.AttemptedValue==null)
           return null;
        else if (valueResult.AttemptedValue == string.Empty)
           return string.Empty;
        return valueResult.AttemptedValue.Trim();
    }
}
Run Code Online (Sandbox Code Playgroud)

基于@haacked帖子:http://haacked.com/archive/2011/03/19/fixing-binding-to-decimals.aspx

  • 这不处理[ValidateInput(false)]控制器属性.它导致"危险请求......"例外. (6认同)
  • 谢谢@CodeGrue随时编辑我的代码以解决此问题:) (2认同)
  • 您必须调用“bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueResult)”,否则 ModelState 值在 MVC 控制器中不可用(因为它们具有默认绑定器)并且通常的接受/拒绝模式与 ModelState.Clear 不再有效正如预期的那样。 (2认同)
  • 对于那些收到"危险请求..."例外的人,请参阅这篇文章 - http://blogs.taiga.nl/martijn/2011/09/29/custom-model-binders-and-request-validation/ (2认同)
  • 我的一个同事实现了这种变化,引起了各种各样的问题:http://issues.umbraco.org/issue/U4-6665我建议在适当时返回null和empty,而不是总是优先选择其中一个(在在你的情况下,即使值为空字符串,也总是返回null). (2认同)
  • 这似乎破坏了模型属性上的 `[AllowHtml]` 属性(以及上面提到的 CodeGrue 的 `[ValidateInput(false)]` (2认同)

小智 41

@takepara答案的一个改进.

有些人在项目中:

public class NoTrimAttribute : Attribute { }
Run Code Online (Sandbox Code Playgroud)

在TrimModelBinder类中更改

if (propertyDescriptor.PropertyType == typeof(string))
Run Code Online (Sandbox Code Playgroud)

if (propertyDescriptor.PropertyType == typeof(string) && !propertyDescriptor.Attributes.Cast<object>().Any(a => a.GetType() == typeof(NoTrimAttribute)))
Run Code Online (Sandbox Code Playgroud)

并且您可以使用[NoTrim]属性标记要从修剪中排除的属性.

  • 我认为这是一个很好的补充,但我会替换`.Cast <object>().任何(a => a.GetType()== typeof(NoTrimAttribute))```.OfType <NoTrimAttribute>().Any( )`.只是一点点清洁. (4认同)

adr*_*ian 17

随着C#6的改进,你现在可以编写一个非常紧凑的模型绑定器来修剪所有字符串输入:

public class TrimStringModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        var attemptedValue = value?.AttemptedValue;

        return string.IsNullOrWhiteSpace(attemptedValue) ? attemptedValue : attemptedValue.Trim();
    }
}
Run Code Online (Sandbox Code Playgroud)

您需要Application_Start()Global.asax.cs文件中的某处包含此行以在绑定时使用模型绑定器string:

ModelBinders.Binders.Add(typeof(string), new TrimStringModelBinder());
Run Code Online (Sandbox Code Playgroud)

我发现最好使用这样的模型绑定器,而不是覆盖默认的模型绑定器,因为无论何时绑定a string,它都将被使用,无论是直接作为方法参数还是作为模型类的属性.但是,如果您按照此处提供的其他答案覆盖默认模型绑定器,那么只有在模型上绑定属性时才会起作用,而不是在您具有string作为操作方法的参数时

编辑:评论者询问在不应验证字段时处理情况.我的原始答案被简化为只处理OP提出的问题,但对于那些感兴趣的人,您可以使用以下扩展模型绑定器来处理验证:

public class TrimStringModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var shouldPerformRequestValidation = controllerContext.Controller.ValidateRequest && bindingContext.ModelMetadata.RequestValidationEnabled;
        var unvalidatedValueProvider = bindingContext.ValueProvider as IUnvalidatedValueProvider;

        var value = unvalidatedValueProvider == null ?
          bindingContext.ValueProvider.GetValue(bindingContext.ModelName) :
          unvalidatedValueProvider.GetValue(bindingContext.ModelName, !shouldPerformRequestValidation);

        var attemptedValue = value?.AttemptedValue;

        return string.IsNullOrWhiteSpace(attemptedValue) ? attemptedValue : attemptedValue.Trim();
    }
}
Run Code Online (Sandbox Code Playgroud)


Kai*_*i G 14

ASP.Net Core 2中,这对我有用.我[FromBody]在我的控制器和JSON输入中使用该属性.要覆盖JSON反序列化中的字符串处理,我注册了自己的JsonConverter:

services.AddMvcCore()
    .AddJsonOptions(options =>
        {
            options.SerializerSettings.Converters.Insert(0, new TrimmingStringConverter());
        })
Run Code Online (Sandbox Code Playgroud)

这是转换器:

public class TrimmingStringConverter : JsonConverter
{
    public override bool CanRead => true;
    public override bool CanWrite => false;

    public override bool CanConvert(Type objectType) => objectType == typeof(string);

    public override object ReadJson(JsonReader reader, Type objectType,
        object existingValue, JsonSerializer serializer)
    {
        if (reader.Value is string value)
        {
            return value.Trim();
        }

        return reader.Value;
    }

    public override void WriteJson(JsonWriter writer, object value,
        JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}
Run Code Online (Sandbox Code Playgroud)


Ton*_*all 13

@ takepara答案的另一个变种,但有一个不同的转折:

1)我更喜欢选择加入"StringTrim"属性机制(而不是@Anton的选择退出"NoTrim"示例).

2)需要另外调用SetModelValue以确保正确填充ModelState并且可以正常使用默认验证/接受/拒绝模式,即应用TryUpdateModel(模型)和接受所有更改的ModelState.Clear().

把它放在你的实体/共享库中:

/// <summary>
/// Denotes a data field that should be trimmed during binding, removing any spaces.
/// </summary>
/// <remarks>
/// <para>
/// Support for trimming is implmented in the model binder, as currently
/// Data Annotations provides no mechanism to coerce the value.
/// </para>
/// <para>
/// This attribute does not imply that empty strings should be converted to null.
/// When that is required you must additionally use the <see cref="System.ComponentModel.DataAnnotations.DisplayFormatAttribute.ConvertEmptyStringToNull"/>
/// option to control what happens to empty strings.
/// </para>
/// </remarks>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class StringTrimAttribute : Attribute
{
}
Run Code Online (Sandbox Code Playgroud)

然后在你的MVC应用程序/库中:

/// <summary>
/// MVC model binder which trims string values decorated with the <see cref="StringTrimAttribute"/>.
/// </summary>
public class StringTrimModelBinder : IModelBinder
{
    /// <summary>
    /// Binds the model, applying trimming when required.
    /// </summary>
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        // Get binding value (return null when not present)
        var propertyName = bindingContext.ModelName;
        var originalValueResult = bindingContext.ValueProvider.GetValue(propertyName);
        if (originalValueResult == null)
            return null;
        var boundValue = originalValueResult.AttemptedValue;

        // Trim when required
        if (!String.IsNullOrEmpty(boundValue))
        {
            // Check for trim attribute
            if (bindingContext.ModelMetadata.ContainerType != null)
            {
                var property = bindingContext.ModelMetadata.ContainerType.GetProperties()
                    .FirstOrDefault(propertyInfo => propertyInfo.Name == bindingContext.ModelMetadata.PropertyName);
                if (property != null && property.GetCustomAttributes(true)
                    .OfType<StringTrimAttribute>().Any())
                {
                    // Trim when attribute set
                    boundValue = boundValue.Trim();
                }
            }
        }

        // Register updated "attempted" value with the model state
        bindingContext.ModelState.SetModelValue(propertyName, new ValueProviderResult(
            originalValueResult.RawValue, boundValue, originalValueResult.Culture));

        // Return bound value
        return boundValue;
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您没有在活页夹中设置属性值,即使您不想更改任何内容,也会完全阻止ModelState中的该属性!这是因为您注册为绑定所有字符串类型,因此它(在我的测试中)显示默认绑定器不会为您执行此操作.


Tuu*_*oos 7

在ASP.NET Core 1.0中搜索如何执行此操作的任何人的额外信息.逻辑发生了很大的变化.

我写了一篇关于如何做的博客文章,它更详细地解释了一些事情

所以ASP.NET Core 1.0解决方案:

模型活页夹进行实际修剪

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

此外,您需要最新版本的Model Binder Provider,这表明此绑定器应该用于此模型

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 = new Dictionary();
            foreach (var property in context.Metadata.Properties)
            {
                propertyBinders.Add(property, context.CreateBinder(property));
            }

            return new TrimmingModelBinder(propertyBinders);
        }

        return null;
    }
}
Run Code Online (Sandbox Code Playgroud)

然后它必须在Startup.cs中注册

 services.AddMvc().AddMvcOptions(options => {  
       options.ModelBinderProviders.Insert(0, new TrimmingModelBinderProvider());
 });
Run Code Online (Sandbox Code Playgroud)


Bas*_*sem 6

我创建了值提供程序来修剪查询字符串参数值和表单值。这已经用 ASP.NET Core 3 测试过并且运行良好。

public class TrimmedFormValueProvider
    : FormValueProvider
{
    public TrimmedFormValueProvider(IFormCollection values)
        : base(BindingSource.Form, values, CultureInfo.InvariantCulture)
    { }

    public override ValueProviderResult GetValue(string key)
    {
        ValueProviderResult baseResult = base.GetValue(key);
        string[] trimmedValues = baseResult.Values.Select(v => v?.Trim()).ToArray();
        return new ValueProviderResult(new StringValues(trimmedValues));
    }
}

public class TrimmedQueryStringValueProvider
    : QueryStringValueProvider
{
    public TrimmedQueryStringValueProvider(IQueryCollection values)
        : base(BindingSource.Query, values, CultureInfo.InvariantCulture)
    { }

    public override ValueProviderResult GetValue(string key)
    {
        ValueProviderResult baseResult = base.GetValue(key);
        string[] trimmedValues = baseResult.Values.Select(v => v?.Trim()).ToArray();
        return new ValueProviderResult(new StringValues(trimmedValues));
    }
}

public class TrimmedFormValueProviderFactory
    : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        if (context.ActionContext.HttpContext.Request.HasFormContentType)
            context.ValueProviders.Add(new TrimmedFormValueProvider(context.ActionContext.HttpContext.Request.Form));
        return Task.CompletedTask;
    }
}

public class TrimmedQueryStringValueProviderFactory
    : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        context.ValueProviders.Add(new TrimmedQueryStringValueProvider(context.ActionContext.HttpContext.Request.Query));
        return Task.CompletedTask;
    }
}
Run Code Online (Sandbox Code Playgroud)

然后ConfigureServices()在Startup.cs的函数中注册值提供者工厂

services.AddControllersWithViews(options =>
{
    int formValueProviderFactoryIndex = options.ValueProviderFactories.IndexOf(options.ValueProviderFactories.OfType<FormValueProviderFactory>().Single());
    options.ValueProviderFactories[formValueProviderFactoryIndex] = new TrimmedFormValueProviderFactory();

    int queryStringValueProviderFactoryIndex = options.ValueProviderFactories.IndexOf(options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>().Single());
    options.ValueProviderFactories[queryStringValueProviderFactoryIndex] = new TrimmedQueryStringValueProviderFactory();
});
Run Code Online (Sandbox Code Playgroud)


Eri*_*son 5

在阅读了上面的出色答案和评论并变得越来越困惑时,我突然想到,嘿,我想知道是否有jQuery解决方案。因此,对于像我一样感到有些困惑的其他人,我提供了以下jQuery代码段,这些代码段在提交表单之前修剪了输入字段。

    $('form').submit(function () {
        $(this).find('input:text').each(function () {
            $(this).val($.trim($(this).val()));
        })
    });
Run Code Online (Sandbox Code Playgroud)


小智 5

如果是MVC Core

活页夹:

using Microsoft.AspNetCore.Mvc.ModelBinding;
using System;
using System.Threading.Tasks;
public class TrimmingModelBinder
    : IModelBinder
{
    private readonly IModelBinder FallbackBinder;

    public TrimmingModelBinder(IModelBinder fallbackBinder)
    {
        FallbackBinder = fallbackBinder ?? throw new ArgumentNullException(nameof(fallbackBinder));
    }

    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

        if (valueProviderResult != null &&
            valueProviderResult.FirstValue is string str &&
            !string.IsNullOrEmpty(str))
        {
            bindingContext.Result = ModelBindingResult.Success(str.Trim());
            return Task.CompletedTask;
        }

        return FallbackBinder.BindModelAsync(bindingContext);
    }
}
Run Code Online (Sandbox Code Playgroud)

提供者:

using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using System;

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

        if (!context.Metadata.IsComplexType && context.Metadata.ModelType == typeof(string))
        {
            return new TrimmingModelBinder(new SimpleTypeModelBinder(context.Metadata.ModelType));
        }

        return null;
    }
}
Run Code Online (Sandbox Code Playgroud)

注册功能:

    public static void AddStringTrimmingProvider(this MvcOptions option)
    {
        var binderToFind = option.ModelBinderProviders
            .FirstOrDefault(x => x.GetType() == typeof(SimpleTypeModelBinderProvider));

        if (binderToFind == null)
        {
            return;
        }

        var index = option.ModelBinderProviders.IndexOf(binderToFind);
        option.ModelBinderProviders.Insert(index, new TrimmingModelBinderProvider());
    }
Run Code Online (Sandbox Code Playgroud)

寄存器:

service.AddMvc(option => option.AddStringTrimmingProvider())
Run Code Online (Sandbox Code Playgroud)