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

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

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

Ответы [ 13 ]

209 голосов
/ 14 ноября 2009
  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.

76 голосов
/ 29 мая 2011

Это @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

41 голосов
/ 30 мая 2011

Одно улучшение @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].

16 голосов
/ 17 февраля 2016

С улучшениями в 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();
    }
}

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

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

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

Редактировать: комментатор спросил о ситуации, когда поле не должно быть проверено. Мой первоначальный ответ был сведен только к вопросу, поставленному ОП, но для тех, кто заинтересован, вы можете справиться с проверкой, используя следующую расширенную модель связующего:

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();
    }
}
14 голосов
/ 28 марта 2018

В ASP.Net Core 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();
    }
}
12 голосов
/ 03 декабря 2013

Другой вариант ответа @ takepara, но с другим поворотом:

1) Я предпочитаю использовать механизм атрибутов «StringTrim» (а не пример «NoTrim» @Anton).

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

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

7 голосов
/ 22 августа 2015

Дополнительная информация для тех, кто ищет, как это сделать в 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);
    }
}

Также вам необходим 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;
    }
}

Тогда он должен быть зарегистрирован в Startup.cs

 services.AddMvc().AddMvcOptions(options => {  
       options.ModelBinderProviders.Insert(0, new TrimmingModelBinderProvider());
 });
5 голосов
/ 30 ноября 2017

В случае ядра 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())
5 голосов
/ 03 августа 2014

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

    $('form').submit(function () {
        $(this).find('input:text').each(function () {
            $(this).val($.trim($(this).val()));
        })
    });
2 голосов
/ 18 ноября 2017

Поздно для вечеринки, но ниже приводится сводная информация о корректировках, необходимых для 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();
    }
}

Global.asax

    protected void Application_Start()
    {
        ...
        ModelBinders.Binders.Add(typeof(string), new TrimStringModelBinder());
        ...
    }
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...