Data Validation in WPF

Veröffentlicht von

In WPF gibt es verschiedene Wege Eingaben zu validieren. Ich möchte in diesem Artikel drei verschiedene Wege vorstellen: ValidatesOnExceptions, ValidationRule und IDataErrorInfo. Die Tatsache, dass es verschiedene Wege gibt, macht einem es erstmal nicht einfach sich für eine Variante zu entscheiden.

Jede Methode hat ihre Vor- und Nachteile oder es ist einfach auch Geschmackssache, was man bevorzugt. Das gesamte Beispielprojekt gibt es am Ende zum Download. Neben den verschiedenen Möglichkeiten zur Validierung, schauen wir uns am Ende noch an, wie wir das Design der Eingabefelder im Fehlerfall anpassen können.

ValidatesOnExceptions – Validierung mit Exceptions

Die Validierung mit Exceptions ist eine sehr einfach Möglichkeit die Validierung zu gestalten. Im Beispiel gehen wir von folgendem Dialog aus:

Der Benutzername soll mindestens 5 Zeichen lang sein. Diese einfach Art der Validierung passiert direkt im Modell:

public string Username
{
    get
    {
        return _username;
    }
    set
    {
        ValidateUserName(value);
        _username = value;
        NotifyPropertyChanged();
    }
}

Bevor der Name in das Property geschrieben wird überprüfen wir diesen. Ich habe dies in eine extra Methode ausgelagert:

private static void ValidateUserName(string value)
{
    if (value.Length < 5)
    {
        throw new ArgumentException("Username must be length > 5.");
    }
}

Die Exception wird im Fehlerfall geworden und verhindert, dass der Wert überhaupt ins Modell gelangt. Ein großer Vorteil dieser Methode. Im Xaml-Code müssen wir nun definieren, dass wir diese Art der Validierung verwenden:

Text="{Binding Username, UpdateSourceTrigger=PropertyChanged, ValidatesOnExceptions=True}"

ValidatesOnExceptions“ wird auf True gesetzt. Die Überprüfung kann also direkt im Modell erfolgen und beispielsweise können auch andere abhängige Properties in die Überprüfung mit eingezogen werden. Der Nachteil dieser Variante ist der gleich, wir haben Logik im Modell. Auch in Sachen Wiederverwendungbarkeit ist es nicht das Gelbe vom Ei.

Überprüfung mit ValidationRule

Die nächste Überprüfung ist die Verwendung einer ValidationRule. Die Überprüfung wird hierbei in eine extra Klasse ausgelagert, welche von ValidationRule abgeleitet ist. Die gleiche Überprüfung wie oben:

public class UsernameValidationRule : ValidationRule
{
    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        string username = (string)value;

        if (username.Length < Length)
        {
            return new ValidationResult(false, "Username must be length >= 5");
        } else
        {
            return new ValidationResult(true, null);
        }
    }

    public int Length { get; set; } = 5;
}

Zusätzlich haben wir hier noch ein Property definiert. Hier können wir die Überprüfung der Länge als Parameter übergeben. Die Methode zur Überprüfung gibt es „ValidationResult“ Objekt zurück. Hier geben wir an, ob die Überprüfung erfolgreich war und können auch eine Fehlermeldung zurück geben.

Im Xaml-Code binden wir die Überprüfung wie folgt ein:

<TextBox x:Name="TextBoxName"
         Validation.ErrorTemplate="{StaticResource ValidationTemplate}"
         Margin="10,0,0,0"
         Width="400">
    <TextBox.Text>
        <Binding Path="Username"
                 UpdateSourceTrigger="PropertyChanged">
            <Binding.ValidationRules>
                <local:UsernameValidationRule Length="5"></local:UsernameValidationRule>
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

Wir benötigen hier etwas mehr Xaml-Code für die längere Syntax. Die Länge wird als Parameter übergeben. Die Übergabe des Parameters geht an der Stelle nur statisch im Xaml-Code. Aber immerhin.

Die Variante hat Vorteile: die ValidationRule kann wiederverwendet werden und an unterschiedlichen Stellen zum Einsatz kommen. Die Logik für die Überprüfung ist nicht im Modell. Einen Nachteil hat die Methode jedoch: der Kontext fehlt. Der ValidationRule wird beim Aufruf nur der zu überprüfende Parameter übergeben:

