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 these resources, please go here. One of the posts listed in the index provides a full source download for the application.

Taking it to the next level

In my last post, Commands in Task-It - Part 1, we looked at a very simple solution that demonstrated how a single command instance (SaveCommand) could be bound to two UI controls, a Button and a RadTreeViewItem. In this example we'll get more complex, binding a single command instance (MoveToCommand) will be bound to multiple RadMenuItems in a RadContextMenu that is tied to a RadGridView. This time we'll also set a separate CommandParameter on each RadMenuItem, so when the command is invoked, we will be able to use that parameter to determine what to do next.

The user interface

This screen shot shows the UI that we will be working with, it is a stripped-down version of Task-It. On the left hand side we have a RadTreeView that lists some task categories and when one is selected the RadGridView on the right displays the tasks for that category. When the user right-clicks on one of the items in the RadGridView a RadContextMenu appears that allows us to move the task to a different category. We're slowly getting closer to the actual Task-It user interface.

Sorry that the image gets a bit mangled by sizing it down!

RadContextMenuItem Commands

How it works

When the app first loads up there are no categories selected on the left, so a message displays on the right stating "Please select a category.". If you select a category that has at least one task in it (the Inbox and Actions categories currently do) the RadGridView will display the tasks in that category. If you select a category that does not have tasks, a message displays stating that "The selected category has no tasks.".

The code

First let's look at the one 'view' involved in the UI. When MainPage.xaml loads up it display this control, which resides in the Views category, Tasks.xaml.

<Grid Margin="6">
    <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition Width="2*"/>
    </Grid.ColumnDefinitions>
    <Border Style="{StaticResource OuterBorderStyle}" Margin="0,0,6,0">
        <telerikNavigation:RadTreeView x:Name="tvwCategories" ItemsSource="{Binding TaskCategories}" SelectedItem="{Binding SelectedTaskCategory, Mode=TwoWay}"/>
    </Border>
    <Controls:GridSplitter Width="6" Opacity="0"/>
    <Border Style="{StaticResource OuterBorderStyle}" Grid.Column="1">
        <Grid>
            <TextBlock Text="Please select a category." Visibility="{Binding ShowSelectMessage, Converter={StaticResource BooleanToVisibilityConverter}}"/>
            <TextBlock Text="The selected category has no tasks." Visibility="{Binding ShowNoTasksMessage, Converter={StaticResource BooleanToVisibilityConverter}}"/>
            <telerikGridView:RadGridView ItemsSource="{Binding Tasks}" SelectedItem="{Binding SelectedTask, Mode=TwoWay}" Visibility="{Binding ShowGridView, Converter={StaticResource BooleanToVisibilityConverter}}" AutoGenerateColumns="False">
                <telerikGridView:RadGridView.Columns>
                    <telerikGridView:GridViewDataColumn Header="Name" DataMemberBinding="{Binding Name}" Width="3*"/>
                    <telerikGridView:GridViewDataColumn Header="Description" DataMemberBinding="{Binding Description}" Width="*"/>
                    <telerikGridView:GridViewDataColumn Header="Due" DataMemberBinding="{Binding DueDate}" DataFormatString="{}{0:d}" Width="80"/>
                    <telerikGridView:GridViewDataColumn Header="Completed" DataMemberBinding="{Binding CompletionDate}" DataFormatString="{}{0:d}" Width="80"/>
                </telerikGridView:RadGridView.Columns>
                <telerikNavigation:RadContextMenu.ContextMenu>
                    <telerikNavigation:RadContextMenu Opened="GridContextMenuOpened">
                        <telerikNavigation:RadMenuItem Header="Move To">
                            <telerikNavigation:RadMenuItem Header="Inbox" Command="{Binding MoveToCommand}" CommandParameter="{Binding TaskCategoryInbox}" Visibility="{Binding IsMoveToInboxCommandAvailable, Converter={StaticResource BooleanToVisibilityConverter}}"/>
                            <telerikNavigation:RadMenuItem Header="Actions" Command="{Binding MoveToCommand}" CommandParameter="{Binding TaskCategoryActions}" Visibility="{Binding IsMoveToActionsCommandAvailable, Converter={StaticResource BooleanToVisibilityConverter}}"/>
                            <telerikNavigation:RadMenuItem Header="Waiting" Command="{Binding MoveToCommand}" CommandParameter="{Binding TaskCategoryWaiting}" Visibility="{Binding IsMoveToWaitingCommandAvailable, Converter={StaticResource BooleanToVisibilityConverter}}"/>
                            <telerikNavigation:RadMenuItem Header="Someday" Command="{Binding MoveToCommand}" CommandParameter="{Binding TaskCategorySomeday}" Visibility="{Binding IsMoveToSomedayCommandAvailable, Converter={StaticResource BooleanToVisibilityConverter}}"/>
                            <telerikNavigation:RadMenuItem Header="Ticklers" Command="{Binding MoveToCommand}" CommandParameter="{Binding TaskCategoryTicklers}" Visibility="{Binding IsMoveToTicklersCommandAvailable, Converter={StaticResource BooleanToVisibilityConverter}}"/>
                            <telerikNavigation:RadMenuItem Header="Reference" Command="{Binding MoveToCommand}" CommandParameter="{Binding TaskCategoryReference}" Visibility="{Binding IsMoveToReferenceCommandAvailable, Converter={StaticResource BooleanToVisibilityConverter}}"/>
                        </telerikNavigation:RadMenuItem>
                    </telerikNavigation:RadContextMenu>
                </telerikNavigation:RadContextMenu.ContextMenu>
            </telerikGridView:RadGridView>
        </Grid>
    </Border>
