Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MVVM - implementing 'IsDirty' functionality to a ModelView in order to save data

Being new to WPF & MVVM I struggling with some basic functionality.

Let me first explain what I am after, and then attach some example code...

I have a screen showing a list of users, and I display the details of the selected user on the right-hand side with editable textboxes. I then have a Save button which is DataBound, but I would only like this button to display when data has actually changed. ie - I need to check for "dirty data".

I have a fully MVVM example in which I have a Model called User:

namespace Test.Model
{
    class User
    {
        public string UserName { get; set; }
        public string Surname { get; set; }
        public string Firstname { get; set; }
    }
}

Then, the ViewModel looks like this:

using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Windows.Input;
using Test.Model;

namespace Test.ViewModel
{
    class UserViewModel : ViewModelBase
    {
        //Private variables
        private ObservableCollection<User> _users;
        RelayCommand _userSave;

        //Properties
        public ObservableCollection<User> User
        {
            get
            {
                if (_users == null)
                {
                    _users = new ObservableCollection<User>();
                    //I assume I need this Handler, but I am stuggling to implement it successfully
                    //_users.CollectionChanged += HandleChange;

                    //Populate with users
                    _users.Add(new User {UserName = "Bob", Firstname="Bob", Surname="Smith"});
                    _users.Add(new User {UserName = "Smob", Firstname="John", Surname="Davy"});
                }
                return _users;
            }
        }

        //Not sure what to do with this?!?!

        //private void HandleChange(object sender, NotifyCollectionChangedEventArgs e)
        //{
        //    if (e.Action == NotifyCollectionChangedAction.Remove)
        //    {
        //        foreach (TestViewModel item in e.NewItems)
        //        {
        //            //Removed items
        //        }
        //    }
        //    else if (e.Action == NotifyCollectionChangedAction.Add)
        //    {
        //        foreach (TestViewModel item in e.NewItems)
        //        {
        //            //Added items
        //        }
        //    } 
        //}

        //Commands
        public ICommand UserSave
        {
            get
            {
                if (_userSave == null)
                {
                    _userSave = new RelayCommand(param => this.UserSaveExecute(), param => this.UserSaveCanExecute);
                }
                return _userSave;
            }
        }

        void UserSaveExecute()
        {
            //Here I will call my DataAccess to actually save the data
        }

        bool UserSaveCanExecute
        {
            get
            {
                //This is where I would like to know whether the currently selected item has been edited and is thus "dirty"
                return false;
            }
        }

        //constructor
        public UserViewModel()
        {

        }

    }
}

The "RelayCommand" is just a simple wrapper class, as is the "ViewModelBase". (I'll attach the latter though just for clarity)

using System;
using System.ComponentModel;

namespace Test.ViewModel
{
    public abstract class ViewModelBase : INotifyPropertyChanged, IDisposable
    {
        protected ViewModelBase()
        { 
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = this.PropertyChanged;
            if (handler != null)
            {
                var e = new PropertyChangedEventArgs(propertyName);
                handler(this, e);
            }
        }

        public void Dispose()
        {
            this.OnDispose();
        }

        protected virtual void OnDispose()
        {
        }
    }
}

Finally - the XAML

<Window x:Class="Test.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:Test.ViewModel"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <vm:UserViewModel/>
    </Window.DataContext>
    <Grid>
        <ListBox Height="238" HorizontalAlignment="Left" Margin="12,12,0,0" Name="listBox1" VerticalAlignment="Top" 
                 Width="197" ItemsSource="{Binding Path=User}" IsSynchronizedWithCurrentItem="True">
            <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel>
                        <TextBlock Text="{Binding Path=Firstname}"/>
                        <TextBlock Text="{Binding Path=Surname}"/>
                </StackPanel>
            </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <Label Content="Username" Height="28" HorizontalAlignment="Left" Margin="232,16,0,0" Name="label1" VerticalAlignment="Top" />
        <TextBox Height="23" HorizontalAlignment="Left" Margin="323,21,0,0" Name="textBox1" VerticalAlignment="Top" Width="120" Text="{Binding Path=User/UserName}" />
        <Label Content="Surname" Height="28" HorizontalAlignment="Left" Margin="232,50,0,0" Name="label2" VerticalAlignment="Top" />
        <TextBox Height="23" HorizontalAlignment="Left" Margin="323,52,0,0" Name="textBox2" VerticalAlignment="Top" Width="120" Text="{Binding Path=User/Surname}" />
        <Label Content="Firstname" Height="28" HorizontalAlignment="Left" Margin="232,84,0,0" Name="label3" VerticalAlignment="Top" />
        <TextBox Height="23" HorizontalAlignment="Left" Margin="323,86,0,0" Name="textBox3" VerticalAlignment="Top" Width="120" Text="{Binding Path=User/Firstname}" />
        <Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="368,159,0,0" Name="button1" VerticalAlignment="Top" Width="75" Command="{Binding Path=UserSave}" />
    </Grid>
</Window>

So basically, when I edit a surname, the Save button should be enabled; and if I undo my edit - well then it should be Disabled again as nothing has changed.

I have seen this in many examples, but have not yet found out how to do it.

Any help would be much appreciated! Brendan

like image 576
Brendan Avatar asked Dec 29 '10 17:12

Brendan


1 Answers

In my experience, if you implement IsDirty in your view model, you probably also want the view model to implement IEditableObject.

Assuming that your view model is the usual sort, implementing PropertyChanged and a private or protected OnPropertyChanged method that raises it, setting IsDirty is simple enough: you just set IsDirty in OnPropertyChanged if it isn't already true.

Your IsDirty setter should, if the property was false and is now true, call BeginEdit.

Your Save command should call EndEdit, which updates the data model and sets IsDirty to false.

Your Cancel command should call CancelEdit, which refreshes the view model from the data model and sets IsDirty to false.

The CanSave and CanCancel properties (assuming you're using a RelayCommand for these commands) just return the current value of IsDirty.

Note that since none of this functionality depends on the specific implementation of the view model, you can put it in an abstract base class. Derived classes don't have to implement any of the command-related properties or the IsDirty property; they just have to override BeginEdit, EndEdit, and CancelEdit.

like image 59
Robert Rossney Avatar answered Sep 19 '22 21:09

Robert Rossney