web-dev-qa-db-fra.com

Classeur de modèles DateTime personnalisé dans Asp.net MVC

Je voudrais écrire mon propre classeur de modèle pour le type DateTime. Tout d'abord, j'aimerais écrire un nouvel attribut que je peux attacher à la propriété de mon modèle, tel que:

[DateTimeFormat("d.M.yyyy")]
public DateTime Birth { get; set,}

C'est la partie facile. Mais la partie reliure est un peu plus difficile. J'aimerais ajouter un nouveau classeur pour le type DateTime. Je peux soit

  • implémenter l'interface IModelBinder et écrire ma propre méthode BindModel()
  • hériter de DefaultModelBinder et surcharger la méthode BindModel()

Mon modèle a une propriété comme ci-dessus (Birth). Ainsi, lorsque le modèle tente de lier les données de requête à cette propriété, la fonction BindModel(controllerContext, bindingContext) de mon classeur de modèle est appelée. Tout va bien, mais. Comment obtenir les attributs de propriété de controller/bindingContext pour analyser ma date correctement ? Comment puis-je accéder à la PropertyDesciptor de la propriété Birth?

Modifier

En raison de la séparation des préoccupations, ma classe de modèle est définie dans un assembly qui ne fait pas (et ne devrait pas) référencer System.Web.MVC Assembly. La définition d'attaches personnalisées (similaire à l'exemple de Scott Hanselman ) n'est pas permise ici.

22
Robert Koritnik

Je ne pense pas que vous devriez mettre des attributs spécifiques aux paramètres régionaux sur un modèle.

Deux autres solutions possibles à ce problème sont:

  • Demandez à vos pages de translittérer les dates du format spécifique aux paramètres régionaux vers un format générique tel que aaaa-mm-jj en JavaScript. (Fonctionne, mais nécessite JavaScript.)
  • Rédigez un classeur de modèle prenant en compte la culture d'interface utilisateur actuelle lors de l'analyse des dates.

Pour répondre à votre question, la manière d'obtenir des attributs personnalisés (pour MVC 2) consiste à écrire un AssociatedMetadataProvider .

3
Craig Stuntz

vous pouvez modifier le classeur de modèle par défaut pour utiliser la culture utilisateur à l'aide de IModelBinder

public class DateTimeBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        bindingContext.ModelState.SetModelValue(bindingContext.ModelName, value);

        return value.ConvertTo(typeof(DateTime), CultureInfo.CurrentCulture);
    }
}

public class NullableDateTimeBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        bindingContext.ModelState.SetModelValue(bindingContext.ModelName, value);

        return value == null
            ? null 
            : value.ConvertTo(typeof(DateTime), CultureInfo.CurrentCulture);
    }
}

Et dans Global.Asax, ajoutez ce qui suit à Application_Start (): 

ModelBinders.Binders.Add(typeof(DateTime), new DateTimeBinder());
ModelBinders.Binders.Add(typeof(DateTime?), new NullableDateTimeBinder());

Plus d’informations sur cet excellent blog qui décrivent pourquoi l’équipe de framework Mvc a implémenté une culture par défaut pour tous les utilisateurs.

80
gdoron

J'ai moi-même eu ce très gros problème et après des heures d'essais et d'échecs, j'ai obtenu une solution efficace, comme vous l'avez demandé.

Tout d'abord, puisqu'il n'est pas possible d'avoir un liant sur une propriété, vous devez implémenter un ModelBinder complet. Puisque vous ne voulez pas que toutes les propriétés simples soient liées, vous pouvez hériter de DefaultModelBinder uniquement de la propriété qui vous tient à cœur, puis lier la propriété unique:

public class DateFiexedCultureModelBinder : DefaultModelBinder
{
    protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
    {
        if (propertyDescriptor.PropertyType == typeof(DateTime?))
        {
            try
            {
                var model = bindingContext.Model;
                PropertyInfo property = model.GetType().GetProperty(propertyDescriptor.Name);

                var value = bindingContext.ValueProvider.GetValue(propertyDescriptor.Name);

                if (value != null)
                {
                    System.Globalization.CultureInfo cultureinfo = new System.Globalization.CultureInfo("it-CH");
                    var date = DateTime.Parse(value.AttemptedValue, cultureinfo);
                    property.SetValue(model, date, null);
                }
            }
            catch
            {
                //If something wrong, validation should take care
            }
        }
        else
        {
            base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
        }
    }
}

Dans mon exemple, je pars dans une culture fiexée, mais ce que vous voulez faire est possible. Vous devez créer un CustomAttribute (comme DateTimeFormatAttribute) et le placer sur votre propriété:

[DateTimeFormat("d.M.yyyy")]
public DateTime Birth { get; set,}

Désormais, dans la méthode BindProperty, au lieu de rechercher une propriété DateTime, vous pouvez rechercher une propriété avec votre DateTimeFormatAttribute, saisir le format que vous avez spécifié dans le constructeur, puis analyser la date avec DateTime.ParseExact 

J'espère que cela aide, il m'a fallu très longtemps pour arriver avec cette solution. Il était en fait facile d’avoir cette solution une fois que je savais comment la rechercher :(

14
Davide Vosti

Vous pouvez implémenter un classeur DateTime personnalisé comme ceci, mais vous devez prendre en compte la culture et la valeur supposées de la demande du client. Pouvez-vous obtenir une date comme mm/jj/aaaa en en-US et que vous souhaitiez la convertir dans la culture système en-GB (ce qui serait comme jj/mm/aaaa) ou une culture invariante, comme nous, alors il faut l’analyser avant et utiliser la façade statique Convert pour la changer dans son comportement.

    public class DateTimeModelBinder : IModelBinder
    {
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            var valueResult = bindingContext.ValueProvider
                              .GetValue(bindingContext.ModelName);
            var modelState = new ModelState {Value = valueResult};

            var resDateTime = new DateTime();

            if (valueResult == null) return null;

            if ((bindingContext.ModelType == typeof(DateTime)|| 
                bindingContext.ModelType == typeof(DateTime?)))
            {
                if (bindingContext.ModelName != "Version")
                {
                    try
                    {
                        resDateTime =
                            Convert.ToDateTime(
                                DateTime.Parse(valueResult.AttemptedValue, valueResult.Culture,
                                    DateTimeStyles.AdjustToUniversal).ToUniversalTime(), CultureInfo.InvariantCulture);
                    }
                    catch (Exception e)
                    {
                        modelState.Errors.Add(EnterpriseLibraryHelper.HandleDataLayerException(e));
                    }
                }
                else
                {
                    resDateTime =
                        Convert.ToDateTime(
                            DateTime.Parse(valueResult.AttemptedValue, valueResult.Culture), CultureInfo.InvariantCulture);
                }
            }
            bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
            return resDateTime;
        }
    }

Quoi qu'il en soit, la culture dépend de l'analyse DateTime dans une application sans état peut être cruelle ... surtout lorsque vous travaillez avec JSON sur des clients javascript et inversement. 

0
Phil