web-dev-qa-db-ger.com

Ordnungsgemäße Validierung mit MVVM

Achtung: Sehr langer und ausführlicher Beitrag.

Okay, Validierung in WPF bei Verwendung von MVVM. Ich habe jetzt viele Dinge gelesen, mir viele SO - Fragen angesehen und viele -Ansätze ausprobiert, aber alles fühlt sich irgendwann etwas hackig an und ich bin wirklich nicht sicher, wie es geht. der richtige Weg ™.

Im Idealfall möchte ich, dass die gesamte Validierung im Ansichtsmodell mit IDataErrorInfo erfolgt. also das habe ich getan. Es gibt jedoch verschiedene Aspekte, aufgrund derer diese Lösung keine vollständige Lösung für das gesamte Validierungsthema darstellt.

Die Situation

Nehmen wir die folgende einfache Form. Wie Sie sehen, ist es nichts Besonderes. Wir haben nur zwei Textfelder, die jeweils an eine string- und int-Eigenschaft im Ansichtsmodell binden. Außerdem haben wir eine Schaltfläche, die an ein ICommand gebunden ist.

Simple form with only a string and integer input

Für die Validierung haben wir nun zwei Möglichkeiten:

  1. Wir können die Überprüfung automatisch ausführen, wenn sich der Wert eines Textfelds ändert. Auf diese Weise erhält der Benutzer eine sofortige Antwort, wenn er etwas Ungültiges eingegeben hat .
    • Wir können noch einen Schritt weitergehen, um die Schaltfläche bei Fehlern zu deaktivieren.
  2. Oder wir können die Validierung nur explizit ausführen, wenn die Schaltfläche gedrückt wird, und gegebenenfalls alle Fehler anzeigen. Natürlich können wir die Schaltfläche bei Fehlern hier nicht deaktivieren.

Idealerweise möchte ich die Option 1 implementieren. Bei normalen Datenbindungen mit aktiviertem ValidatesOnDataErrors ist dies das Standardverhalten. Wenn sich der Text ändert, aktualisiert die Bindung die Quelle und löst die IDataErrorInfo-Überprüfung für diese Eigenschaft aus. Fehler werden in der Ansicht zurückgemeldet. So weit, ist es gut.

Validierungsstatus im Ansichtsmodell

Das interessante Bit ist, das Ansichtsmodell oder die Schaltfläche in diesem Fall wissen zu lassen, ob Fehler vorliegen. Die Funktionsweise von IDataErrorInfo besteht hauptsächlich darin, Fehler an die Ansicht zu melden. So kann die Ansicht leicht erkennen, ob Fehler aufgetreten sind, diese anzeigen und sogar Anmerkungen mit Validation.Errors anzeigen. Darüber hinaus erfolgt die Überprüfung immer auf eine einzelne Eigenschaft.

Daher ist es schwierig, das Ansichtsmodell zu wissen, wenn Fehler aufgetreten sind oder ob die Validierung erfolgreich war. Eine gängige Lösung besteht darin, einfach die IDataErrorInfo-Validierung für alle Eigenschaften im Ansichtsmodell selbst auszulösen. Dies geschieht häufig mit einer separaten IsValid-Eigenschaft. Der Vorteil ist, dass dies auch leicht zum Deaktivieren des Befehls verwendet werden kann. Der Nachteil ist, dass dies die Validierung aller Eigenschaften ein bisschen zu oft ausführt, die meisten Validierungen sollten jedoch einfach genug sein, um die Leistung nicht zu beeinträchtigen. Eine andere Lösung wäre, sich zu merken, welche Eigenschaften bei der Validierung Fehler erzeugt haben, und nur diese zu überprüfen. Dies scheint jedoch zu kompliziert und für die meisten Fälle unnötig zu sein.

Die Quintessenz ist, dass dies gut funktionieren könnte. IDataErrorInfo stellt die Validierung für alle Eigenschaften bereit, und wir können diese Schnittstelle im Ansichtsmodell selbst verwenden, um auch dort die Validierung für das gesamte Objekt auszuführen. Das Problem wird vorgestellt:

