簡體   English   中英

數據輸入后修剪字符串的最佳方法。 我應該創建自定義模型綁定器嗎?

[英]Best way to trim strings after data entry. Should I create a custom model binder?

我正在使用 ASP.NET MVC,我希望在將所有用戶輸入的字符串字段插入數據庫之前對其進行修剪。 而且由於我有許多數據輸入表單,我正在尋找一種優雅的方式來修剪所有字符串,而不是顯式地修剪每個用戶提供的字符串值。 我很想知道人們如何以及何時修剪字符串。

我考慮過可能創建一個自定義模型綁定器並在那里修剪任何字符串值......這樣,我所有的修剪邏輯都包含在一個地方。 這是一個好方法嗎? 是否有任何代碼示例可以做到這一點?

  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);
    }
  }

這段代碼怎么樣?

ModelBinders.Binders.DefaultBinder = new TrimModelBinder();

設置 global.asax Application_Start 事件。

這是 @takepara 相同的分辨率,但作為 IModelBinder 而不是 DefaultModelBinder 以便在 global.asax 中添加模型綁定器是通過

ModelBinders.Binders.Add(typeof(string),new TrimModelBinder());

班上:

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();
    }
}

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

@takepara 答案的一項改進。

有人在項目中:

public class NoTrimAttribute : Attribute { }

在 TrimModelBinder 類更改

if (propertyDescriptor.PropertyType == typeof(string))

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

並且您可以使用 [NoTrim] 屬性標記要從修剪中排除的屬性。

ASP.Net Core 2中,這對我有用。 我在控制器和 JSON 輸入中使用[FromBody]屬性。 為了覆蓋 JSON 反序列化中的字符串處理,我注冊了自己的 JsonConverter:

services.AddMvcCore()
    .AddJsonOptions(options =>
        {
            options.SerializerSettings.Converters.Insert(0, new TrimmingStringConverter());
        })

這是轉換器:

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();
    }
}

隨着 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();
    }
}

您需要在Global.asax.cs文件的Application_Start()中的某處包含此行,以便在綁定string s 時使用模型綁定器:

ModelBinders.Binders.Add(typeof(string), new TrimStringModelBinder());

我發現最好使用這樣的模型綁定器,而不是覆蓋默認模型綁定器,因為這樣無論何時綁定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();
    }
}

@takepara 答案的另一個變體,但有不同的轉折:

1)我更喜歡選擇加入“StringTrim”屬性機制(而不是@Anton 的選擇退出“NoTrim”示例)。

2) 需要額外調用 SetModelValue 以確保正確填充 ModelState,並且可以正常使用默認驗證/接受/拒絕模式,即 TryUpdateModel(model) 應用和 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
{
}

然后在您的 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;
    }
}

如果您沒有在活頁夾中設置屬性值,即使您不想更改任何內容,您也會從 ModelState 中完全阻止該屬性! 這是因為您已注冊為綁定所有字符串類型,因此(在我的測試中)默認綁定器不會為您執行此操作。

對於在 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);
    }
}

您還需要最新版本的模型綁定器提供程序,這告訴該綁定器是否應該用於此模型

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;
    }
}

然后它必須在 Startup.cs 中注冊

 services.AddMvc().AddMvcOptions(options => {  
       options.ModelBinderProviders.Insert(0, new TrimmingModelBinderProvider());
 });

在 MVC 核心的情況下

粘合劑:

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);
    }
}

提供者:

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;
    }
}

注冊功能:

    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());
    }

登記:

service.AddMvc(option => option.AddStringTrimmingProvider())

我創建了值提供程序來修剪查詢字符串參數值和表單值。 這已使用 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;
    }
}

然后在 Startup.cs 的ConfigureServices()函數中注冊值提供者工廠

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();
});

看了上面優秀的答案和評論,越看越糊塗,突然想,哎,不知道有沒有jQuery的解決方案。 因此,對於像我一樣覺得 ModelBinders 有點令人困惑的其他人,我提供了以下 jQuery 片段,它在表單提交之前修剪輸入字段。

    $('form').submit(function () {
        $(this).find('input:text').each(function () {
            $(this).val($.trim($(this).val()));
        })
    });

更新:對於最新版本的 ASP.NET Core,此答案已過時。 請改用Bassem 的答案


對於ASP.NET Core ,將ComplexTypeModelBinderProvider替換為修剪字符串的提供程序。

在您的啟動代碼ConfigureServices方法中,添加以下內容:

services.AddMvc()
    .AddMvcOptions(s => {
        s.ModelBinderProviders[s.ModelBinderProviders.TakeWhile(p => !(p is ComplexTypeModelBinderProvider)).Count()] = new TrimmingModelBinderProvider();
    })

像這樣定義TrimmingModelBinderProvider

/// <summary>
/// Used in place of <see cref="ComplexTypeModelBinderProvider"/> to trim beginning and ending whitespace from user input.
/// </summary>
class TrimmingModelBinderProvider : IModelBinderProvider
{
    class TrimmingModelBinder : ComplexTypeModelBinder
    {
        public TrimmingModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders) : base(propertyBinders) { }

