web-dev-qa-db-fra.com

Validation correcte avec MVVM

Attention: très long et détaillé post.

OK, validation dans WPF lors de l’utilisation de MVVM. J'ai lu beaucoup de choses à présent, j'ai examiné de nombreuses SO questions et j'ai essayé beaucoup de faire, mais tout semble un peu hacky à un moment donné et je ne sais vraiment pas comment le faire dans le bon sens ™.

Dans l’idéal, je souhaite que toutes les validations soient effectuées dans le modèle de vue à l’aide de IDataErrorInfo ; c’est ce que j’ai fait. Il existe cependant différents aspects qui font que cette solution ne constitue pas une solution complète pour l’ensemble du sujet de validation.

La situation

Prenons la forme simple suivante. Comme vous pouvez le constater, cela n’a rien d’extraordinaire. Nous avons juste deux zones de texte qui se lient à une propriété string et int dans le modèle de vue chacune. De plus, nous avons un bouton qui est lié à une ICommand.

Simple form with only a string and integer input

Donc, pour la validation, nous avons maintenant deux choix:

  1. Nous pouvons exécuter la validation automatiquement chaque fois que la valeur d'une zone de texte change. En tant que tel, l'utilisateur reçoit une réponse instantanée lorsqu'il écrit quelque chose d'invalide.
    • Nous pouvons aller plus loin pour désactiver le bouton en cas d'erreur.
  2. Ou nous pouvons exécuter la validation uniquement explicitement lorsque le bouton est enfoncé, puis afficher toutes les erreurs, le cas échéant. Nous ne pouvons évidemment pas désactiver le bouton en cas d'erreur.

Dans l’idéal, je veux implémenter le choix 1. Pour les liaisons de données normales avec ValidatesOnDataErrors activé), le comportement par défaut est activé. Ainsi, lorsque le texte est modifié, la liaison met à jour la source et déclenche la validation IDataErrorInfo pour cette propriété; vue jusqu'ici tout va bien.

Statut de validation dans le modèle de vue

Le bit intéressant est de laisser le modèle de vue, ou le bouton dans ce cas, savoir s’il ya des erreurs. La manière dont fonctionne IDataErrorInfo est principalement utilisée pour signaler des erreurs à la vue. Ainsi, la vue peut facilement voir s'il y a des erreurs, les afficher et même afficher des annotations à l'aide de Validation.Errors . De plus, la validation se produit toujours en regardant une seule propriété.

Il est donc délicat de savoir si le modèle de vue sait quand il y a des erreurs ou si la validation a réussi. Une solution courante consiste simplement à déclencher la validation IDataErrorInfo pour toutes les propriétés du modèle de vue lui-même. Cela est souvent fait en utilisant une propriété IsValid séparée. L'avantage est que cela peut également être facilement utilisé pour désactiver la commande. L'inconvénient est que cela pourrait exécuter la validation sur toutes les propriétés un peu trop souvent, mais la plupart des validations devraient simplement suffire pour ne pas nuire aux performances. Une autre solution serait de se souvenir des propriétés générant des erreurs lors de l'utilisation de la validation et de ne les vérifier que, mais cela semble un peu trop compliqué et inutile dans la plupart des cas.

L'essentiel est que cela pourrait bien fonctionner. IDataErrorInfo fournit la validation pour toutes les propriétés, et nous pouvons simplement utiliser cette interface dans le modèle de vue lui-même pour y exécuter la validation également pour l'objet entier. Présentant le problème:

Exceptions contraignantes

Le modèle de vue utilise les types réels pour ses propriétés. Ainsi, dans notre exemple, la propriété integer est une valeur réelle int. Cependant, la zone de texte utilisée dans la vue ne prend en charge que texte. Ainsi, lors de la liaison à la variable int dans le modèle de vue, le moteur de liaison de données effectuera automatiquement les conversions de types, ou du moins, il tentera. Si vous pouvez saisir du texte dans une zone de texte destinée à des nombres, il y a de fortes chances pour qu'il n'y ait pas toujours de nombres valides à l'intérieur: le moteur de liaison de données ne parviendra pas à convertir et à lancer une FormatException.

