web-dev-qa-db-fra.com

Meilleure façon de couper les chaînes après la saisie des données. Devrais-je créer un classeur de modèle personnalisé?

J'utilise ASP.NET MVC et j'aimerais que tous les champs de chaîne saisis par l'utilisateur soient supprimés avant leur insertion dans la base de données. Et comme j'ai beaucoup de formulaires de saisie de données, je cherche un moyen élégant de couper toutes les chaînes au lieu de couper explicitement chaque valeur de chaîne fournie par l'utilisateur. Je suis intéressé de savoir comment et quand les gens coupent les chaînes.

Je pensais peut-être à la création d'un classeur de modèle personnalisé et au rognage des valeurs de chaîne ... De cette façon, toute ma logique de rognage est contenue à un seul endroit. Est-ce une bonne approche? Y at-il des exemples de code qui font cela?

162
Johnny Oshika
  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);
    }
  }

Que diriez-vous de ce code?

ModelBinders.Binders.DefaultBinder = new TrimModelBinder();

Définissez l'événement global.asax Application_Start.

207
takepara

@Takepara est la même résolution mais en tant qu’IModelBinder au lieu de DefaultModelBinder, de sorte que l’ajout du modelbinder dans global.asax se fait par

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

La classe:

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

basé sur @haacked post: http://haacked.com/archive/2011/03/19/fixing-binding-to-decimals.aspx

76
Korayem

Une amélioration à la réponse @takepara.

Quelque chose en projet:

public class NoTrimAttribute : Attribute { }

Dans TrimModelBinder, changement de classe

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

à

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

et vous pouvez marquer les propriétés à exclure du découpage avec l'attribut [NoTrim].

41
Anton

Grâce aux améliorations apportées à C # 6, vous pouvez désormais écrire un classeur de modèle très compact qui coupera toutes les entrées de chaîne:

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

Vous devez inclure cette ligne quelque part dans Application_Start() dans votre fichier Global.asax.cs pour utiliser le classeur de modèle lors de la liaison de strings:

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

Je trouve qu'il est préférable d'utiliser un classeur de ce type, plutôt que de remplacer le classeur par défaut, car il sera utilisé chaque fois que vous liez une string, que ce soit directement en tant qu'argument de méthode ou en tant que propriété d'une classe de modèle. Toutefois, si vous remplacez le classeur de modèle par défaut, comme le suggèrent d’autres réponses, uniquement fonctionnera lors de la liaison de propriétés sur des modèles, not lorsque vous avez un string comme argument d’une méthode d’action

Éditer: un intervenant a demandé comment gérer la situation lorsqu'un champ ne devrait pas être validé. Ma réponse initiale était réduite à la question posée par le PO, mais pour ceux qui sont intéressés, vous pouvez traiter la validation en utilisant le classeur de modèle étendu suivant:

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

Dans ASP.Net Core 2 cela a fonctionné pour moi. J'utilise l'attribut [FromBody] dans mes contrôleurs et entrée JSON. Pour redéfinir le traitement des chaînes dans la désérialisation JSON, j'ai enregistré mon propre JsonConverter:

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

Et voici le convertisseur:

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
Kai G

Une autre variante de la réponse de @ takepara mais avec une touche différente:

1) Je préfère le mécanisme d'attribut optionnel "StringTrim" (plutôt que l'exemple opt-out "NoTrim" de @Anton).

2) Un appel supplémentaire à SetModelValue est requis pour s’assurer que ModelState est correctement rempli et que le modèle de validation/accepter/rejet par défaut peut être utilisé normalement, c’est-à-dire TryUpdateModel (modèle) à appliquer et ModelState.Clear () à accepter toutes les modifications.

Mettez ceci dans votre entité/bibliothèque partagée:

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

Ensuite ceci dans votre application/bibliothèque 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;
    }
}

Si vous ne définissez pas la valeur de la propriété dans le classeur, même si vous ne souhaitez rien modifier, vous bloquerez cette propriété de ModelState! Ceci est dû au fait que vous êtes enregistré en tant que liaison de tous les types de chaîne. Il est donc apparu (dans mes tests) que le classeur par défaut ne le ferait pas pour vous ensuite.

10
Tony Wall

Informations supplémentaires pour les personnes cherchant comment procéder dans ASP.NET Core 1.0. La logique a beaucoup changé. 

J'ai écrit un article de blog sur la façon de le faire , il explique les choses un peu plus en détail

Donc, solution ASP.NET Core 1.0:

Modèle de classeur pour faire la taille réelle

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

De plus, vous avez besoin du fournisseur de classeur de modèle dans la dernière version. Cela indique que si ce classeur est utilisé pour ce modèle

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

Ensuite, il doit être enregistré dans Startup.cs

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

En lisant les excellentes réponses et commentaires ci-dessus et en devenant de plus en plus confus, je me suis soudainement dit, hé, je me demande s’il existe une solution jQuery. Ainsi, pour les autres qui, comme moi, trouvent ModelBinders un peu déconcertant, j'offre l'extrait de code jQuery suivant qui réduit les champs de saisie avant que le formulaire ne soit soumis.

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

En cas de MVC Core

Classeur:

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

Fournisseur:

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

Fonction d'enregistrement:

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

Registre:

service.AddMvc(option => option.AddStringTrimmingProvider())
4
Vikash Kumar

Pour ASP.NET Core , remplacez ComplexTypeModelBinderProvider par un fournisseur qui limite les chaînes.

Dans votre méthode de code de démarrage ConfigureServices, ajoutez ceci:

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

Définissez TrimmingModelBinderProvider comme ceci:

/// <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 partie laide de ceci est le copier-coller de la logique GetBinder de ComplexTypeModelBinderProvider, mais il ne semble pas y avoir de crochet pour vous permettre d'éviter cela.

2
Edward Brey

En retard pour la partie, mais voici un résumé des ajustements nécessaires pour MVC 5.2.3 si vous devez gérer l'exigence skipValidation des fournisseurs de valeur intégrés.

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());
        ...
    }
1
Aaron Hudon

Je ne suis pas d'accord avec la solution . Vous devez remplacer GetPropertyValue car les données de SetProperty peuvent également être renseignées par le 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();
    }

}

Filtrez par propriétéDescriptor PropertyType si vous n'êtes vraiment intéressé que par les valeurs de chaîne, mais cela ne devrait pas avoir d'importance, car tout ce qui entre est fondamentalement une chaîne.

1
rudimenter

Il y a eu beaucoup de publications suggérant une approche attributaire. Voici un paquet qui a déjà un attribut trim et bien d’autres: Dado.ComponentModel.Mutations ou 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();

Après l'appel à Mutate (), user.UserName sera muté en m@x_speed.01!.

Cet exemple réduira les espaces et la casse en minuscule. Il n'introduit pas de validation, mais le System.ComponentModel.Annotations peut être utilisé à côté de Dado.ComponentModel.Mutations.

0
roydukkey