</Grid>

It looks like a lot of code, but it's no that bad. It doesn't look as daunting in VisualStudio where it doesn't wrap, and a lot of the code is for the RadContextMenu.

At the top level we have a Grid with 2 columns. The Width="2*" attribute on the second column makes it twice as wide as the first. Each column contains a Border control with a style that gives it a black border, rounded corners and a bit of padding (see OuterBorderStyle in Assets/Styles/Styles.xaml). The first column also has a bit of margin on the right side where a GridSplitter will allow us to resize the two sides if we'd like.

The Border in the first column contains one element, the RadTreeView:

<telerikNavigation:RadTreeView x:Name="tvwCategories" ItemsSource="{Binding TaskCategories}" SelectedItem="{Binding SelectedTaskCategory, Mode=TwoWay}"/>

Notice that the ItemsSource is bound to a property called TaskCategories in our view model (yes, we are continuing to use the MVVM pattern), and the SelectedItem is bound to a property called SelectedTaskCategory. We'll look at the view model shortly.

The Border in the second column contains three controls, two TextBlocks and the RadGridView. This is just a partial listing:

<Grid>
    <TextBlock Text="Please select a category." Visibility="{Binding ShowSelectMessage, Converter={StaticResource BooleanToVisibilityConverter}}"/>
    <TextBlock Text="The selected category has no tasks." Visibility="{Binding ShowNoTasksMessage, Converter={StaticResource BooleanToVisibilityConverter}}"/>
    <telerikGridView:RadGridView ItemsSource="{Binding Tasks}" SelectedItem="{Binding SelectedTask, Mode=TwoWay}" Visibility="{Binding ShowGridView, Converter={StaticResource BooleanToVisibilityConverter}}" AutoGenerateColumns="False">
...
    </telerikGridView:RadGridView>
</Grid>

First let's talk about the RadGridView. It's ItemsSource is bound to a property in our view model called Tasks, and SelectedItem is bound to a property called SelectedTask.

Now the Visibility of each of the three elements is determined by binding to properties in our view model. Notice the use of the BooleanToVisibilityConverter that Telerik kindly provides in it's Telerik.Windows.Controls namespace, so we don't have to add our own converter class. This converter is declared as a static resource in App.xaml so we can use it throughout our app:

<telerikControls:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>

 

The code-behind

So now let's take a look at the code-behind, Tasks.xaml.cs:

public partial class Tasks
{
    public Tasks()
    {
        InitializeComponent();
        var viewModel = new TasksViewModel();
        DataContext = viewModel;
    }
 
    private void GridContextMenuOpened(object sender, RoutedEventArgs e)
    {
        Utils.GridContextMenuOpened(sender, e);
    }
}

There is hardly anything to it, which is what we want when using the MVVM pattern. In the constructor we insantiate our view model class and set it as the DataContext of our UserControl. The only other thing we have to do is include a method that is called by our RadContextMenu. You can take a look at the GridContextMenuOpened method in the Utils classs, but this simply tells the RadGridView that the row that we right-click on should become selected. You'll see this same code in other Telerik samples. I just put it in a Utils class so I can reuse it across multiple RadGridViews in my UI.

