Click here to Skip to main content
15,884,739 members
Articles / Desktop Programming / XAML

A Sample Silverlight 4 Application Using MEF, MVVM, and WCF RIA Services - Part 2

Rate me:
Please Sign up or sign in to vote.
4.95/5 (50 votes)
8 Jul 2011CPOL5 min read 187.8K   124   31
Part 2 of a series describing the creation of a Silverlight business application using MEF, MVVM Light, and WCF RIA Services. In this second part, we will go through various topics on how MVVM Light Toolkit is used in our sample application.
  • Download source files and setup package from Part 1

Article Series

This article is part two of a series on developing a Silverlight business application using MEF, MVVM Light, and WCF RIA Services.

Image 1

Contents

Introduction

In this second part, we will go through various topics on how the MVVM Light Toolkit is used in our sample application. I chose this toolkit mainly because it is lightweight. Also, it is one of the most popular MVVM frameworks supporting Silverlight 4.

RelayCommand

One of the new features in Silverlight 4 is a pair of properties added to the ButtonBase class named Command and CommandParameter. This commanding infrastructure makes MVVM implementations a lot easier in Silverlight. Let's take a look at how RelayCommand is used for the "Delete User" button from the User Maintenance screen. First, we define the XAML code of the button as follows:

XML
<Button Grid.Row="2" Grid.Column="0"
        VerticalAlignment="Top" HorizontalAlignment="Right" 
        Width="75" Height="23" Margin="0,5,167,5" 
        Content="Delete User"
        Command="{Binding Path=RemoveUserCommand}"
        CommandParameter="{Binding SelectedItem, ElementName=comboBox_UserName, 
                          ValidatesOnNotifyDataErrors=False}"/>

The code above specifies that we should call the RemoveUserCommand defined in UserMaintenanceViewModel.cs and pass in a parameter of the currently selected user when the "Delete User" button is clicked. And, the RelayCommand RemoveUserCommand is defined as:

C#
private RelayCommand<User> _removeUserCommand = null;

public RelayCommand<User> RemoveUserCommand
{
    get
    {
        if (_removeUserCommand == null)
        {
            _removeUserCommand = new RelayCommand<User>(
                OnRemoveUserCommand,
                g => (issueVisionModel != null) && 
                       !(issueVisionModel.HasChanges) && (g != null));
        }
        return _removeUserCommand;
    }
}

private void OnRemoveUserCommand(User g)
{
    try
    {
        if (!_issueVisionModel.IsBusy)
        {
            // cancel any changes before deleting a user
            if (_issueVisionModel.HasChanges)
            {
                _issueVisionModel.RejectChanges();
            }

            // ask to confirm deleting the current user
            var dialogMessage = new DialogMessage(
                this,
                Resources.DeleteCurrentUserMessageBoxText,
                s =>
                {
                    if (s == MessageBoxResult.OK)
                    {
                        // if confirmed, removing CurrentUser
                        _issueVisionModel.RemoveUser(g);

                        // cache the current user name as empty string
                        _userNameToDisplay = string.Empty;

                        _operation = UserMaintenanceOperation.Delete;
                        IsUpdateUser = true;
                        IsAddUser = false;

                        _issueVisionModel.SaveChangesAsync();
                    }
                })
            {
                Button = MessageBoxButton.OKCancel,
                Caption = Resources.ConfirmMessageBoxCaption
            };

            AppMessages.PleaseConfirmMessage.Send(dialogMessage);
        }
    }
    catch (Exception ex)
    {
        // notify user if there is any error
        AppMessages.RaiseErrorMessage.Send(ex);
    }
}

The code snippet above, when called, will first display a message asking to confirm whether to delete the selected user or not. If confirmed, the functions RemoveUser() and SaveChangesAsync(), both defined in the IssueVisionModel class, will get called, thus removing the selected user from the database.

The second parameter of the RelayCommand is the CanExecute method. In the sample code above, it is defined as "

g => (_issueVisionModel != null) && 
!(_issueVisionModel.HasChanges) && (g != null)
", which means that the "Delete User" button is only enabled when there are no pending changes and the selected user is not null. Unlike WPF, this CanExecute method is not automatically polled in Silverlight when the HasChanges property changes, and we need to call the RaiseCanExecuteChanged method manually, like the following:

C#
private void _issueVisionModel_PropertyChanged(object sender, 
             PropertyChangedEventArgs e)
{
    if (e.PropertyName.Equals("HasChanges"))
    {
        AddUserCommand.RaiseCanExecuteChanged();
        RemoveUserCommand.RaiseCanExecuteChanged();
        SubmitChangeCommand.RaiseCanExecuteChanged();
        CancelChangeCommand.RaiseCanExecuteChanged();
    }
}

