Eingabeüberprüfung

Erzwingen komplexer Regeln für Geschäftsdaten mit WPF

Brian Noyes

Herunterladen des Codebeispiels.

Microsoft Windows Presentation Foundation (WPF) weist ein umfassendes Datenbindungssystem auf. Abgesehen davon, dass das Datenbindungssystem wesentlich zur lockeren Kopplung der Benutzeroberflächendefinition aus der unterstützenden Logik und den unterstützenden Daten über das MVVM-Muster (Model-View-ViewModel) beiträgt, bietet es auch eine leistungsstarke und flexible Unterstützung für Überprüfungsszenarios für Unternehmensdaten. Der Datenbindungsmechanismus in WPF umfasst mehrere Optionen zur Auswertung der Gültigkeit von Eingabedaten, wenn Sie eine bearbeitbare Ansicht erstellen. Außerdem haben Sie über die WPF-Vorlagen- und Stilfunktionen für Steuerelemente die Möglichkeit, die Art und Weise, wie dem Benutzer Überprüfungsfehler angezeigt werden, ganz einfach anzupassen.

Zur Unterstützung komplexer Regeln und zum Anzeigen von Überprüfungsfehlern für den Benutzer müssen Sie im Allgemeinen eine Kombination der verfügbaren Überprüfungsmechanismen verwenden. Sogar ein scheinbar einfaches Dateneingabeformular kann eine Herausforderung für die Überprüfung darstellen, wenn die Unternehmensregeln komplex werden. Gängige Szenarien umfassen sowohl einfache Regeln auf der Ebene einzelner Eigenschaften sowie untereinander verbundene Eigenschaften, bei denen die Gültigkeit einer Eigenschaft von dem Wert einer anderen Eigenschaft abhängig ist. Mit der Überprüfungsunterstützung in der WPF-Datenbindung können diese Herausforderungen jedoch problemlos gemeistert werden.

In diesem Artikel erfahren Sie, wie die IDataErrorInfo-Schnittstellenimplementierung, ValidationRules, BindingGroups, Ausnahmen und überprüfungsbezogene angefügte Eigenschaften und Ereignisse für Ihre Datenüberprüfungsanforderungen verwendet werden. Sie erfahren auch, wie die Anzeige von Überprüfungsfehlern mit eigenen ErrorTemplates und QuickInfos angepasst werden kann. In diesem Artikel wird davon ausgegangen, dass Sie mit den grundlegenden Datenbindungsfunktionen von WPF vertraut sind. Weitere Informationen zu diesem Thema finden Sie im folgenden Artikel vom Dezember 2007 von John Papa im MSDN Magazin: „Datenbindung in WPF“.

Übersicht über die Datenüberprüfung

Fast jedes Mal, wenn Sie Daten in eine Anwendung eingeben oder darin ändern, müssen Sie sicherstellen, dass die Daten gültig sind, bevor diese zu sehr von der Quelle der Änderungen, in diesem Fall der Benutzer, abweichen. Außerdem müssen Sie Benutzern mitteilen, wenn die eingegebenen Daten ungültig sind, und ihnen vielleicht auch Lösungsvorschläge unterbreiten. Dies ist mit der WPF kein Problem, solange Sie wissen, welche Funktion Sie wann verwenden müssen.

Wenn Sie die Datenbindung in WPF zur Darstellung von Unternehmensdaten nutzen, verwenden Sie in der Regel ein Bindungsobjekt, um eine Datenpipeline zwischen einer einzelnen Eigenschaft in einem Zielsteuerelement und einer Datenquellen-Objekteigenschaft zu schaffen. Damit die Überprüfung relevant ist, sollte eine TwoWay-Datenbindung erfolgen. Dies bedeutet, dass neben den Daten, die zur Anzeige von der Quelleigenschaft zur Zieleigenschaft fließen, die bearbeiteten Daten auch vom Ziel zur Quelle fließen, wie in Abbildung 1 dargestellt.

Figure 1 Data Flow in TwoWay Data Binding
Abbildung 1 Datenfluss in TwoWay-Datenbindung

Es gibt drei Mechanismen, um zu ermitteln, ob über ein datengebundenes Steuerelement eingegebene Daten gültig sind. Diese sind in Abbildung 2 zusammengefasst.

Abbildung 2 Mechanismen für Bindungsüberprüfung

Überprüfungsmechanismus Beschreibung
Ausnahmen Durch Festlegen der ValidatesOnExceptions-Eigenschaft in einem Binding-Objekt wird ein Überprüfungsfehler für diese Bindung festgelegt, wenn während des Festlegens des geänderten Werts in der Quellobjekteigenschaft eine Ausnahme ausgelöst wird.
ValidationRules Die Binding-Klasse besitzt eine Eigenschaft, um eine Sammlung von von ValidationRule abgeleiteten Klasseninstanzen bereitzustellen. Diese ValidationRules müssen eine Validate-Methode überschreiben, die immer dann von der Bindung aufgerufen wird, wenn sich die Daten in dem gebundenen Steuerelement ändern. Wenn die Validate-Methode ein ungültiges ValidationResult-Objekt zurückgibt, wird für diese Bindung ein Überprüfungsfehler festgelegt.
IDataErrorInfo Durch Implementieren der IDataErrorInfo-Schnittstelle in ein gebundenes Datenquellenobjekt und durch Festlegen der ValidatesOnDataErrors-Eigenschaft in einem Binding-Objekt, führt die Bindung Aufrufe der IDataErrorInfo-API aus, die von dem gebundenen Datenquellenobjekt verfügbar gemacht wird. Wenn von diesen Eigenschaftenaufrufen Zeichenfolgen zurückgegeben werden, die nicht Null oder nicht leer sind, wird ein Überprüfungsfehler für diese Bindung festgelegt.