Data binding engine throws an exception and that’s displayed in the view

Du côté de la vue, on peut facilement le voir. Les exceptions du moteur de liaison sont automatiquement capturées par WPF et affichées sous forme d'erreurs. Il n'est même pas nécessaire d'activer Binding.ValidatesOnExceptions qui serait requis pour les exceptions levées dans le sélecteur. Les messages d'erreur ont un texte générique, bien que Cela a donc pu être un problème. Je l’ai résolu moi-même en utilisant un Binding.UpdateSourceExceptionFilter handler), en inspectant l’exception levée, en examinant la propriété source et en générant un message d’erreur moins générique. ma propre extension de balisage de liaison, afin que je puisse avoir tous les paramètres par défaut dont j'ai besoin.

Donc la vue est belle. L'utilisateur commet une erreur, voit un retour d'erreur et peut le corriger. Le modèle de vue cependant est perdu. Comme le moteur de liaison a lancé l'exception, la source n'a jamais été mise à jour. Ainsi, le modèle de vue est toujours sur l’ancienne valeur, ce qui n’est pas ce qui est affiché à l’utilisateur, et la validation IDataErrorInfo ne s’applique évidemment pas.

Pire encore, il n’ya pas de bonne façon pour le modèle de vue de le savoir. Au moins, je n’ai pas encore trouvé de bonne solution. Ce qui serait possible, c’est que la vue rapporte au modèle de la vue qu’une erreur s’est produite. Cela peut être fait en reliant les données Validation.HasError propriété au modèle de vue (ce qui n’est pas possible directement), afin que le modèle de vue puisse vérifier en premier l’état de la vue.

Une autre option consisterait à relayer l’exception gérée dans Binding.UpdateSourceExceptionFilter dans le modèle de vue afin qu’il en soit également informé. Le modèle de vue pourrait même fournir une interface permettant à la liaison de signaler ces choses, permettant ainsi des messages d'erreur personnalisés au lieu de messages génériques par type. Mais cela créerait un couplage plus fort du point de vue au modèle de vue, ce que je veux généralement éviter.

Une autre «solution» serait de se débarrasser de toutes les propriétés typées, d’utiliser les propriétés plain string et d’effectuer la conversion dans le modèle de vue. Cela déplacerait évidemment toute la validation vers le modèle d'affichage, mais signifierait également une quantité incroyable de duplication des tâches que le moteur de liaison de données prend généralement en charge. De plus, cela changerait la sémantique du modèle de vue. Pour moi, une vue est construite pour le modèle de vue et non l’inverse. Bien entendu, la conception du modèle de vue dépend de ce que nous imaginons faire de la vue, mais il reste une liberté générale. Ainsi, le modèle de vue définit une propriété int car il existe un nombre; la vue peut maintenant utiliser une zone de texte (autorisant tous ces problèmes) ou utiliser quelque chose qui fonctionne de manière native avec des nombres. Donc non, changer les types des propriétés en string n'est pas une option pour moi.

En fin de compte, c'est un problème de vue. La vue (et son moteur de liaison de données) est chargée de donner au modèle de vue les valeurs appropriées à utiliser. Mais dans ce cas, il ne semble pas y avoir de bon moyen de dire au modèle de vue qu'il doit invalider l'ancienne valeur de la propriété.

LiaisonsGroupes

Les groupes de liaisons sont un moyen que j'ai essayé de résoudre. Les groupes de liaisons ont la possibilité de regrouper toutes les validations, y compris les exceptions IDataErrorInfo et levées. Si elles sont disponibles pour le modèle d'affichage, elles ont même le moyen de vérifier l'état de validation pour toutes de ces sources de validation, par exemple, en utilisant CommitEdit .

