asp.net - mvc - La mejor forma de cortar cuerdas después de la entrada de datos. ¿Debo crear un archivador de modelo personalizado?
model binding mvc 5 (13)
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 anula 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.
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?
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();
}
}
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
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<ApplicationUser>(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
.
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());
});
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()));
})
});
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.
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:
/// <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
{
}
Luego esto en tu aplicación MVC / biblioteca:
/// <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;
}
}
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.
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:
/// <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;
}
}
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 (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());
...
}
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<object>().Any(a => a.GetType() == typeof(NoTrimAttribute)))
y puede marcar las propiedades que se excluirán del recorte con el atributo [NoTrim].
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())
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.