Лучший способ обрезать строки после ввода данных. Должен ли я создать пользовательскую модель связующего?
Я использую ASP.NET MVC, и я хотел бы, чтобы все пользовательские строковые поля были обрезаны до того, как они были вставлены в базу данных. И поскольку у меня много форм ввода данных, я ищу элегантный способ обрезать все строки, а не явно подрезать каждое введенное пользователем значение строки. Мне интересно знать, как и когда люди обрезают строки.
Я подумал о том, чтобы создать настраиваемое связующее устройство и обрезать любые строковые значения... таким образом, вся моя логика обрезки содержится в одном месте. Это хороший подход? Существуют ли какие-либо образцы кода, которые это делают?
Ответы
Ответ 1
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.
Ответ 2
Это @takepara же разрешение, но как IModelBinder вместо DefaultModelBinder, так что добавление modelbinder в 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
Ответ 3
Одно улучшение ответа @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].
Ответ 4
С улучшениями в С# 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
:
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();
}
}
Ответ 5
В 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();
}
}
Ответ 6
Другой вариант ответа @takepara, но с другим завихрением:
1) Я предпочитаю использовать механизм атрибута "StringTrim" (а не исключить "NoTrim" пример @Anton).
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! Это связано с тем, что вы зарегистрированы как связывающие все типы строк, поэтому он (в моем тестировании) указывает, что связующее по умолчанию не сделает этого для вас.
Ответ 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);
}
}
Также вам нужна модель 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());
});
Ответ 8
Прочитав превосходные ответы и комментарии выше, и все больше смущаясь, я вдруг подумал, эй, интересно, есть ли решение jQuery. Поэтому для других, которые, как и я, находят ModelBinders немного недоумением, я предлагаю следующий фрагмент jQuery, который обрезает поля ввода перед отправкой формы.
$('form').submit(function () {
$(this).find('input:text').each(function () {
$(this).val($.trim($(this).val()));
})
});
Ответ 9
В случае ядра 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);
}
}
Provider:
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())
Ответ 10
Позднее стороне, но ниже приводится сводка настроек, необходимых для 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());
...
}
Ответ 11
Я не согласен с решением.
Вы должны переопределить 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, если вы действительно интересуетесь только строковыми значениями, но это не должно иметь значения, потому что все, что приходит, в основном представляет собой строку.
Ответ 12
Для 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;
}
}
Уродливой частью этого является копирование и вставка логики GetBinder
из ComplexTypeModelBinderProvider
, но, похоже, нет никакого крючка, чтобы вы могли избежать этого.
Ответ 13
Было много постов, предлагающих атрибутивный подход. Вот пакет, который уже имеет атрибут обрезки и многие другие: Dado.ComponentModel.Mutations или NuGet
public partial class ApplicationUser
{
[Trim, ToLower]
public virtual string UserName { get; set; }
}
// Then to preform mutation
var user = new ApplicationUser() {
UserName = " [email protected]_speed.01! "
}
new MutationContext<ApplicationUser>(user).Mutate();
После вызова Mutate() user.UserName будет преобразовано в [email protected]_speed.01!
.
Этот пример урезает пробелы и переводит строку в нижний регистр. Он не вводит валидацию, но System.ComponentModel.Annotations
может использоваться вместе с Dado.ComponentModel.Mutations
.
Ответ 14
Я разместил это в другой теме. В 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();
}