Par défaut, les groupes de liaison implémentent le choix 2 ci-dessus. Ils font la mise à jour des liaisons de manière explicite, en ajoutant essentiellement un état supplémentaire non engagé. Ainsi, lorsque vous cliquez sur le bouton, la commande peut valider ces modifications, déclencher les mises à jour de la source et toutes les validations et obtenir un résultat unique si elle réussit. L’action de la commande pourrait donc être la suivante:

 if (bindingGroup.CommitEdit())
     SaveEverything();

CommitEdit ne retournera vrai que si toutes les validations ont réussi. Il prendra en compte IDataErrorInfo et vérifiera également les exceptions de liaison. Cela semble être une solution parfaite pour le choix 2. La seule chose qui soit un peu fastidieuse est de gérer le groupe de liaisons avec les liaisons, mais je me suis construit quelque chose qui prend généralement soin de cela ( related ) .

Si un groupe de liaison est présent pour une liaison, la liaison utilisera par défaut un UpdateSourceTrigger .) Explicite. Pour implémenter le choix 1 ci-dessus à l'aide de groupes de liaison, nous devons fondamentalement changer le déclencheur. Comme j'ai de toute façon une extension de liaison personnalisée, c'est assez simple, je viens de le régler sur LostFocus pour tous.

Alors maintenant, les liaisons seront toujours mises à jour chaque fois qu'un champ de texte change. Si le code source peut être mis à jour (le moteur de liaison ne lève aucune exception), IDataErrorInfo sera exécuté normalement. S'il ne peut pas être mis à jour, la vue est toujours capable de le voir. Et si nous cliquons sur notre bouton, la commande sous-jacente peut appeler CommitEditmême si rien ne doit être validé) et obtenir le résultat de validation total pour voir s'il peut continuer.