Messenger

The Messenger class from MVVM Light Toolkit uses a simple Publish/Subscribe model to allow loosely coupled messaging. This facilitates communication between the different ViewModel classes as well as communication from the ViewModel class to the View class. In our sample, we define a static class called AppMessages that encapsulates all the messages used in this application.

C#
/// <summary>
/// class that defines all messages used in this application
/// </summary>
public static class AppMessages
{
    ......

    public static class ChangeScreenMessage
    {
        public static void Send(string screenName)
        {
            Messenger.Default.Send(screenName, MessageTypes.ChangeScreen);
        }

        public static void Register(object recipient, Action<string> action)
        {
            Messenger.Default.Register(recipient, 
                      MessageTypes.ChangeScreen, action);
        }
    }

    public static class RaiseErrorMessage
    {
        public static void Send(Exception ex)
        {
            Messenger.Default.Send(ex, MessageTypes.RaiseError);
        }

        public static void Register(object recipient, Action<Exception> action)
        {
            Messenger.Default.Register(recipient, 
                      MessageTypes.RaiseError, action);
        }
    }

    public static class PleaseConfirmMessage
    {
        public static void Send(DialogMessage dialogMessage)
        {
            Messenger.Default.Send(dialogMessage, 
                      MessageTypes.PleaseConfirm);
        }

        public static void Register(object recipient, Action<DialogMessage> action)
        {
            Messenger.Default.Register(recipient, 
                      MessageTypes.PleaseConfirm, action);
        }
    }

    public static class StatusUpdateMessage
    {
        public static void Send(DialogMessage dialogMessage)
        {
            Messenger.Default.Send(dialogMessage, 
                      MessageTypes.StatusUpdate);
        }

        public static void Register(object recipient, Action<DialogMessage> action)
        {
            Messenger.Default.Register(recipient, 
                      MessageTypes.StatusUpdate, action);
        }
    }

    ......
}

In the code-behind file MainPage.xaml.cs, four AppMessages are registered. ChangeScreenMessage is registered to handle requests from the menu for switching between different screens. The other three AppMessages are all system-wide messages:

  • RaiseErrorMessage will display an error message if something goes wrong, and immediately logs off from the database.
  • PleaseConfirmMessage is used to display a message asking for user confirmation, and processes the call back based on user feedback.
  • StatusUpdateMessage is used to update the user on certain status changes, like a new issue has been successfully created and saved, etc.

Here is how we register the StatusUpdateMessage:

C#
public MainPage()
{
    InitializeComponent();

    // register for StatusUpdateMessage
    AppMessages.StatusUpdateMessage.Register(this, OnStatusUpdateMessage);
    
    ......
}

#region "StatusUpdateMessage"

private static void OnStatusUpdateMessage(DialogMessage dialogMessage)
{
    if (dialogMessage != null)
    {
        MessageBoxResult result = MessageBox.Show(dialogMessage.Content,
            dialogMessage.Caption, dialogMessage.Button);

        dialogMessage.ProcessCallback(result);
    }
}

#endregion "StatusUpdateMessage"

And, here is how we can send a message to the StatusUpdateMessage:

C#
......
// notify user of the new issue ID
var dialogMessage = new DialogMessage(
         this,
         Resources.NewIssueCreatedText + addedIssue.IssueID,
         null)
{
    Button = MessageBoxButton.OK,
    Caption = Resources.NewIssueCreatedCaption
};

AppMessages.StatusUpdateMessage.Send(dialogMessage);
......

EventToCommand

EventToCommand is a Blend behavior that is added as a new feature in the MVVM Light Toolkit V3, and is used to bind an event to an ICommand directly in XAML, which gives us the power to handle pretty much any event with RelayCommand from the ViewModel class.

Following is an example of how drag and drop of files is implemented in the "New Issue" screen. Let's check the XAML code first:

XML
<ListBox x:Name="listBox_Files" Grid.Row="1" Grid.Column="0"
         AllowDrop="True"
         ItemsSource="{Binding Path=CurrentIssue.Files, ValidatesOnNotifyDataErrors=False}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Path=FileName, ValidatesOnNotifyDataErrors=False}" />
                <TextBlock Text="{Binding Path=Data.Length, StringFormat=' - \{0:F0\} bytes',
                                  ValidatesOnNotifyDataErrors=False}" />
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Drop">
            <cmd:EventToCommand PassEventArgsToCommand="True"
                    Command="{Binding Path=HandleDropCommand, Mode=OneWay}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</ListBox>

