2011-05-04 16 views
17

Actualmente, tengo la obligación de notificar a mi usuario de la aplicación si se han cambiado/actualizado campos en una vista.WPF MVVM: cómo detectar si una vista es "sucia"

Por ejemplo, si el usuario cambia un campo de fecha en la Vista y luego intenta cerrar la Vista, la aplicación mostrará un mensaje solicitando al usuario Continuar y perder cambios o Cancelar para poder hacer clic en el botón Guardar.

El problema es: ¿Cómo puedo detectar que haya cambiado alguno de los campos de datos en la Vista?

la esperanza que esto tenga sentido, de lo que de antemano, saludos,

+13

En MVVM usted preguntaría si el modelo o tal vez el modelo de vista es sucio. No es la vista. –

Respuesta

30

Un enfoque que puede tomar es aprovechar las interfaces IChangeTracking y INotifyPropertyChanged.

Si crea una clase base abstracta que sus modelos de vista heredan de (ViewModelBase) que implementa las interfaces IChangeTracking y INotifyPropertyChanged, usted puede tener su vista base modelo de adjuntar a la notificación de cambios de propiedad (en efecto, lo que indica que el modelo de vista tiene sido modificado) y que configurará la propiedad IsChanged como verdadera para indicar que el modelo de vista está 'sucio'.

Al usar este enfoque, se basa en la notificación de cambio de propiedad a través del enlace de datos para rastrear los cambios y reiniciar el seguimiento de cambios después de realizar las confirmaciones.

En el caso que describió podría manejar el evento Unloaded o Closing de su vista para inspeccionar DataContext; y si DataContext implementa IChangeTracking, puede usar la propiedad IsChanged para determinar si se han realizado cambios no aceptados.

ejemplo simple:

/// <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

gracias, esto fue útil para mí también – ganeshran

+0

Estaba buscando exactamente esto para usar en mi aplicación MVVM y WPF. Sin embargo, cuando intento usarlo, el IsChanged siempre es verdadero. ¿Alguna idea? – Juan

+0

Este enfoque no detectará correctamente el estado sucio de TextBox durante la edición. Esta información está "bloqueada dentro" de la clase WPF Binding y no se puede acceder mediante View o ViewModel. Es posible implementar un enlace personalizado, pero es un trabajo muy difícil y requiere XAML no estándar para funcionar. Es difícil de creer que esto sea 2013, ¿verdad? – Jack

11

En MVVM una vista binded a una vista de modelo, que a su vez se enganchan a un modelo.

La vista no puede estar sucia, ya que sus cambios se reflejan inmediatamente en el modelo de visualización.

Si desea que los cambios que se aplicarán al modelo sólo en "OK" o "Aceptar", se unen
Ver a una vista de modelo que no se aplica a los cambios de modelo,
hasta que un ApplyCommand o AcceptCommand (que usted define e implementa) se ejecuta.

(. Los comandos que la vista se enganchan a son implementados por la Vista-Modelo)

Ejemplo - 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)); 
    } 
} 

Ejemplo - Vista:

<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}" 
     /> 

Ejemplo - vista - código detrás:

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

No es correcto. En WPF, el control TextBox se establece de forma predeterminada en UpdateSourceTrigger = LostFocus. Esto significa que la Vista puede estar sucia mientras ViewModel no.El motivo de LostFocus es que, de lo contrario, modificaciones parciales (a DateTime, por ejemplo, generarán un error de validación). Este es un error de diseño en WPF. Las aplicaciones robustas deben considerar IsDirty = ViewModel.IsDirty || View.IsDirty ... – Jack

+0

@Jack, la vista puede consistir en muchos tipos diferentes de controles. El valor predeterminado para los cuadros de texto es evitar la actualización del modelo de vista en cada clic de tecla. Comprobar si un texto ha sido cambiado mientras el usuario todavía está editando es probablemente una mala idea. –

+1

¿Entonces este cuadro de comentario no debería decir 566 caracteres restantes? ¿Y los botones Guardar y Cancelar en un formulario deben permanecer desactivados incluso cuando el usuario ha cambiado el texto en un cuadro de texto? ¡Venga! – Jack