MVVM in Task-It

Wednesday, March 24, 2010 by Ross Wozniak | Comments 19

As I'm gearing up to write a post about dynamic XAP loading with MEF, I'd like to first talk a bit about MVVM, the Model-View-ViewModel pattern, as I will be leveraging this pattern in my future posts.

Download Source Code

Why MVVM?

Your first question may be, "why do I need this pattern? I've been using a code-behind approach for years and it works fine." Well, you really don't have to make the switch to MVVM, but let me first explain some of the benefits I see for doing so.

MVVM Benefits

  • Testability - This is the one you'll probably hear the most about when it comes to MVVM. Moving most of the code from your code-behind to a separate view model class means you can now write unit tests against the view model without any knowledge of a view (UserControl).
  • Multiple UIs - Let's just say that you've created a killer app, it's running in the browser, and maybe you've even made it run out-of-browser. Now what if your boss comes to you and says, "I heard about this new Windows Phone 7 device that is coming out later this year. Can you start porting the app to that device?". Well, now you have to create a new UI (UserControls, etc.) because you have a lot less screen real estate to work with. So what do you do, copy all of your existing UserControls, paste them, rename them, and then start changing the code? Hmm, that doesn't sound so good. But wait, if most of the code that makes your browser-based app tick lives in view model classes, now you can create new view (UserControls) for Windows Phone 7 that reference the same view model classes as your browser-based app.
  • Page state - In Silverlight you're at some point going to be faced with the same issue you dealt with for years in ASP.NET, maintaining page state. Let's say a user hits your Products page, does some stuff (filters record, etc.), then leaves the page and comes back later. It would be best if the Products page was in the same state as when they left it right? Well, if you've thrown away your view (UserControl or Page) and moved off to another part of the UI, when you come back to Products you're probably going to re-instantiate your view...which will put it right back in the state it was when it started. Hmm, not good. Well, with a little help from MEF you can store the state in your view model class, MEF will keep that view model instance hanging around in memory, and then you simply rebind your view to the view model class. I made that sound easy, but it's actually a bit of work to properly store and restore the state. At least it can be done though, which will make your users a lot happier! I'll talk more about this in an upcoming blog post.

No event handlers?

Another nice thing about MVVM is that you can bind your UserControls to the view model, which may eliminate the need for event handlers in your code-behind. So instead of having a Click handler on a Button (or RadMenuItem), for example, you can now bind your control's Command property to a DelegateCommand in your view model (I'll talk more about Commands in an upcoming post).

Instead of having a SelectionChanged event handler on your RadGridView you can now bind its SelectedItem property to a property in your view model, and each time the user clicks a row, the view model property's setter will be called. Now through the magic of binding we can eliminate the need for traditional code-behind based event handlers on our user interface controls, and the best thing is that the view model knows about everything that's going on...which means we can test things without a user interface.

The brains of the operation

So what we're seeing here is that the view is now just a dumb layer that binds to the view model, and that the view model is in control of just about everything, like what happens when a RadGridView row is selected, or when a RadComboBoxItem is selected, or when a RadMenuItem is clicked. It is also responsible for loading data when the page is hit, as well as kicking off data inserts, updates and deletions. Once again, all of this stuff can be tested without the need for a user interface. If the test works, then it'll work regardless of whether the user is hitting the browser-based version of your app, or the Windows Phone 7 version. Nice!

The database

Before running the code for this app you will need to create the database. First, create a database called MVVMProject in SQL Server, then run MVVMProject.sql in the MVVMProject/Database directory of your downloaded .zip file. This should give you a Task table with 3 records in it. When you fire up the solution you will also need to update the connection string in web.config to point to your database instead of IBM12\SQLSERVER2008.

The code

One note about this code is that it runs against the latest Silverlight 4 RC and WCF RIA Services code. Please see my first blog post about updating to the RC bits.

Beta to RC - Part 1

At the top of this post is a link to a sample project that demonstrates a sample application with a Tasks page that uses the MVVM pattern. This is a simplified version of how I have implemented the Tasks page in the Task-It application. You’ll notice that Tasks.xaml has very little code to it. Just a TextBlock that displays the page title and a ContentControl.

<StackPanel>
    <TextBlock Text="Tasks" Style="{StaticResource PageTitleStyle}"/>
    <Rectangle Style="{StaticResource StandardSpacerStyle}"/>
    <ContentControl x:Name="ContentControl1"/>
</StackPanel>

In List.xaml we have a RadGridView. Notice that the ItemsSource is bound to a property in the view model class call Tasks, SelectedItem is bound to a property in the view model called SelectedItem, and IsBusy is bound to a property in the view model called IsLoading.