ValidationResult Validate(object value, CultureInfo cultureInfo)

Dieser kann überprüft werden, aber ohne Kontext und Abhängigkeiten. Quasi man wird auf der Straße gefragt ob 42 ein guter Wert sei, aber man nicht weiß in welchem Zusammenhang. 🙂

Für einfache wiederverwendbare Überprüfungen aber perfekt.

Überprüfung mit IDataErrorInfo

IDataErrorInfo ist mein Favorit. Durch die Implementierung des Interfaces können wir ein Objekt validieren. Das Interface fügt dem Objekt einen Indexer hinzu, welcher die Überprüfung vornimmt. Wir erweitern das Beispiel um ein weiteres Property:

public string Username
{
    get
    {
        return _username;
    }
    set
    {
        if (_username != value)
        {
          _username = value;
          NotifyPropertyChanged();
        }
    }
}

private int _age = 0;

public int Age
{
    get
    {
        return _age;
    }

    set
    {
        if (_age != value)
        {
            _age = value;
            NotifyPropertyChanged();
        }
    }
}

Die Überprüfung:

public string Error { get; set; } = "";

public string this[string propertyName]
{
    get
    {
        return GetErrorForProperty(propertyName);
    }
}

public string this[string columnName]“ aus dem Interface habe ich zu „public string this[string propertyName]“ abgeändert, da wir hier Properties überprüfen und keine Spalten.

Auch hier lagere ich die Überprüfung wieder in eine extra Methode aus:

private string GetErrorForProperty(string propertyName)
{
    Error = "";

    switch (propertyName)
    {
        case "Username":
            if (_username.Length < 5)
            {
                Error = "Username length must be >= 5";
                return Error;
            }                    
            break;
        case "Age":
            if (_age < 10 || _age > 99)
            {
                Error = "Age must be between 10 and 99";
                return Error;
            }
            break;
    }

    return string.Empty;
}

Jedes Property des Objekts wird hier überprüft und ggf. eine Fehlermeldung zurück gegeben. Dazu verwende ich das Error-Property. Das ist so nicht notwendig, da Error normalerweise die Fehlermeldung für das gesamte Objekt enthält. Im Beispiel verwende ich die Eigenschaft um beim Schließen zu Überprüfen, ob ein Fehler vorliegt.

protected override void OnClosing(CancelEventArgs e)
{
    if (this.DialogResult == true)
    {
        if (!string.IsNullOrEmpty(Error))
        {
            MessageBox.Show("Please correct the input first.");
            e.Cancel = true;
        }
    }
}

Im Xaml-Code aktiveren wir die Überprüfung indem wir „ValidatesOnDataErrors“ auf true setzen.

Text="{Binding Age, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}"

Dies ließe sich natürlich auch anders lösen. Ich halte die Methode für die flexibelste Variante von allen, da ich hier auch den Kontext überprüfen kann. Der Nachteil der Methode ist, dass der Wert vor der Überprüfung bereits in das zu prüfende Property wandert.

Darstellung in der GUI

Standardmäßig werden nicht validierte Felder in WPF mit einem roten Rahmen versehen:

Dies erlaubt zwar recht leicht zu erkennen, dass hier etwas nicht korrekt ist, aber es gibt keine Ausgabe was hier verkehrt ist. Dies können wir mit einem „ControlTemplate“ beheben.

Beispiel 1: Tooltip

Definieren wir folgendes ControlTemplate:

<ControlTemplate x:Key="ValidationTemplate">
    <Border BorderBrush="Red"
            BorderThickness="1">
        <Grid Background="Transparent"
              ToolTip="{Binding Path=/ErrorContent}">
            <AdornedElementPlaceholder />
        </Grid>
    </Border>
</ControlTemplate>	

Unserer Textbox fügen wir das Template im Xaml-Code hinzu:

Validation.ErrorTemplate="{StaticResource ValidationTemplate}"

