Лучший способ обрезать строки после ввода данных. Должен ли я создать пользовательскую модель связующего?


Я использую ASP.NET MVC и я хотели бы, чтобы все введенные пользователем строковые поля были обрезаны до их вставки в базу данных. И поскольку у меня есть много форм ввода данных, я ищу элегантный способ обрезать все строки вместо явной обрезки каждого предоставленного пользователем строкового значения. Мне интересно знать, как и когда люди обрезают строки.

Я думал о том, чтобы, возможно, создать пользовательскую модель связующего и обрезать любые строковые значения там...таким образом, все мои обрезки логика содержится в одном месте. Это хороший подход? Есть ли примеры кода, которые делают это?

13 159

13 ответов:

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

настройки глобальных.событие Asax Application_Start.

это @takepara такое же разрешение, но как IModelBinder вместо DefaultModelBinder, чтобы добавить modelbinder в global.асакс прошел

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 post: 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].

с улучшениями в C# 6 Теперь вы можете написать очень компактную модель binder, которая будет обрезать все строковые входы:

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

вы должны включить эту строку где-то в Application_Start() в своем Global.asax.cs файл для использования модели binder при привязке strings:

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

Я считаю, что лучше использовать связыватель модели, как это, а не переопределять связыватель модели по умолчанию, потому что тогда он будет использоваться всякий раз, когда вы связываете string, является ли это непосредственно как метод аргумент или как свойство класса модели. Однако, если вы переопределите привязку модели по умолчанию, как предлагают другие ответы здесь, это будет только работа при связывании свойств на моделях,не когда у вас есть string в качестве аргумента в метод действия

Edit: комментатор спросил о работе с ситуацией, когда поле не должно быть проверено. Мой первоначальный ответ был сведен к рассмотрению только вопроса, поставленного 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) я предпочитаю механизм атрибута opt-in "StringTrim" (а не пример opt-out "NoTrim" @Anton).

2) дополнительный вызов SetModelValue требуется для обеспечения правильного заполнения ModelState и шаблон проверки/принятия/отклонения по умолчанию может использоваться как обычный, т. е. TryUpdateModel(модель) для применения и ModelState.Очистить (), чтобы принять все изменения.

ставим это в вашей сущности / общей библиотеке:

/// <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! Это связано с тем, что вы зарегистрированы как привязка всех типов строк, поэтому кажется (в моем тестировании), что связыватель по умолчанию не будет делать это за вас.

In ASP.Net Ядро 2 это работает для меня. Я использую [FromBody] атрибут в моих контроллерах и вход JSON. Чтобы переопределить обработку строк в десериализации 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();
    }
}

дополнительная информация для тех, кто ищет, как это сделать в ASP.NET ядро 1.0. Логика сильно изменилась.

я писал в блоге о том, как это сделать, он объясняет вещи в немного более подробно

Итак ASP.NET ядро 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());
 });

читая отличные ответы и комментарии выше, и все больше запутываясь, я вдруг подумал: Эй, интересно, есть ли решение jQuery. Поэтому для других, кто, как и я, находит ModelBinders немного сбивающим с толку, я предлагаю следующий фрагмент jQuery, который обрезает поля ввода перед отправкой формы.

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

в случае ядра MVC

Binder:

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 ядро заменить 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;
    }
}

уродливая часть этого является копией и вставкой GetBinder логику ComplexTypeModelBinderProvider, но там, кажется, нет никакого крючка, чтобы позволить вам избежать этого.

Я не согласен с решением. Вы должны переопределить 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 если вы действительно заинтересованы только в строковых значениях, но это не должно иметь значения, потому что все, что приходит в основном строка.

поздно на вечеринку, но ниже приводится краткое изложение корректировок, необходимых для MVC 5.2.3, если вы должны обрабатывать skipValidation требование поставщиков встроенных значений.

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

глобальные.асакс

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

было много сообщений, предлагающих атрибутивный подход. Вот пакет, который уже имеет атрибут trim и многие другие:Дадо.ComponentModel.Мутации или NuGet

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 (), пользователь.Имя пользователя будет изменено на m@x_speed.01!.

этот пример будет обрезать пробелы и регистр строки в нижнем регистре. Он не вводит проверку, но System.ComponentModel.Annotations может использоваться вместе с Dado.ComponentModel.Mutations.