Wenn ein Benutzer Daten in eine TwoWay-Datenbindung eingibt oder darin ändert, wird ein Workflow gestartet:

  • Daten werden vom Benutzer über Tastaturanschläge, Maus-, Touchpad- oder Stiftinteraktionen mit dem Element eingegeben oder geändert, was zu einer Änderung einer Eigenschaft in dem Element führt.
  • Die Daten werden ggf. in den Datenquellen-Eigenschaftentyp umgewandelt.
  • Der Wert für die Quelleigenschaft wird festgelegt.
  • Das mit Binding.SourceUpdated verknüpfte Ereignis wird ausgelöst.
  • Ausnahmen werden von der Bindung abgefangen, wenn diese vom Setter der Datenquelleneigenschaft ausgelöst werden; diese können zur Angabe eines Überprüfungsfehlers verwendet werden.
  • IDataErrorInfo-Eigenschaften werden, falls implementiert, im Datenquellenobjekt aufgerufen.
  • Der Benutzer erhält Überprüfungsfehlerangaben, und das mit Validation.Error verknüpfte Ereignis wird ausgelöst.

Wie Sie sehen können, gibt es mehrere Punkte in dem Prozess, an denen es zu Überprüfungsfehlern kommen kann, je nachdem, für welchen Mechanismus Sie sich entscheiden. In der Liste ist nicht dargestellt, wo die ValidationRules ausgelöst werden. Dies liegt daran, dass sie in Abhängigkeit von dem Wert, den Sie für die ValidationStep-Eigenschaft in der ValidationRule festlegen, an unterschiedlichen Punkten in dem Prozess ausgelöst werden können, beispielsweise vor der Typkonvertierung, nach der Konvertierung, nachdem die Eigenschaft aktualisiert wurde oder wenn der geänderte Wert übermittelt wird (wenn das Datenobjekt IEditableObject implementiert). Der Standardwert ist RawProposedValue (vor der Typkonvertierung). Der Punkt, an dem die Daten aus dem Eigenschaftentyp des Zielsteuerelements in den Eigenschaftentyp des Datenquellobjekts konvertiert werden, erfolgt in der Regel implizit, ohne dass dafür Code bearbeitet werden muss, beispielsweise bei einer numerischen Eingabe in ein TextBox. Dieser Typkonvertierungsprozess kann Ausnahmen auslösen, die verwendet werden sollten, um den Benutzer auf einen Überprüfungsfehler hinzuweisen.

Wenn der Wert nicht einmal in die Quellobjekteigenschaft geschrieben werden kann, handelt es sich eindeutig um eine ungültige Eingabe. Wenn Sie ValidationRules einrichten, werden diese an dem von der ValidationStep-Eigenschaft angegebenen Punkt in dem Prozess aufgerufen; sie können basierend auf der Logik, die darin eingebettet oder daraus aufgerufen wird, Überprüfungsfehler zurückgeben. Wenn der Setter der Quellobjekteigenschaft eine Ausnahme auslöst, muss diese fast immer wie ein Überprüfungsfehler behandelt werden, wie es bei der Typkonvertierung der Fall ist.

Wenn Sie schließlich IDataErrorInfo implementieren, wird die Indexereigenschaft, die Sie dem Datenquellobjekt für diese Schnittstelle hinzufügen, für die Eigenschaft aufgerufen, die festgelegt wurde, um zu sehen, ob basierend auf der von der Schnittstelle zurückgegebenen Zeichenfolge ein Überprüfungsfehler vorliegt. Auf die einzelnen Mechanismen werde ich später noch im Detail eingehen.

Wann die Überprüfung stattfinden soll, ist eine weitere Entscheidung, die Sie treffen müssen. Die Überprüfung findet statt, wenn die Bindung die Daten in die zugrunde liegende Quellobjekteigenschaft schreibt. Wann die Überprüfung stattfindet, wird von der UpdateSourceTrigger-Eigenschaft der Bindung angegeben, die für die meisten Eigenschaften auf PropertyChanged festgelegt ist. Manche Eigenschaften, z. B. TextBox.Text, ändern den Wert in FocusChange; dies bedeutet, dass die Überprüfung stattfindet, wenn der Fokus das Steuerelement verlässt, das zur Bearbeitung der Daten verwendet wird. Der Wert kann auch auf „Explicit“ festgelegt werden, was bedeutet, dass die Überprüfung für die Bindung explizit aufgerufen werden muss. Die BindingGroup, über die ich später in diesem Artikel sprechen werde, verwendet den Modus „Explicit“.

In Überprüfungsszenarien, insbesondere bei TextBoxes, möchten Sie dem Benutzer in der Regel unmittelbares Feedback geben. Um dies zu unterstützen, sollten Sie die UpdateSourceTrigger-Eigenschaft in der Bindung auf PropertyChanged festlegen:

Text="{Binding Path=Activity.Description, UpdateSourceTrigger=PropertyChanged}

Für viele reale Überprüfungsszenarien müssen Sie mehrere dieser Mechanismen anwenden. Jeder Mechanismus hat je nach der Art des Überprüfungsfehlers, der auftritt, und in Abhängigkeit davon, wo die Überprüfungslogik gespeichert werden kann, seine Vor- und Nachteile.

Unternehmensbezogenes Überprüfungsszenario