Verbindliche Ausnahmen

Das Ansichtsmodell verwendet tatsächliche Typen für seine Eigenschaften. In unserem Beispiel ist die Ganzzahl-Eigenschaft also eine tatsächliche int. Das in der Ansicht verwendete Textfeld unterstützt jedoch nativ nur Text. Beim Binden an int im Ansichtsmodell führt das Datenbindungsmodul daher automatisch Typkonvertierungen durch - oder zumindest versucht es. Wenn Sie Text in ein Textfeld eingeben können, das für Zahlen gedacht ist, sind die Chancen groß, dass sich darin nicht immer gültige Zahlen befinden: Die Datenbindungs-Engine kann also nicht konvertieren und FormatException werfen.

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

Auf der Ansichtsseite können wir das leicht sehen. Ausnahmen vom Bindungsmodul werden automatisch von WPF abgefangen und als Fehler angezeigt. Es ist nicht einmal erforderlich, Binding.ValidatesOnExceptions zu aktivieren, was für Ausnahmen erforderlich ist, die im Setter ausgelöst werden. Die Fehlermeldungen haben jedoch einen generischen Text, so dass dies ein Problem sein kann. Ich habe das Problem für mich gelöst, indem ich einen Binding.UpdateSourceExceptionFilter - Handler verwendet habe, die Ausnahmebedingung untersucht und die Quelleigenschaft durchgesehen habe und dann stattdessen eine weniger allgemeine Fehlermeldung generiert habe. Das alles hat sich in meiner eigenen Binding-Markup-Erweiterung verkapselt, sodass ich alle Standardeinstellungen habe, die ich brauche.

Die Aussicht ist also gut. Der Benutzer macht einen Fehler, sieht eine Fehlerrückmeldung und kann diese korrigieren. Das Ansichtsmodell jedoch geht verloren. Da die Bindungsmaschine die Ausnahme ausgelöst hat, wurde die Quelle nie aktualisiert. Daher ist das Ansichtsmodell immer noch auf dem alten Wert, der dem Benutzer nicht angezeigt wird, und die Validierung IDataErrorInfo ist offensichtlich nicht anwendbar.

Was noch schlimmer ist, es gibt keine gute Möglichkeit für das Ansichtsmodell, dies zu wissen. Zumindest habe ich noch keine gute Lösung dafür gefunden. Möglich wäre es, wenn die Ansicht über einen Fehler an das Ansichtsmodell zurückmeldet. Dies kann durch Datenbindung der Validation.HasError - Eigenschaft an das Ansichtsmodell erfolgen (was nicht direkt möglich ist), sodass das Ansichtsmodell zuerst den Status der Ansicht überprüfen kann.Eine andere Option wäre, die in Binding.UpdateSourceExceptionFilter behandelte Ausnahme an das Ansichtsmodell weiterzuleiten, damit es auch darüber informiert wird. Das Ansichtsmodell könnte sogar eine Schnittstelle für die Bindung bereitstellen, um diese Dinge zu melden, wodurch benutzerdefinierte Fehlermeldungen anstelle von generischen Fehlern pro Typ möglich werden. Dies würde aber eine stärkere Kopplung von der Sicht zum Sichtmodell schaffen, die ich generell vermeiden möchte.

Eine andere "Lösung" wäre, alle typisierten Eigenschaften loszuwerden, einfache string-Eigenschaften zu verwenden und stattdessen die Konvertierung im Ansichtsmodell durchzuführen. Dies würde natürlich die gesamte Validierung in das Ansichtsmodell verschieben, würde aber auch eine unglaubliche Menge von Dingen bedeuten, für die die Datenbindungs-Engine normalerweise sorgt. Außerdem würde sich die Semantik des Ansichtsmodells ändern. Für mich ist eine Ansicht für das Ansichtsmodell aufgebaut und nicht umgekehrt: Natürlich hängt das Design des Ansichtsmodells davon ab, was wir uns für die Ansicht vorstellen, aber es gibt immer noch allgemeine Freiheit, wie die Ansicht dies tut. Das Ansichtsmodell definiert also eine int-Eigenschaft, da eine Zahl vorhanden ist. Die Ansicht kann jetzt ein Textfeld verwenden (das all diese Probleme zulässt) oder etwas verwenden, das nativ mit Zahlen funktioniert. Also nein, das Ändern der Typen der Eigenschaften in string ist für mich keine Option.

