Telerik blogs

Download Source Code

Task-It Series

This post is part of a series of blog posts and videos about the Task-It (task management) application that I have been building with Silverlight 4 and Telerik's RadControls for Silverlight 4. For a full index of a posts, please go here. One of the posts listed in the index provides a full source download for the application.

Why MVVM?

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.

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 code

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.When you open the solution in VisualStudio 2010 RC be sure to set MVVMProject.Web as the StartUp Project and MVVMProjectTestPage.aspx as the Start Page.

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();
}


Comments

Comments are disabled in preview mode.