Damit Sie sich eine konkrete Vorstellung hiervon machen können, werden wir uns ein Bearbeitungsszenario mit einem teilweise realen Unternehmenskontext ansehen. In diesem Szenario können Sie sehen, wie die einzelnen Mechanismen ins Spiel kommen. Dieses Szenario und die Überprüfungsregeln basieren auf einer realen Anwendung, die ich für einen Kunden entwickelt habe. Ein recht einfaches Formular erforderte aufgrund der unterstützenden Unternehmensregeln für die Überprüfung die Verwendung fast aller Überprüfungsmechanismen. Für die einfachere Anwendung in diesem Artikel werde ich die einzelnen Mechanismen verwenden, um ihre jeweilige Verwendung zu demonstrieren, obwohl sie nicht alle explizit erforderlich sind.

Angenommen, Sie müssen eine Anwendung für die Unterstützung von Außendiensttechnikern erstellen, die Supportanrufe beim Kunden zu Hause tätigen (beispielsweise ein Kabelverleger, der aber auch versucht, zusätzliche Funktionen und Services zu verkaufen). Für jede Aktivität, die der Techniker im Außendienst durchführt, muss er einen Aktivitätsbericht eingeben, in dem aufgeführt ist, welche Aufgaben er ausgeführt hat; außerdem werden diese Aktivitäten mit bestimmten Datenstücken verknüpft. Das Objektmodell ist in Abbildung 3 zu sehen.

Figure 3 Object Model for the Sample Application
Abbildung 3 Objektmodell für die Beispielanwendung

Das wichtigste Datenelement, das Benutzer ausfüllen, ist ein Activity-Objekt, einschließlich Title, ActivityDate, ActivityType (eine Dropdownauswahl vordefinierter Aktivitätstypen) sowie einer Description. Die Benutzer müssen die Aktivitäten auch einer von drei Optionen zuweisen. Sie müssen entweder einen Kunden aus einer Liste mit ihnen zugewiesenen Kunden oder ein Ziel des Unternehmens, in dessen Zusammenhang die Aktivität stand, aus einer Liste von Unternehmenszielen auswählen. Es kann auch manuell ein Grund eingegeben werden, wenn weder ein Kunde noch ein Ziel für diese Aktivität zutreffen.

Nachfolgend finden Sie die Überprüfungsregeln, die die Anwendung erzwingen muss:

  • Title und Description sind erforderliche Felder.
  • Das ActivityDate darf nicht mehr als sieben Tage vor dem aktuellen Datum und sieben Tage nach dem aktuellen Datum liegen.
  • Wenn der ActivityType Install ausgewählt wurde, ist das Inventory-Feld erforderlich und sollte die Gegenstände aus dem Transporter des Technikers angeben, die verbraucht wurden. Die Lagervorräte müssen als eine durch Trennzeichen getrennte Liste mit einer erwarteten Modellnummernstruktur für die Eingabeelemente eingegeben werden.
  • Es muss mindestens ein Kunde, ein Ziel oder ein Grund angegeben werden.

Dies mag zwar nach recht einfachen Anforderungen aussehen, die beiden letzten sind aber insbesondere nicht so einfach, da sie auf eine Verbindung zwischen Eigenschaften untereinander hindeuten. Die ausgeführte Anwendung mit einigen ungültigen Daten (durch das rote Kästchen gekennzeichnet) ist in Abbildung 4 dargestellt.

Figure 4 A Dialog Showing ToolTips and Invalid Data
Abbildung 4 Ein Dialogfeld mit QuickInfos und ungültigen Daten

Ausnahmeüberprüfung

Die einfachste Form der Überprüfung besteht darin, dass eine Ausnahme, die während dem Festlegen der Zieleigenschaft ausgelöst wird, als Überprüfungsfehler behandelt wird. Die Ausnahme könnte aus dem Typkonvertierungsprozess resultieren, bevor die Bindung überhaupt die Zieleigenschaft festlegt; sie könnte auch aus dem expliziten Auslösen einer Ausnahme im Eigenschaften-Setter resultieren oder aus einem Aufruf eines Unternehmensobjekts aus dem Setter, wobei die Ausnahme später in dem Stapel ausgelöst wird.

Um diesen Mechanismus zu verwenden, legen Sie die ValidatesOnExceptions-Eigenschaft im Binding-Objekt einfach auf „true“ fest:

Text="{Binding Path=Activity.Title, ValidatesOnExceptions=True}"

Wenn bei dem Versuch, die Quellobjekteigenschaft (in diesem Fall Activity.Title) festzulegen, eine Ausnahme ausgelöst wird, wird in dem Steuerelement ein Überprüfungsfehler festgelegt. Die standardmäßige Kennzeichnung für einen Überprüfungsfehler ist ein roter Rahmen um das Steuerelement, wie in Abbildung 5 dargestellt.

Figure 5 A Validation Error
Abbildung 5 Überprüfungsfehler

Da Ausnahmen während des Typkonvertierungsprozesses auftreten können, empfiehlt es sich, diese Eigenschaft in Eingabebindungen festzulegen, wenn die Möglichkeit besteht, dass die Typkonvertierung fehlschlägt, auch dann, wenn die dahinter liegende Eigenschaft lediglich den Wert in einer Membervariablen ohne Möglichkeit einer Ausnahme festlegt.

Nehmen wir beispielsweise an, Sie verwenden ein TextBox als Eingabesteuerelement für eine DateTime-Eigenschaft. Wenn ein Benutzer eine Zeichenfolge eingibt, die nicht konvertiert werden kann, ist ValidatesOnExceptions die einzige Möglichkeit für die Bindung, einen Fehler anzuzeigen, da die Quellobjekteigenschaft niemals aufgerufen wird.

