2012-05-15 9 views
13

Ich stecke in einer anscheinend gemeinsamen Anforderung fest. Ich habe eine WPF Prism (für MVVM) -Anwendung. Mein Modell implementiert IDataErrorInfo zur Validierung. Die IDataErrorInfo funktioniert hervorragend für nicht numerische Eigenschaften. Wenn der Benutzer jedoch bei numerischen Eigenschaften ungültige Zeichen eingibt (die nicht numerisch sind), erreichen die Daten das Modell nicht einmal, weil wpf es nicht in einen numerischen Typ konvertieren kann.Übergabe des Status von WPF ValidationRule zum Anzeigen von Modell in MVVM

Also musste ich WPF ValidationRule verwenden, um dem Benutzer eine sinnvolle Nachricht für ungültige numerische Einträge zu geben. Alle Schaltflächen in der Ansicht sind an DelegateCommand von prism (im Ansichtsmodell) gebunden und das Aktivieren/Deaktivieren von Schaltflächen erfolgt in View Model selbst.

Wenn nun eine wpf ValidationRule für einige TextBox fehlschlägt, wie gebe ich diese Informationen an View Model weiter, damit die Schaltflächen in der Ansicht entsprechend deaktiviert werden können?

Antwort

5

Nirvan

Der einfachste Weg, um dieses besondere Problem zu lösen, ist eine numerische Textbox, zu verwenden, die von dem Benutzer verhindert Geben Sie einen ungültigen Wert ein (Sie können dies entweder über einen Drittanbieter tun oder eine Open-Source-Lösung finden, z. B. eine von Textbox abgeleitete Klasse, die nicht numerische Eingaben unterdrückt).

Die zweite Möglichkeit, dies in MVVM ohne das oben beschriebene zu behandeln, besteht darin, ein anderes Feld in Ihrem ViewModel zu definieren, das eine Zeichenfolge ist, und dieses Feld an Ihr Textfeld zu binden. Dann können Sie im Setzer Ihres Zeichenkettenfelds die Ganzzahl einstellen und Ihrem numerischen Feld einen Wert zuweisen:

Hier ist ein grobes Beispiel: (Ich habe es nicht getestet, aber es sollte Ihnen die Idee geben)

+0

