다음을 통해 공유


DataModel-View-ViewModel pattern: 2

In part 1, I gave an overview of a pattern we use in the UI development of Max. In this post, I plan to talk about DataModels. In part 1, I wrote:

DataModel

DataModel is responsible for exposing data in a way that is easily consumable by WPF. All of its public APIs must be called on the UI thread only. It must implement INotifyPropertyChanged and/or INotifyCollectionChanged as appropriate. When data is expensive to fetch, it abstracts away the expensive operations, never blocking the UI thread (that is evil!). It also keeps the data "live". These sorts of classes are fairly straightforward to unit test.

I want to talk a bit about the threading model. Making all public APIs single threaded on a DataModel may seem like overkill. It's certainly possible to make some methods thread safe. And, you might know that WPF data binding will actually handle property changed events from other threads. But, in my experience, making the APIs of DataModel single thread significantly simplifies the models and eliminates many possible bugs. The rules become very simple. Of course, DataModels usually need to do operations on background threads, but they can use the Dispatcher to dispatch the results back to the UI thread.

Let's go through a possible DataModel base class. I will use it in the sample moving forward.

First, the defition and constructor:

    public class DataModel : INotifyPropertyChanged

    {

        public DataModel()

        {

            _dispatcher = Dispatcher.CurrentDispatcher;

        }

We grab the current Dispatcher in the constructor. We now have it available for any background operations that need to be dispatch results to the UI thread.

Now, here's the definition of the possible states of the model:

        public enum ModelState

        {

            Fectching,  // The model is fetching data

            Invalid,    // The model is in an invalid state

            Active      // The model has fetched its data

        }

I think these are pretty self explanatory. Basically, the model will be in a fetching state if it's fetching data asynchronously. Otherwise, it will be in the invalid or activate state. The state is made available through the following property:

        public ModelState State

        {

            get

            {

                VerifyCalledOnUIThread();

                return _state;

            }

 

            set

            {

                VerifyCalledOnUIThread();

                if (value != _state)

                {

                    _state = value;

                    SendPropertyChanged("State");

                }

            }

        }

This is the basic pattern we'll use for most model properties. When getting the value, we verify that we're on the UI thread and just return the cached value. When setting the property, we also verify that we're on the UI thread. And, if the value changed, we send the event that it changed. Let me fill in a couple of the utility functions now. The first is VerifyCalledOnUIThread():

        [Conditional("Debug")]

        protected void VerifyCalledOnUIThread()

        {

            Debug.Assert(Dispatcher.CurrentDispatcher == this.Dispatcher,

                "Call must be made on UI thread.");

        }

Basically, we make sure that we're on the right thread by checking the current dispatcher. The Conditional attribute makes it so this code isn't executed in retail bits. Sprinkling asserts in the code like this makes it easy to catch these violations early. Otherwise, you might end up tracing down hard to reproduce race conditions.

Next, here's our NotifyPropertyChanged event. We define our own add/remove handlers so we can verify that things are called on the UI thread. If handlers were added/removed from another thread, we'd run into threading issues.

        public event PropertyChangedEventHandler PropertyChanged

        {

            add

            {

                VerifyCalledOnUIThread();

                _propertyChangedEvent += value;

            }

            remove

            {

                VerifyCalledOnUIThread();

                _propertyChangedEvent -= value;

            }

        }

And, SendPropertyChanged is a helper function that notifies listeners that the named property changed:

        protected void SendPropertyChanged(string propertyName)

        {

            VerifyCalledOnUIThread();

            if (_propertyChangedEvent != null)

            {

                _propertyChangedEvent(this, new PropertyChangedEventArgs(propertyName));

            }

        }

That just leaves the fields:

        private ModelState _state;

        private Dispatcher _dispatcher;

        private PropertyChangedEventHandler _propertyChangedEvent;

    }

I'll be using this base class in my sample moving forward. If anyone's interested, I could make the source code available, but it's all here and I plan to make the full version with comments available when I get further along in the sample.

This class is missing one major feature that I plan to talk more about later. But, I think I need a disclaimer here in case anyone starts any of their own code based on this. As mentioned above, one of the roles of a DataModel is to keep its data "live". This can lead to memory leaks. To keep data live, the model will need to register for change notifications from some other source, or maybe set up a timer. This will keep the DataModel object alive. If there's a DataTemplate pointing to the DataModel with data binding set up, it will have registered for property changed notifications from the DataModel, so the DataModel will be keeping the UI in the DataTemplate alive, even after it's been unloaded from the UI. The solution we use is to have a reference counted activate/deactivate pattern in our DataModel classes where a model is only live while active. We activate the model when the UI pointing to it is Loaded, and deactivate when Unloaded. I'll blog about this more in the future...

Comments

  • Anonymous
    July 23, 2006
    PingBack from http://blogs.msdn.com/dancre/archive/2006/07/23/676272.aspx

  • Anonymous
    July 25, 2006
    Control Licensing in Cider (WPF designer for VS)James provides somegreat information on supporting...

  • Anonymous
    July 26, 2006
    Just wanted to let you know that you've got at least one reader on the edge of his seat. These posts are extremely valuable. Eagerly waiting for the next one...

  • Anonymous
    July 26, 2006
    In part 2, I showed a base class for DataModels. In this post, I will describe a sample implementation....

  • Anonymous
    July 28, 2006
    This series of articles is great. I was attempting to try it out but I cannot seem to find the Dispatcher namespace. I have searched the .net 2.0 assemblies to no avail. I do not have the .net 3.0 previews installed. Is the Dispatcher a new class? Thanks for the great work.

    John

  • Anonymous
    July 28, 2006
    Glad you've found the posts useful! Dispatcher is something new in .Net 3.0 (aka WinFX) and is heavily used by WPF (aka Avalon).

  • Anonymous
    July 28, 2006
    Is there a comperable class in the 2.0 framework? I'm trying to retrofit this concept back into some code. I thought the BackgroundWorker may work but it only allows one to run at a time. Maybe you could recommend some reading or ideas on this.
    Thanks!

  • Anonymous
    July 30, 2006
    Is this for a WinForms application? If so, you might use a custom window message instead of the dispatcher. It's fundamentally the same thing, although it's wrapped in a nice API with Dispatcher.

  • Anonymous
    September 27, 2006
    If you're doing WPF development, you really need to check out Dan Crevier 's series on DataModel-View-ViewModel.

  • Anonymous
    October 11, 2006
    I thought I should add a post with the full list of posts in the D-V-VM pattern. They are: DataModel-View-ViewModel

  • Anonymous
    October 15, 2006
    Picky picky, but should this: Debug.Assert(Dispatcher.CurrentDispatcher == this.Dispatcher reference this._dispatcher instead of this.Dispatcher?  (I'm working through the entries now so this may be correct in the final version). Sorry. John

  • Anonymous
    October 16, 2006
    You are right, I forgot to include the Dispatcher property code above. Once you add a Dispatcher property with a get that returns _dispatcher, then either works.

  • Anonymous
    January 06, 2007
    In part 2 , I showed a base class for DataModels. In this post, I will describe a sample implementation.