2009-10-01 4 views
30

Używam powiązania danych WPF z podmiotami implementującymi interfejs IDataErrorInfo. W ogóle mój kod wygląda następująco: jednostkaJak powstrzymać sprawdzanie poprawności, gdy nic nie zostanie wprowadzone?

działalności:

public class Person : IDataErrorInfo 
{ 
    public string Name { get; set;} 

    string IDataErrorInfo.this[string columnName] 
    { 
    if (columnName=="Name" && string.IsNullOrEmpty(Name)) 
     return "Name is not entered"; 
    return string.Empty; 
    } 
} 

Xaml file:

<TextBox Text="{Binding Path=Name, Mode=TwoWay, ValidatesOnDataErrors=true}" /> 

Gdy użytkownik kliknie na "Utwórz nową osobę" następujący kod jest wykonywany:

DataContext = new Person(); 

Problem polega na tym, że po utworzeniu osoby jej nazwa jest pusta, a WPF natychmiast rysuje czerwoną ramkę i pokazuje komunikat o błędzie. Chcę, aby pokazywał błąd tylko wtedy, gdy nazwa była już edytowana i utracono ostrość. Czy ktoś wie, jak to zrobić?

+3

Oddajemy nagrodę za to pytanie w nadziei na niehackowe rozwiązanie, jeśli takie istnieje. –

+0

Nie możesz po prostu utworzyć osoby przed wywołaniem funkcji InitializeComponent()? – markmnl

+1

Dodano nagrodę, aby uzyskać dobre, niegroźne rozwiązanie. –

Odpowiedz

15

można zmienić klasę osoby do ognia błąd sprawdzania poprawności tylko wtedy, gdy właściwość Name kiedykolwiek zmieniło:

public class Person : IDataErrorInfo { 

    private bool nameChanged = false; 
    private string name; 
    public string Name { 
     get { return name; } 
     set { 
      name = value; 
      nameChanged = true; 
     } 
    } 

//... skipped some code 

    string IDataErrorInfo.this[string columnName] { 
     get { 
      if(nameChanged && columnName == "Name" && string.IsNullOrEmpty(Name)) 
       return "Name is not entered"; 
      return string.Empty; 
     } 
    } 
} 
+5

Tak, mogę, ale chciałbym zmodyfikować powiązanie WPF, a następnie zmienić moje jednostki biznesowe.Ponieważ nie jestem ekspertem WPF, mam nadzieję, że istnieje stosunkowo łatwe rozwiązanie tego problemu. Wydaje się, że jest to typowe zachowanie - nie pokazywanie alertów dla wszystkich pól, gdy formularz jest po prostu otwarty. –

+2

Nie sądzę, że można to kontrolować na poziomie ustawień XAML lub wiązania. Dane są poprawne lub nie, więc to do IDataErrorInfo, aby je zweryfikować. Alternatywnie możesz sprawdzić "if (columnName ==" Name "&& Name ==" ")" traktując w ten sposób początkową wartość "null" jako ważną, która zmieni się w niepoprawny pusty ciąg podczas edycji. Nie mogę wymyślić innego sposobu. –

+0

@AlexKofman Wiem, że to stary post, ale to jest coś, nad czym teraz walczę. Twoja uwaga dotycząca zmiany jednostek biznesowych jest nieistotna, gdy masz już zaimplementować interfejs IDataErrorInfo. Lepiej jest umieścić swój obiekt za ViewModelem, jak sugerował Uri i umieścić tam swoją logikę prezentacji. – Bronumski

4

Nie ma innego rozwiązania, które znalazłem, ale nie podoba mi się bardzo. Musisz wyczyścić sprawdzanie poprawności przy ładowaniu strony.

co mam na myśli to, co musisz zrobić to:

Validation.ClearInvalid(...) na przykład, jeśli masz pole tekstowe nie chcesz być zatwierdzone powinien zadzwonić

Validation.ClearInvalid(txtSomething.GetBindingExpression(TextBox.TextProperty)) lub coś podobnego.

Powinieneś to zrobić dla każdej kontroli, którą chcesz wyczyścić z walidacji.

Nie podobało mi się rozwiązanie, ale to było najlepsze, jakie znalazłem. Miałem nadzieję, że wpf ma coś "z pudełka", które działało, ale go nie znalazło.

3

Cholera, zajęło mi to trochę czasu, ale jak zwykle ... przywiązane zachowania na ratunek.

To, na co patrzysz, to brudne śledzenie stanu. Można to zrobić na wiele sposobów za pomocą narzędzia ViewModel, ale ponieważ nie chcesz zmieniać swoich jednostek, najlepszym sposobem jest stosowanie zachowań.