Am Ende ist dies ein Problem der Sicht. Die Ansicht (und ihre Datenbindungs-Engine) ist dafür verantwortlich, dem Ansichtsmodell die richtigen Werte zur Verfügung zu stellen. In diesem Fall scheint es jedoch keine gute Möglichkeit zu geben, dem Ansichtsmodell mitzuteilen, dass es den alten Eigenschaftswert ungültig machen sollte.

BindingGroups.

Binding Groups sind eine Möglichkeit, dies zu bewältigen. Bindungsgruppen können alle Validierungen gruppieren, einschließlich IDataErrorInfo und ausgelöste Ausnahmen. Falls für das Ansichtsmodell verfügbar, haben sie sogar einen Mittelwert, um den Validierungsstatus für all dieser Validierungsquellen zu überprüfen, beispielsweise mit CommitEdit .

Standardmäßig implementieren Bindungsgruppen die Option 2 von oben. Sie führen die Aktualisierungen der Bindungen explizit durch und fügen im Wesentlichen einen zusätzlichen nicht festgeschrieben Status hinzu. Wenn Sie also auf die Schaltfläche klicken, kann der Befehl commit diese Änderungen übernehmen, die Quellupdates und alle Validierungen auslösen und bei Erfolg ein einzelnes Ergebnis erhalten. Die Aktion des Befehls könnte also folgende sein:.

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

Wenn eine Bindungsgruppe für eine Bindung vorhanden ist, wird für die Bindung standardmäßig ein expliziter UpdateSourceTrigger verwendet. Um die Auswahl 1 von oben mithilfe von Bindungsgruppen zu implementieren, müssen wir grundsätzlich den Auslöser ändern. Da ich ohnehin eine benutzerdefinierte Bindungserweiterung habe, ist diese ziemlich einfach. Ich habe sie für alle auf LostFocus gesetzt.

Die Bindungen werden also immer noch aktualisiert, wenn sich ein Textfeld ändert. Wenn die Quelle aktualisiert werden konnte (das Bindungsmodul löst keine Ausnahme aus), wird IDataErrorInfo wie üblich ausgeführt. Wenn es nicht aktualisiert werden konnte, kann die Ansicht es immer noch sehen. Wenn wir auf unsere Schaltfläche klicken, kann der zugrunde liegende Befehl CommitEdit aufrufen (obwohl nichts festgeschrieben werden muss) und das Gesamtvalidierungsergebnis abrufen, um zu sehen, ob er fortfahren kann.

Wir können den Button auf diese Weise möglicherweise nicht einfach deaktivieren. Zumindest nicht aus dem Ansichtsmodell. Eine ständige Überprüfung der Gültigkeitsprüfung ist nicht wirklich eine gute Idee, nur um den Befehlsstatus zu aktualisieren. Das Ansichtsmodell wird nicht benachrichtigt, wenn ohnehin eine Ausnahme für das Bindungsmodul ausgelöst wird (was dann die Schaltfläche deaktivieren sollte) - oder, wenn es nicht weitergeht Aktivieren Sie die Schaltfläche erneut. Wir könnten immer noch einen Auslöser hinzufügen, um die Schaltfläche in der Ansicht zu deaktivieren, indem Sie den Validation.HasError verwenden, damit dies nicht unmöglich ist.

Lösung?.

