使用 MEF、MVVM 和 WCF RIA Services 的 Silverlight 4 示例应用程序 - 第二部分
本系列文章的第二部分,描述了如何使用 MEF、MVVM Light 和 WCF RIA Services 创建一个 Silverlight 业务应用程序。在本第二部分中,我们将介绍 MVVM Light Toolkit 在我们的示例应用程序中的各种用法。
- 从 第一部分 下载源文件和设置包
文章系列
本文是关于使用 MEF、MVVM Light 和 WCF RIA Services 开发 Silverlight 业务应用程序的系列文章的第二部分。
- 第一部分 - 引言、安装和通用应用程序设计主题
- 第二部分 - MVVM Light 主题
- 第三部分 - 自定义身份验证、密码重置和用户维护
目录
引言
在本第二部分中,我们将介绍 MVVM Light Toolkit 在我们的示例应用程序中的各种用法。我选择这个工具包主要是因为它轻量级。而且,它是支持 Silverlight 4 的最流行的 MVVM 框架之一。
RelayCommand
Silverlight 4 的新功能之一是 ButtonBase
类新增了一对名为 Command
和 CommandParameter
的属性。这种命令基础设施使得在 Silverlight 中实现 MVVM 更加容易。让我们看看在用户维护屏幕上如何为“删除用户”按钮使用 RelayCommand
。首先,我们定义按钮的 XAML 代码如下:
<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}"/>
上面的代码指定,当点击“删除用户”按钮时,我们应该调用 UserMaintenanceViewModel.cs 中定义的 RemoveUserCommand
,并传递当前选定用户的参数。并且,RelayCommand RemoveUserCommand
定义如下:
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);
}
}
上面的代码片段在被调用时,会先显示一个消息,询问用户是否确认删除选定的用户。如果确认,则会调用 IssueVisionModel
类中定义的 RemoveUser()
和 SaveChangesAsync()
函数,从而将选定的用户从数据库中删除。
RelayCommand
的第二个参数是 CanExecute
方法。在上面的示例代码中,它被定义为“g => (_issueVisionModel != null) && !(_issueVisionModel.HasChanges) && (g != null)
”,这意味着只有在没有待处理的更改且选定的用户不是 null
时,“删除用户”按钮才会被启用。与 WPF 不同,在 Silverlight 中,当 HasChanges
属性改变时,不会自动轮询此 CanExecute
方法,我们需要手动调用 RaiseCanExecuteChanged
方法,如下所示:
private void _issueVisionModel_PropertyChanged(object sender,
PropertyChangedEventArgs e)
{
if (e.PropertyName.Equals("HasChanges"))
{
AddUserCommand.RaiseCanExecuteChanged();
RemoveUserCommand.RaiseCanExecuteChanged();
SubmitChangeCommand.RaiseCanExecuteChanged();
CancelChangeCommand.RaiseCanExecuteChanged();
}
}
Messenger
MVVM Light Toolkit 中的 Messenger
类使用简单的发布/订阅模型来实现松耦合的消息传递。这有助于不同 ViewModel 类之间的通信,以及 ViewModel 类与 View 类之间的通信。在我们的示例中,我们定义了一个名为 AppMessages
的静态类,它封装了此应用程序中使用的所有消息。
/// <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);
}
}
......
}
在代码隐藏文件 MainPage.xaml.cs 中,注册了四个 AppMessages
。ChangeScreenMessage
用于处理菜单请求,以便在不同屏幕之间切换。另外三个 AppMessages
都是系统范围的消息:
RaiseErrorMessage
会在发生错误时显示错误消息,并立即从数据库注销。PleaseConfirmMessage
用于显示请求用户确认的消息,并根据用户反馈处理回调。StatusUpdateMessage
用于向用户更新某些状态更改,例如新问题已成功创建并保存等。
以下是我们如何注册 StatusUpdateMessage
:
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"
然后,以下是我们如何发送消息到 StatusUpdateMessage
:
......
// 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
是 MVVM Light Toolkit V3 中的一个新功能,是一个 Blend behavior,用于直接在 XAML 中将事件绑定到 ICommand
,这使我们能够使用 ViewModel 类中的 RelayCommand
处理几乎任何事件。
以下是“新建问题”屏幕中实现文件拖放的示例。我们先来看看 XAML 代码:
<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>
上面的代码基本上指定 ListBox
支持拖放,并且当 Drop
事件触发时,会调用 IssueEditorViewModel
类中的 HandleDropCommand
。接下来,我们看看 HandleDropCommand
的实现:
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
将遍历用户拖放的文件列表,读取每个文件的内容,然后将它们添加到 Files
EntityCollection
中。数据将在用户保存更改时保存到数据库。
ICleanup 接口
每当用户从菜单中选择不同的屏幕时,都会发送一个 ChangeScreenMessage
,该消息最终会调用以下 OnChangeScreenMessage
方法:
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();
}
}
从上面的代码可以看出,每次切换到新屏幕时,都会先测试当前屏幕是否支持 ICleanup
接口。如果是,则在切换到新屏幕之前调用 Cleanup()
方法。实际上,除了主屏幕(不绑定到任何 ViewModel 类)之外,所有屏幕都实现了 ICleanup
接口。
在任何 View 类中定义的 Cleanup()
方法将首先调用其 ViewModel 类上的 Cleanup()
方法来取消注册任何事件处理程序和 AppMessages
。接下来,它将取消注册 View 类本身使用的任何 AppMessages
,最后一步是通过调用 ReleaseExport<ViewModelBase>(_viewModelExport)
来释放 ViewModel 类,从而确保没有内存泄漏。让我们看一个例子:
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"
......
}
以下是其 ViewModel 类中的 Cleanup()
方法:
#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"
下一步
在本文中,我们回顾了 MVVM Light Toolkit 的使用方法:即 RelayCommand
、Messenger
、EventToCommand
和 ICleanup
。在最后一篇中,我们将重点介绍如何通过 WCF RIA Services 完成自定义身份验证、密码重置和用户维护。
希望您觉得本文有用,请在下方评分和/或留下反馈。谢谢!
历史
- 2010 年 5 月 - 初始发布
- 2010 年 7 月 - 基于反馈的次要更新
- 2010 年 11 月 - 更新以支持 VS2010 Express Edition
- 2011 年 2 月 - 更新以修复包括内存泄漏问题在内的多个 bug
- 2011 年 7 月 - 更新以修复多个 bug