2011-05-04 10 views
17

Mam obecnie obowiązek powiadomienia użytkownika aplikacji, jeśli jakiekolwiek pola zostały zmienione/zaktualizowane w widoku.WPF MVVM - Jak wykryć, czy widok jest "brudny"

Na przykład, jeśli użytkownik zmieni pole daty w widoku, a następnie spróbuje zamknąć widok, aplikacja wyświetli komunikat z prośbą o kontynuację i utratę zmian lub anulowanie, aby mogli kliknąć przycisk Zapisz.

Problem: Jak wykryć, że zmieniono którekolwiek z pól danych w widoku?

Nadzieja to sens, niż ty z góry, uważa,

+13

W MVVM byś zapytać, czy model czy może ViewModel jest brudna. Nie widok. –

Odpowiedz

30

Jednym podejściem, które można zastosować, jest wykorzystanie interfejsów IChangeTracking i INotifyPropertyChanged.

Jeśli utworzysz abstrakcyjną klasę podstawową, z której dziedziczą modele widoku (ViewModelBase), która implementuje interfejsy IChangeTracking i INotifyPropertyChanged, możesz mieć dołączoną bazę modelu widoku do powiadomienia o zmianach właściwości (co sygnalizuje, że model widoku ma został zmodyfikowany) i który ustawi właściwość IsChanged na wartość true, aby wskazać, że model widoku jest "brudny".

Korzystając z tego podejścia, polegasz na powiadomieniu o zmianie właściwości poprzez powiązanie danych, aby śledzić zmiany i zresetować śledzenie zmian po dokonaniu każdego zatwierdzenia.

W opisanym przypadku można obsłużyć zdarzenie Unloaded lub Closing widoku w celu sprawdzenia, czy nie jest to DataContext; a jeśli DataContext implementuje IChangeTracking, możesz użyć właściwości IsChanged w celu ustalenia, czy zostały wprowadzone nieakceptowane zmiany.

Prosty przykład:

/// <summary> 
/// Provides a base class for objects that support property change notification 
/// and querying for changes and resetting of the changed status. 
/// </summary> 
public abstract class ViewModelBase : IChangeTracking, INotifyPropertyChanged 
{ 
    //======================================================== 
    // Constructors 
    //======================================================== 
    #region ViewModelBase() 
    /// <summary> 
    /// Initializes a new instance of the <see cref="ViewModelBase"/> class. 
    /// </summary> 
    protected ViewModelBase() 
    { 
     this.PropertyChanged += new PropertyChangedEventHandler(OnNotifiedOfPropertyChanged); 
    } 
    #endregion 

    //======================================================== 
    // Private Methods 
    //======================================================== 
    #region OnNotifiedOfPropertyChanged(object sender, PropertyChangedEventArgs e) 
    /// <summary> 
    /// Handles the <see cref="INotifyPropertyChanged.PropertyChanged"/> event for this object. 
    /// </summary> 
    /// <param name="sender">The source of the event.</param> 
    /// <param name="e">A <see cref="PropertyChangedEventArgs"/> that contains the event data.</param> 
    private void OnNotifiedOfPropertyChanged(object sender, PropertyChangedEventArgs e) 
    { 
     if (e != null && !String.Equals(e.PropertyName, "IsChanged", StringComparison.Ordinal)) 
     { 
      this.IsChanged = true; 
     } 
    } 
    #endregion 

    //======================================================== 
    // IChangeTracking Implementation 
    //======================================================== 
    #region IsChanged 
    /// <summary> 
    /// Gets the object's changed status. 
    /// </summary> 
    /// <value> 
    /// <see langword="true"/> if the object’s content has changed since the last call to <see cref="AcceptChanges()"/>; otherwise, <see langword="false"/>. 
    /// The initial value is <see langword="false"/>. 
    /// </value> 
    public bool IsChanged 
    { 
     get 
     { 
      lock (_notifyingObjectIsChangedSyncRoot) 
      { 
       return _notifyingObjectIsChanged; 
      } 
     } 

     protected set 
     { 
      lock (_notifyingObjectIsChangedSyncRoot) 
      { 
       if (!Boolean.Equals(_notifyingObjectIsChanged, value)) 
       { 
        _notifyingObjectIsChanged = value; 

        this.OnPropertyChanged("IsChanged"); 
       } 
      } 
     } 
    } 
    private bool _notifyingObjectIsChanged; 
    private readonly object _notifyingObjectIsChangedSyncRoot = new Object(); 
    #endregion 