<Grid>
    <telerikGridView:RadGridView ItemsSource="{Binding Tasks}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}"                  
               IsBusy="{Binding IsLoading}" AutoGenerateColumns="False" IsReadOnly="True" RowIndicatorVisibility="Collapsed"
               IsFilteringAllowed="False" ShowGroupPanel="False">
        <telerikGridView:RadGridView.Columns>
            <telerikGridView:GridViewDataColumn Header="Name" DataMemberBinding="{Binding Name}" Width="3*"/>
            <telerikGridView:GridViewDataColumn Header="Due" DataMemberBinding="{Binding DueDate}" DataFormatString="{}{0:d}" Width="*"/>
        </telerikGridView:RadGridView.Columns>
    </telerikGridView:RadGridView>
</Grid>

In Details.xaml we have a Save button that is bound to a property called SaveCommand in our view model. We also have a simple form (I’m using a couple of controls here from Silverlight.FX for the form layout, FormPanel and Label simply because they make for a clean XAML layout). Notice that the FormPanel is also bound to the SelectedItem in the view model (the same one that the RadGridView is). The two form controls, the TextBox and RadDatePicker) are bound to the SelectedItem's Name and DueDate properties. These are properties of the Task object that WCF RIA Services creates.

<StackPanel>
    <Button Content="Save" Command="{Binding SaveCommand}" HorizontalAlignment="Left"/>
    <Rectangle Style="{StaticResource StandardSpacerStyle}"/>
    <fxui:FormPanel DataContext="{Binding SelectedItem}" Style="{StaticResource FormContainerStyle}">
        <fxui:Label Text="Name:"/>
        <TextBox Text="{Binding Name, Mode=TwoWay}"/>
        <fxui:Label Text="Due:"/>
        <telerikInput:RadDatePicker SelectedDate="{Binding DueDate, Mode=TwoWay}"/>
    </fxui:FormPanel>
</StackPanel>

In the code-behind of the Tasks control, Tasks.xaml.cs, I created an instance of the view model class (TasksViewModel) in the constructor and set it as the DataContext for the control. The Tasks page will load one of two child UserControls depending on whether you are viewing the list of tasks (List.xaml) or the form for editing a task (Details.xaml).

// Set the DataContext to an instance of the view model class
var viewModel = new TasksViewModel();
DataContext = viewModel;
 
// Child user controls (inherit DataContext from this user control)
List = new List(); // RadGridView
Details = new Details(); // Form

When the page first loads, the List is loaded into the ContentControl.

// Show the RadGridView first
ContentControl1.Content = List;

In the code-behind we also listen for a couple of the view model’s events. The ItemSelected event will be fired when the user clicks on a record in the RadGridView in the List control. The SaveCompleted event will be fired when the user clicks Save in the Details control (the form). Here the view model is in control, and is letting the view know when something needs to change.

// Listeners for the view model's events
viewModel.ItemSelected += OnItemSelected;
viewModel.SaveCompleted += OnSaveCompleted;

The event handlers toggle the view between the RadGridView (List) and the form (Details).

void OnItemSelected(object sender, RoutedEventArgs e)
{
    // Show the form
    ContentControl1.Content = Details;
}
 
void OnSaveCompleted(object sender, RoutedEventArgs e)
{
    // Show the RadGridView
    ContentControl1.Content = List;
}

In TasksViewModel, we instantiate a DataContext object and a SaveCommand in the constructor. DataContext is a WCF RIA Services object that we’ll use to retrieve the list of Tasks and to save any changes to a task. I’ll talk more about this and Commands in future post, but for now think of the SaveCommand as an event handler that is called when the Save button in the form is clicked.

DataContext = new DataContext();
SaveCommand = new DelegateCommand(OnSave);

When the TasksViewModel constructor is called we also make a call to LoadTasks. This sets IsLoading to true (which causes the RadGridView’s busy indicator to appear) and retrieves the records via WCF RIA Services.

        public LoadOperation<Task> LoadTasks()
        {
            // Show the loading message
            IsLoading = true;

            // Get the data via WCF RIA Services. When the call has returned, called OnTasksLoaded.
            return DataContext.Load(DataContext.GetTasksQuery(), OnTasksLoaded, false);
        }

When the data is returned, OnTasksLoaded is called. This sets IsLoading to false (which hides the RadGridView’s busy indicator), and fires property changed notifications to the UI to let it know that the IsLoading and Tasks properties have changed. This property changed notification basically tells the UI to rebind.