Alex, Danke für Ihre Antwort. Ich verwende den von Ihnen vorgeschlagenen zweiten Weg. Obwohl diese Vorgehensweise für das aktuelle Problem ausreichend ist, suchte ich nach einer generischen Lösung für die Verwendung von ValidationRule in Kombination mit IDateErrorInfo. Ich stieß auf ein Problem während der Lösung und wird hier veröffentlicht (http://stackoverflow.com/questions/10629278/strange-order-of-firing-of-validation-error-event-added-fired-before-removed). Ich wäre sehr dankbar für Ihren Rat in dieser Frage. Da mein aktuelles Problem gelöst ist, werde ich Ihre Antwort als richtig markieren. Prost. Nirvan – Jatin

+0

Ich würde dringend davon abraten, Validierungsregeln zu verwenden. Sie sind auf der Bindungsebene und wurden entwickelt, um in Code hinter verwendet zu werden, und sind nicht gut für MVVM geeignet. Möglicherweise können Sie sie zur Zusammenarbeit bringen, aber dazu müssten Sie Ihr ViewModel mit der Ansicht verbinden und durch die logische Baumprüfung von Bindungen navigieren und diese an ViewModel binden. Ich schaudere, wenn ich daran denke. Schau dir das an: http://stackoverflow.com/questions/63646/wpf-data-binding-and-validation-rules-best-practices –

+0

Für mich würde ich immer versuchen, dem Pfad des geringsten Widerstands zu folgen, und Verwenden Sie einfach ein Steuerelement, das keine ungültigen Eingaben enthält, die nicht zum ViewModel gelangen. Ich möchte, dass sich der gesamte Validierungscode in ViewModel befindet, also an einem Ort und unter meiner Kontrolle. –

0

Wenn Sie eine benutzerdefinierte Implementierung bereitstellen, können Sie den erhaltenen Wert speichern und das letzte Ergebnis speichern. Pseudo-Code:

public class IsInteger : ValidationRule 
{ 
    private int parsedValue; 

    public IsInteger() { } 

    public string LastValue{ get; private set; } 

    public bool LastParseSuccesfull{ get; private set; } 

    public int ParsedValue{ get{ return parsedValue; } } 

    public override ValidationResult Validate(object value, CultureInfo cultureInfo) 
    { 
    LastValue = (string) value; 
    LastParseSuccesfull = Int32.TryParse(LastValue, cultureInfo, ref parsedValue); 
    return new ValidationResult(LastParseSuccesfull, LastParseSuccesfull ? "not a valid number" : null); 
    } 
} 
+0

Ich bin nicht sicher, ob ich voll und ganz verstehen, was Sie versuchen zu erklären. Mein Problem ist, dass der ungültige Wert (für eine Eigenschaft) niemals das Model erreicht, daher kann ich IDataErrorInfo nicht verwenden, um die Eigenschaft zu validieren. Angenommen, es gibt ein Textfeld für die Eigenschaft alge. Der erste Benutzer gibt ein gültiges Alter ein (z. B. 20).Dann ändert er später das Alter Textfeld in etwas nicht numerisches (sagen wir abc). Nun erreicht dieser falsche Wert nie Model, obwohl die ValidationRule in der Lage ist, dasselbe zu erkennen. Das Alter des Modells ist immer noch der alte gültige Wert (20). Alle Buttons sind an ViewModel gebunden und ich kann sie nur von dort aus deaktivieren. – Jatin

+0

ah Entschuldigung, das war mir nicht klar, vielleicht ist die Bearbeitung besser .. – stijn

9

Für MVVM bevorzuge ich Attached Properties für diese Art von Sache, weil sie wiederverwendbar sind und es hält die Ansicht Modelle sauber.

Um die Validation.HasError-Eigenschaft an Ihr Ansichtsmodell zu binden, müssen Sie eine angefügte Eigenschaft erstellen, die über CoerceValueCallback den Wert der angefügten Eigenschaft mit der Validation.HasError-Eigenschaft des Steuerelements, das Sie validieren, synchronisiert Eingabe an.

This Artikel erläutert, wie diese Technik verwendet wird, um das Problem zu beheben, das Ansichtsmodell von WPF ValidationRule-Fehlern zu melden. Der Code war in VB, also portierte ich ihn auf C#, wenn Sie keine VB-Person sind.

Die angefügte Eigenschaft

public static class ValidationBehavior 
{ 
    #region Attached Properties 

    public static readonly DependencyProperty HasErrorProperty = DependencyProperty.RegisterAttached(
     "HasError", 
     typeof(bool), 
     typeof(ValidationBehavior), 
     new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, null, CoerceHasError)); 

    private static readonly DependencyProperty HasErrorDescriptorProperty = DependencyProperty.RegisterAttached(
     "HasErrorDescriptor", 
     typeof(DependencyPropertyDescriptor), 
     typeof(ValidationBehavior)); 

    #endregion 

    private static DependencyPropertyDescriptor GetHasErrorDescriptor(DependencyObject d) 
    { 
     return (DependencyPropertyDescriptor)d.GetValue(HasErrorDescriptorProperty); 
    } 

    private static void SetHasErrorDescriptor(DependencyObject d, DependencyPropertyDescriptor value) 
    { 
     d.SetValue(HasErrorDescriptorProperty, value); 
    } 

    #region Attached Property Getters and setters 

    public static bool GetHasError(DependencyObject d) 
    { 
     return (bool)d.GetValue(HasErrorProperty); 
    } 

    public static void SetHasError(DependencyObject d, bool value) 
    { 
     d.SetValue(HasErrorProperty, value); 
    } 

    #endregion 

    #region CallBacks 

    private static object CoerceHasError(DependencyObject d, object baseValue) 
    { 
     var result = (bool)baseValue; 
     if (BindingOperations.IsDataBound(d, HasErrorProperty)) 
     { 
      if (GetHasErrorDescriptor(d) == null) 
      { 
       var desc = DependencyPropertyDescriptor.FromProperty(System.Windows.Controls.Validation.HasErrorProperty, d.GetType()); 
       desc.AddValueChanged(d, OnHasErrorChanged); 
       SetHasErrorDescriptor(d, desc); 
       result = System.Windows.Controls.Validation.GetHasError(d); 
      } 
     } 
     else 
     { 
      if (GetHasErrorDescriptor(d) != null) 
      { 
       var desc = GetHasErrorDescriptor(d); 
       desc.RemoveValueChanged(d, OnHasErrorChanged); 
       SetHasErrorDescriptor(d, null); 
      } 
     } 
     return result; 
    } 
    private static void OnHasErrorChanged(object sender, EventArgs e) 
    { 
     var d = sender as DependencyObject; 
     if (d != null) 
     { 
      d.SetValue(HasErrorProperty, d.GetValue(System.Windows.Controls.Validation.HasErrorProperty)); 
     } 
    } 

    #endregion 
} 

mit dem beiliegenden Eigentum in XAML