    #region AcceptChanges() 
    /// <summary> 
    /// Resets the object’s state to unchanged by accepting the modifications. 
    /// </summary> 
    public void AcceptChanges() 
    { 
     this.IsChanged = false; 
    } 
    #endregion 

    //======================================================== 
    // INotifyPropertyChanged Implementation 
    //======================================================== 
    #region PropertyChanged 
    /// <summary> 
    /// Occurs when a property value changes. 
    /// </summary> 
    public event PropertyChangedEventHandler PropertyChanged; 
    #endregion 

    #region OnPropertyChanged(PropertyChangedEventArgs e) 
    /// <summary> 
    /// Raises the <see cref="INotifyPropertyChanged.PropertyChanged"/> event. 
    /// </summary> 
    /// <param name="e">A <see cref="PropertyChangedEventArgs"/> that provides data for the event.</param> 
    protected void OnPropertyChanged(PropertyChangedEventArgs e) 
    { 
     var handler = this.PropertyChanged; 
     if (handler != null) 
     { 
      handler(this, e); 
     } 
    } 
    #endregion 

    #region OnPropertyChanged(string propertyName) 
    /// <summary> 
    /// Raises the <see cref="INotifyPropertyChanged.PropertyChanged"/> event for the specified <paramref name="propertyName"/>. 
    /// </summary> 
    /// <param name="propertyName">The <see cref="MemberInfo.Name"/> of the property whose value has changed.</param> 
    protected void OnPropertyChanged(string propertyName) 
    { 
     this.OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); 
    } 
    #endregion 

    #region OnPropertyChanged(params string[] propertyNames) 
    /// <summary> 
    /// Raises the <see cref="INotifyPropertyChanged.PropertyChanged"/> event for the specified <paramref name="propertyNames"/>. 
    /// </summary> 
    /// <param name="propertyNames">An <see cref="Array"/> of <see cref="String"/> objects that contains the names of the properties whose values have changed.</param> 
    /// <exception cref="ArgumentNullException">The <paramref name="propertyNames"/> is a <see langword="null"/> reference (Nothing in Visual Basic).</exception> 
    protected void OnPropertyChanged(params string[] propertyNames) 
    { 
     if (propertyNames == null) 
     { 
      throw new ArgumentNullException("propertyNames"); 
     } 

     foreach (var propertyName in propertyNames) 
     { 
      this.OnPropertyChanged(propertyName); 
     } 
    } 
    #endregion 
} 
+0

dziękuję, to było pomocne dla mnie też – ganeshran

+0

Szukałem właśnie tej rzeczy do użytku w mojej aplikacji MVVM i WPF. Jednak kiedy próbuję go użyć, IsChanged jest zawsze prawdziwy. Jakieś pomysły? – Juan

+0

To podejście nie będzie poprawnie wykrywać stanu brudu dla TextBox podczas edycji. Ta informacja jest "zablokowana wewnątrz" klasy Powiązania WPF i nie można uzyskać do niej dostępu za pomocą View lub ViewModel. Możliwe jest zaimplementowanie niestandardowego wiązania, ale jest to bardzo ciężka praca i wymaga niestandardowego XAML do pracy. Trudno uwierzyć, że to rok 2013, nie jest to. – Jack

11

W MVVM a View jest binded do View-model, który z kolei jest zbindowanych do modelu.

Widok nie może być brudny, ponieważ zmiany są natychmiast odzwierciedlane w widoku modelu.

Jeśli chcesz zmiany, które należy stosować do modelu tylko na „OK” lub „Akceptuj”,
powiązania widoku do View-model, który nie stosuje się do zmiany modelu,
dopóki ApplyCommand lub AcceptCommand (czyli definiujesz i wdrażasz) jest wykonywany.

(. Polecenia że widok jest oprawiony w celu są realizowane w widoku z modelu)

Przykład - VM:

public class MyVM : INotifyPropertyChanged 
{ 
    public string MyText 
    { 
     get 
     { 
      return _MyText; 
     } 
     set 
     { 
      if (value == _MyText) 
       return; 

      _MyText = value; 
      NotifyPropertyChanged("MyText"); 
     } 
    } 
    private string _MyText; 