The code above basically specifies that the ListBox allows drag-and-drop, and when a Drop event fires, the HandleDropCommand from the IssueEditorViewModel class gets called. Next, let's look at how HandleDropCommand is implemented:

C#
private RelayCommand<DragEventArgs> _handleDropCommand = null;

public RelayCommand<DragEventArgs> HandleDropCommand
{
    get
    {
        if (_handleDropCommand == null)
        {
            _handleDropCommand = new RelayCommand<DragEventArgs>(
                OnHandleDropCommand,
                e => CurrentIssue != null);
        }
        return _handleDropCommand;
    }
}

private void OnHandleDropCommand(DragEventArgs e)
{
    try
    {
        // get a list of files as FileInfo objects
        var files = e.Data.GetData(DataFormats.FileDrop) as FileInfo[];

        if (files != null)
        {
            // loop through the list and read each file
            foreach (var file in files)
            {
                using (var fs = file.OpenRead())
                using (MemoryStream ms = new MemoryStream())
                {
                    fs.CopyTo(ms);
                    // and then add each file into the Files entity collection
                    CurrentIssue.Files.Add(
                        new Data.Web.File()
                        {
                            FileID = Guid.NewGuid(),
                            IssueID = CurrentIssue.IssueID,
                            FileName = file.Name,
                            Data = ms.GetBuffer()
                        });
                }
            }
        }
    }
    catch (Exception ex)
    {
        // notify user if there is any error
        AppMessages.RaiseErrorMessage.Send(ex);
    }
}

HandleDropCommand will loop through the list of files dropped by the user, reads the content of each file, and then adds them into the Files EntityCollection. The data will later be saved to the database when the user saves the changes.

ICleanup Interface

Whenever the user chooses a different screen from the menu, a ChangeScreenMessage is sent, which eventually calls the following OnChangeScreenMessage method:

C#
private void OnChangeScreenMessage(string changeScreen)
{
    // call Cleanup() on the current screen before switching
    var currentScreen = mainPageContent.Content as ICleanup;
    if (currentScreen != null)
        currentScreen.Cleanup();

    // reset noErrorMessage
    _noErrorMessage = true;

    switch (changeScreen)
    {
        case ViewTypes.HomeView:
            mainPageContent.Content = new Home();
            break;
        case ViewTypes.NewIssueView:
            mainPageContent.Content = new NewIssue();
            break;
        case ViewTypes.AllIssuesView:
            mainPageContent.Content = new AllIssues();
            break;
        case ViewTypes.MyIssuesView:
            mainPageContent.Content = new MyIssues();
            break;
        case ViewTypes.BugReportView:
            mainPageContent.Content = new Reports();
            break;
        case ViewTypes.MyProfileView:
            mainPageContent.Content = new MyProfile();
            break;
        case ViewTypes.UserMaintenanceView:
            mainPageContent.Content = new UserMaintenance();
            break;
        default:
            throw new NotImplementedException();
    }
}

From the code above, we can see that every time we switch to a new screen, the current screen is first being tested to see whether it supports the ICleanup interface. If it is, the Cleanup() method is called before switching to the new screen. In fact, any screen, except the Home screen which does not bind to any ViewModel class, implements the ICleanup interface.

The Cleanup() method defined in any of the View classes will first call the Cleanup() method on its ViewModel class to unregister any event handlers and AppMessages. Next, it will unregister any AppMessages used by the View class itself, and the last step is to release the ViewModel class by calling ReleaseExport<ViewModelBase>(_viewModelExport), thus making sure that there are no memory leaks. Let's look at an example:

C#
public partial class Reports : UserControl, ICleanup
{
    #region "Private Data Members"
    private const double MinimumWidth = 640;
    private Lazy<ViewModelBase> _viewModelExport;
    #endregion "Private Data Members"

    #region "Constructor"
    public Reports()
    {
        InitializeComponent();
        // initialize the UserControl Width & Height
        Content_Resized(this, null);

        // register for GetChartsMessage
        AppMessages.GetChartsMessage.Register(this, OnGetChartsMessage);

        if (!ViewModelBase.IsInDesignModeStatic)
        {
            // Use MEF To load the View Model
            _viewModelExport = App.Container.GetExport<ViewModelBase>(
                ViewModelTypes.BugReportViewModel);
            if (_viewModelExport != null) DataContext = _viewModelExport.Value;
        }
    }
    #endregion "Constructor"

    #region "ICleanup interface implementation"