<Window 
    x:Class="MySolution.MyProject.MainWindow" 
    xmlns:v="clr-namespace:MyNamespace;assembly=MyAssembly"> 
    <TextBox 
     v:ValidationBehavior.HasError="{Binding MyPropertyOnMyViewModel}"> 
     <TextBox.Text> 
     <Binding 
      Path="ValidationText" 
      UpdateSourceTrigger="PropertyChanged"> 
      <Binding.ValidationRules> 
      <v:SomeValidationRuleInMyNamespace/> 
      </Binding.ValidationRules> 
     </Binding> 
    </TextBox.Text> 
    </TextBox> 
</ Window > 

Nun ist die Eigenschaft auf Ihrer Ansicht Modell wird mit Validation.HasError auf Textbox synchronisiert werden.

+0

Funktioniert hervorragend für grundlegende in-line-XAML, scheint aber nicht für Datenvorlagenelemente zu funktionieren. Der Koerzitivwert-Callback wird anfänglich aufgerufen, aber bevor die Datenbindung eingerichtet zu sein scheint (d. H. "IsDataBound()" gibt "false" zurück). Ich kann den Check für 'IsDataBound()' entfernen und es funktioniert, aber dann riskiere ich einen Speicherverlust. Gibt es eine Idee, wie dieser Ansatz im Datenschablonenszenario funktioniert? –

1

Sie müssen abhängig von der Bindungstypeigenschaft custome user control angeben. Zum Beispiel, wenn Ihre Eigenschaft int-Typ ist, müssen Sie Steuerelement, das nicht anderen Wert als interenger Typ zulassen.

Die Logik, die Sie in PreviewTextInput = "NumberValidationTextBox" einfügen können.

private void NumberValidationTextBox(object sender, TextCompositionEventArgs e) 
    { 
     Regex regex = new Regex("[^0-9]+"); 
     e.Handled = regex.IsMatch(e.Text); 
    } 

Geben Sie einfach Ihre Logik ein oder legen Sie die Kontrolle über Ihre Kunden ein und Sie sind fertig.

Defend muss auch mvvm Validierung implementieren.

+0

OK, das war einfach und nicht sehr gut. Hier ist jetzt die Lösung. Es ist besser. – Petar

0
  1. Implementieren Sie IDataErrorInfo in Ihrem Modell oder Viewmodel abhängig Logik der Bindungseigenschaft. Sie können in beiden Klassen implementieren.

  2. Implementieren Sie dies auch in Ihrer Basisvalidierungsklasse. Hier wird die Validierung ausgelöst, wenn IDataErrorInfo nicht funktioniert.

    public virtual bool HasError 
    { 
        get { return _hasError; } 
        set 
        { 
         // if (value.Equals(_hasError)) return; 
         _hasError = value; 
         RaisePropertyChanged(() => HasError); 
        } 
    } 
    
  3. Als Nächstes fügen globale Klasse

    public class ProtocolSettingsLayout 
    { 
        public static readonly DependencyProperty MVVMHasErrorProperty = DependencyProperty.RegisterAttached("MVVMHasError", typeof(bool), typeof(ProtocolSettingsLayout), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, null, CoerceMVVMHasError)); 
    
        public static bool GetMVVMHasError(DependencyObject d) 
        { 
         return (bool)d.GetValue(MVVMHasErrorProperty); 
        } 
    
        public static void SetMVVMHasError(DependencyObject d, bool value) 
        { 
         d.SetValue(MVVMHasErrorProperty, value); 
        } 
    
        private static object CoerceMVVMHasError(DependencyObject d, Object baseValue) 
        { 
         bool ret = (bool)baseValue; 
    
         if (BindingOperations.IsDataBound(d, MVVMHasErrorProperty)) 
         { 
          if (GetHasErrorDescriptor(d) == null) 
          { 
           DependencyPropertyDescriptor desc = DependencyPropertyDescriptor.FromProperty(Validation.HasErrorProperty, d.GetType()); 
           desc.AddValueChanged(d, OnHasErrorChanged); 
           SetHasErrorDescriptor(d, desc); 
           ret = System.Windows.Controls.Validation.GetHasError(d); 
          } 
         } 
         else 
         { 
          if (GetHasErrorDescriptor(d) != null) 
          { 
           DependencyPropertyDescriptor desc = GetHasErrorDescriptor(d); 
           desc.RemoveValueChanged(d, OnHasErrorChanged); 
           SetHasErrorDescriptor(d, null); 
          } 
         } 
         return ret; 
        } 
    
        private static readonly DependencyProperty HasErrorDescriptorProperty = DependencyProperty.RegisterAttached("HasErrorDescriptor", 
                          typeof(DependencyPropertyDescriptor), 
                          typeof(ProtocolSettingsLayout)); 
    
        private static DependencyPropertyDescriptor GetHasErrorDescriptor(DependencyObject d) 
        { 
         var ret = d.GetValue(HasErrorDescriptorProperty); 
         return ret as DependencyPropertyDescriptor; 
        } 
    
        private static void OnHasErrorChanged(object sender, EventArgs e) 
        { 
         DependencyObject d = sender as DependencyObject; 
    
         if (d != null) 
         { 
          d.SetValue(MVVMHasErrorProperty, d.GetValue(Validation.HasErrorProperty)); 
         } 
        } 
    
        private static void SetHasErrorDescriptor(DependencyObject d, DependencyPropertyDescriptor value) 
        { 
         var ret = d.GetValue(HasErrorDescriptorProperty); 
         d.SetValue(HasErrorDescriptorProperty, value); 
        } 
    } 
    
  4. XAML

    <TextBox PreviewTextInput="NumValidationTextBox" Text="{Binding ESec, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=true, NotifyOnValidationError=true, ValidatesOnExceptions=True, NotifyOnSourceUpdated=True, NotifyOnTargetUpdated=True, TargetNullValue='0', FallbackValue='0' }" Validation.ErrorTemplate="{StaticResource validationTemplate}" viewmodels:ProtocolSettingsLayout.MVVMHasError="{Binding Path=HasError}" />  
    