void OnTasksLoaded(LoadOperation<Task> lo)
{
    // Hide the loading message
    IsLoading = false;
 
    // Notify the UI that Tasks and IsLoading properties have changed
    this.OnPropertyChanged(p => p.Tasks);
    this.OnPropertyChanged(p => p.IsLoading);
}

Next let’s look at the view model’s SelectedItem property. This is the one that’s bound to both the RadGridView and the form. When the user clicks a record in the RadGridView its setter gets called (set a breakpoint and see what I mean). The other code in the setter lets the UI know that the SelectedItem has changed (so the form displays the correct data), and fires the event that notifies the UI that a selection has occurred (which tells the UI to switch from List to Details).

public Task SelectedItem
{
    get { return _selectedItem; }
    set
    {
        _selectedItem = value;
 
        // Let the UI know that the SelectedItem has changed (forces it to re-bind)
        this.OnPropertyChanged(p => p.SelectedItem);
        // Notify the UI, so it can switch to the Details (form) page
        NotifyItemSelected();
    }
}

One last thing, saving the data. When the Save button in the form is clicked it fires the SaveCommand, which calls the OnSave method in the view model (once again, set a breakpoint to see it in action).

public void OnSave()
{
    // Save the changes via WCF RIA Services. When the save is complete, call OnSaveCompleted.
    DataContext.SubmitChanges(OnSaveCompleted, null);
}

In OnSave, we tell WCF RIA Services to submit any changes, which there will be if you changed either the Name or the Due Date in the form. When the save is completed, it calls OnSaveCompleted. This method fires a notification back to the UI that the save is completed, which causes the RadGridView (List) to show again.

public virtual void OnSaveCompleted(SubmitOperation so)
{
    // Clear the item that is selected in the grid (in case we want to select it again)
    SelectedItem = null;
    // Notify the UI, so it can switch back to the List (RadGridView) page
    NotifySaveCompleted();
}