    public void Cleanup()
    {
        // call Cleanup on its ViewModel
        ((ICleanup)DataContext).Cleanup();
        // cleanup itself
        Messenger.Default.Unregister(this);
        // set DataContext to null and call ReleaseExport()
        DataContext = null;
        App.Container.ReleaseExport(_viewModelExport);
        _viewModelExport = null;
    }

    #endregion "ICleanup interface implementation"

    ......
}

And here is the Cleanup() method in its ViewModel class:

C#
#region "ICleanup interface implementation"
public override void Cleanup()
{
    if (_issueVisionModel != null)
    {
        // unregister all events
        _issueVisionModel.GetAllUnresolvedIssuesComplete -= 
             _issueVisionModel_GetAllUnresolvedIssuesComplete;
        _issueVisionModel.GetActiveBugCountByMonthComplete -= 
             _issueVisionModel_GetActiveBugCountByMonthComplete;
        _issueVisionModel.GetResolvedBugCountByMonthComplete -= 
             _issueVisionModel_GetResolvedBugCountByMonthComplete;
        _issueVisionModel.GetActiveBugCountByPriorityComplete -= 
             _issueVisionModel_GetActiveBugCountByPriorityComplete;
        _issueVisionModel.PropertyChanged -= 
             _issueVisionModel_PropertyChanged;
        _issueVisionModel = null;
    }
    // set properties back to null
    AllIssues = null;
    ActiveBugCountByMonth = null;
    ResolvedBugCountByMonth = null;
    ActiveBugCountByPriority = null;
    // unregister any messages for this ViewModel
    base.Cleanup();
}
#endregion "ICleanup interface implementation"

Next Steps

In this article, we visited the topics of how the MVVM Light toolkit is used: namely, RelayCommand, Messenger, EventToCommand, and ICleanup. In our last part, we will focus on how custom authentication, reset password, and user maintenance are done through WCF RIA Services.

I hope you find this article useful, and please rate and/or leave feedback below. Thank you!

History

  • May 2010 - Initial release
  • July 2010 - Minor update based on feedback
  • November 2010 - Update to support VS2010 Express Edition
  • February 2011 - Update to fix multiple bugs including memory leak issues
  • July 2011 - Update to fix multiple bugs

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
Weidong has been an information system professional since 1990. He has a Master's degree in Computer Science, and is currently a MCSD .NET

Comments and Discussions

 
QuestionWhat a great article Pin
essdataylor30-Aug-12 8:57
essdataylor30-Aug-12 8:57 
AnswerRe: What a great article Pin
Weidong Shen30-Aug-12 14:21
Weidong Shen30-Aug-12 14:21 
GeneralMy vote of 4 Pin
Edgars Palacis5-Jul-12 9:59
Edgars Palacis5-Jul-12 9:59 
GeneralThis article is a Fraud (please read on)... Pin
remesqny3-Jan-11 10:33
remesqny3-Jan-11 10:33 
I am going to go out on a limb here, so if I insult anyone because I am WRONG, then I apologize in advance. However, if I am anywhere near correct I am hopeful that the CodeProject.com God's figure out a way to correct the situation (if they deem it necessary to do so).

First, I invested A LOT of time going over the 4 articles associated with this Issue Vision, supposed issue tracker "example". I am no sleuth, but I was nonetheless fooled by these articles (as I will point out). I am also no expert, and so I rely on many articles on codeproject.com to learn. When I saw this "example" I was excited to have a framework to build upon for my own personal projects. So whatever you might say in response, yes, it's my own fault for falling for this fraud.

Second, this article is exactly that, a fraud. I was having trouble creating a new view in this project. No matter what I did (even what Mr. Shen suggested in his response - before he suggested it), I kept getting an error. I scanned around the messages to each of the 4 articles to see if a similar question had been asked. It had not been asked (surprisingly, leading me to believe I was truly out of my realm for what seemed like a basic task). But, and this was my first indication of the fraud, I noticed that Mr. Shen's answers to many specific "how to" questions were vague and common sense responses without any indiciation of what someone should do for their particular request. So, I posted the question of how to add views. Mr. Shen's response was so vague as to cause me to spend 1/2 hour deciding whether or not I should continue investing time with the "example". I had already invested over 20 hours reading, learning and trying to build off the example.

Initially, I just thought that Mr. Shen was a busy man and probably took the approach of "teacher" by trying to point me in the right direction without giving me the answer expecting me to learn it (the "teach a man to fish" scenario). I could have respected that. But then I found this: MS Line of Business Sample.

That, for those of you who are unaware of the fraud, is Microsoft's very own Line of Business Sample for Silverlight 4. It is exactly Issue Vision as Mr. Shen presents it. If you scroll down to the sample's "Home" page rendition, you will not it has the same language as in Issue Vision. Note that in article 1 on this, Mr. Shen states:

"This three part article series is my effort at creating such a sample. The choice of an issue tracking application comes from David Poll's PDC09 talk, and the design architecture is from Shawn Wildermuth's blog posts."

I did not find anything about this architecture from Mr. Wildermuth's posts (other than just his posts about an architecture such as that used in Issue Vision by Mr. Shen). However, I must admit that after that 1/2 hour of deliberating I abandoned the idea of trying to learn from Issue Vision because I could not figure out the errors I was getting when trying to add a new view.

So, IMHO, Mr. Shen failed to answer my question only because i suspect he does not know the answer. All he did was dress up Issue Vision from the MS example and called it his own.

I am not so naive as to believe this has never happened on codeproject.com or anywhere else on the web. However, given the response from Mr. Shen, and given my INADVERDENT discovery of the MS example, and given the time I invested with Mr. Shen's "example", I felt it warranted my post to warn others and hopefully to get codeproject.com to act. I have been using this site for over 7 years to help out with learning this stuff, and it's a shame.

Why do I care, you might ask? Because you know that Mr. Shen is pimping this article out there in the world claiming to have created something he did not. Plagarism, Mr. Shen, is not teaching.

I will be cross-posting this for maximum exposure. If Mr. Shen is able to delete comments, then the integrity of codeproject.com is seriously compromised.
GeneralRe: This article is a Fraud (please read on)... [modified] Pin
Julien Villers5-Aug-11 5:01
professionalJulien Villers5-Aug-11 5:01 
QuestionHow do you add new views (User Control) Pin
remesqny30-Dec-10 14:25
remesqny30-Dec-10 14:25 
AnswerRe: How do you add new views (User Control) Pin
Weidong Shen30-Dec-10 17:57
Weidong Shen30-Dec-10 17:57 
AnswerRe: How do you add new views (User Control) Pin
Drammy5-Jan-11 3:44
Drammy5-Jan-11 3:44 
GeneralRe: How do you add new views (User Control) Pin
Member 767071323-Mar-11 21:07
Member 767071323-Mar-11 21:07 
GeneralRe: How do you add new views (User Control) Pin
Weidong Shen24-Mar-11 3:48
Weidong Shen24-Mar-11 3:48 
QuestionWhy to register to Messages at Code-behind and not at ViewModel Level? Pin
TheCubanP20-Dec-10 11:33
TheCubanP20-Dec-10 11:33 
AnswerRe: Why to register to Messages at Code-behind and not at ViewModel Level? Pin
Weidong Shen20-Dec-10 14:43
Weidong Shen20-Dec-10 14:43 
GeneralMy vote of 5 Pin
levteck15-Dec-10 8:01
levteck15-Dec-10 8:01 
GeneralMy vote of 5 Pin
Nitu8417-Nov-10 6:30
Nitu8417-Nov-10 6:30 
GeneralRe: My vote of 5 Pin
Weidong Shen23-Nov-10 4:20
Weidong Shen23-Nov-10 4:20 
GeneralMy vote of 5 Pin
Abhinav S23-Oct-10 18:09
Abhinav S23-Oct-10 18:09 
GeneralRe: My vote of 5 Pin
Weidong Shen29-Oct-10 14:39
Weidong Shen29-Oct-10 14:39 
GeneralBig Fan - Any Ideas on Using EventToCommand In DataGridTemplateColumn.TemplateCell Pin
Joseph DeMilo15-Oct-10 9:20
Joseph DeMilo15-Oct-10 9:20 
GeneralRe: Big Fan - Any Ideas on Using EventToCommand In DataGridTemplateColumn.TemplateCell Pin
Weidong Shen17-Oct-10 4:32
Weidong Shen17-Oct-10 4:32 
GeneralMy vote of 5 Pin
niflens21-Jul-10 4:42
niflens21-Jul-10 4:42 
GeneralRe: My vote of 5 Pin
Weidong Shen26-Jul-10 8:51
Weidong Shen26-Jul-10 8:51 
QuestionHow do you wire up the service with the ViewModel? Pin
chapamanuel26-Jun-10 9:24
chapamanuel26-Jun-10 9:24 
AnswerRe: How do you wire up the service with the ViewModel? Pin
Weidong Shen26-Jun-10 11:20
Weidong Shen26-Jun-10 11:20 
Questionawesome work | Password Problems [modified] Pin
omacall8-Jun-10 23:09
omacall8-Jun-10 23:09 
AnswerRe: awesome work | Password Problems Pin
Weidong Shen9-Jun-10 5:15
Weidong Shen9-Jun-10 5:15 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.