Friday, 27 July 2012

Data-Binding Pitfalls on WPF, WP7, Silverlight and WinRT Part 2

Most of my data binding worries stem from implementing my view model for MVVM.

One of the things I noticed was that it was very difficult to get things right when multiple properties are combined to form another property, so a change to any input would need to raise notifications that the derived property had changed. 

This led to code that was difficult to debug, write and maintain.  In addition we found that in many cases, properties weren’t changed in isolation.  Changing one property on an object often meant others would soon change too.

So, to deal with this issue, we built a class that coalesced property notifications, and also had a representation of dependent properties which would fire when an input changed.  With the addition of a FreezeNotifications and UnfreezeNotifications mechanism, we could queue up all of the changes, and then fire each change only once.

However, we still end up with many properties on the same object potentially firing, and still requiring recalculation of layout multiple times.

In the previous post we were concerned with how data binding worked on a control, and in a control the properties are generally a Dependency Property.  In this example the properties in question simply implement INotifyPropertyChanged, and the objects in question aren’t based on the Control class, so calling Invalidate won’t suffice to defer our work.

In the end, here’s what we did. 

We had several objects that needed to connect to the data to get property notifications.  When a property changed, we marked our object as dirty in such a way we could determine what needed to be updated.

We then fired off a Task, which collected the data from the database, and built up the information required.  In the case where a previous task was already running, we called Cancel on the existing task, and then started a new task to load the newly loaded data.

This worked ok.  The tasks ran on another thread on the multi core machine, but we found that we were cancelling the task about 6 times before finally allowing it to run to completion.

How can I Stop Wasting Cycles?

We thought there had to be a better way.  And that turned out to be our old friend Dispather.BeginInvoke.  Instead of launching the recalculation immediately, we asked for it to happen when we finally got around to processing messages again.

Initially we passed it with the DispatcherPriority.DataBind, to get it into the DataBinding queue.  That indeed worked, and we no longer get any cancellation requests, as change detection all works on the UI thread, and when we finally relinquish control, we then start our task, and mark the object as not dirty.

When subsequent notifications arrive, the work is already done, so no further action is taken.  There’s probably a further improvement to be had by checking that we haven’t already marked the object as dirty, and avoiding queuing the call altogether.

Finally, we wondered if we really needed to drop to that priority level, and the answer was no; Normal priority works just fine.

The Moral of the Story?

Don’t be in a big hurry to do the work, as someone might change their mind, and you’ll just have to start all over again.  Instead, schedule the work to be done using Dispatcher.BeginInvoke. 

But BeginInvoke doesn’t exist on WINRT

It appears that CoreDispatcher.RunAsync gives us 3 priority levels, so changing to this method on WinRT will probably continue to work (if you can manage to get your hands on a CoreDispatcher object in the first place)

No comments:

Post a Comment