La mejor forma de cortar cuerdas después de la entrada de datos. ¿Debo crear un archivador de modelo personalizado?

Estoy usando ASP.NET MVC y me gustaría que todos los campos de cadena ingresados ​​por el usuario sean recortados antes de que se inserten en la base de datos. Y dado que tengo muchos formularios de entrada de datos, estoy buscando una forma elegante de recortar todas las cadenas en lugar de recortar explícitamente cada valor de cadena proporcionado por el usuario. Me interesa saber cómo y cuándo las personas están recortando cadenas.

Pensé en tal vez crear un archivador de modelo personalizado y recortar cualquier valor de cadena allí … de esa manera, toda mi lógica de recorte está contenida en un solo lugar. ¿Es este un buen enfoque? ¿Hay ejemplos de código que hacen esto?

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

¿Qué tal este código?

 ModelBinders.Binders.DefaultBinder = new TrimModelBinder(); 

Establezca el evento global.asax Application_Start.

Esta es @takepara la misma resolución, pero como un IModelBinder en lugar de DefaultModelBinder, por lo que la adición del encuadernador en global.asax es a través de

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

La clase:

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

basado en la publicación de @haacked: http://haacked.com/archive/2011/03/19/fixing-binding-to-decimals.aspx

Una mejora para @takepara responder.

Algunos estaban en proyecto:

 public class NoTrimAttribute : Attribute { } 

En el cambio de clase TrimModelBinder

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

a

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

y puede marcar las propiedades que se excluirán del recorte con el atributo [NoTrim].

Con las mejoras en C # 6, ahora puede escribir un archivador modelo muy compacto que recortará todas las entradas de cadena:

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

Debe incluir esta línea en algún lugar de Application_Start() en su archivo Global.asax.cs para usar el enlace de modelo al enlazar string s:

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

Encuentro que es mejor usar un archivador de modelo como este, en lugar de anular el archivador de modelo predeterminado, porque luego se usará siempre que esté vinculando una string , ya sea directamente como argumento de método o como propiedad en una clase de modelo. Sin embargo, si reemplaza la carpeta de modelo predeterminada como sugieren otras respuestas aquí, eso solo funcionará cuando se vinculen propiedades en modelos, no cuando se tiene una string como argumento para un método de acción

Editar: un comentarista preguntó acerca de cómo lidiar con la situación cuando un campo no debe ser validado. Mi respuesta original se redujo a tratar solo con la pregunta que el PO había planteado, pero para aquellos que estén interesados, puede lidiar con la validación mediante el uso del siguiente enlace de modelo extendido:

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

Otra variante de la respuesta de @parapara, pero con un giro diferente:

1) Prefiero el mecanismo de atributo opcional “StringTrim” (en lugar del ejemplo de exclusión voluntaria “NoTrim” de @Anton).

2) Se requiere una llamada adicional a SetModelValue para asegurar que ModelState se rellena correctamente y el patrón de validación / aceptación / rechazo predeterminado se puede usar como normal, es decir, TryUpdateModel (modelo) para aplicar y ModelState.Clear () para aceptar todos los cambios.

Pon esto en tu entidad / biblioteca compartida:

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

Luego esto en tu aplicación MVC / biblioteca:

 ///  /// MVC model binder which trims string values decorated with the . ///  public class StringTrimModelBinder : IModelBinder { ///  /// Binds the model, applying trimming when required. ///  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().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; } } 

Si no establece el valor de la propiedad en la carpeta, incluso cuando no quiera cambiar nada, ¡bloqueará esa propiedad de ModelState por completo! Esto se debe a que está registrado como vinculante para todos los tipos de cadena, por lo que parece (en mi prueba) que el encuadernador predeterminado no lo hará por usted en ese momento.

Información adicional para cualquier persona que busque cómo hacer esto en ASP.NET Core 1.0. La lógica ha cambiado bastante.

Escribí una publicación de blog sobre cómo hacerlo , explica las cosas en un poco más detallado

Así que la solución ASP.NET Core 1.0:

Carpeta de modelo para hacer el recorte real

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

También necesita el Proveedor de carpetas de modelo en la versión más reciente, esto indica que se debe usar esta carpeta para este modelo

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

Luego debe registrarse en Startup.cs

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

En ASP.Net Core 2, esto funcionó para mí. Estoy usando el atributo [FromBody] en mis controladores y la entrada JSON. Para anular el manejo de cadenas en la deserialización JSON, registré mi propio JsonConverter:

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

Y este es el convertidor:

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

Mientras leía las excelentes respuestas y los comentarios anteriores, y se volvía cada vez más confundido, de repente pensé, hey, me pregunto si hay una solución jQuery. Entonces, para otros que, como yo, encuentran que ModelBinders es un poco desconcertante, ofrezco el siguiente fragmento de jQuery que recorta los campos de entrada antes de que se envíe el formulario.

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

En caso de MVC Core

Aglutinante:

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

Proveedor:

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

Función de registro:

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

Registro:

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

No estoy de acuerdo con la solución. Debe anular GetPropertyValue porque los datos de SetProperty también podrían llenarse con ModelState. Para capturar los datos brutos de los elementos de entrada, escriba esto:

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

Filtra por PropertyDescriptor PropertyType si realmente solo te interesan los valores de cadena, pero no debería importar porque todo lo que entra es básicamente una cadena.

Para ASP.NET Core , reemplace ComplexTypeModelBinderProvider con un proveedor que recorta cadenas.

En su método de inicio ConfigureServices , agregue esto:

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

Defina TrimmingModelBinderProvider como este:

 ///  /// Used in place of  to trim beginning and ending whitespace from user input. ///  class TrimmingModelBinderProvider : IModelBinderProvider { class TrimmingModelBinder : ComplexTypeModelBinder { public TrimmingModelBinder(IDictionary 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(); 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; } } 

La parte fea de esto es copiar y pegar de la lógica de GetBinder de ComplexTypeModelBinderProvider , pero no parece haber ningún gancho que te permita evitarlo.

Tarde para la fiesta, pero el siguiente es un resumen de los ajustes necesarios para MVC 5.2.3 si debe manejar el requisito de skipValidation de los proveedores de valor 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 (eg [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()); ... } 

Ha habido muchos mensajes que sugieren un enfoque de atributos. Aquí hay un paquete que ya tiene un atributo de recorte y muchos otros: Dado.ComponentModel.Mutations o 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(user).Mutate(); 

Después de la llamada a Mutate (), user.UserName se m@x_speed.01! a m@x_speed.01! .

Este ejemplo recortará el espacio en blanco y colocará la cadena en minúsculas. No introduce validación, pero System.ComponentModel.Annotations se puede usar junto con Dado.ComponentModel.Mutations .