On the RadContextMenu you'll see where it is called:

<telerikNavigation:RadContextMenu Opened="GridContextMenuOpened">

 

The commands

OK, the main thing we're here to look at are the commands, so let's look at the RadMenuItems:

<telerikNavigation:RadMenuItem Header="Inbox" Command="{Binding MoveToCommand}" CommandParameter="{Binding TaskCategoryInbox}" Visibility="{Binding IsMoveToInboxCommandAvailable, Converter={StaticResource BooleanToVisibilityConverter}}"/>
<telerikNavigation:RadMenuItem Header="Actions" Command="{Binding MoveToCommand}" CommandParameter="{Binding TaskCategoryActions}" Visibility="{Binding IsMoveToActionsCommandAvailable, Converter={StaticResource BooleanToVisibilityConverter}}"/>
<telerikNavigation:RadMenuItem Header="Waiting" Command="{Binding MoveToCommand}" CommandParameter="{Binding TaskCategoryWaiting}" Visibility="{Binding IsMoveToWaitingCommandAvailable, Converter={StaticResource BooleanToVisibilityConverter}}"/>
<telerikNavigation:RadMenuItem Header="Someday" Command="{Binding MoveToCommand}" CommandParameter="{Binding TaskCategorySomeday}" Visibility="{Binding IsMoveToSomedayCommandAvailable, Converter={StaticResource BooleanToVisibilityConverter}}"/>
<telerikNavigation:RadMenuItem Header="Ticklers" Command="{Binding MoveToCommand}" CommandParameter="{Binding TaskCategoryTicklers}" Visibility="{Binding IsMoveToTicklersCommandAvailable, Converter={StaticResource BooleanToVisibilityConverter}}"/>
<telerikNavigation:RadMenuItem Header="Reference" Command="{Binding MoveToCommand}" CommandParameter="{Binding TaskCategoryReference}" Visibility="{Binding IsMoveToReferenceCommandAvailable, Converter={StaticResource BooleanToVisibilityConverter}}"/>

As we mentioned earlier, each one is bound to MoveToCommand (in our view model, naturally), but each one has a CommandParameter property that is once again bound to properties in our view model. When the command is invoked, this parameter will determine which category to move the task to. Also notice that each RadMenuItem has a Visibility property that is bound to a property in our view model. The reason for this is if we are looking at the tasks for a certain category, Inbox for example, it wouldn't make sense for Inbox to show up in the context menu. How can we move a task to the Inbox if it's already in the Inbox, right?

The view model

As I've talked about in my other blog posts, with the MVVM pattern the view model is the brains of the operation. It is responsible for retrieving the data, showing and hiding various UI elements, and responding when the user interacts with something in the user interface. Because everything lives there, it can be tested independently of the UI, and the same view models could support a different UI, perhaps for a Windows Phone 7 version of an application.

In our view model constructor, we do two things. Create an instance of the DataContext object (this is the WCF RIA Services object that is responsible for retrieving and updating data) and retrieve the TaskCategories.

public TasksViewModel()
{
    DataContext = new DataContext();
    LoadData();
}
 
private void LoadData()
{
    LoadTaskCategories();
}

 

When the categories have loaded (via WCF RIA Services), we assign them to the TaskCategories property, and fire an OnPropertyChanged notification. This tells the RadTreeView to re-bind, and the categories appear in the tree:

public LoadOperation<TaskCategory> LoadTaskCategories()
{
    // Get the data via WCF RIA Services. When the call has returned, called OnTaskCategoriesLoaded.
    return DataContext.Load(DataContext.GetTaskCategoriesQuery(), OnTaskCategoriesLoaded, false);
}
 
void OnTaskCategoriesLoaded(LoadOperation<TaskCategory> lo)
{
    TaskCategories = lo.Entities;
 
    // Notify the UI that TaskCategories property has changed
    this.OnPropertyChanged(m => m.TaskCategories);
}

Now when the user selects a category in the RadTreeView, the setter of the SelectedTaskCategory property is called. It stores the selected category, sends a bunch of notifications that determine whether UI elements should be visible or non-visible, and loads the tasks for the selected category:

