次の方法で共有


DM-V-VM part 6: Revisiting the data model

Sorry it's taking me so long to get the posts out. The series turned out to be a little longer than I anticipated :-) I got a lot of good feedback on the Data Model stuff.

First off, I want to mention layering. The DataModel typically is a layer on top of some other lower level data model that's not optimized to WPF. This might be something specific to the database technology you are using. Or, it could wrap some native object that's accessed via interop (I've run into a couple of examples of people doing this recently).

Also, I made some simplifications in the first post that made them much less interesting. The big simplification was that the models only fetched their data once and weren't live. Things get a lot more interesting when the models keep their data up to date.

Once you make them live, you run into a life time issue. If you have a large set of items, you only want to keep the visible items live. We'll do this by giving models Activate and Deactivate functions that control when it is live. Let's start with the DataModel changes. I'm not going to list the full class here, just the modifications. I'll post the entire sample soon when I finish up the series. If you have any question about how to apply these changes, let me know.

First, an IsActive property, which is implemented much like the State property. We could make it settable to activate and deactivate the model, but I like to think of those as methods rather than a property change:

         public bool IsActive
        {
            get
            {
                VerifyCalledOnUIThread();
                return _isActive;
            }

            private set
            {
                VerifyCalledOnUIThread();
                if (value != _isActive)
                {
                    _isActive = value;
                    SendPropertyChanged("IsActive");
                }
            }
        }

And, the Activate/Deactivate methods:

         public void Activate()
        {
            VerifyCalledOnUIThread();

            if (!_isActive)
            {
                this.IsActive = true;
                OnActivated();
            }
        }
         public void Deactivate()
        {
            VerifyCalledOnUIThread();

            if (_isActive)
            {
                this.IsActive = false;
                OnDeactivated();
            }
        }

And, some simple overridable stubs:

         protected virtual void OnActivated()
        {
        }
         protected virtual void OnDeactivated()
        {
        }

This is all pretty simple, we can just activate it and deactivate it. Subclasses can override the behavior when activated and deactivated.

Now, let's modify the StockModel to be live while activated. We'll use a DispatcherTimer to update on an interval. We'll start the timer and do the first update when activated:

         protected override void OnActivated()
        {
            VerifyCalledOnUIThread();

            base.OnActivated();

            _timer = new DispatcherTimer(DispatcherPriority.Background);
            _timer.Interval = TimeSpan.FromMinutes(5);
            _timer.Tick += delegate { ScheduleUpdate(); };
            _timer.Start();

            ScheduleUpdate();
        }

And, we'll stop the timer when deactivated:

         protected override void OnDeactivated()
        {
            VerifyCalledOnUIThread();

            base.OnDeactivated();

            _timer.Stop();
            _timer = null;
        }

When we're ready to do an update, we'll use a background thread as before:

         private void ScheduleUpdate()
        {
            VerifyCalledOnUIThread();

            // Queue a work item to fetch the quote
            if (ThreadPool.QueueUserWorkItem(new WaitCallback(FetchQuoteCallback)))
            {
                this.State = ModelState.Fetching;
            }
        }

Note: We could have used a System.Threading.Timer to do the updates where we'd be called on a background thread directly, but then we couldn't set the model state to fetching. We'd have to send that back to the UI thread.

Ok, now we've made it so you can activate it and deactivate it, but when do we do so? Let's say we've got thousands of our models in a ListBox. It's only going to only show a few of them on the screen at a time and we only want the ones on screen to be active. We'll use the attached property trick to do this without having to write custom code each time we want to activate and deactivate models. The basic idea is that we're going to display our model in a DataTemplate and we want to activate the model when FrameworkElements in the UI are loaded, and deactivate the model when they are unloaded. With the attached property trick, our DataTemplate Xaml just has to be:

    <DataTemplate DataType="{x:Type local:StockModel}">

      <StackPanel Orientation="Horizontal" local:ActivateModel.Model="{Binding}">

        <TextBlock Text="{Binding Symbol}" Width="100"/>

        <TextBlock Text="{Binding Quote}" />

      </StackPanel>

    </DataTemplate>

Note the local:ActivateModel.Model={Binding}. Now, here's how we implement that magic property! First, we need define the DependencyProperty and the accessor functions:

     public static class ActivateModel
    {
        public static readonly DependencyProperty ModelProperty
           = DependencyProperty.RegisterAttached("Model", typeof(DataModel), typeof(ActivateModel),
                new PropertyMetadata(new PropertyChangedCallback(OnModelInvalidated)));

        public static DataModel GetModel(DependencyObject sender)
        {
            return (DataModel)sender.GetValue(ModelProperty);
        }

        public static void SetModel(DependencyObject sender, DataModel model)
        {
            sender.SetValue(ModelProperty, model);
        }

We've registered a PropertyChangedCallback on the property, so any time it is changed (including when it's initially set), OnModelInvalidated is going to be called. In OnModelInvalidated, we're going to register for Loaded and Unloaded events on the the FrameworkElement we are attached to. We also have to do a bit of bookkeeping to clean up if we were previously pointing to a different model. 

         private static void OnModelInvalidated(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
        {
            FrameworkElement element = (FrameworkElement)dependencyObject;

            // Add handlers if necessary
            if (e.OldValue == null && e.NewValue != null)
            {
                element.Loaded += OnElementLoaded;
                element.Unloaded += OnElementUnloaded;
            }

            // Or, remove if necessary
            if (e.OldValue != null && e.NewValue == null)
            {
                element.Loaded -= OnElementLoaded;
                element.Unloaded -= OnElementUnloaded;
            }

            // If loaded, deactivate old model and activate new one
            if (element.IsLoaded)
            {
                if (e.OldValue != null)
                {
                    ((DataModel)e.OldValue).Deactivate();
                }

                if (e.NewValue != null)
                {
                    ((DataModel)e.NewValue).Activate();
                }
            }
        }

And, here are the Loaded/Unloaded handlers.

         static void OnElementLoaded(object sender, RoutedEventArgs e)
        {
            FrameworkElement element = (FrameworkElement)sender;
            DataModel model = GetModel(element);
            model.Activate();
        }

        static void OnElementUnloaded(object sender, RoutedEventArgs e)
        {
            FrameworkElement element = (FrameworkElement)sender;
            DataModel model = GetModel(element);
            model.Deactivate();
        }

Pretty neat trick, isn't it? This means it's really easy for us to activate models when they are visible in the UI by simply adding the ActivateModel.Model property to the UI element. Since we know all FrameworkElements will get unloaded when they go away, we know we won't have to worry about leaking anything. It doesn't require any custom activate/deactivate code per view!

We get a form of data virtualization out of this trick. If the data is very expensive, the models can act as a relatively cheap shell. When you go to view a a large collection of items, you just need to provide a collection of the data models instead of the full items. The expensive data can then be accessed only when the UI for the data is visible on the screen and then thrown out when the data goes offscreen.

I'll confess, I've made another simplification from what we've done in Max. The lifetime management of a lot of our models is slightly more complex. And, as much as we hated to do so, we ended up adding a reference counting to our models. So we keep track of multiple levels of activation and a model is live as long as that count is greater than zero. To get something a bit like smart pointers, we have Activate return an IDisposable which we call an "activation cookie". Disposing of the activation cookie decrements the activation count. The cookie is the only way to decrement the count, there's no public method on the model. It's smart enough to not let you decrement multiple times. And, in debug builds, we have a finalizer on the cookie that asserts if Dispose wasn't called, to help catch leaks. We were quite happy to leaving reference counting when moving to managed code, so it hurt a bit to bring it back :-)

Ok, I think I'm just a couple of posts from wrapping this up!

Comments

  • Anonymous
    August 29, 2006
    Hi Dan!

    very great series! I'm really impressed, thats really tricky!

    I'm going  to try implementing this pattren for 3DObjects(MV3D). I know 3DObjects are not FrameWorkElements or UIElements, but I think not all but some of concepts of this pattern could be very usefull to implement binding between a 3DObject and a DataModel (to get the position in 3D space for example could be a possible Data from DataModel). Thanks a lot for great work! Waiting for next/last couples of posts...

    PS: your session at deep dive was very cool! Thanks.

  • Anonymous
    September 13, 2006
    How about posting those last couple of posts to wrap this up? :)

  • Anonymous
    September 14, 2006
    Oh, Yes, I'm waiting for that too. I hope it comes soon :). Let's wrap it up.....

  • Anonymous
    September 14, 2006
    Sorry, I'll get to it this weekend at the latest!

  • Anonymous
    September 15, 2006
    In part 5, I talked about commands and how they are used for behavior. Now, I want to talk about a better...

  • Anonymous
    September 18, 2006
    Hi Dan,

    Thanks a lot! I'm looking forward for other posts.

    One question came to my mind while reading DM-M-VM series:
    Why don't you use async bindings for fetching data in background?

    Thanks,
    paul

  • Anonymous
    September 19, 2006
    There are a few things that steer me away from asynchronous data binding. The first is that the property getters will now be callable by multiple threads, and that makes the threading model for the object much more complicated. Second, I think it would be surprising that some properties could hang indefinitely, but others wouldn't. As a consumer of the class, how would you know what's safe to call? And, finally, you get more control if you do it yourself. With asynchronous binding, WPF creates the thread for you, but with this mechanism you have more control over scheduling updates and such.

  • Anonymous
    September 19, 2006
    Great series! Thanks for the efforts!

    In reviewing all of the parts of this series tonight, the statement "we ended up adding a reference counting to our models" got me curious. You obviously weren't happy about having to do it, so what exactly motivated you to implement reference counting in Max?

  • Anonymous
    September 20, 2006
    Thanks for reply, Dan!

    I see your point.
    Actually, I guessed a reason behind your solution, just, you know, it seemed curious: you hadn't mentioned async bindings at all in your articles.

    Now it's clear,
    Thanks

    paul

  • Anonymous
    September 20, 2006
    The activation model I presented here works fine if there is only one "owner" that ever activates the model. But, as soon as multiple things want to activate the same model, you need the reference counting. As a simple example, what if you wanted to show the same model in two places in the UI?

  • 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
    In part 5 , I talked about commands and how they are used for behavior. Now, I want to talk about a better

  • 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 26, 2006
    This is just a little thing but when you call Activate() you immediately call your DataModel's VerifyCalledOnUIThread(); then you set the property using this.IsActive = true which then immediately calls VerifyCalledOnUIThread(); is this an oversight or am I not getting something...seems like these VerifyCalledOnUIThread methods should only ever be called when you are changing properties.  Unless you method directly manipulates the member data itself.

  • Anonymous
    October 26, 2006
    I agree it's overkill since the IsActive's setter also checks, but I think it's nice to have it explicitly there.