Sean Blakemore's Blog

Like trying to fit a square peg in a round hole

11. July 2010 19:36
by Sean
3 Comments

Dirty Checking your Silverlight and WPF data entry forms

11. July 2010 19:36 by Sean | 3 Comments

Let’s just dive right in and have a demo to see what we’re trying to accomplish. Below is a Silverlight application with some interesting behaviour, if you start changing any data the application will notice and the save button becomes enabled, the state of the form is ‘dirty’. That isn’t very interesting on it’s own, what is interesting is that if you then put the data back to the way it was when you started, the save button becomes disabled again and the form returns to the ‘clean’ state. Here, have a play…

Get Microsoft Silverlight

How is it done

Ok, pretty cool. Let’s have a look at the code behind file and you will see that there is nothing much going on.

namespace SBlakemore.DirtyChecking
{
    public partial class MainPage
    {
        public MainPage()
        {
            InitializeComponent();
        }
    }
}

And here is the XAML markup for the data entry form, now we’re starting to get some clues about how this is implemented. Note: I’ve removed the UserControl declaration and the styles which are in it’s resource dictionary for clarity, however the StackPanel below is the root of the UserControl.

    <StackPanel>
        <local:Tracker x:Name="tracker" HorizontalContentAlignment="Stretch">
            <Grid Margin="20 20 20 0">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="3*" />
                    <ColumnDefinition Width="7*" />
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition Height="35" />
                    <RowDefinition Height="35" />
                    <RowDefinition Height="35" />
                    <RowDefinition Height="35" />
                </Grid.RowDefinitions>
                
                <TextBlock Text="Artist:" />
                <TextBox local:Tracker.Property="Text" Text="Michael Jackson" />

                <TextBlock Grid.Row="1" Text="Title:" />
                <TextBox Grid.Row="1" local:Tracker.Property="Text" Text="Thriller" />

                <TextBlock Grid.Row="2" Text="Explicit Lyrics:" />
                <CheckBox Grid.Row="2" local:Tracker.Property="IsChecked"/>

                <TextBlock Grid.Row="3" Text="Genre:" />
                <ComboBox Grid.Row="3" local:Tracker.Property="SelectedItem" SelectedIndex="3">
                    <sys:String>Blues</sys:String>
                    <sys:String>Country</sys:String>
                    <sys:String>Jazz</sys:String>
                    <sys:String>Pop</sys:String>
                    <sys:String>Soul</sys:String>
                    <sys:String>Hip Hop</sys:String>
                </ComboBox>
            </Grid>
        </local:Tracker>
        
        <Button Content="Save" Width="80" Height="30" Margin="10"
                IsEnabled="{Binding IsChanged, ElementName=tracker}" />
    </StackPanel>

Looking Closer

There are three things to note here.

  • I’ve wrapped the data entry controls in something called Tracker.
  • All of the data entry controls have a value set for an attached property which is defined by Tracker, and that value is the name of the property which holds the data for that particular data entry control. For example for a TextBox the attached property is set to “Text”, and for a Checkbox the attached property is set to “IsChecked”.
  • The save button is being enabled and disabled by being bound to the IsChanged property on our tracker.

You will probably by now have figured out in principle how this works. When the Tracker control is loaded it recurses through all of it’s children looking for anything which declares a value for Tracker.Property. When it finds one it looks at the value set for Tracker.Property and then finds the DependencyProperty of that name on the declaring control, it will then take a snapshot of the value and subscribe to change notifications from the DependencyProperty. Whenever a change notification is received it will read the new value and compare it to the snapshot to see if the value is ‘dirty’ and has been changed. The IsChanged property on Tracker returns true if one of more of it’s children have changed.

This is pretty cool because implementing this feature in the model quickly becomes a nasty mess of INotifyPropertyChanged madness and as soon as the model and data entry form become anything more than trivial you will run up against complications and edge cases. In addition you end up with event subscriptions everywhere trying to track your properties and because of this could easily cause memory leaks.

Much better to pull this complexity out of your ViewModels and implement it instead as a cross cutting concern which is applied to your data entry forms declaratively.

Show me the Code!

Here is the code for the Tracker class.