Wenn Sie in der Ansicht eine spezielle Aufgabe ausführen müssen und ungültige Daten vorhanden sind, beispielsweise einen Befehl deaktivieren, können Sie das angefügte Validation.Error-Ereignis mit dem Steuerelement verknüpfen. Sie müssen auch die NotifyOnValidationError-Eigenschaft in der Bindung auf „true“ festlegen.

<TextBox Name="ageTextBox" 
  Text ="{Binding Path=Age, 
    ValidatesOnExceptions=True, 
    NotifyOnValidationError=True}" 
    Validation.Error="OnValidationError".../>

Überprüfung über ValidationRule

In einigen Szenarien möchten Sie die Überprüfung vielleicht auf der Benutzeroberflächenebene verknüpfen, und Sie benötigen eine kompliziertere Logik, um zu ermitteln, ob die Eingabe gültig ist. Sehen Sie sich in der Beispielanwendung die Überprüfungsregel für das Inventory-Feld an. Wenn Daten eingegeben werden, müssen diese in Form einer durch Trennzeichen getrennten Liste mit Modellnummern vorliegen, die einem bestimmten Muster folgen. Dies kann problemlos in eine ValidationRule integriert werden, da sie voll und ganz von dem festgelegten Wert abhängig ist. Die ValidationRule kann einen string.Split-Aufruf verwenden, um die Eingabe in ein Zeichenfolgenarray umzuwandeln. Anschließend kann ein regulärer Ausdruck verwendet werden, um zu überprüfen, ob die einzelnen Teile mit einem bestimmten Muster übereinstimmen. Hierfür können Sie eine ValidationRule definieren, wie in Abbildung 6 dargestellt.

Abbildung 6 ValidationRule für die Überprüfung eines Zeichenfolgenarrays

public class InventoryValidationRule : ValidationRule {

  public override ValidationResult Validate(
    object value, CultureInfo cultureInfo) {

    if (InventoryPattern == null)
      return ValidationResult.ValidResult;

    if (!(value is string))
      return new ValidationResult(false, 
     "Inventory should be a comma separated list of model numbers as a string");

    string[] pieces = value.ToString().Split(‘,’);
    Regex m_RegEx = new Regex(InventoryPattern);

    foreach (string item in pieces) {
      Match match = m_RegEx.Match(item);
      if (match == null || match == Match.Empty)
        return new ValidationResult(
          false, "Invalid input format");
    }

    return ValidationResult.ValidResult;
  }

  public string InventoryPattern { get; set; }
}

Eigenschaften, die in einer ValidationRule verfügbar gemacht werden, können von der XAML am Punkt der Verwendung festgelegt werden, was eine größere Flexibilität ermöglicht. Diese Überprüfungsregel ignoriert Werte, die nicht in ein Zeichenfolgenarray konvertiert werden können. Wenn die Regel aber string-Split ausführen kann, wird anschließend RegEx verwendet, um zu überprüfen, ob jede Zeichenfolge in der durch Trennzeichen getrennten Liste mit dem Muster übereinstimmt, das von der InventoryPattern-Eigenschaft festgelegt wurde.

Wenn Sie ein ValidationResult zurückgeben und das gültige Flag auf „false“ festgelegt ist, kann die bereitgestellte Fehlermeldung in der Benutzeroberfläche verwendet werden, um den Fehler für den Benutzer anzuzeigen. Darauf werde ich später noch eingehen. Ein Nachteil von ValidationRules besteht darin, dass Sie ein erweitertes Binding-Element in der XAML benötigen, um diese einzurichten. Dies ist im folgenden Code dargestellt:

<TextBox Name="inventoryTextBox"...>
  <TextBox.Text>
    <Binding Path="Activity.Inventory" 
             ValidatesOnExceptions="True" 
             UpdateSourceTrigger="PropertyChanged" 
             ValidatesOnDataErrors="True">
      <Binding.ValidationRules>
        <local:InventoryValidationRule 
          InventoryPattern="^\D?(\d{3})\D?\D?(\d{3})\D?(\d{4})$"/>
      </Binding.ValidationRules>
    </Binding>
  </TextBox.Text>
</TextBox>

In diesem Beispiel werden aufgrund der Tatsache, dass die ValidatesOnExceptions-Eigenschaft auf „true“ festgelegt ist, durch die Bindung nach wie vor Überprüfungsfehler ausgelöst, wenn eine Ausnahme auftritt. Ich unterstütze auch die IDataErrorInfo-Überprüfung basierend auf der Tatsache, dass ValidatesOnDataErrors auf „true“ festgelegt ist. Darüber werde ich als Nächstes sprechen.

Wenn Sie mehrere ValidationRules an dieselbe Eigenschaft angefügt haben, können diese Regeln jeweils unterschiedliche Werte für die ValidationStep-Eigenschaft oder denselben Wert aufweisen. Regeln innerhalb desselben ValidationStep werden in der Reihenfolge der Deklaration ausgewertet. Regeln in früheren ValidationSteps werden also vor den Regeln in späteren ValidationSteps ausgeführt. Was vielleicht nicht ganz so offensichtlich ist, ist die Tatsache, dass, wenn eine ValidationRule einen Fehler zurückgibt, keine der darauffolgenden Regeln ausgewertet wird. Es wird daher nur auf den ersten Überprüfungsfehler hingewiesen, wenn die Fehler aus ValidationRules resultieren.

Überprüfung über IDataErrorInfo

Die IDataErrorInfo-Schnittstelle erfordert, dass der Implementierer eine Eigenschaft und einen Indexer verfügbar macht:

public interface IDataErrorInfo {
  string Error { get; }
  string this[string propertyName] { get; }
}

Die Error-Eigenschaft wird verwendet, um einen Fehler für das Objekt als Ganzes anzugeben, und der Indexer wird verwendet, um Fehler auf den einzelnen Eigenschaftenebenen anzugeben. Beide funktionieren auf die gleiche Art und Weise: Wenn eine Zeichenfolge zurückgegeben wird, die nicht Null oder nicht leer ist, deutet dies auf einen Überprüfungsfehler hin. Außerdem kann die zurückgegebene Zeichenfolge verwendet werden, um den Fehler für den Benutzer anzuzeigen. Darauf werde ich später noch eingehen.

Wenn Sie mit einzelnen Steuerelementen arbeiten, die an einzelne Eigenschaften in einem Datenquellobjekt gebunden sind, ist der Indexer der wichtigste Teil der Schnittstelle. Die Error-Eigenschaft wird nur in Szenarien verwendet, wenn das Objekt in einem DataGrid oder in einer BindingGroup verwendet wird. Die Error-Eigenschaft wird verwendet, um einen Fehler auf Zeilenebene anzugeben, wohingegen der Indexer verwendet wird, um einen Fehler auf Zellebene anzugeben.

Die Implementierung von IDataErrorInfo hat einen großen Nachteil: Die Implementierung des Indexers führt in der Regel zu einer umfangreichen switch-case-Anweisung mit einem case für jeden Eigenschaftsnamen im Objekt, und um einen Fehler anzugeben, müssen Sie basierend auf Zeichenfolgen umschalten und abgleichen und Zeichenfolgen zurückgeben. Außerdem wird die Implementierung von IDataErrorInfo erst aufgerufen, nachdem der Eigenschaftswert bereits in dem Objekt festgelegt wurde. Wenn andere Objekte INotifyPropertyChanged.PropertyChanged in Ihrem Objekt abonniert haben, wurden diese bereits über die Änderung benachrichtigt und hätten basierend auf Daten, die von der IDataErrorInfo-Implementierung gleich als ungültig erklärt werden, mit der Arbeit beginnen können. Wenn das ein Problem für Ihre Anwendung darstellen könnte, müssen Sie Ausnahmen aus den Eigenschaften-Settern auslösen, wenn Sie mit dem festgelegten Wert nicht zufrieden sind.

Der Vorteil von IDataErrorInfo besteht darin, dass untereinander verbundene Eigenschaften einfacher verarbeitet werden können. Abgesehen davon, dass die ValidationRule zur Überprüfung des Eingabeformats des Inventory-Felds verwendet wird, dürfen Sie die Anforderung, dass das Inventory-Feld ausgefüllt sein muss, wenn der ActivityType „Install“ lautet, nicht vergessen. Die ValidationRule selbst hat keinen Zugriff auf die anderen Eigenschaften in dem datengebundenen Objekt. Sie wird nur als Wert übergeben, der für die Eigenschaft festgelegt wird, mit dem die Bindung verknüpft ist. Zur Erfüllung dieser Anforderung müssen Sie, wenn die ActivityType-Eigenschaft festgelegt wird, eine Überprüfung in der Inventory-Eigenschaft auslösen und ein ungültiges Ergebnis zurückgeben, wenn der ActivityType auf „Install“ festgelegt ist, wenn der Wert von Inventory leer ist.

Hierfür benötigen Sie IDataErrorInfo, damit Sie sowohl die Inventory- als auch die ActivityType-Eigenschaft bei der Auswertung von Inventory überprüfen können, wie nachfolgend dargestellt:

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

private string IsValid(string propertyName) {
  switch (propertyName) {
    ...
    case "Inventory":
      if (ActivityType != null && 
        ActivityType.Name == "Install" &&  
        string.IsNullOrWhiteSpace(Inventory))
        return "Inventory expended must be entered for installs";
      break;
}

Außerdem müssen Sie dafür sorgen, dass die Inventory-Bindung eine Überprüfung aufruft, wenn sich die ActivityType-Eigenschaft ändert. Normalerweise fragt eine Bindung nur die IDataErrorInfo-Implementierung ab oder ruft ValidationRules auf, wenn diese Eigenschaft in der Benutzeroberfläche geändert wird. In diesem Fall möchte ich die erneute Auswertung der Bindungsüberprüfung auslösen, obwohl sich die Inventory-Eigenschaft nicht geändert hat, der verknüpfte ActivityType jedoch schon.

Es gibt zwei Möglichkeiten, um zu erreichen, dass die Inventory-Bindung eine Aktualisierung durchführt, wenn sich die ActivityType-Eigenschaft ändert. Die erste und einfachste Möglichkeit besteht darin, das PropertyChanged-Ereignis für Inventory zu veröffentlichen, wenn Sie den ActivityType festlegen:

ActivityType _ActivityType;
public ActivityType ActivityType {
  get { return _ActivityType; }
  set { 
    if (value != _ActivityType) {
      _ActivityType = value;
      PropertyChanged(this, 
        new PropertyChangedEventArgs("ActivityType"));
      PropertyChanged(this, 
        new PropertyChangedEventArgs("Inventory"));
    }
  }
}

Dadurch wird verursacht, dass die Bindung aktualisiert und die Überprüfung dieser Bindung erneut ausgewertet wird.

Die zweite Möglichkeit besteht darin, das angefügte Binding.SourceUpdated-Ereignis mit dem ActivityTyp-Kombinationsfeld oder einem seiner übergeordneten Elemente zu verknüpfen und eine Aktualisierung der Bindung aus dem Code-Behind-Handler für dieses Ereignis auszulösen:

<ComboBox Name="activityTypeIdComboBox" 
  Binding.SourceUpdated="OnPropertySet"...

private void OnPropetySet(object sender, 
  DataTransferEventArgs e) {

  if (activityTypeIdComboBox == e.TargetObject) {
    inventoryTextBox.GetBindingExpression(
      TextBox.TextProperty).UpdateSource();
  }
}

Durch Aufrufen von UpdateSource in einer Bindung wird der aktuelle Wert in dem gebundenen Zielelement programmgesteuert in die Quelleigenschaft geschrieben, wodurch die Überprüfungskette so ausgelöst wird, als hätte der Benutzer das Steuerelement gerade bearbeitet.

Verwenden von BindingGroup für untereinander verbundene Eigenschaften

Die BindingGroup-Funktion wurde in Microsoft .NET Framework 3.5 SP1 hinzugefügt. Eine BindingGroup ist speziell so konzipiert, dass Sie die Überprüfung in einer Gruppe von Bindungen gleichzeitig auswerten können. Sie könnten einem Benutzer beispielsweise ermöglichen, ein ganzes Formular auszufüllen, und warten, bis er die Schaltfläche zum Übermitteln oder Speichern gedrückt hat, um die Überprüfungsregeln für das Formular auszuwerten, und dann die Überprüfungsfehler alle gleichzeitig anzeigen. In der Beispielanwendung hatte ich die Anforderung, dass mindestens ein Kunde, Ziel oder Grund angegeben werden musste. Eine BindingGroup kann auch verwendet werden, um eine Teilmenge eines Formulars auszuwerten.

Um eine BindingGroup zu verwenden, benötigen Sie eine Gruppe von Steuerelementen mit normalen Bindungen, die ein Vorgängerelement gemeinsam verwenden. In der Beispielanwendung befinden sich die Kombinationsfelder für den Kunden, das Ziel oder den Grund alle in demselben Raster für das Layout. BindingGroup ist eine Eigenschaft in FrameworkElement. Sie weist eine ValidationRules-Auflistungseigenschaft auf, die Sie mit einem oder mehreren ValidationRule-Objekten auffüllen können. In der folgenden XAML ist die BindingGroup-Einrichtung für die Beispielanwendung dargestellt:

<Grid>...
<Grid.BindingGroup>
  <BindingGroup>
    <BindingGroup.ValidationRules>
      <local:CustomerObjectiveOrReasonValidationRule 
        ValidationStep="UpdatedValue" 
        ValidatesOnTargetUpdated="True"/>
    </BindingGroup.ValidationRules>
  </BindingGroup>
</Grid.BindingGroup>
</Grid>

In diesem Beispiel habe ich der Auflistung eine Instanz der CustomerObjectiveOrReasonValidationRule hinzugefügt. Mit der ValidationStep-Eigenschaft erhalten Sie mehr Kontrolle über den Wert, der an die Regel übergeben wird. UpdatedValue bedeutet, dass der Wert, der in das Datenquellobjekt geschrieben wurde, verwendet wird, nachdem er geschrieben wurde. Sie können auch Werte für ValidationStep, die eine Verwendung der Eingabe des Benutzers ermöglichen, den Wert, nachdem die Typ- und Wertkonvertierung angewendet wurde, oder den „gespeicherten“ Wert auswählen, was bedeutet, dass die IEditableObject-Schnittstelle für transaktionale Änderungen an den Eigenschaften Ihres Objekts implementiert wird.

Das ValidatesOnTargetUpdated-Flag verursacht, dass die Regel jedes Mal, wenn die Zieleigenschaft über die Bindungen festgelegt wird, ausgewertet wird. Dazu gehört auch der Zeitpunkt, zu dem sie das erste Mal festgelegt wird, es gibt also unmittelbar Überprüfungsfehlerangaben, wenn die Anfangsdaten ungültig sind, und auch jedes Mal, wenn der Benutzer die Werte in den Steuerelementen ändert, die Teil der BindingGroup sind.

Eine ValidationRule, die mit einer BindingGroup verknüpft ist, funktioniert etwas anders als eine ValidationRule, die mit einer einzigen Bindung verknüpft ist. Abbildung 7 zeigt die ValidationRule, die mit der im vorherigen Codebeispiel dargestellten BindingGroup verknüpft ist.

Abbildung 7 ValidationRule für eine BindingGroup

public class CustomerObjectiveOrReasonValidationRule : 
  ValidationRule {

  public override ValidationResult Validate(
    object value, CultureInfo cultureInfo) {

    BindingGroup bindingGroup = value as BindingGroup;
    if (bindingGroup == null) 
      return new ValidationResult(false, 
        "CustomerObjectiveOrReasonValidationRule should only be used with a BindingGroup");

    if (bindingGroup.Items.Count == 1) {
      object item = bindingGroup.Items[0];
      ActivityEditorViewModel viewModel = 
        item as ActivityEditorViewModel;
      if (viewModel != null && viewModel.Activity != null && 
        !viewModel.Activity.CustomerObjectiveOrReasonEntered())
        return new ValidationResult(false, 
          "You must enter one of Customer, Objective, or Reason to a valid entry");
    }
    return ValidationResult.ValidResult;
  }
}

In einer ValidationRule, die mit einer einzelnen Bindung verknüpft ist, ist der übergebene Wert der einzige Wert aus der Datenquelleigenschaft, der als Pfad der Bindung festgelegt ist. Im Fall einer BindingGroup ist der Wert, der an die ValidationRule übergeben wird, die BindingGroup selbst. Er enthält eine Items-Auflistung, die vom DataContext des enthaltenden Elements aufgefüllt wird, in diesem Fall das Raster.

Für die Beispielanwendung verwende ich das MVVM-Muster, der DataContext der Ansicht ist also das ViewModel selbst. Die Items-Auflistung enthält nur einen einzigen Verweis auf das ViewModel. Aus dem ViewModel kann ich die die Activity-Eigenschaft darin aufrufen. Die Aktivitätsklasse weist in diesem Fall die Überprüfungsmethode auf, die bestimmt, ob mindestens ein Kunde, Ziel oder Grund eingegeben wurde, daher muss ich diese Logik in der ValidationRule nicht duplizieren.

Wie bei den zuvor behandelten ValidationRules können Sie ein ValidationResult.ValidResult zurückgeben, wenn Sie mit den Werten der übergebenen Daten zufrieden sind. Wenn Sie nicht zufrieden sind, erstellen Sie ein neues ValidationResult mit einem gültigen Flag, das auf „false“ festgelegt ist, und einer Zeichenfolgenmeldung, die das Problem angibt, die dann zu Anzeigezwecken verwendet werden kann.

Das Festlegen des ValidatesOnTargetUpdated-Flags reicht jedoch nicht aus, um zu erreichen, dass die ValidationRules automatisch ausgelöst werden. Die BindingGroup wurde basierend auf dem Konzept der expliziten Auslösung der Überprüfung für eine ganze Gruppe von Steuerelementen entwickelt, in der Regel durch ein Ereignis wie das Klicken auf die Schaltfläche „Übermitteln“ und „Speichern“ in einem Formular. In manchen Szenarien möchten Benutzer erst über Überprüfungsfehler benachrichtigt werden, wenn sie den Bearbeitungsvorgang als abgeschlossen betrachten, die BindingGroup ist daher auf diesen Ansatz ausgerichtet.

In der Beispielanwendung möchte ich dem Benutzer jedes Mal, wenn etwas in dem Formular geändert wird, unmittelbar Feedback zu Überprüfungsfehlern geben. Um dies mit einer BindingGroup zu erzielen, müssen Sie das entsprechende Änderungsereignis in den einzelnen Eingabesteuerelementen, die Teil der Gruppe bilden, verknüpfen und bewirken, dass der Ereignishandler für diese Ereignisse die Auswertung der BindingGroup auslöst. In der Beispielanwendung bedeutet dies, dass das ComboBox.SelectionChanged-Ereignis in den beiden ComboBoxes und das TextBox.TextChanged im TextBox verknüpft werden. Diese können auf eine einzelne Behandlungsmethode im Code-Behind zeigen:

private void OnCommitBindingGroup(
  object sender, EventArgs e) {

  CrossCoupledPropsGrid.BindingGroup.CommitEdit();
}

Beachten Sie, dass für die Überprüfungsanzeige der standardmäßige rote Rahmen im FrameworkElement angezeigt wird, in dem sich die BindingGroup befindet, wie z. B. das Raster in der Beispielanwendung, wie in Abbildung 4 dargestellt. Sie können auch ändern, wo die Überprüfungshinweise angezeigt werden, indem Sie die angefügte Validation.ValidationAdornerSite- und die angefügte Validation.ValidationAdornerSiteFor-Eigenschaft verwenden. Standardmäßig werden in den einzelnen Steuerelementen auch rote Rahmen für die einzelnen Überprüfungsfehler angezeigt. In der Beispielanwendung habe ich diese Rahmen deaktiviert, indem ich die ErrorTemplate über Stile auf Null festgelegt habe.

Bei der BindingGroup in .NET Framework 3.5 SP1 treten möglicherweise Probleme mit der korrekten Anzeige von Überprüfungsfehlern beim anfänglichen Laden des Formulars auf, auch wenn Sie die ValidatesOnTargetUpdated-Eigenschaft in der ValidationRule festlegen. Eine Problemumgehung, die ich gefunden habe, besteht darin, eine der gebundenen Eigenschaften in der BindingGroup „loszurütteln“. In der Beispielanwendung könnten Sie ein Leerzeichen am Ende des Texts, der zu Beginn in dem TextBox im Loaded-Ereignis der Ansicht vorhanden ist, folgendermaßen hinzufügen und entfernen:

string originalText = m_ProductTextBox.Text;
m_ProductTextBox.Text += " ";
m_ProductTextBox.Text = originalText;

Dadurch wird bewirkt, dass die ValidationRules der BindingGroup ausgelöst werden, da sich eine der enthaltenen Binding-Eigenschaften geändert hat, was dazu führt, dass die Überprüfung für die einzelnen Bindungen aufgerufen wird. Dieses Verhalten wurde in .NET Framework 4.0 korrigiert, die Problemumgehung sollte also nicht erforderlich sein, um eine anfängliche Anzeige von Überprüfungsfehlern zu erhalten. Legen Sie einfach die ValidatesOnTargetUpdated-Eigenschaft in den Überprüfungsregeln auf „true“ fest.

Anzeige von Überprüfungsfehlern

Wie zuvor bereits erwähnt, besteht die standardmäßige Vorgehensweise zum Anzeigen von Überprüfungsfehlern in WPF darin, einen roten Rahmen um das Steuerelement zu zeichnen. In manchen Fällen wünschen Sie sich vielleicht eine Anpassung dieser Vorgehensweise, um Fehler anders anzuzeigen. Außerdem wird die mit dem Überprüfungsfehler verknüpfte Fehlermeldung nicht standardmäßig angezeigt. Eine häufige Anforderung besteht darin, die Fehlermeldung in einer QuickInfo nur dann anzuzeigen, wenn ein Überprüfungsfehler auftritt. Das Anpassen der Überprüfungsfehleranzeige ist dank einer Kombination von Stilen und einem Satz angefügter Eigenschaften, die mit der Überprüfung verknüpft sind, ganz einfach.

Das Hinzufügen einer QuickInfo, die den Fehlertext anzeigt, ist ganz einfach. Sie müssen einfach einen Stil definieren, der auf das Eingabesteuerelement angewendet wird, der die ToolTip-Eigenschaft im Steuerelement immer dann auf den Überprüfungsfehlertext festlegt, wenn ein Überprüfungsfehler vorhanden ist. Um dies zu unterstützen, gibt es zwei angefügte Eigenschaften, die Sie verwenden müssen: Validation.HasError und Validation.Errors. Nachfolgend ist ein Stil dargestellt, der auf den TextBox-Typ abzielt, der den ToolTip festlegt:

<Style TargetType="TextBox">
  <Style.Triggers>
    <Trigger Property="Validation.HasError" 
             Value="True">
      <Setter Property="ToolTip">
        <Setter.Value>
          <Binding 
            Path="(Validation.Errors).CurrentItem.ErrorContent"
            RelativeSource="{x:Static RelativeSource.Self}" />
        </Setter.Value>
      </Setter>
    </Trigger>
  </Style.Triggers>
</Style>

Sie können sehen, dass der Stil lediglich einen Eigenschaftsauslöser für die angefügte Validation.HasError-Eigenschaft enthält. Die HasError-Eigenschaft wird auf „true“ festgelegt, wenn eine Bindung ihre Quellobjekteigenschaft aktualisiert und die Überprüfungsmechanismen einen Fehler generieren. Dies könnte auf eine Ausnahme, eine ValidationRule oder einen IDataErrorInfo-Aufruf zurückzuführen sein. Der Stil verwendet dann die angefügte ValidationErrors-Eigenschaft, die eine Auflistung von Fehlerzeichenfolgen enthält, wenn ein Überprüfungsfehler vorhanden ist. Sie können die CurrentItem-Eigenschaft in diesem Auflistungstyp verwenden, um nur die ersten Zeichenfolge in der Auflistung aufzurufen. Oder Sie können etwas entwerfen, was Daten an die Auflistung bindet und die ErrorContent-Eigenschaft für jedes Element in einem listenorientierten Steuerelement anzeigt.

Um die standardmäßige Überprüfungsfehleranzeige für ein Steuerelement in etwas anderes als den roten Rahmen zu ändern, müssen Sie die angefügte Validation.ErrorTemplate-Eigenschaft auf eine neue Vorlage in dem Steuerelement festlegen, das Sie anpassen möchten. In der Beispielanwendung wird anstelle eines roten Rahmens ein kleiner Kreis mit einem roten Farbverlauf rechts neben jedem Steuerelement mit einem Fehler angezeigt. Hierfür definieren Sie eine Steuerelementvorlage, die als ErrorTemplate verwendet wird.

<ControlTemplate x:Key="InputErrorTemplate">
  <DockPanel>
    <Ellipse DockPanel.Dock="Right" Margin="2,0" 
             ToolTip="Contains invalid data"
             Width="10" Height="10">
      <Ellipse.Fill>
        <LinearGradientBrush>
          <GradientStop Color="#11FF1111" Offset="0" />
          <GradientStop Color="#FFFF0000" Offset="1" />
        </LinearGradientBrush>
      </Ellipse.Fill>
    </Ellipse>
    <AdornedElementPlaceholder />
  </DockPanel>
</ControlTemplate>

Um diese Steuerelementvorlage mit einem Steuerelement zu verknüpfen, müssen Sie einfach die Validation.ErrorTemplate-Eigenschaft für das Steuerelement festlegen; auch dies ist wieder über einen Stil möglich:

<Style TargetType="TextBox">
  <Setter Property="Validation.ErrorTemplate" 
    Value="{StaticResource InputErrorTemplate}" />
  ...
</Style>

Zusammenfassung

In diesem Artikel habe ich Ihnen gezeigt, wie Sie die drei Überprüfungsmechanismen der WPF-Datenbindung für unterschiedlichen Überprüfungsszenarien für Unternehmensdaten verwenden können. Sie haben erfahren, wie Ausnahmen, ValidationRules und die IDataErrorInfo-Schnittstelle für die Überprüfung einer einzelnen Eigenschaft sowie für Eigenschaften verwendet werden können, deren Überprüfungsregeln von den aktuellen Werten anderer Eigenschaften im Steuerelement abhängig sind. Außerdem wurde behandelt, wie BindingGroups zur Auswertung mehrerer Bindungen gleichzeitig verwendet werden und wie die Fehleranzeige über die standardmäßigen Einstellungen von WPF hinaus angepasst werden können.

Die Beispielanwendung für diesen Artikel weist den vollständigen Überprüfungssatz auf, der den beschriebenen Unternehmensregeln in einer einfachen Anwendungen genügt, die MVVM verwendet, um die Ansicht mit den Daten zu verknüpfen, die dies unterstützen.         

Brian Noyes* ist leitender Architekt bei IDesign (idesign.net), Microsoft Regional Director und Microsoft-MVP. Er ist Autor und hält häufig Vorträge bei Microsoft Tech Ed, DevConnections, DevTeach und anderen internationalen Konferenzen. Sie können ihn über seinen Blog unter briannoyes.net erreichen*.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels:*** Sam Bent***