Dieses fügt einen roten Rahmen um unsere Textbox und ein Grid mit einem Tooltip zur Fehlerausgabe. Wichtig ist hier der „AdornedElementPlaceholder„, dies ist der Platzhalter für das ursprüngliche Control, in unserem Fall die Textbox, welche an diesem Ort wieder dargestellt wird.

Das Ergebnis:

Beipiel 2: Rote Markierung neben Textfeld

Ein weiteres Beispiel:

<ControlTemplate x:Key="ValidationTemplate">
    <StackPanel Orientation="Horizontal">
        <AdornedElementPlaceholder />
        <Ellipse Fill="Red"
                 Width="10"
                 Height="10"
                 ToolTip="{Binding Path=/ErrorContent}"
                 Margin="5"></Ellipse>
    </StackPanel>
</ControlTemplate>

Hier wird neben dem Control ein roter Punkt erzeugt. Geht der Benutzer mit der Maus auf diesen Punkt, dann wird ein Tooltip mit der Fehlermeldung angezeigt.

![](wpf_validation_5.jpg „“)

Überprüfung beim Schließen vom Fenster

Die Überprüfung arbeitet erstmal nur einzeln auf den Steuerelementen. Nun möchte man aber zum Beispiel, dass der Anwender ein Fenster nicht schließen kann, wenn einzelne Validierungen fehlschlagen. Dies können wir durch eine explizite Überprüfung beim Schließen umsetzen:

protected override void OnClosing(CancelEventArgs e)
{
    TextBoxName.GetBindingExpression(TextBox.TextProperty).UpdateSource();
    bool hasError = (bool)TextBoxName.GetValue(Validation.HasErrorProperty);

    if (this.DialogResult == true && hasError)
    {
        MessageBox.Show("There are still errors, cannot close");
        e.Cancel = true;
    }
}

Zuerst updaten wir das Binding:

TextBoxName.GetBindingExpression(TextBox.TextProperty).UpdateSource();

und anschließend überprüfen wir, ob das Binding einen Fehler hat:

bool hasError = (bool)TextBoxName.GetValue(Validation.HasErrorProperty);

Fazit

Dies waren ein paar einfache Beispiele, wie wir eine Validierung in WPF umsetzen können. Es gibt noch weitere Möglichkeiten. Für asynchrone Überprüfungen bietet sich „INotifyDataErrorInfo“ an. Auch DataAnnotations bieten eine weitere Möglichkeit.

Download des Beispiels

4 Kommentare

  1. Wenn man das „ValidationTemplate“ aus Beispiel 1 so wie hier übernimmt, dann ist es bei einer Fehlerhaften Eingabe nicht mehr möglich in das Textfeld zu klicken, da das Grid aus dem Template „im Weg“ ist. (Man kann theoretisch noch reintabben).
    Das „IsHitTestVisible“ Property auf ‚false‘ stellen bringt auch nichts, da der Tooltip dann gar nicht mehr angezeigt wird (Das hovern über dem Grid wird ja in dem Fall gar nicht mehr wahrgenommen).

    Teilweise Lösung:
    Wenn man das Style von dem Tooltip nicht verändern möchte reicht es, direkt das Tooltip Property von der jeweiligen Textbox auf „Error“ zu binden.
    Also z.B.:

    1. Textbox Text=“{Binding [Name des Properties]“ ToolTip=“{Binding Error}“

      (Und halt die Spitzen Klammern außen drum. Wenn ich die hier im Kommentar mit rein packe, wird die ganze Zeile nicht mit angezeigt :/ )

  2. Sehr schöner Artikel. Vielen Dank.
    An einem Schönheitsfehler hänge ich jedoch und finde keine Lösung:
    Die Grenzwerte sind fest verdrahtet.
    Ich möchte diese aber gerne dynamisch aus einer Variablen / Eigenschaft im DataContext an die Fehlerprüfung übergeben. Im Markup ist der DataContext aber nicht mehr bekannt bzw. nicht der ValidationRule zu übergeben. Sämtliche Bindingversuche sind misslungen.
    Hast Du dazu vielleicht eine Idee? Das ist doch eigentlich Standard, warum gibt es dazu keine Standardlösung? Alle Beispiele die ich gesehen habe, arbeiten mit Hardcoding. Vielen Dank und besten Grüße

Kommentar hinterlassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert