2012-04-26 3 views
18

Ich beginne mit der Verwendung von ValidationRules in meiner WPF-Anwendung, aber ziemlich verwirrt.ValidationRule mit ValidationStep = "UpdatedValue" wird mit BindingExpression anstelle des aktualisierten Werts aufgerufen

Ich habe die folgende einfache Regel:

class RequiredRule : ValidationRule 
{ 
    public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo) 
    { 
     if (String.IsNullOrWhiteSpace(value as string)) 
     { 
      return new ValidationResult(false, "Must not be empty"); 
     } 
     else 
     { 
      return new ValidationResult(true, null); 
     } 

    } 
} 

in XAML wie folgt verwendet:

<TextBox> 
    <TextBox.Text> 
     <Binding Path="Identity.Name"> 
      <Binding.ValidationRules> 
       <validation:RequiredRule/> 
      </Binding.ValidationRules> 
     </Binding> 
    </TextBox.Text> 
</TextBox> 

Diese meist funktioniert, wie ich erwarten würde. Ich war überrascht zu sehen, dass meine Quelleigenschaft (Identity.Name) nicht gesetzt wurde; Ich habe eine Rückgängig-Funktion, die nie die Änderung sieht, und es gibt keine Möglichkeit, den Wert zurückzusetzen, außer es neu einzugeben (nicht gut).

Microsofts Data Binding Overview beschreibt den Validierungsprozess im unteren Bereich, der dieses Verhalten sehr gut erklärt. Basierend darauf würde ich meinen ValidationStep auf UpdatedValue setzen lassen wollen.

<validation:RequiredRule ValidationStep="UpdatedValue"/> 

Dies ist, wo Dinge für mich komisch werden. Anstatt, dass Validate() aufgerufen wird, wobei der Objektwert der Eigenschaftswert ist, der gesetzt wurde (d. H. Eine Zeichenfolge), erhalte ich eine System.Windows.Data.BindingExpression! Ich sehe nichts in der Microsoft-Dokumentation, die dieses Verhalten beschreibt.

Im Debugger kann ich das Quellobjekt (DataContext von TextBox) sehen, den Pfad zur Eigenschaft navigieren und sehen, dass der Wert festgelegt wurde. Ich sehe jedoch keine gute Möglichkeit, innerhalb der Validierungsregel zur richtigen Eigenschaft zu gelangen.

Hinweis: Mit ValidationStep als ConvertedProposedValue, bekomme ich die eingegebene Zeichenfolge (ich habe keinen Konverter in Verwendung), aber es blockiert auch die Aktualisierung der Quelleigenschaften, wenn die Validierung wie erwartet fehlschlägt. Mit CommittedValue bekomme ich die BindingExpression anstelle der Zeichenfolge.

Es gibt mehrere Fragen hier:

  1. Warum erhalte ich einen inkonsistentes Argument Typen() auf der Grundlage der ValidationStep Einstellung zu bestätigen weitergegeben werden?

  2. Wie kann ich den tatsächlichen Wert von der BindingExpression erhalten?

  3. Gibt es alternativ eine gute Möglichkeit, dem Benutzer die TextBox in den vorherigen (gültigen) Status zurückzusetzen? (Wie ich bereits erwähnt, meine eigene Undo-Funktion sieht nie die Änderung.)

+0

Ich bin überrascht, überhaupt keine Rückmeldung zu sehen, vor allem, da dies aufgewertet wurde. Für mich scheint dies der Höhepunkt des ValidationRule-Ansatzes zu sein, der ansonsten sehr gut und intuitiv aussieht. Ist es besser, IDataErrorInfo zu verwenden, obwohl es im Vergleich klobig erscheint? – mbmcavoy

Antwort

13

Ich habe das Problem gelöst, den Wert von BindingExpression mit einer kleinen Einschränkung zu extrahieren.

Zuerst etwas vollständigere XAML:

<Window x:Class="ValidationRuleTest.MainWindow" 
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
     xmlns:local="clr-namespace:ValidationRuleTest" 
     Title="MainWindow" Height="100" Width="525"> 
    <Window.DataContext> 
     <local:MainWindowViewModel/> 
    </Window.DataContext> 
    <Grid> 
     <Grid.RowDefinitions> 
      <RowDefinition Height="Auto"/> 
      <RowDefinition Height="Auto"/> 
     </Grid.RowDefinitions> 
     <Grid.ColumnDefinitions> 
      <ColumnDefinition Width="50"/> 
      <ColumnDefinition Width="*"/> 
     </Grid.ColumnDefinitions> 
     <TextBlock Text="String 1"/> 
     <TextBox Grid.Column="1"> 
      <TextBox.Text> 
       <Binding Path="String1" UpdateSourceTrigger="PropertyChanged"> 
        <Binding.ValidationRules> 
         <local:RequiredRule ValidationStep="RawProposedValue"/> 
        </Binding.ValidationRules> 
       </Binding> 
      </TextBox.Text> 
     </TextBox> 
     <TextBlock Text="String 2" Grid.Row="1"/> 
     <TextBox Grid.Column="1" Grid.Row="1"> 
      <TextBox.Text> 
       <Binding Path="String2" UpdateSourceTrigger="PropertyChanged"> 
        <Binding.ValidationRules> 
         <local:RequiredRule ValidationStep="UpdatedValue"/> 
        </Binding.ValidationRules> 
       </Binding> 
      </TextBox.Text> 
     </TextBox> 
    </Grid> 
</Window> 

Beachten Sie, dass die erste TextBox verwendet ValidationStep="RawProposedValue" (Standardeinstellung), während der zweite ValidationStep="UpdatedValue" verwendet, aber beide die gleiche Gültigkeitsregel.

Ein einfaches Viewmodel (INPC und andere nützliche Dinge zu vernachlässigen):

class MainWindowViewModel 
{ 
    public string String1 
    { get; set; } 

    public string String2 
    { get; set; } 
} 

Und schließlich die neue RequiredRule:

class RequiredRule : ValidationRule 
{ 
    public override ValidationResult Validate(object value, 
     System.Globalization.CultureInfo cultureInfo) 
    { 
     // Get and convert the value 
     string stringValue = GetBoundValue(value) as string; 

     // Specific ValidationRule implementation... 
     if (String.IsNullOrWhiteSpace(stringValue)) 
     { 
      return new ValidationResult(false, "Must not be empty"); 
     } 
     else 
     { 
      return new ValidationResult(true, null); 
     } 
    } 

    private object GetBoundValue(object value) 
    { 
     if (value is BindingExpression) 
     { 
      // ValidationStep was UpdatedValue or CommittedValue (Validate after setting) 
      // Need to pull the value out of the BindingExpression. 
      BindingExpression binding = (BindingExpression)value; 

      // Get the bound object and name of the property 
      object dataItem = binding.DataItem; 
      string propertyName = binding.ParentBinding.Path.Path; 

      // Extract the value of the property. 
      object propertyValue = dataItem.GetType().GetProperty(propertyName).GetValue(dataItem, null); 

      // This is what we want. 
      return propertyValue; 
     } 
     else 
     { 
      // ValidationStep was RawProposedValue or ConvertedProposedValue 
      // The argument is already what we want! 
      return value; 
     } 
    } 
} 

Die GetBoundValue() Methode wird den Wert Ich kümmere mich um, wenn es ausgraben Ruft eine BindingExpression ab oder kickt das Argument einfach zurück, wenn dies nicht der Fall ist. Der wirkliche Schlüssel war, den "Pfad" zu finden und dann diesen zu verwenden, um die Eigenschaft und ihren Wert zu erhalten.

Die Einschränkung: In meiner ursprünglichen Frage hatte meine Bindung Path="Identity.Name", als ich in Unterobjekte meines ViewModel graben. Dies funktioniert nicht, da der obige Code den Pfad direkt zu einer Eigenschaft auf dem gebundenen Objekt erwartet. Glücklicherweise habe ich mein ViewModel bereits abgeflacht, so dass dies nicht länger der Fall ist, aber eine Abhilfe könnte darin bestehen, den Datenkontext des Steuerelements zuerst als Unterobjekt festzulegen.

Ich möchte Eduardo Brites ein wenig Anerkennung zollen, da seine Antwort und Diskussion mich dazu gebracht hat, darüber nachzudenken, und habe ein Stück zu seinem Puzzle geliefert. Während ich dabei war, die ValidationRules komplett abzulegen und stattdessen IDataErrorInfo zu verwenden, mag ich seinen Vorschlag, sie zusammen für verschiedene Arten und Komplexitäten der Validierung zu verwenden.

+1

Es gibt Situationen, in denen IDataErrorInfo nicht ausreicht, etwa wenn Sie eindeutige Felder in einem Datagrid validieren möchten und Sie Zeilen vergleichen müssen. Ich bin froh, dass du gefunden hast, wonach du gesucht hast. Prost! –

+0

Siehe meine Antwort, um die Einschränkung zu entfernen. –

1

Um Ihre 2 Frage zu beantworten:

String strVal = (string) ((Binding) Wert). DataItem

+0

Das * fast * funktioniert, tut es aber nicht.DataItem enthält nicht den Wert der Eigenschaft, sondern den DataContext (ja, die Eigenschaft ist dort). Leider weiß ich nicht, welche Eigenschaft * diese * Validierungsregel-Instanz überprüfen soll. Ich sehe nichts in der BindingExpression, mit dem ich feststellen kann, welche Eigenschaft gebunden ist (d. H. Der Pfad). – mbmcavoy

+0

Ich dachte, dass Ihr "RequiredRule" Zweck irgendwie generisch war, dass es egal ist, welche Eigenschaft es überprüft, solange es als String war ... –

+0

Ja, die RequiredRule-Klasse sollte in der Lage sein, eine beliebige String-Eigenschaft zu validieren. Im Falle des obigen XAML ist die TextBox mit Path = "Identity.Name" gebunden. Ich habe eine andere TextBox mit Pfad = "Identity.Description" gebunden, mit einer RequiredRule angewendet. Da das übergebene DataTem der BindingExpression der Datenkontext ist, kann ich * sowohl * Identity.Name und Identity.Description (als auch alles andere in meinem ViewModel) sehen. Während ich den Wert, der mir im Debugger wichtig ist, sehen kann, sehe ich keinen Weg für die ValidationRule, um zu bestimmen, welche Eigenschaft validiert werden soll. – mbmcavoy

3

Dies ist eine Erweiterung von answer von mbmcavoy.

Ich habe die GetBoundValue Methode geändert, um die Beschränkung für Bindungspfade zu entfernen. Die BindingExpression hat bequemerweise die Eigenschaften ResolvedSource und ResolvedSourcePropertyName, die im Debugger sichtbar, aber nicht über normalen Code zugänglich sind. Es ist jedoch kein Problem, sie über Reflektion zu erhalten, und diese Lösung sollte mit jedem Bindungspfad funktionieren.

private object GetBoundValue(object value) 
{ 
    if (value is BindingExpression) 
    { 
     // ValidationStep was UpdatedValue or CommittedValue (validate after setting) 
     // Need to pull the value out of the BindingExpression. 
     BindingExpression binding = (BindingExpression)value; 

     // Get the bound object and name of the property 
     string resolvedPropertyName = binding.GetType().GetProperty("ResolvedSourcePropertyName", BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance).GetValue(binding, null).ToString(); 
     object resolvedSource = binding.GetType().GetProperty("ResolvedSource", BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance).GetValue(binding, null); 

     // Extract the value of the property 
     object propertyValue = resolvedSource.GetType().GetProperty(resolvedPropertyName).GetValue(resolvedSource, null); 

     return propertyValue; 
    } 
    else 
    { 
     return value; 
    } 
} 
+1

Für alle, die auf .NET 4.5 abzielen, wurden ResolvedSource und ResolvedSourcePropertyName in .NET 4.5 hinzugefügt, damit dies nicht funktioniert. – user2339814

+0

Sehr praktisch. Ich habe einen Fall gefunden, in dem das validierte Steuerelement gelöscht wurde (in einer Grid-Kontrollzelle). Die Validierung wurde ein letztes Mal mit einem Nullwert für ResolvedSourcePropertyName aufgerufen. Also musste ich eine Nullprüfung für den Rückgabewert von GetValue() hinzufügen, um den Aufruf von ToString für diesen Nullwert zu vermeiden. –

3

Dies ist eine alternative Erweiterung der Antwort von mbmcavoy und adabyron.

Um die Begrenzung für die Bindung Pfade zu entfernen, erhalte ich den Eigenschaftswert ein solches Verfahren unter Verwendung von:

public static object GetPropertyValue(object obj, string propertyName) 
{ 
    foreach (String part in propertyName.Split('.')) 
    { 
     if (obj == null) { return null; } 

     Type type = obj.GetType(); 
     PropertyInfo info = type.GetProperty(part); 
     if (info == null) { return null; } 

     obj = info.GetValue(obj, null); 
    } 

    return obj; 
} 

einfach Jetzt

ändern
object propertyValue = dataItem.GetType().GetProperty(propertyName).GetValue(dataItem, null); 

zu

object propertyValue = GetPropertyValue(dataItem, propertyName); 

Related Post: Get property value from string using reflection in C#

Verwandte Themen