Insgesamt scheint dies die perfekte Lösung zu sein. Was ist mein Problem damit? Um ehrlich zu sein, bin ich mir nicht ganz sicher. Bindungsgruppen sind eine komplexe Sache, die normalerweise in kleineren Gruppen verwendet wird und möglicherweise mehrere Bindungsgruppen in einer einzigen Ansicht enthält. Durch die Verwendung einer großen Bindungsgruppe für die gesamte Ansicht, nur um meine Bestätigung sicherzustellen, fühlt es sich an, als würde ich sie missbrauchen. Und ich denke immer wieder, dass es einen besseren Weg geben muss, um diese ganze Situation zu lösen, denn ich kann nicht der einzige sein, der diese Probleme hat. Und bis jetzt habe ich nicht wirklich gesehen, dass viele Leute überhaupt verbindliche Gruppen für die Validierung mit MVVM verwendet haben. Es fühlt sich einfach seltsam an.Was ist also der richtige Weg, um die Validierung in WPF mit MVVM durchzuführen und gleichzeitig auf Ausnahmen für Bindungsmodule zu prüfen?

.

Meine Lösung (/ Hack)


Zunächst einmal vielen Dank für Ihre Beiträge! Wie ich oben geschrieben habe, verwende ich __VARIABLE bereits für meine Datenvalidierung, und ich persönlich glaube, dass es das bequemste Werkzeug ist, um die Validierungsaufgabe zu erledigen. Ich verwende Dienstprogramme, die denen ähneln, die Sheridan in seiner Antwort unten vorgeschlagen hat.

Am Ende war mein Problem auf das Problem der verbindlichen Ausnahmen zurückzuführen, bei dem das Ansichtsmodell nicht wusste, wann es passiert war. Ich konnte zwar mit verbindlichen Gruppen wie oben beschrieben umgehen, entschied mich aber trotzdem dagegen, da ich mich einfach nicht so wohl fühlte. Also, was habe ich stattdessen gemacht?

Wie bereits erwähnt, erkenne ich verbindliche Ausnahmen auf der Ansichtsseite, indem ich die IDataErrorInfo-Verknüpfung einer Bindung höre. Dort kann ich aus dem Bindungsausdruck UpdateSourceExceptionFilter einen Verweis auf das Ansichtsmodell abrufen. Ich habe dann eine Schnittstelle DataItem, die das Ansichtsmodell als möglichen Empfänger für Informationen über Bindungsfehler registriert. Ich benutze das dann, um den Bindungspfad und die Ausnahme an das Ansichtsmodell zu übergeben:.

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

Im Ansichtsmodell erinnere ich mich dann immer, wenn ich über den Bindungsausdruck eines Pfads informiert werde:

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

Und wann immer IReceivesBindingErrorInformation eine Eigenschaft validiert, weiß ich, dass die Bindung funktioniert hat, und ich kann die Eigenschaft aus dem Hash-Set löschen.

IDataErrorInfo revalidates a property, I know that the binding worked, and I can clear the property from the hash set.

In the view model I then can check if the hash set contains any items and abort any action that requires the data to be validated completely. It might not be the nicest solution due to the coupling from the view to the view model, but using that interface it’s at least somewhat less a problem.

55
poke

Warnung: Lange Antwort auch

Ich benutze die IDataErrorInfo-Schnittstelle zur Validierung, habe sie aber an meine Bedürfnisse angepasst. Ich denke, Sie werden feststellen, dass es auch einige Ihrer Probleme löst. Ein Unterschied zu Ihrer Frage ist, dass ich sie in meiner Basisdatentypklasse implementiere.

Wie Sie bereits erwähnt haben, handelt es sich bei dieser Schnittstelle nur um eine Eigenschaft, aber dies ist in der heutigen Zeit eindeutig nicht der Fall. Also habe ich einfach eine Sammlungseigenschaft hinzugefügt, die stattdessen verwendet werden soll:

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

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

Um Ihr Problem zu beheben, dass externe Fehler nicht angezeigt werden können (in Ihrem Fall aus der Ansicht, aber in meinem aus dem Ansichtsmodell), habe ich einfach eine weitere Sammlungseigenschaft hinzugefügt:

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

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

Ich habe eine HasError-Eigenschaft, die meine Sammlung betrachtet:

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

Dies ermöglicht es mir, dies mit einer benutzerdefinierten BoolToVisibilityConverter an Grid.Visibility zu binden, z. um eine Grid mit einem Collection-Steuerelement anzuzeigen, das die Fehler anzeigt, wenn es welche gibt. Ich kann auch eine Brush in Red ändern, um einen Fehler hervorzuheben (mit einer anderen Converter), aber ich schätze, Sie bekommen die Idee.

Dann überschreibe ich in jedem Datentyp oder jeder Modellklasse die Errors-Eigenschaft und implementiere den Item-Indexer (in diesem Beispiel vereinfacht):

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

Die AddUniqueIfNotEmpty-Methode ist eine benutzerdefinierte extension-Methode und 'tut, was auf der Dose steht'. Beachten Sie, wie jede Eigenschaft aufgerufen wird, die ich überprüfen möchte, und eine Auflistung daraus erstellen, wobei doppelte Fehler ignoriert werden.

Mit der ExternalErrors-Sammlung kann ich Dinge überprüfen, die ich in der Datenklasse nicht überprüfen kann:

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

Um Ihren Punkt bezüglich der Situation anzusprechen, in der ein Benutzer ein alphabetisches Zeichen in ein int-Feld eingibt, neige ich dazu, einen benutzerdefinierten IsNumeric AttachedProperty für die TextBox zu verwenden, z. Ich lasse nicht zu, dass sie solche Fehler machen. Ich habe immer das Gefühl, dass es besser ist, es zu stoppen, als es geschehen zu lassen und es dann zu reparieren.

Insgesamt bin ich mit meiner Validierungsfähigkeit in WPF sehr zufrieden und habe überhaupt keine Wünsche offen.

Zum Schluss und zur Vollständigkeit wollte ich Sie darauf aufmerksam machen, dass es jetzt eine INotifyDataErrorInfo-Schnittstelle gibt, die einige dieser zusätzlichen Funktionen enthält. Weitere Informationen finden Sie auf der Seite INotifyDataErrorInfo Interface auf MSDN.


UPDATE >>>

Ja, die ExternalErrors-Eigenschaft lasst mich einfach Fehler hinzufügen, die sich auf ein Datenobjekt außerhalb dieses Objekts beziehen ... Entschuldigung, mein Beispiel war nicht vollständig ... Wenn ich Ihnen die IsGenreNameUnique-Methode gezeigt hätte, hätten Sie das gesehen Es verwendet LinQ für all der Genre-Datenelemente in der Auflistung, um festzustellen, ob der Name des Objekts eindeutig ist oder nicht:

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

Was Ihr Problem intstring betrifft, so kann ich nur feststellen, dass Sie _/diese -Fehler in Ihrer Datenklasse erhalten, wenn Sie alle Ihre Eigenschaften als object deklarieren. Vielleicht könnten Sie Ihre Eigenschaften wie folgt verdoppeln:

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

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

Wenn dann Foo im Code und FooObject in der Binding verwendet wurde, können Sie Folgendes tun:

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

Auf diese Weise könnten Sie Ihre Anforderungen erfüllen, aber Sie müssen viel zusätzlichen Code hinzufügen.

17
Sheridan

Der Nachteil ist, dass hierdurch die Überprüfung aller Eigenschaften a .__ ausgeführt werden kann. etwas zu oft, aber die meisten Validierungen sollten einfach nicht ausreichen. die Leistung verletzt Eine andere Lösung wäre, sich an welche Bei der Validierung erzeugten Eigenschaften Fehler und nur die aber das scheint für die meisten Zeiten ein wenig überkompliziert und unnötig zu sein.

Sie müssen nicht nachverfolgen, welche Eigenschaften Fehler aufweisen. Sie müssen nur wissen, dass Fehler vorliegen. Das Ansichtsmodell kann eine Fehlerliste enthalten (auch nützlich für die Anzeige einer Fehlerzusammenfassung), und die IsValid-Eigenschaft kann einfach eine Widerspiegelung dessen sein, ob die Liste etwas enthält. Sie müssen nicht jedes Mal alles prüfen, wenn IsValid aufgerufen wird, solange Sie sicherstellen, dass die Fehlerzusammenfassung aktuell ist und IsValid bei jeder Änderung aktualisiert wird.