19 Comments

  • Ben Hayat 24 Mar 2010
    Ross, I have a simple question that is puzzling me. How do you create a multip XAP application? Is it done within one solution? How do you tell VS10 to make each project as a separate XAP?
    That was something I asked you before the webinar and I guess you didn't get time to talk about it. Could you make a blog post with a couple of screen shot of the solution that holds multiple XAP for a MEF project?

    Thanks!
    ..Ben
  • Ross 25 Mar 2010
    Hi Ben,

    I'll actually be blogging about that today, and hopefully will have the blog along with an attached solution posted by the end of day.

    The key is that you have to add the projects that you want to 'import' via MEF as Silverlight Applications. When the next dialog comes up, leave the "Host the Silverlight application in a new or existing Web site in the solution" (it'll already have your .Web project showing in the dropdown). This will put the .xap for that project in the .Web project's ClientBin folder, where you want it to be.

    You can also uncheck the checkbox that says "Add a text page that references the application".

    After that you can delete App.xaml and MainPage.xaml, but I'll cover all of this in the post. I just wanted to get this info in the meantime.

    Sorry for the delay on getting the post written and published, moving from the beta to the RC and following up on webinar questions set my schedule back a bit.

    Ross
  • Ben Hayat 25 Mar 2010
    The key is that you have to add the projects that you want to 'import' via MEF as Silverlight Applications. When the next dialog comes up, leave the "Host the Silverlight application in a new or existing Web site in the solution" (it'll already have your .Web project showing in the dropdown). This will put the .xap for that project in the .Web project's ClientBin folder, where you want it to be.

    Ahh, this is where I missed and I was ending up with different solutions with different .Web projects. Thank you for clearing that up. Sometime you get used to these drop downs and you don't pay attention.

    Sorry for the delay on getting the post written and published, moving from the beta to the RC and following up on webinar questions set my schedule back a bit.


    No rush Ross.
  • David Yardy 25 Mar 2010
    Excellent article.  Keep up the great work.  It is really nice to see the architecture/code and approaches behind real SL applications.  There are so few blog entries (or documentation from Microsoft) that fully explain implementations to this depth.  Most of the time the documentation is explaining 10 lines of code which really doesn't put it all together.

    Thank you -dave
  • Kris 26 Mar 2010
    Thank you Ross.  I'll be linking this to the new community I'm setting up to discuss design patterns.  Mainly for WPF/Silverlight starting out but we're open to asp.net MVC and more.

    http://www.compositedevpatterns.com

    Dave Yardy, I couldn't agree with you more.  We're starting this community for the very reasons you state.  

    I'm starting a series of lessons to hopefully ease the learning curve of starting an app and working all the way up to a real world app.  I'll be open to requests as I have time.

    Also, I would love for anybody that has time or passion for WPF/SL & design patterns to check it out and provide input.

    Thanks again Ross.  Great article.
  • Harold Rusty 26 Mar 2010
    Thanks for the great articles Ross.

    Looking into the benefits of MVVM approach and after watching your webinar I see that you point out that testability is one of the key arguments here. However I cannot see any unit test in your sample projects. Are you doing TDD? Does Task-It app have any test at all?
  • Voss 26 Mar 2010
    Ross,
    Thanks for the post.
    Not sure if I have something setup wrong, but I'm getting the following errror when clicking on the "Tasks" link (top right corner).


    Page not found: "/Tasks"

    thanks.
    voss.
  • Ross 26 Mar 2010
    Thanks David and Kris!

    David you said exactly what I've been thinking for quite a while now, there are a lot of posts out there showing a little technique here or there, or a framework, or a circle that animates across the screen when you click a button, but I really haven't come across content that tries to put it all together. I will take my best shot at doing that over the coming weeks and months.

    Having said that, keep in mind that I am just one guy trying my best to come up with best practices, and I'm always looking for a better/simpler way to do things. Also, sometimes the answer to "how is the best way to do this" is, "well, it depends". For example, in some cases Prism fits the bill, and in others, MEF does (I'll actually blog about this soon).

    I do appreciate your feedback very much, so please keep it coming. Oh, and keep in mind that no matter how you do it, there is always a better way somewhere out there. :-)

    Ross

    P.S. Thank you for the link Kris. Much appreciated!
  • Ross 26 Mar 2010
    Hi Voss,

    Sorry that it is not working properly for you. Have you definitely updated to the latest RC bits (for VisualStudio and Silverlight)?

    Try setting a breakpoint in the Application_UnhandledException method of App.xaml.cs, and another in ContentFrame_NavigationFailed in MainPage.xaml.cs and see if one of them gets hits, and gives you more info about the error. When you do, send me an e-mail at wozniak@telerik.com to let me know what you find.

    Thanks,
    Ross
  • Ross 26 Mar 2010
    Hi Voss,

    Sorry that it is not working properly for you. Have you definitely updated to the latest RC bits (for VisualStudio and Silverlight)?

    Try setting a breakpoint in the Application_UnhandledException method of App.xaml.cs, and another in ContentFrame_NavigationFailed in MainPage.xaml.cs and see if one of them gets hits, and gives you more info about the error. When you do, send me an e-mail at wozniak@telerik.com to let me know what you find.

    Thanks,
    Ross
  • Ross 26 Mar 2010
    Hi Harold,

    I'm a bit embarrassed to admit that I have not done TDD on the Task-It project. I wish I had the time, but had to get the app together in very short order, so I had to just dive in and start writing code. I think TDD is definitely the way to go, but in this case I'm the sole developer on a limited schedule. At least MVVM has set me up for proper unit testing in the future. :-)

    When time allows, I will add in unit and functional tests using our WebUI Test Studio and perhaps WebAii as well, and blog about it.

    Ross
  • Thomas 26 Mar 2010
    Hi Voss
    Try setting MVVMProject.web as your startup-project.
  • Ross 30 Mar 2010
    Fixed the download. Shouldn't have to set the startup project/page any more.

    Ross
  • Voss 01 Apr 2010
    Ross,

    Really great article.

    Is there a way to attach a command event of a button that is inside a

    ControlTemplate (GridViewRow)?

    I have buttons in my template, but they are not firing the Command. If I just put a button on the page outside the grid it works fine.

    thanks,
    voss.

  • Ross 01 Apr 2010
    Ah, a great question Voss. I am assuming that you are inside something like a RowDetailsTemplate in a RadGridView, correct? The problem is that once you are inside a template the DataContext changes...it is no longer the view model class.

    There is a way to 'reset' the DataContext of the template using something called DataContextProxy (I got it from John Papa). I'll try to get a post together on that fairly soon.

    Ross
  • Sean 06 Apr 2010
    Hi Ross, 

    Nice sample! and Microsoft you can suck on it!

    I was wondering if you can also add the RadScheduler control into play. It would be great if you could also demonstrate on how to create a datamodel to work with the RadScheduler control .

    thanks
    Sean


  • Ross 06 Apr 2010
    Sounds good Sean. I'm using the RadScheduler in Task-It and will lead up to that in future posts.
  • Jeff Circeo 18 May 2010
    I like the ViewModelExtension, strongly-typed property changed notifications are the way to go. Nice code.            
  • Ross 21 May 2010
    Thanks for the reminder Jeff, just blogged about it. :-)

Add comment

  1. Formatting options
       
     
     
     
     
       
  2. (optional, emails won't be shown on public pages)
  3. (optional)