    public string MyTextTemp 
    { 
     get 
     { 
      return _MyTextTemp; 
     } 
     set 
     { 
      if (value == _MyTextTemp) 
       return; 

      _MyTextTemp = value; 
      NotifyPropertyChanged("MyTextTemp"); 
      NotifyPropertyChanged("IsTextDirty"); 
     } 
    } 
    private string _MyTextTemp; 

    public bool IsTextDirty 
    { 
     get 
     { 
      return MyText != MyTextTemp; 
     } 
    } 

    public bool IsMyTextBeingEdited 
    { 
     get 
     { 
      return _IsMyTextBeingEdited; 
     } 
     set 
     { 
      if (value == _IsMyTextBeingEdited) 
       return; 

      _IsMyTextBeingEdited = value; 

      if (!value) 
      { 
       MyText = MyTextTemp; 
      } 

      NotifyPropertyChanged("IsMyTextBeingEdited"); 
     } 
    } 
    private bool _IsMyTextBeingEdited; 


    public event PropertyChangedEventHandler PropertyChanged; 

    protected void NotifyPropertyChanged(string propertyName) 
    { 
     if (PropertyChanged != null) 
      PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); 
    } 
} 

przykład - zobacz:

<Label Content="{Binding MyText}" /> 

    <!-- You can translate the events to commands by using a suitable framework --> 
    <!-- or use code behind to update a new dependency property as in this example --> 
    <TextBox 
     LostFocus="TextBox_LostFocus" 
     GotFocus="TextBox_GotFocus" 
     Text="{Binding Path=MyTextTemp, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" 
     /> 

przykład - widok - kod za:

public MainWindow() 
    { 
     InitializeComponent(); 

     SetBinding(IsTextBoxFocusedProperty, 
      new Binding 
      { 
       Path = new PropertyPath("IsMyTextBeingEdited"), 
       Mode = BindingMode.OneWayToSource, 
      }); 
    } 

    private void TextBox_LostFocus(object sender, RoutedEventArgs e) 
    { 
     IsTextBoxFocused = false; 
    } 

    private void TextBox_GotFocus(object sender, RoutedEventArgs e) 
    { 
     IsTextBoxFocused = true; 
    } 

    #region IsTextBoxFocused 

    /// <summary> 
    /// Gets or Sets IsTextBoxFocused 
    /// </summary> 
    public bool IsTextBoxFocused 
    { 
     get 
     { 
      return (bool)this.GetValue(IsTextBoxFocusedProperty); 
     } 
     set 
     { 
      this.SetValue(IsTextBoxFocusedProperty, value); 
     } 
    } 

    /// <summary> 
    /// The backing DependencyProperty behind IsTextBoxFocused 
    /// </summary> 
    public static readonly DependencyProperty IsTextBoxFocusedProperty = DependencyProperty.Register(
     "IsTextBoxFocused", typeof(bool), typeof(MainWindow), new PropertyMetadata(default(bool))); 

    #endregion 
+1

Nieprawidłowe. W trybie WPF kontrolka TextBox domyślnie przyjmuje wartość UpdateSourceTrigger = LostFocus. Oznacza to, że widok może być brudny, podczas gdy ViewModel nie jest.Powodem LostFocus jest to, że w przeciwnym razie częściowe zmiany (na przykład w przypadku obiektu DateTime powodują błąd sprawdzania poprawności) Jest to wada projektowa w WPF. Solidne aplikacje muszą uwzględniać IsDirty = ViewModel.IsDirty || View.IsDirty ... – Jack

+0

@Jack, Widok może składać się z wielu różnych typów kontrolek. Domyślnym polem tekstowym jest zapobieganie aktualizacji modelu widoku przy każdym kliknięciu klawisza. Sprawdzenie, czy tekst został zmieniony, gdy użytkownik wciąż edytuje, jest prawdopodobnie złym pomysłem. –

+1

Czy w tym polu komentarza nie powinno pozostać 566 znaków? A przyciski Zapisz i Anuluj w formularzu powinny pozostać wyłączone, nawet jeśli użytkownik zmienił tekst w TextBox? Daj spokój! – Jack