Am Ende ist dies ein Problem der Sicht. Die Ansicht (und ihre Daten Bindungs-Engine) ist dafür verantwortlich, dem Ansichtsmodell die richtigen Werte zu geben arbeiten mit. In diesem Fall scheint es jedoch keine gute Methode zu geben Das Ansichtsmodell, bei dem der alte Eigenschaftswert ungültig werden soll.

Sie können Fehler innerhalb des Containers überwachen, der an das Ansichtsmodell gebunden ist:

container.AddHandler(Validation.ErrorEvent, Container_Error);

...

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

Dadurch werden Sie benachrichtigt, wenn Fehler hinzugefügt oder entfernt werden. Sie können Bindungsausnahmen dadurch identifizieren, ob e.Error.Exception vorhanden ist. Ihre Ansicht kann also eine Liste von Bindungsausnahmen verwalten und das Ansichtsmodell darüber informieren.

Eine Lösung für dieses Problem ist jedoch immer ein Hack, weil die Ansicht ihre Rolle nicht richtig ausfüllt, wodurch dem Benutzer die Möglichkeit gegeben wird, die Struktur des Ansichtsmodells zu lesen und zu aktualisieren. Dies sollte als temporäre Lösung betrachtet werden, bis Sie dem Benutzer eine Art " integer box" anstelle einer text - Box vorlegen.

1
nmclean

Meines Erachtens liegt das Problem in der Validierung an zu vielen Orten. Ich wollte auch alle meine Validierungsanmeldungen in ViewModel schreiben, aber alle diese Zahlenverknüpfung machte meine ViewModel verrückt.

Ich habe dieses Problem durch Erstellen einer Bindung gelöst, die niemals fehlschlägt. Wenn eine Bindung immer erfolgreich ist, muss der Typ die Fehlerbedingungen natürlich ordnungsgemäß behandeln.

Fehlbarer Werttyp

Ich habe mit der Erstellung eines generischen Typs begonnen, der die fehlgeschlagenen Konvertierungen elegant unterstützt:

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

Beachten Sie, dass selbst wenn der Typ aufgrund ungültiger Eingabezeichenfolge (zweiter Konstruktor) nicht initialisiert werden kann, der ungültige Zustand zusammen mitungültigem Textruhig gespeichert wird. Dies ist erforderlich, um den Roundtrip der Bindungauch bei falscher Eingabezu unterstützen.

Generischer Wertkonverter

Ein generischer Wertkonverter könnte mit dem obigen Typ geschrieben werden:

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

XAML-Handykonverter

Da das Erstellen und Verwenden der Generics-Instanzen in XAML sehr schmerzhaft ist, können Sie statische Instanzen gängiger Konverter erstellen:

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

Andere Werttypen können problemlos erweitert werden.

Verwendungszweck

Die Verwendung ist ziemlich einfach, Sie müssen lediglich den Typ von int in Failable<int> ändern: 

ViewModel

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

XAML

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

Auf diese Weise können Sie denselben Überprüfungsmechanismus (IDataErrorInfo oder INotifyDataErrorInfo oder irgendetwas anderes) in ViewModel verwenden, indem Sie die IsValid-Eigenschaft überprüfen. Wenn IsValid wahr ist, können Sie die Value direkt verwenden.

1
Hemant

Hier ist ein Versuch, die Dinge zu vereinfachen, wenn Sie nicht jede Menge zusätzlichen Code implementieren möchten ...

Das Szenario ist, dass Sie in Ihrem Ansichtsmodell eine int -Eigenschaft haben (dies kann dezimal oder ein anderer Nicht-String-Typ sein) und Sie binden ein Textfeld in Ihrer Ansicht daran.

Sie haben eine Validierung in Ihrem Viewmodel, die im Setter der Eigenschaft ausgelöst wird.

