23

I currently have a requirement to notify my application user if any fields have been changed/updated on a View.

For example, if the user changes a date field on the View and then tries to close the View, the application would display a message asking the user to Continue and lose changes or Cancel so that they can click the Save button.

Problem is: How do I detect that any of the data fields changed in the View?

Hope this makes sense, than you in advance, regards,

ElMatador
  • 557
  • 1
  • 6
  • 15
  • 17
    In MVVM you would ask if the Model or maybe the ViewModel is dirty. Not the View. – H H May 04 '11 at 21:54

3 Answers3

37

One approach you can take is to leverage the IChangeTracking and INotifyPropertyChanged interfaces.

If you create an abstract base class that your view models inherit from (ViewModelBase) which implements the IChangeTracking and INotifyPropertyChanged interfaces, you can have your view model base attach to notification of property changes (in effect signaling that the view model has been modified) and which will set the IsChanged property to true to indicate that the view model is 'dirty'.

Using this approach, you are relying on property change notification via data binding to track changes and would reset the change tracking after any commits are made.

In the case you described you could handle the Unloaded or Closing event of your view to inspect the DataContext; and if the DataContext implements IChangeTracking you can use the IsChanged property to determine if any unaccepted changes have been made.

Simple example:

/// <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
}
Andrew Truckle
  • 17,769
  • 16
  • 66
  • 164
Oppositional
  • 11,141
  • 6
  • 50
  • 63
  • I was looking for just this thing for use in my MVVM and WPF application. However when I try to use it, the IsChanged is always true. ANy thoughts? – Juan Dec 12 '12 at 17:34
  • This approach won't correctly detect dirty state for TextBox during editing. This information is "locked inside" the WPF Binding class and can not be accessed by either View or ViewModel. It is possible to implement a custom Binding but it is very hard work and requires non-standard XAML to work. Hard to believe this is 2013 isn't it. – Jack Feb 23 '13 at 18:06
  • 1
    Hey, I'm having problem with this approach because when I load entities from db they automatically get IsChanged = true, because the properties are set, before they can be accessed. Any ideas how to tell it not to set IsChanged, when the values come from database? – adminSoftDK Dec 03 '14 at 09:34
  • @adminSoftDK you could inject your Model via an Constructor overload and set the private Variables directly without using the Properties. Alternatively you could create a Method `setModel(MyModel m)` which does the same like the constructor – WiiMaxx Jul 22 '15 at 11:00
  • 2
    @adminSoftDK just call the AcceptChanges function after the initial loading. If have a list of models you can easily do it with Linq DataOrViewModel.ForEach(x => x.AcceptChanges()); – Swifty Nov 11 '15 at 11:25
13

In MVVM a View is binded to a View-Model which in turn is binded to a Model.

The view can not be dirty, since it's changes are reflected immediately to the View-Model.

If you want changes to be applied to Model only on "OK" or "Accept",
bind View to a View-Model that doesn't apply changes to Model,
until an ApplyCommand or AcceptCommand (that you define and implement) is executed.

(The commands that the View is binded to are implemented by the View-Model.)

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

Example - View:

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

Example - view - code behind:

    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
Danny Varod
  • 17,324
  • 5
  • 69
  • 111
  • 2
    Not correct. In WPF the TextBox control defaults to UpdateSourceTrigger = LostFocus. This means that the View can be dirty while the ViewModel is not. The reason for LostFocus is that otherwise partial edits (to a DateTime for instance will raise a validation error) This is a design flaw in WPF. Robust applications must consider IsDirty = ViewModel.IsDirty || View.IsDirty ... – Jack Feb 22 '13 at 11:49
  • @Jack, The view can consist of many different types of controls. The default for text boxes is to prevent update of the view model on every key click. Checking if a texts has been changed while the user is still editing it is probably a bad idea. – Danny Varod Feb 22 '13 at 17:16
  • 1
    So this comment box shouldn't say 566 characters left then? And the Save and Cancel buttons on a form should stay disabled even when the user has changed the text in a TextBox? Come on! – Jack Feb 23 '13 at 07:59
  • @Jack, Add two bindings to your text box, one that is two way with the regular trigger and one that is one way to source to a different property in the VM with the on change trigger. Test the second property in the VM to tell if the state is dirty, not the view. – Danny Varod Feb 25 '13 at 00:33
  • @DannyVarod, I'm trying but failing to properly parse your last comment. Do you have an example somewhere that I could learn from? – Benjol Mar 08 '13 at 14:56
  • 1
    @DannyVarod, AHA! the magic incantation is `UpdateSourceTrigger=PropertyChanged`... thanks! And by the look of things, if I'm only interested in dirty (and not 'editing'), I can do without any of the code behind. – Benjol Mar 13 '13 at 10:09
  • @DannyVarod wouldn't it be easyer to just set `UpdateSourceTrigger=PropertyChanged` because if the User needs to click the Savebutton anyway there is no need for `IsTextBoxFocused`. The same thing is true if we let `UpdateSourceTrigger` on `LostFocus` and Save without Button (by Input) because if he clicks the 'x' LostFocus will be executed and we will save ... – WiiMaxx Jul 22 '15 at 11:13
  • @WiiMaxx a. that is set, b. that has nothing to do with using or not using the MVVM pattern, b. consider false notifications, e.g. you type in the wrong field by mistake, click Ctrl+Z, exit field, in this case you may want to check only on loose focus, especially if you are running online validations, c. why do you assume there is a save button? – Danny Varod Jul 22 '15 at 12:41
  • @DannyVarod to a. so there is no need for your `IsTextBoxFocused`, b. this depends on your IsDirty if you would do `public bool IsDirty { get { return mytemp != model.mytemp;}}` you can always validate with changed. c. Like i sayed if you have a save Button you can use `UpdateSourceTrigger=PropertyChanged` and of you don't have on use `LostFocus` because what ever you do after your changing `LostFocus` will be executed first – WiiMaxx Jul 22 '15 at 13:25
  • @WiiMaxx I wrote this code over 4 years ago (directly into the site, never compiled or ran it), I just went over it again and it seems I used the focused to decide when to update the model, assuming no save button. – Danny Varod Jul 22 '15 at 13:42
-1

Idea:  check  entitystate:

Problem is that this refers to the whole VIEW, so when a new participant (refreshes form) is selected before any editing,  the value is also "Modified". After a save, if nothing else changes and we don’t switch participants, the value is  "Unchanged" 

 

 

Allan
  • 1
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jan 19 '22 at 21:29