public class Tracker : ContentControl
{
    public bool IsChanged
    {
        get { return (bool)GetValue(IsChangedProperty); }
        set { SetValue(IsChangedProperty, value); }
    }
    public static readonly DependencyProperty IsChangedProperty =
        DependencyProperty.Register("IsChanged", typeof(bool), typeof(Tracker), new PropertyMetadata(false));

    public static string GetProperty(DependencyObject obj)
    {
        return (string)obj.GetValue(PropertyProperty);
    }
    public static void SetProperty(DependencyObject obj, string value)
    {
        obj.SetValue(PropertyProperty, value);
    }
    public static readonly DependencyProperty PropertyProperty =
        DependencyProperty.RegisterAttached("Property", typeof(string), typeof(Tracker), new PropertyMetadata(null));

    private readonly Dictionary<PropertyWatcher, object> trackedPropertySnapshot = new Dictionary<PropertyWatcher, object>();
    private readonly List<PropertyWatcher> changedProperties = new List<PropertyWatcher>();
    private readonly object nullProperty = new object();

    public Tracker()
    {
        Loaded += (s, e) =>
        {
            var content = Content as DependencyObject;
            if (content != null)
                WalkDownVisualTree(content);
        };
    }

    public void AcceptChanges()
    {
        changedProperties.Clear();

        IsChanged = false;
    }

    private void WalkDownVisualTree(DependencyObject current)
    {
        var property = current.ReadLocalValue(PropertyProperty);
        if (property != DependencyProperty.UnsetValue)
            RegisterTrackedProperty(current, (string)property);

        var count = VisualTreeHelper.GetChildrenCount(current);
        for (int i = 0; i < count; i++)
        {
            WalkDownVisualTree(VisualTreeHelper.GetChild(current, i));
        }
    }

    private void RegisterTrackedProperty(DependencyObject item, string propertyName)
    {
        var notifier = new PropertyWatcher(item, propertyName);
        notifier.ValueChanged += TrackedPropertyChanged;

        trackedPropertySnapshot.Add(notifier, notifier.Value);
    }

    private void TrackedPropertyChanged(object sender, EventArgs e)
    {
        var notifier = ((PropertyWatcher)sender);
        var original = trackedPropertySnapshot[notifier] jQuery15209391723657026887_1327589005650 nullProperty;
        var current = notifier.Value ?? nullProperty;

        if (!original.Equals(current))
        {
            if (!changedProperties.Contains(notifier))
                changedProperties.Add(notifier);
        }
        else
            changedProperties.Remove(notifier);

        IsChanged = changedProperties.Count != 0;
    }
}

It took me a bit of playing around to figure out how to subscribe to change notifications for the relevant property, something which is not too big of a deal in WPF, and in the end I came up with a very handy little class I called PropertyWatcher. The constructor takes a source object and a string with the name of a property on the source object.

public class PropertyWatcher : DependencyObject
{
    public DependencyObject Source { get; protected set; }

    public PropertyWatcher(DependencyObject source, string propertyName)
    {
        Source = source;

        var path = new PropertyPath(propertyName);
        var binding = new Binding { Path = path, Mode = BindingMode.OneWay, Source = source };
        BindingOperations.SetBinding(this, ValueProperty, binding);
    }

    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.Register("Value", typeof(object), typeof(PropertyWatcher), new PropertyMetadata(null, OnPropertyChanged));

    private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var notifier = (PropertyWatcher)sender;
        notifier.ValueChanged(notifier, EventArgs.Empty);
    }

    public object Value
    {
        get { return GetValue(ValueProperty); }
        set { SetValue(ValueProperty, value); }
    }

    public event EventHandler ValueChanged = delegate { };
}

Final Thoughts

It’s a pretty nice feature to be able to not have to prompt a user to save an entity which, in their mind, they haven’t actually changed. I’ve used a specialised and enhanced version of the code above in a pretty complex WPF data entry form and am pleased with how it’s working.

One possible nice enhancement would be to try to encapsulate this into a Behaviour to make the whole thing Blendable.

How do you feel about tracking changes in this way? Code download below.

Comments (3) -

This is much cleaner than the way I was handling this. Thanks.

@kamran and @littlecharva

Thanks guys. Smile

Pingbacks and trackbacks (2)+

Comments are closed