Po pierwsze usuń ValidatesOnDataErrors z powiązania Xaml. Utwórz zachowanie dla formantu, nad którym pracujesz (jak pokazano poniżej dla TextBox) iw zdarzeniu (lub dowolnie wybranym zdarzeniu) zresetuj powiązanie z tym, które jest sprawdzane pod kątem błędów danych. Proste naprawdę.

W ten sposób twoje jednostki nie muszą się zmieniać, twój Xaml jest utrzymywany w rozsądnej czystości i dostajesz swoje zachowanie.

Oto zachowanie Code

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Windows; 
using System.Windows.Controls; 
using System.Windows.Data; 

    namespace IDataErrorInfoSample 
    { 
     public static class DirtyStateBehaviours 
     { 


      public static string GetDirtyBindingProperty(DependencyObject obj) 
      { 
       return (string)obj.GetValue(DirtyBindingPropertyProperty); 
      } 

      public static void SetDirtyBindingProperty(DependencyObject obj, string value) 
      { 
       obj.SetValue(DirtyBindingPropertyProperty, value); 
      } 

      // Using a DependencyProperty as the backing store for DirtyBindingProperty. This enables animation, styling, binding, etc... 
      public static readonly DependencyProperty DirtyBindingPropertyProperty = 
       DependencyProperty.RegisterAttached("DirtyBindingProperty", typeof(string), typeof(DirtyStateBehaviours), 
       new PropertyMetadata(new PropertyChangedCallback(Callback))); 


      public static void Callback(DependencyObject obj, 
       DependencyPropertyChangedEventArgs args) 
      { 
       var textbox = obj as TextBox; 
       textbox.TextChanged += (o, s) => 
       { 
        Binding b = new Binding(GetDirtyBindingProperty(textbox)); 
        b.ValidatesOnDataErrors = true; 
        textbox.SetBinding(TextBox.TextProperty, b); 
       }; 

      } 
     } 
    } 

I XAML jest dość prosta zbyt.

<Window x:Class="IDataErrorInfoSample.MainWindow" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:local="clr-namespace:IDataErrorInfoSample" 
    xmlns:sys="clr-namespace:System;assembly=mscorlib" 
    Title="MainWindow" 
    Height="350" 
    Width="525"> 

<Window.DataContext> 
    <local:Person /> 
</Window.DataContext> 
<StackPanel Margin="20"> 
    <TextBox Height="20" 
      Margin="0,0,0,10" 
      local:DirtyStateBehaviours.DirtyBindingProperty="Name" 
      Text="{Binding Path=Name}"> 
    </TextBox> 
    <Button Content="Go" /> 
</StackPanel> 

HTH, Stimul8d.

+2

Powinieneś skopiować nazwę wiążącego celu i nie zapomnij usunąć zdarzenia dla zdarzenia TextChanged lub nowe powiązanie będzie za każdym razem wpisywać w polu tekstowym. –

3

Może to dla ciebie opcja, aby przenieść weryfikację do widoku: Zamiast implementować IDataErrorInfo, możesz włączyć NotifyOnValidationError w powiązaniu i dodać ValidationRule, która sprawdza. Dla ValidationRules istnieje standard way to control, if the rule should be applied when the object changes (nie wartość nieruchomości bezpośrednio)

To już zapewni wizualną informację zwrotną dla użytkownika (zostanie zastosowany ErrorTemplate). Jeśli potrzebujesz więcej, np. wyłączyć niektóre przyciski itp., możesz połączyć zdarzenia Validation.Error swojego widoku z ViewModel lub BusinessEntity, s.th. możesz go tam zidentyfikować, jeśli wystąpi jakikolwiek błąd.

4

Uważam, że podejście @Stanislava Kniazeva jest prawidłowe. Twój komentarz dotyczący nie dodawania logiki do obiektu biznesowego jest również prawidłowy. Aby uzyskać czysty rozdział obaw, jak zachować osobę w warstwie biznesowej (lub w warstwie modelu danych) i wprowadzić nową klasę PersonVm z logiką widoku. Dla warstwy VM bardziej podoba mi się wzór powstrzymywania niż dziedziczenie, a na tej warstwie implementuję również INotifyPropertyChanged, która jest również własnością VM, a nie modelu danych.

public class PersonVm : IDataErrorInfo, INotifyPropertyChanged 
{ 
    private Person _person; 

    public PersonVm() { 
     // default constructor 
     _person = new Person(); 
     _dirty = false; 
    } 

    public PersonVm(Person p) { 
     // User this constructor when you get a Person from database or network 
     _person = p; 
     _dirty = false; 
    } 

