A Sample Silverlight 4 Application Using MEF, MVVM, and WCF RIA Services - Part 2
- 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.
- Part 1 - Introduction, Installation, and General Application Design Topics
- Part 2 - MVVM Light Topics
- Part 3 - Custom Authentication, Reset Password, and User Maintenance
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:
<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:
private RelayCommand<IssueVision.Data.Web.User> _removeUserCommand = null;
public RelayCommand<IssueVision.Data.Web.User> RemoveUserCommand
{
get
{
if (_removeUserCommand == null)
{
_removeUserCommand = new RelayCommand<Data.Web.User>(
g => this.OnRemoveUserCommand(g),
g => (this._issueVisionModel != null) &&
!(this._issueVisionModel.HasChanges) && (g != null));
}
return _removeUserCommand;
}
}
private void OnRemoveUserCommand(Data.Web.User g)
{
try
{
if (!_issueVisionModel.IsBusy)
{
// cancel any changes before deleting a user
if (_issueVisionModel.HasChanges)
{
this._issueVisionModel.RejectChanges();
}
// ask to confirm deleting the current user
DialogMessage dialogMessage = new DialogMessage(
this,
Resources.DeleteCurrentUserMessageBoxText,
s =>
{
if (s == MessageBoxResult.OK)
{
// if confirmed, removing CurrentUser
this._issueVisionModel.RemoveUser(g);
// cache the current user name as empty string
_userNameToDisplay = string.Empty;
this._operation = UserMaintenanceOperation.Delete;
IsUpdateUser = true;
IsAddUser = false;
this._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 => (this._issueVisionModel != null) && !(this._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:
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.
/// <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<string>(screenName, MessageTypes.ChangeScreen);
}
public static void Register(object recipient, Action<string> action)
{
Messenger.Default.Register<string>(recipient,
MessageTypes.ChangeScreen, action);
}
}
public static class RaiseErrorMessage
{
public static void Send(Exception ex)
{
Messenger.Default.Send<Exception>(ex, MessageTypes.RaiseError);
}
public static void Register(object recipient, Action<Exception> action)
{
Messenger.Default.Register<Exception>(recipient,
MessageTypes.RaiseError, action);
}
}
public static class PleaseConfirmMessage
{
public static void Send(DialogMessage dialogMessage)
{
Messenger.Default.Send<DialogMessage>(dialogMessage,
MessageTypes.PleaseConfirm);
}
public static void Register(object recipient, Action<DialogMessage> action)
{
Messenger.Default.Register<DialogMessage>(recipient,
MessageTypes.PleaseConfirm, action);
}
}
public static class StatusUpdateMessage
{
public static void Send(DialogMessage dialogMessage)
{
Messenger.Default.Send<DialogMessage>(dialogMessage,
MessageTypes.StatusUpdate);
}
public static void Register(object recipient, Action<DialogMessage> action)
{
Messenger.Default.Register<DialogMessage>(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
:
public MainPage()
{
InitializeComponent();
......
// register for StatusUpdateMessage
AppMessages.StatusUpdateMessage.Register(this, OnStatusUpdateMessage);
}
#region "StatusUpdateMessage"
private 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
:
......
// notify user of the new issue ID
DialogMessage 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:
<UserControl.Resources>
<DataTemplate x:Key="listBox_FilesDataTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=FileName, ValidatesOnNotifyDataErrors=False}" />
<TextBlock Text="{Binding Path=Data.Length, StringFormat=' - \{0:F0\} bytes',
ValidatesOnNotifyDataErrors=False}" />
</StackPanel>
</DataTemplate>
</UserControl.Resources>
......
<ListBox x:Name="listBox_Files" Grid.Row="1" Grid.Column="0"
AllowDrop="True"
ItemsSource="{Binding Path=CurrentIssue.Files, ValidatesOnNotifyDataErrors=False}"
ItemTemplate="{StaticResource listBox_FilesDataTemplate}">
<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:
private RelayCommand<DragEventArgs> _handleDropCommand = null;
public RelayCommand<DragEventArgs> HandleDropCommand
{
get
{
if (_handleDropCommand == null)
{
_handleDropCommand = new RelayCommand<DragEventArgs>(
e => this.OnHandleDropCommand(e),
e => this.CurrentIssue != null);
}
return _handleDropCommand;
}
}
private void OnHandleDropCommand(DragEventArgs e)
{
try
{
if (e.Data != null)
{
// get a list of files as FileInfo objects
var files = e.Data.GetData(DataFormats.FileDrop) as FileInfo[];
// 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
this.CurrentIssue.Files.Add(
new Data.Web.File()
{
FileID = Guid.NewGuid(),
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:
private void OnChangeScreenMessage(string changeScreen)
{
// call Cleanup() on the current screen before switching
ICleanup currentScreen = this.mainPageContent.Content as ICleanup;
if (currentScreen != null)
currentScreen.Cleanup();
// reset noErrorMessage
this.noErrorMessage = true;
switch (changeScreen)
{
case ViewTypes.HomeView:
this.mainPageContent.Content = new Home();
break;
case ViewTypes.NewIssueView:
this.mainPageContent.Content = new NewIssue();
break;
case ViewTypes.AllIssuesView:
this.mainPageContent.Content = new AllIssues();
break;
case ViewTypes.MyIssuesView:
this.mainPageContent.Content = new MyIssues();
break;
case ViewTypes.BugReportView:
this.mainPageContent.Content = new Reports();
break;
case ViewTypes.MyProfileView:
this.mainPageContent.Content = new MyProfile();
break;
case ViewTypes.UserMaintenanceView:
this.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:
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
this.Content_Resized(this, null);
if (!ViewModelBase.IsInDesignModeStatic)
{
// Use MEF To load the View Model
_viewModelExport = App.Container.GetExport<ViewModelBase>(
ViewModelTypes.BugReportViewModel);
this.DataContext = _viewModelExport.Value;
}
// register for GetChartsMessage
AppMessages.GetChartsMessage.Register(this, OnGetChartsMessage);
}
#endregion "Constructor"
#region "ICleanup interface implementation"
public void Cleanup()
{
// call Cleanup on its ViewModel
((ICleanup)this.DataContext).Cleanup();
// cleanup itself
Messenger.Default.Unregister(this);
// set DataContext to null and call ReleaseExport()
this.DataContext = null;
App.Container.ReleaseExport<ViewModelBase>(_viewModelExport);
_viewModelExport = null;
}
#endregion "ICleanup interface implementation"
......
}
And here is the Cleanup()
method in its ViewModel class:
#region "ICleanup interface implementation"
public override void Cleanup()
{
if (_issueVisionModel != null)
{
// unregister all events
_issueVisionModel.GetAllUnresolvedIssuesComplete -=
new EventHandler<EntityResultsArgs<Issue>>(
_issueVisionModel_GetAllUnresolvedIssuesComplete);
_issueVisionModel.GetActiveBugCountByMonthComplete -=
new EventHandler<InvokeOperationEventArgs>(
_issueVisionModel_GetActiveBugCountByMonthComplete);
_issueVisionModel.GetResolvedBugCountByMonthComplete -=
new EventHandler<InvokeOperationEventArgs>(
_issueVisionModel_GetResolvedBugCountByMonthComplete);
_issueVisionModel.GetActiveBugCountByPriorityComplete -=
new EventHandler<InvokeOperationEventArgs>(
_issueVisionModel_GetActiveBugCountByPriorityComplete);
_issueVisionModel.PropertyChanged -=
new System.ComponentModel.PropertyChangedEventHandler(
_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.
发表评论
ei2esz Im thankful for the blog post.Really thank you! Keep writing.
Your article is brilliant. The points you make are valid and well represented. I have read other articles like this but they paled in comparison to what you have here.
Wow! This can be one particular of the most useful blogs We have ever arrive across on this subject. Actually Great. I am also an expert in this topic therefore I can understand your effort.
Looking around While I was browsing yesterday I noticed a great article concerning
Major thanks for the article.Really thank you! Really Cool.
I'аve read several exceptional stuff here. Undoubtedly worth bookmarking for revisiting. I surprise how a lot attempt you set to make this kind of wonderful informative web site.
Your style is so unique in comparison to other folks I have read stuff from. Thanks for posting when you ave got the opportunity, Guess I all just book mark this page.
we came across a cool website that you just may possibly get pleasure from. Take a look in the event you want
Im no expert, but I think you just made a very good point point. You certainly comprehend what youre talking about, and I can actually get behind that. Thanks for being so upfront and so genuine.
Some truly prize blog posts on this internet site , bookmarked.
Muchos Gracias for your blog article.Much thanks again. Fantastic.
I truly appreciate this post. I?аАТаЂаve been looking all over for this! Thank goodness I found it on Bing. You ave made my day! Thanks again
that it appears they might be able to do that. We have, as
Thank you ever so for you article. Great.
Thanks for sharing this excellent piece. Very interesting ideas! (as always, btw)
Really informative article post.Really thank you! Great.
I usually do not create a bunch of responses, however i did a few searching and wound up right here?? -
Major thankies for the article.Much thanks again. Fantastic.
wow, awesome blog article.Thanks Again. Fantastic.
Wow! This can be one particular of the most useful blogs We have ever arrive across on this subject. Actually Excellent. I am also a specialist in this topic so I can understand your hard work.
Perfectly composed articles , thankyou for selective information.
I truly appreciate this post. I have been looking all over for this! Thank goodness I found it on Bing. You ave made my day! Thank you again!
Isabel Marant Sneakers Pas Cher WALSH | ENDORA
When June arrives for the airport, a man named Roy (Tom Cruise) bumps into her.
My brother recommended I might like this website. He was totally right. This post truly made my day. You can not imagine simply how much time I had spent for this info! Thanks!
I truly appreciate this post. I have been looking everywhere for this! Thank God I found it on Bing. You have made my day! Thank you again.
Very good blog post. I definitely appreciate this site. Continue the good work!
You have made some really good points there. I looked on the web to learn more about the issue and found most people will go along with your views on this website.
Wonderful post however , I was wanting to know if you could write a litte more on this subject? I ad be very grateful if you could elaborate a little bit further. Appreciate it!
It as very straightforward to find out any matter on net as compared to books, as I found this post at this site.
Very good post. I will be going through many of these issues as well..
Regards for helping out, good info. Our individual lives cannot, generally, be works of art unless the social order is also. by Charles Horton Cooley.
Really enjoyed this blog article.Thanks Again. Keep writing.
Usually I don at learn article on blogs, however I would like to say that this write-up very pressured me to take a look at and do it! Your writing taste has been amazed me. Thanks, quite great post.
Looking forward to reading more. Great blog article.Much thanks again. Fantastic.
Your style is very unique in comparison to other folks I have read stuff from. I appreciate you for posting when you ave got the opportunity, Guess I all just bookmark this site.
I think this is a real great article post.Really thank you! Cool.
You have remarked very interesting points! ps nice site.
Wow, superb blog layout! How long have you been blogging for? you made blogging look easy. The overall glance of your site is excellent, let alone the content material!
There as certainly a lot to know about this topic. I really like all of the points you have made.
Remarkable! Its actually remarkable post, I have got much clear idea on the topic of from this post.
Im grateful for the blog article.Thanks Again. Great.
Some genuinely great articles on this web site , thankyou for contribution.
What as Happening i am new to this, I stumbled upon this I ave found It absolutely useful and it has helped me out loads. I hope to contribute & assist other users like its aided me. Good job.
Wow! This could be one particular of the most beneficial blogs We ave ever arrive across on this subject. Actually Fantastic. I am also a specialist in this topic so I can understand your hard work.
I truly appreciate this post. I ave been looking everywhere for this! Thank goodness I found it on Bing. You have made my day! Thanks again!
Try to remember the fact that you want to own an virtually all comprehensive older getaway.
Well I sincerely enjoyed reading it. This tip procured by you is very helpful for accurate planning.
we came across a cool web-site that you just might appreciate. Take a search if you want
Looking forward to reading more. Great blog.Thanks Again. Keep writing.