Nous pourrions ne pas être en mesure de désactiver le bouton facilement de cette façon. Du moins pas du modèle de vue. Vérifier la validation à plusieurs reprises n'est pas vraiment une bonne idée de mettre à jour le statut de la commande, et le modèle de vue n'est pas averti lorsqu'une exception de moteur de liaison est quand même levée (ce qui devrait alors désactiver le bouton) - ou lorsqu'il disparaît activez à nouveau le bouton. Nous pourrions toujours ajouter un déclencheur pour désactiver le bouton dans la vue en utilisant le _ (Validation.HasError afin que ce ne soit pas impossible.

Solution?

Donc dans l’ensemble, cela semble être la solution parfaite. Quel est mon problème avec cela cependant? Pour être honnête, je ne suis pas tout à fait sûr. Les groupes de liaison sont une chose complexe qui semble être généralement utilisée dans des groupes plus petits, pouvant avoir plusieurs groupes de liaison dans une seule vue. En utilisant un seul grand groupe de liens pour l’ensemble de la vue, juste pour assurer ma validation, c’est comme si j’en abusais. Et je ne cesse de penser qu’il doit exister un meilleur moyen de résoudre toute cette situation, car je ne peux certainement pas être le seul à avoir ces problèmes. Et jusqu’à présent, je n’ai pas vraiment vu beaucoup de gens utiliser des groupes de liaison pour la validation avec MVVM, donc cela me semble étrange.

Alors, quelle est la bonne façon de valider dans WPF avec MVVM tout en pouvant vérifier les exceptions de moteur de liaison?


Ma solution (/ bidouille)

Tout d'abord, merci pour votre contribution! Comme je l’ai écrit ci-dessus, j’utilise déjà IDataErrorInfo pour valider mes données et j’estime personnellement que c’est l’utilitaire le plus confortable pour effectuer le travail de validation. J'utilise des utilitaires similaires à ceux suggérés par Sheridan dans sa réponse ci-dessous.

En fin de compte, mon problème se résumait au problème des exceptions contraignantes, dans lequel le modèle de vue ne savait tout simplement pas à quel moment il se produisait. Même si je pouvais gérer cela avec des groupes de liaison comme détaillé ci-dessus, j’ai quand même décidé de ne pas le faire, car je ne me sentais pas à l’aise avec cela. Alors qu'est-ce que j'ai fait à la place?

Comme je l’ai mentionné ci-dessus, je détecte les exceptions de liaison du côté de la vue en écoutant la variable UpdateSourceExceptionFilter d’une liaison. Là, je peux obtenir une référence au modèle de vue à partir de l'expression de liaison DataItem . J'ai ensuite une interface IReceivesBindingErrorInformation qui enregistre le modèle de vue en tant que destinataire potentiel pour des informations sur les erreurs de liaison. Je l'utilise ensuite pour transmettre le chemin de liaison et exception au modèle de vue:

object OnUpdateSourceExceptionFilter(object bindExpression, Exception exception)
{
    BindingExpression expr = (bindExpression as BindingExpression);
    if (expr.DataItem is IReceivesBindingErrorInformation)
    {
        ((IReceivesBindingErrorInformation)expr.DataItem).ReceiveBindingErrorInformation(expr.ParentBinding.Path.Path, exception);
    }

    // check for FormatException and produce a nicer error
    // ...
 }

Dans le modèle de vue, je me souviens de chaque fois que je suis informé de l’expression de liaison d’un chemin:

HashSet<string> bindingErrors = new HashSet<string>();
void IReceivesBindingErrorInformation.ReceiveBindingErrorInformation(string path, Exception exception)
{
    bindingErrors.Add(path);
}

Et chaque fois que la IDataErrorInfo revalide une propriété, je sais que la liaison a fonctionné et je peux effacer la propriété de l'ensemble de hachage.

Dans le modèle de vue, je peux alors vérifier si l'ensemble de hachage contient des éléments et abandonner toute action nécessitant la validation complète des données. Ce n'est peut-être pas la solution la plus intéressante en raison du couplage de la vue à la vue, mais utiliser cette interface est au moins moins un problème.

55
poke

Attention: Réponse longue aussi

J'utilise l'interface IDataErrorInfo pour la validation, mais je l'ai personnalisée selon mes besoins. Je pense que vous constaterez que cela résout également certains de vos problèmes. Une différence par rapport à votre question est que je l'implémente dans ma classe de type de données de base.

Comme vous l'avez fait remarquer, cette interface ne traite que d'une propriété à la fois, mais de nos jours, cela ne sert à rien. J'ai donc ajouté une propriété de collection à utiliser à la place:

protected ObservableCollection<string> errors = new ObservableCollection<string>();

public virtual ObservableCollection<string> Errors
{
    get { return errors; }
}

Pour résoudre votre problème de ne pas pouvoir afficher les erreurs externes (dans votre cas, dans la vue, mais dans le mien, dans le modèle de vue), j'ai simplement ajouté une autre propriété de collection:

protected ObservableCollection<string> externalErrors = new ObservableCollection<string>();

public ObservableCollection<string> ExternalErrors
{
    get { return externalErrors; }
}

J'ai une propriété HasError qui regarde ma collection:

public virtual bool HasError
{
    get { return Errors != null && Errors.Count > 0; }
}

Cela me permet de lier ceci à Grid.Visibility en utilisant une BoolToVisibilityConverter personnalisée, par exemple. pour montrer une Grid avec un contrôle de collection à l'intérieur qui montre les erreurs quand il y en a. Cela me permet également de changer une Brush en Red pour mettre en évidence une erreur (en utilisant une autre Converter), mais je suppose que vous avez compris l'idée.

Ensuite, dans chaque type de données ou classe de modèle, je remplace la propriété Errors et implémente l'indexeur Item (simplifié dans cet exemple):

public override ObservableCollection<string> Errors
{
    get
    {
        errors = new ObservableCollection<string>();
        errors.AddUniqueIfNotEmpty(this["Name"]);
        errors.AddUniqueIfNotEmpty(this["EmailAddresses"]);
        errors.AddUniqueIfNotEmpty(this["SomeOtherProperty"]);
        errors.AddRange(ExternalErrors);
        return errors;
    }
}

public override string this[string propertyName]
{
    get
    {
        string error = string.Empty;
        if (propertyName == "Name" && Name.IsNullOrEmpty()) error = "You must enter the Name field.";
        else if (propertyName == "EmailAddresses" && EmailAddresses.Count == 0) error = "You must enter at least one e-mail address into the Email address(es) field.";
        else if (propertyName == "SomeOtherProperty" && SomeOtherProperty.IsNullOrEmpty()) error = "You must enter the SomeOtherProperty field.";
        return error;
    }
}

La méthode AddUniqueIfNotEmpty est une méthode personnalisée extension et permet de «faire ce qui est dit en boîte». Notez comment il appellera chaque propriété que je veux valider à son tour et compilera une collection à partir d’elles, en ignorant les erreurs de duplication.

À l'aide de la collection ExternalErrors, je peux valider des éléments que je ne peux pas valider dans la classe de données:

private void ValidateUniqueName(Genre genre)
{
    string errorMessage = "The genre name must be unique";
    if (!IsGenreNameUnique(genre))
    {
        if (!genre.ExternalErrors.Contains(errorMessage)) genre.ExternalErrors.Add(errorMessage);
    }
    else genre.ExternalErrors.Remove(errorMessage);
}

Pour répondre à votre question concernant la situation dans laquelle un utilisateur entre un caractère alphabétique dans un champ int, j’ai tendance à utiliser un IsNumeric AttachedProperty personnalisé pour la TextBox, par exemple. Je ne les laisse pas faire ce genre d'erreurs. J'ai toujours le sentiment qu'il vaut mieux l'arrêter que de le laisser arriver et ensuite le réparer.

Globalement, je suis vraiment satisfait de ma capacité de validation dans WPF et je ne manque pas du tout.

Pour terminer et pour être complet, j’ai pensé que je devais vous alerter sur le fait qu’il existe maintenant une interface INotifyDataErrorInfo qui inclut certaines de ces fonctionnalités ajoutées. Vous pouvez en savoir plus à partir de la page INotifyDataErrorInfo Interface sur MSDN.


MISE À JOUR >>>

Oui, la propriété ExternalErrors me permet d'ajouter des erreurs relatives à un objet de données situé en dehors de cet objet ... désolé, mon exemple n'était pas complet ... si je vous avais montré la méthode IsGenreNameUnique, vous auriez vu que il utilise LinQ sur all des éléments de données Genre de la collection pour déterminer si le nom de l'objet est unique ou non:

private bool IsGenreNameUnique(Genre genre)
{
    return Genres.Where(d => d.Name != string.Empty && d.Name == genre.Name).Count() == 1;
}

En ce qui concerne votre problème intstring, le seul moyen de voir les erreurs _/ces dans votre classe de données est de déclarer toutes vos propriétés en tant que object, mais vous auriez alors beaucoup de choix à faire. Peut-être pourriez-vous doubler vos propriétés comme ceci:

public object FooObject { get; set; } // Implement INotifyPropertyChanged

public int Foo
{
    get { return FooObject.GetType() == typeof(int) ? int.Parse(FooObject) : -1; }
}

Ensuite, si Foo a été utilisé dans le code et que FooObject a été utilisé dans Binding, vous pouvez faire ceci:

public override string this[string propertyName]
{
    get
    {
        string error = string.Empty;
        if (propertyName == "FooObject" && FooObject.GetType() != typeof(int)) 
            error = "Please enter a whole number for the Foo field.";
        ...
        return error;
    }
}

De cette façon, vous pourrez répondre à vos exigences, mais vous aurez beaucoup de code supplémentaire à ajouter.

17
Sheridan

L'inconvénient est que cela pourrait exécuter la validation sur toutes les propriétés a un peu trop souvent, mais la plupart des validations devraient suffire tout simplement à ne pas nuire à la performance. Une autre solution serait de se rappeler laquelle les propriétés ont généré des erreurs lors de l'utilisation de la validation et ne vérifient que celles, mais cela semble un peu trop compliqué et inutile pour la plupart des temps.

Vous n'avez pas besoin de savoir quelles propriétés comportent des erreurs. vous devez seulement savoir que des erreurs existent. Le modèle de vue peut gérer une liste d'erreurs (utile également pour afficher un résumé d'erreur) et la propriété IsValid peut simplement indiquer si la liste contient ou non quelque chose. Il n'est pas nécessaire de tout vérifier à chaque appel de IsValid, tant que vous vous assurez que le résumé de l'erreur est actuel et que IsValid est actualisé à chaque modification.


En fin de compte, c'est un problème de vue. La vue (et ses données Moteur de liaison) est chargée de donner au modèle de vue les valeurs appropriées travailler avec. Mais dans ce cas, il ne semble pas y avoir de bon moyen de le savoir le modèle de vue qui doit invalider l'ancienne valeur de la propriété.

Vous pouvez écouter les erreurs dans le conteneur lié au modèle de vue:

container.AddHandler(Validation.ErrorEvent, Container_Error);

...

void Container_Error(object sender, ValidationErrorEventArgs e) {
    ...
}

Cela vous avertit lorsque des erreurs sont ajoutées ou supprimées, et vous pouvez identifier les exceptions de liaison en indiquant si e.Error.Exception existe, afin que votre vue puisse gérer une liste des exceptions de liaison et en informer le modèle de vue.

Mais toute solution à ce problème sera toujours un hack, car la vue ne remplit pas correctement son rôle, ce qui donne à l'utilisateur un moyen de lire et de mettre à jour la structure du modèle de vue. Cela doit être considéré comme une solution temporaire jusqu'à ce que vous présentiez correctement à l'utilisateur un type de " entier box" au lieu d'un text box.

1
nmclean

À mon avis, le problème réside dans la validation se produisant dans trop d'endroits. Je souhaitais également écrire tous mes identifiants de validation dans ViewModel, mais toutes ces liaisons de nombres rendaient ma ViewModel folle.

J'ai résolu ce problème en créant une liaison qui n'échoue jamais. De toute évidence, si une liaison réussit toujours, le type lui-même doit gérer les conditions d'erreur avec élégance.

Type de valeur disponible

J'ai commencé par créer un type générique qui supporterait gracieusement les conversions ayant échoué:

public struct Failable<T>
{
    public T Value { get; private set; }
    public string Text { get; private set; }
    public bool IsValid { get; private set; }

    public Failable(T value)
    {
        Value = value;

        try
        {
            var converter = TypeDescriptor.GetConverter(typeof(T));
            Text = converter.ConvertToString(value);
            IsValid = true;
        }
        catch
        {
            Text = String.Empty;
            IsValid = false;
        }
    }

    public Failable(string text)
    {
        Text = text;

        try
        {
            var converter = TypeDescriptor.GetConverter(typeof(T));
            Value = (T)converter.ConvertFromString(text);
            IsValid = true;
        }
        catch
        {
            Value = default(T);
            IsValid = false;
        }
    }
}

Notez que même si le type ne parvient pas à s'initialiser en raison d'une chaîne d'entrée non valide (second constructeur), il stocke discrètement l'état non valide avec le texteinvalid text. Ceci est nécessaire pour permettre l'aller-retour de la liaisonmême en cas de saisie erronée.

Convertisseur de valeur générique

Un convertisseur de valeur générique peut être écrit en utilisant le type ci-dessus:

public class StringToFailableConverter<T> : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value.GetType() != typeof(Failable<T>))
            throw new InvalidOperationException("Invalid value type.");

        if (targetType != typeof(string))
            throw new InvalidOperationException("Invalid target type.");

        var rawValue = (Failable<T>)value;
        return rawValue.Text;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value.GetType() != typeof(string))
            throw new InvalidOperationException("Invalid value type.");

        if (targetType != typeof(Failable<T>))
            throw new InvalidOperationException("Invalid target type.");

        return new Failable<T>(value as string);
    }
}