+0

100% ist es Arbeit. Ich habe es getestet. Es müssen keine Validierungsregeln oder Kundenkontrollen implementiert werden. – Petar

2

Seit .NET 4.5, ValidationRule eine Überlastung der Validate-Methode hat:

public ValidationResult Validate(object value, CultureInfo cultureInfo, 
    BindingExpressionBase owner) 

Sie können es außer Kraft setzen und das View-Modell auf diese Weise erhalten:

public override ValidationResult Validate(object value, 
    CultureInfo cultureInfo, BindingExpressionBase owner) 
{ 
    ValidationResult result = base.Validate(value, cultureInfo, owner); 
    var vm = (YourViewModel)((BindingExpression)owner).DataItem; 
    // ... 
    return result; 
} 
+0

Vielleicht vermisse ich etwas, aber wie wird das aus der Sicht aufgerufen? Das Festlegen der Validierungsregel für die Bindung verwendet standardmäßig die abstrakte Validate-Methode, soweit ich das beurteilen kann. –

+0

Die abstrakte Validate-Methode wird vom virtuellen aufgerufen. Der Quellcode ist hier: https://referencesource.microsoft.com/#PresentationFramework/Framework/System/Windows/Controls/ValidationRule.cs8d9199b8be6d367d – Maxence

0

traf ich das gleiche Problem und löste es mit einem Trick. Siehe Konverter unter:

public class IntValidationConverter : IValueConverter 
{ 
    static string[] AllValuse = new string[100000]; 
    static int index = 1; 
    public static int StartOfErrorCodeIndex = -2000000000; 
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 
    { 
     if (value == null) return null; 
     if (value.ToString() == "") return null; 

     int iValue = (int)(value); 

     if (iValue == int.MinValue) return null; 

     if (iValue >= StartOfErrorCodeIndex) return value; 
     if ((iValue < IntValidationConverter.StartOfErrorCodeIndex) && (iValue > int.MinValue)) return AllValuse[StartOfErrorCodeIndex - iValue]; 

     return null; 
    } 

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 
    { 
     if (value == null) return int.MinValue; 
     if (value.ToString() == "") return int.MinValue; 

     int result; 
     bool success = int.TryParse(value.ToString(), out result); 
     if (success) return result; 

     index++; 
     AllValuse[index] = value.ToString(); 
     return StartOfErrorCodeIndex - index; 
    } 
} 
1

ich das gleiche Problem mit Ihnen zu haben, aber ich in einer anderen Art und Weise zu lösen, benutze ich den Trigger die Taste zu deaktivieren, wenn die Eingabe ungültig ist. Unterdessen sollte die Textbox Bindung verwenden ValidatesOnExceptions=true

<Style TargetType="{x:Type Button}"> 
<Style.Triggers> 
    <DataTrigger Binding="{Binding ElementName=tbInput1, Path=(Validation.HasError)}" Value="True"> 
     <Setter Property="IsEnabled" Value="False"></Setter> 
    </DataTrigger> 

    <DataTrigger Binding="{Binding ElementName=tbInput2, Path=(Validation.HasError)}" Value="True"> 
     <Setter Property="IsEnabled" Value="False"></Setter> 
    </DataTrigger> 
</Style.Triggers> 

Verwandte Themen