    void fire(string prop) { 
     PropertyChanged(this, new PropertyChangedEventArgs(prop)); 
    } 

    public string name { 
     get { return _person.name; } 
     set { _person.name = value; fire("name"); dirty = true; } 
    } 

    ... 

    string IDataErrorInfo.this[string columnName] { 
     get { 
      if(dirty) return _person[columnName]; 
     } 
    } 

} 

Chodzi o to, aby umieścić logikę każdej warstwy w odpowiedniej klasie. W warstwie modelu danych dokonuje się sprawdzania poprawności, które dotyczą wyłącznie danych. W warstwie Widok modelu dodajesz logikę, która dotyczy modelu widoku (jak również logiki widoku i innych modeli widoku).

+0

Zgadzam się, prawdopodobnie nie należy ujawniać swoich podmiotów bezpośrednio do widoku. A kwestia zmiany podmiotu biznesowego jest kwestią dyskusyjną, kiedy już musieliście go zabrudzić interfejsem IDataErrorInfo – Bronumski

2

I zostały wdrożone następujące rozwiązanie:

public class SkipValidationOnFirstLoadBehavior : Behavior<TextBox> 
{ 
     protected override void OnAttached() 
     { 
      AssociatedObject.LostFocus += AssociatedObjectOnLostFocus; 
     } 

     private void AssociatedObjectOnLostFocus(object sender, RoutedEventArgs routedEventArgs) 
     { 
      //Execute only once 
      AssociatedObject.LostFocus -= AssociatedObjectOnLostFocus; 

      //Get the current binding 
      BindingExpression expression = AssociatedObject.GetBindingExpression(TextBox.TextProperty); 
      if (expression == null) return; 
      Binding parentBinding = expression.ParentBinding; 

      //Create a new one and trigger the validation 
      Binding updated = new Binding(parentBinding.Path.Path); 
      updated.ValidatesOnDataErrors = true; 
      updated.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged; 
      AssociatedObject.SetBinding(TextBox.TextProperty, updated); 
     } 
} 

Przykład użycia:

<TextBox Text="{Binding Email}"> 
     <i:Interaction.Behaviors> 
      <local:SkipValidationOnFirstLoadBehavior/> 
     </i:Interaction.Behaviors> 
    </TextBox> 
1

Jestem tylko młodszy programista, którzy nie mają wiele wiedzy, ale naprawiłem to w ten sposób .

W mojej klasie sprawdzania poprawności stworzyłem konstruktor bez parametrów zwracających poprawne wyniki sprawdzania poprawności.

public class NotEmptyValidation : ValidationRule 
{ 
    public override ValidationResult Validate(object value, CultureInfo cultureInfo) 
    { 
     if (string.IsNullOrEmpty(value as string)) 
     { 
      return new ValidationResult(false,"Veld kan niet leeg zijn"); 
     } 

     return new ValidationResult(true,null); 

} 
    public NotEmptyValidation() : base() 
    { 
     Validate(); 
    } 


    public ValidationResult Validate() 
    { 
     return new ValidationResult(true,null); 
    } 
} 

Mój kod XAML wygląda następująco

<!--TEXTBOXES--> 
       <TextBox Grid.Column="1" Grid.ColumnSpan="2" Margin="5"> 
        <TextBox.Text> 
         <Binding Path="SelectedEntity.Code" UpdateSourceTrigger="PropertyChanged"> 
          <Binding.ValidationRules> 
           <val:NotEmptyValidation /> 
          </Binding.ValidationRules> 
         </Binding> 
        </TextBox.Text> 
       </TextBox> 
       <TextBox Grid.Column="1" Grid.ColumnSpan="3" Grid.Row="1" Margin="5"> 
        <TextBox.Text> 
         <Binding Path="SelectedEntity.Name" UpdateSourceTrigger="PropertyChanged"> 
          <Binding.ValidationRules> 
           <val:NotEmptyValidation /> 
          </Binding.ValidationRules> 
         </Binding> 
        </TextBox.Text> 
       </TextBox> 

Kiedy moi mnóstwo postaci, robi ogień walidacja po załadowaniu okna, ale jeśli mogę wyczyścić pole tekstowe, to nie ogień.

Wadą jest to, że jeśli załaduję nieprawidłowy Entity, który ma nazwę emty lub kod, sprawdzanie poprawności nie uruchamia się po załadowaniu okna, robi to jednak po wypełnieniu pola tekstowego i wyczyszczeniu go. Ale tak się nie dzieje, ponieważ sprawdzam wszystkie pola podczas tworzenia jednostki.

To nie jest idealne rozwiązanie, ale działa dla mnie.