Convertisseurs XAML Handy

La création et l'utilisation d'instances de génériques étant une tâche ardue en XAML, permet de créer des instances statiques de convertisseurs courants:

public static class Failable
{
    public static StringToFailableConverter<Int32> Int32Converter { get; private set; }
    public static StringToFailableConverter<double> DoubleConverter { get; private set; }

    static Failable()
    {
        Int32Converter = new StringToFailableConverter<Int32>();
        DoubleConverter = new StringToFailableConverter<Double>();
    }
}

D'autres types de valeur peuvent être étendus facilement.

Usage

L'utilisation est assez simple, il suffit de changer le type de int à Failable<int>

ViewModel

public Failable<int> NumberValue
{
    //Custom logic along with validation
    //using IsValid property
}

XAML

<TextBox Text="{Binding NumberValue,Converter={x:Static local:Failable.Int32Converter}}"/>

De cette façon, vous pouvez utiliser le même mécanisme de validation (IDataErrorInfo ou INotifyDataErrorInfo ou quoi que ce soit d'autre) dans ViewModel en cochant la propriété IsValid. Si IsValid est vrai, vous pouvez directement utiliser Value.

1
Hemant

Voici un effort pour simplifier les choses si vous ne souhaitez pas implémenter des tonnes de code supplémentaire ...

Le scénario est que vous avez une propriété int dans votre modèle de vue (qu'il s'agisse d'un type décimal ou d'un autre type non-chaîne) et que vous liez une zone de texte à votre vue.

Vous avez une validation dans votre viewmodel qui se déclenche dans le setter de la propriété.

Dans la vue, l'utilisateur entre 123abc et la logique de la vue met en évidence l'erreur dans la vue, mais ne peut pas définir la propriété, car la valeur est d'un type incorrect. Le passeur ne se fait jamais appeler.

La solution la plus simple consiste à changer votre propriété int dans le modèle de vue pour qu'elle devienne une propriété de chaîne et à en extraire les valeurs à partir du modèle. Cela permet au texte incorrect de toucher le créateur de votre propriété et votre code de validation peut alors vérifier les données et les rejeter comme il convient.

La validation IMHO dans WPF est cassée, comme en témoignent les méthodes élaborées (et ingénieuses) utilisées pour contourner le problème présenté précédemment. Pour moi, je ne veux pas ajouter une quantité énorme de code supplémentaire ni implémenter mes propres classes de types pour permettre à une zone de texte de valider. Il est donc possible de vivre avec ces propriétés sur des chaînes, même si kludge.

Microsoft doit résoudre ce problème de manière à ce que le scénario d’une entrée utilisateur non valide dans une zone de texte liée à une propriété int ou decimal puisse d’une manière ou d’une autre communiquer ce fait avec élégance au modèle de vue. Par exemple, il devrait être possible pour eux de créer une nouvelle propriété liée pour un contrôle XAML afin de communiquer les erreurs de validation de la vue à une propriété du modèle de vue.

Merci et respect aux autres gars qui ont fourni des réponses détaillées à ce sujet.

0
Richard Moore

Ok, je pense avoir trouvé la réponse que vous cherchiez ... 
Ce ne sera pas facile à expliquer - mais ..
Très facile à comprendre une fois expliqué ...
Je pense que c’est le plus précis/"certifié" par MVVM considéré comme "standard" ou au moins essayé comme standard.

Mais avant de commencer, vous devez modifier un concept auquel vous êtes habitué en ce qui concerne MVVM:

"De plus, cela changerait la sémantique du modèle de vue. Pour moi, Une vue est construite pour le modèle de vue et non l'inverse - bien sûr, la conception Du modèle de vue dépend de ce que nous imaginons faire, mais il y a toujours une liberté générale comment la vue fait que "

Ce paragraphe est la source de votre problème .. - pourquoi?

Parce que vous déclarez que View-Model n’a aucun rôle à s’ajuster à la View.
C'est faux à bien des égards - comme je vais vous le prouver très simplement ..

Si vous avez une propriété telle que: 

public Visibility MyPresenter { get...

Qu'est-ce que Visibility sinon quelque chose qui sert la vue? 
Le type lui-même et le nom qui sera donné à la propriété est définitivement composé pour la vue.

Selon mon expérience, il existe deux catégories de modèles de vues dans MVVM:

  • Modèle de présentation du présentateur - à connecter aux boutons, menus, éléments de tabulation, etc. 
  • Modèle de vue d'entité - qui doit être associé à des contrôles qui affichent les données d'entité à l'écran.

Ce sont deux préoccupations complètement différentes.

Et maintenant à la solution:

public abstract class ViewModelBase : INotifyPropertyChanged
{
   public event PropertyChangedEventHandler PropertyChanged;

   public void RaisePropertyChanged([CallerMemberName] string propertyName = null)
   {
      if (PropertyChanged != null)
         PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
   }
}


public class VmSomeEntity : ViewModelBase, INotifyDataErrorInfo
{
    //This one is part of INotifyDataErrorInfo interface which I will not use,
    //perhaps in more complicated scenarios it could be used to let some other VM know validation changed.
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged; 

    //will hold the errors found in validation.
    public Dictionary<string, string> ValidationErrors = new Dictionary<string, string>();

    //the actual value - notice it is 'int' and not 'string'..
    private int storageCapacityInBytes;

    //this is just to keep things sane - otherwise the view will not be able to send whatever the user throw at it.
    //we want to consume what the user throw at us and validate it - right? :)
    private string storageCapacityInBytesWrapper;

    //This is a property to be served by the View.. important to understand the tactic used inside!
    public string StorageCapacityInBytes
    {
       get { return storageCapacityInBytesWrapper ?? storageCapacityInBytes.ToString(); }
       set
       {
          int result;
          var isValid = int.TryParse(value, out result);
          if (isValid)
          {
             storageCapacityInBytes = result;
             storageCapacityInBytesWrapper = null;
             RaisePropertyChanged();
          }
          else
             storageCapacityInBytesWrapper = value;         

          HandleValidationError(isValid, "StorageCapacityInBytes", "Not a number.");
       }
    }

    //Manager for the dictionary
    private void HandleValidationError(bool isValid, string propertyName, string validationErrorDescription)
    {
        if (!string.IsNullOrEmpty(propertyName))
        {
            if (isValid)
            {
                if (ValidationErrors.ContainsKey(propertyName))
                    ValidationErrors.Remove(propertyName);
            }
            else
            {
                if (!ValidationErrors.ContainsKey(propertyName))
                    ValidationErrors.Add(propertyName, validationErrorDescription);
                else
                    ValidationErrors[propertyName] = validationErrorDescription;
            }
        }
    }

    // this is another part of the interface - will be called automatically
    public IEnumerable GetErrors(string propertyName)
    {
        return ValidationErrors.ContainsKey(propertyName)
            ? ValidationErrors[propertyName]
            : null;
    }

    // same here, another part of the interface - will be called automatically
    public bool HasErrors
    {
        get
        {
            return ValidationErrors.Count > 0;
        }
    }
}

Et maintenant, quelque part dans votre code, votre méthode de commande de bouton 'CanExecute' peut ajouter à son implémentation un appel à VmEntity.HasErrors.

Et que la paix soit sur votre code concernant la validation à partir de maintenant :)

0
G.Y