In der Ansicht gibt ein Benutzer 123abc ein, und die Ansichtslogik hebt den Fehler in der Ansicht hervor, kann jedoch die Eigenschaft nicht festlegen, da der Wert vom falschen Typ ist. Der Setter wird nie gerufen.

Die einfachste Lösung besteht darin, Ihre int -Eigenschaft im Ansichtsmodell in eine Zeichenfolgeeigenschaft zu ändern und die Werte in das Modell zu importieren und daraus zu entfernen. Dadurch kann der fehlerhafte Text den Setter Ihrer Eigenschaft treffen und Ihr Validierungscode kann dann die Daten prüfen und gegebenenfalls ablehnen.

Die IMHO-Validierung in WPF ist fehlgeschlagen, wie die aufwändige (und geniale) Art und Weise zeigt, wie Menschen versucht haben, das zuvor genannte Problem zu umgehen. Für mich möchte ich nicht viel zusätzlichen Code hinzufügen oder meine eigenen Typenklassen implementieren, damit ein Textfeld überprüft werden kann. Wenn ich also diese Eigenschaften auf Strings stütze, kann ich damit leben, auch wenn es sich anfühlt kludge.

Microsoft sollte das Problem beheben, damit das Szenario einer ungültigen Benutzereingabe in einem Textfeld, das an eine int- oder decimal-Eigenschaft gebunden ist, diese Tatsache auf elegante Weise an das Viewmodel weitergeben kann. Es sollte beispielsweise möglich sein, eine neue gebundene Eigenschaft für ein XAML-Steuerelement zu erstellen, um Überprüfungsfehler bei der Ansichtslogik an eine Eigenschaft im Viewmodel zu übermitteln.

Vielen Dank und Respekt den anderen Jungs, die detaillierte Antworten auf dieses Thema gegeben haben.

0
Richard Moore

Ok, ich glaube, ich habe die Antwort gefunden, nach der Sie gesucht haben ... 
Es wird nicht leicht zu erklären sein - aber ...
Sehr einfach zu verstehen, sobald erklärt ...
Ich denke, es ist sehr genau/"zertifiziert" für MVVM, das als "Standard" oder zumindest als Standardversuch betrachtet wird.

Aber bevor wir beginnen ... müssen Sie ein Konzept ändern, an das Sie sich in Bezug auf MVVM gewöhnt haben:

"Außerdem würde dies die Semantik des Ansichtsmodells ändern. Für mich ist eine Ansicht für das Ansichtsmodell und nicht umgekehrt aufgebaut - natürlich hängt das Design des Ansichtsmodells davon ab, was wir uns für die Ansicht vorstellen do, aber es gibt noch allgemeine Freiheit, wie die Ansicht das tut. "

Dieser Absatz ist die Quelle Ihres Problems. Warum?

Da Sie angeben, hat das Ansichtsmodell keine Rolle, um sich an die Ansicht anzupassen.
Das ist in vielerlei Hinsicht falsch - ich werde es Ihnen ganz einfach beweisen.

Wenn Sie eine Eigenschaft haben wie: 

public Visibility MyPresenter { get...

Was ist Visibility wenn nicht etwas, das der Ansicht dient? 
Der Typ selbst und der Name, der der Eigenschaft zugewiesen wird, wird definitiv für die Ansicht festgelegt.

In MVVM gibt es nach meiner Erfahrung zwei unterscheidbare View-Models-Kategorien:

  • Presenter View Model - das mit Schaltflächen, Menüs, Registerkartenelementen usw. verbunden ist. 
  • Entity View Model (Entitätsansicht-Modell) - Dieses Steuerelement ist an Steuerelemente gebunden, mit denen die Entitätsdaten angezeigt werden.

Dies sind zwei verschiedene - völlig unterschiedliche Anliegen.

Und nun zur Lösung:

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

Und jetzt irgendwo in Ihrem Code - Ihre "CanExecute" -Methode des Schaltflächenbefehls kann zu ihrer Implementierung einen Aufruf von VmEntity.HasErrors hinzufügen.

Und möge ab jetzt Frieden auf Ihrem Code bezüglich der Validierung sein :)

0
G.Y