public TaskCategory SelectedTaskCategory
{
    get { return _selectedTaskCategory; }
    set
    {
        _selectedTaskCategory = value;
        this.OnPropertyChanged(m => m.SelectedTaskCategory);
        this.OnPropertyChanged(m => m.ShowSelectMessage);
        this.OnPropertyChanged(m => m.IsMoveToInboxCommandAvailable);
        this.OnPropertyChanged(m => m.IsMoveToActionsCommandAvailable);
        this.OnPropertyChanged(m => m.IsMoveToWaitingCommandAvailable);
        this.OnPropertyChanged(m => m.IsMoveToSomedayCommandAvailable);
        this.OnPropertyChanged(m => m.IsMoveToTicklersCommandAvailable);
        this.OnPropertyChanged(m => m.IsMoveToReferenceCommandAvailable);               
        LoadTasks();
    }
}

 

Once again the Tasks are loaded via WCF RIA Services and the return collection is assigned to the Tasks property. Notifications are then sent to the UI to determine which items should be visible (based on whether any tasks are returned:

public LoadOperation<Task> LoadTasks()
{
    // Get the data via WCF RIA Services. When the call has returned, called OnTasksLoaded.           
    return DataContext.Load(DataContext.GetTasksByCategoryQuery(SelectedTaskCategory.ID), OnTasksLoaded, false);
}
 
void OnTasksLoaded(LoadOperation<Task> lo)
{
    Tasks = lo.Entities;
 
    // Notify the UI that Tasks property has changed
    this.OnPropertyChanged(m => m.Tasks);
    this.OnPropertyChanged(m => m.ShowNoTasksMessage);
    this.OnPropertyChanged(m => m.ShowGridView);
}

 

How the commands work

We saw earlier how each of the RadMenuItems in the RadContextMenu have a CommandParameter that is bound to a property in the view model. These values are actually enums of type TaskCategoryEnum. These can be found in Core/Enums.cs:

public enum TaskCategoryEnum
{
    Inbox = 1,
    Actions = 2,
    Waiting = 3,
    Someday = 4,
    Ticklers = 5,
    Reference = 6
}

Each of these enums has an int value that corresponds to the id of that category in the database's TaskCategory table:

TaskCategory table

As I mentioned in my previous post, I have chosen to use the MVVM Light Toolkit's RelayCommand class for my commands. In that post the RelayCommand I used for saving did not pass a parameter. In this case, we do pass a TaskCategoryEnum:

private RelayCommand<TaskCategoryEnum> _moveToCommand;
public RelayCommand<TaskCategoryEnum> MoveToCommand
{
    get { return _moveToCommand ?? (_moveToCommand = new RelayCommand<TaskCategoryEnum>(OnMoveTo)); }
}

When one of the RadMenuItems is clicked, the command is invoked, and the OnMoveTo method is called. This obtains the int value of the TaskCategoryEnum that was passed as a CommandParameter, sets the TaskCategoryID property of the SelectedTask (the one the user right-clicked on) to that int value, and uses WCF RIA Services to save the task (effectively 'moving' it to a different category):

public void OnMoveTo(TaskCategoryEnum taskCategoryEnum)
{
    // Set the TaskCategoryID to that of the item that was selected from the RadContextMenu
    SelectedTask.TaskCategoryID = Convert.ToInt32(taskCategoryEnum);
    DataContext.SubmitChanges(MoveToCompleted, null);
}
 
public virtual void MoveToCompleted(SubmitOperation so)
{
    // Only show the tasks for the selected category
    Tasks = Tasks.Where(t => t.TaskCategoryID == SelectedTaskCategory.ID);
 
    // Inform the UI that the Tasks collection and HasTasks property has changed
    this.OnPropertyChanged(m => m.Tasks);
    this.OnPropertyChanged(m => m.ShowNoTasksMessage);
    this.OnPropertyChanged(m => m.ShowGridView);
}

 

Wrap up

In part 1 of the Commands post we implemented a very simple SaveCommand and bound it to two UI controls. In this part our code became a bit more complex and demonstrated how we can bind a command to multiple context menu items, allowing us to move items from one category to the next.

Commands are a huge addition to Silverlight 4, and I hope you can see the power of leveraging them in your user interfaces!


Related Posts

Comments

Comments are disabled in preview mode.