        protected override void SetProperty(ModelBindingContext bindingContext, string modelName, ModelMetadata propertyMetadata, ModelBindingResult result)
        {
            var value = result.Model as string;
            if (value != null)
                result = ModelBindingResult.Success(value.Trim());
            base.SetProperty(bindingContext, modelName, propertyMetadata, result);
        }
    }

    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.IsComplexType && !context.Metadata.IsCollectionType) {
            var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
            for (var i = 0; i < context.Metadata.Properties.Count; i++) {
                var property = context.Metadata.Properties[i];
                propertyBinders.Add(property, context.CreateBinder(property));
            }
            return new TrimmingModelBinder(propertyBinders);
        }
        return null;
    }
}

丑陋的部分是從ComplexTypeModelBinderProvider復制和粘貼GetBinder邏輯,但似乎沒有任何掛鈎可以讓您避免這種情況。

遲到了,但如果您要處理內置值提供程序的skipValidation要求,以下是 MVC 5.2.3 所需調整的摘要。

public class TrimStringModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        // First check if request validation is required
        var shouldPerformRequestValidation = controllerContext.Controller.ValidateRequest && 
            bindingContext.ModelMetadata.RequestValidationEnabled;

        // determine if the value provider is IUnvalidatedValueProvider, if it is, pass in the 
        // flag to perform request validation (e.g. [AllowHtml] is set on the property)
        var unvalidatedProvider = bindingContext.ValueProvider as IUnvalidatedValueProvider;

        var valueProviderResult = unvalidatedProvider?.GetValue(bindingContext.ModelName, !shouldPerformRequestValidation) ??
            bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

        return valueProviderResult?.AttemptedValue?.Trim();
    }
}

全球.asax

    protected void Application_Start()
    {
        ...
        ModelBinders.Binders.Add(typeof(string), new TrimStringModelBinder());
        ...
    }

我不同意這個解決方案。 您應該覆蓋 GetPropertyValue,因為 SetProperty 的數據也可以由 ModelState 填充。 要從輸入元素中捕獲原始數據,請編寫以下代碼:

 public class CustomModelBinder : System.Web.Mvc.DefaultModelBinder
{
    protected override object GetPropertyValue(System.Web.Mvc.ControllerContext controllerContext, System.Web.Mvc.ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, System.Web.Mvc.IModelBinder propertyBinder)
    {
        object value = base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);

        string retval = value as string;

        return string.IsNullOrWhiteSpace(retval)
                   ? value
                   : retval.Trim();
    }

}

如果您真的只對字符串值感興趣,請按 propertyDescriptor PropertyType 過濾,但這無關緊要,因為進來的所有內容基本上都是字符串。

有很多帖子建議使用屬性方法。 這是一個已經具有 trim 屬性和許多其他屬性的包: Dado.ComponentModel.MutationsNuGet

public partial class ApplicationUser
{
    [Trim, ToLower]
    public virtual string UserName { get; set; }
}

// Then to preform mutation
var user = new ApplicationUser() {
    UserName = "   M@X_speed.01! "
}

new MutationContext<ApplicationUser>(user).Mutate();

調用 Mutate() 后,user.UserName 將被變異為m@x_speed.01! .

此示例將修剪空格並將字符串大小寫為小寫。 它沒有引入驗證,但System.ComponentModel.Annotations可以與Dado.ComponentModel.Mutations一起使用。

我在另一個線程中發布了這個。 在 asp.net core 2 中,我走向了不同的方向。 我使用了一個動作過濾器。 在這種情況下,開發人員可以對其進行全局設置,也可以將其用作他/她想要應用字符串修剪的操作的屬性。 此代碼在模型綁定發生后運行,它可以更新模型對象中的值。

這是我的代碼,首先創建一個動作過濾器:

public class TrimInputStringsAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        foreach (var arg in context.ActionArguments)
        {
            if (arg.Value is string)
            {
                string val = arg.Value as string;
                if (!string.IsNullOrEmpty(val))
                {
                    context.ActionArguments[arg.Key] = val.Trim();
                }

                continue;
            }

            Type argType = arg.Value.GetType();
            if (!argType.IsClass)
            {
                continue;
            }

            TrimAllStringsInObject(arg.Value, argType);
        }
    }

    private void TrimAllStringsInObject(object arg, Type argType)
    {
        var stringProperties = argType.GetProperties()
                                      .Where(p => p.PropertyType == typeof(string));

        foreach (var stringProperty in stringProperties)
        {
            string currentValue = stringProperty.GetValue(arg, null) as string;
            if (!string.IsNullOrEmpty(currentValue))
            {
                stringProperty.SetValue(arg, currentValue.Trim(), null);
            }
        }
    }
}

要使用它,要么注冊為全局過濾器,要么使用 TrimInputStrings 屬性裝飾您的操作。

[TrimInputStrings]
public IActionResult Register(RegisterViewModel registerModel)
{
    // Some business logic...
    return Ok();
}

好的,我有這個東西,它有點工作:

class TrimmingModelBinder : IModelBinder
{
  public Task BindModelAsync (ModelBindingContext ctx)
  {
  if
  (
    ctx .ModelName is string name
    && ctx .ValueProvider .GetValue (name) .FirstValue is string v)
  ctx .ModelState .SetModelValue
  (
    name,
    new ValueProviderResult
    ((ctx .Result = ModelBindingResult .Success (v .Trim ())) .Model as string));
  return Task .CompletedTask; }}

class AutoTrimAttribute : ModelBinderAttribute
{
  public AutoTrimAttribute ()
  { this .BinderType = typeof (TrimmingModelBinder); }}

遺憾的是,這沒有標准功能。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM