65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 MEF、MVVM 和 WCF RIA Services 的 Silverlight 4 示例应用程序 - 第二部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (49投票s)

2010 年 5 月 19 日

CPOL

5分钟阅读

viewsIcon

196044

downloadIcon

3

本系列文章的第二部分,描述了如何使用 MEF、MVVM Light 和 WCF RIA Services 创建一个 Silverlight 业务应用程序。在本第二部分中,我们将介绍 MVVM Light Toolkit 在我们的示例应用程序中的各种用法。

文章系列

本文是关于使用 MEF、MVVM Light 和 WCF RIA Services 开发 Silverlight 业务应用程序的系列文章的第二部分。

目录

引言

在本第二部分中,我们将介绍 MVVM Light Toolkit 在我们的示例应用程序中的各种用法。我选择这个工具包主要是因为它轻量级。而且,它是支持 Silverlight 4 的最流行的 MVVM 框架之一。

RelayCommand

Silverlight 4 的新功能之一是 ButtonBase 类新增了一对名为 CommandCommandParameter 的属性。这种命令基础设施使得在 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 中,注册了四个 AppMessagesChangeScreenMessage 用于处理菜单请求,以便在不同屏幕之间切换。另外三个 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 的使用方法:即 RelayCommandMessengerEventToCommandICleanup。在最后一篇中,我们将重点介绍如何通过 WCF RIA Services 完成自定义身份验证、密码重置和用户维护。

希望您觉得本文有用,请在下方评分和/或留下反馈。谢谢!

历史

  • 2010 年 5 月 - 初始发布
  • 2010 年 7 月 - 基于反馈的次要更新
  • 2010 年 11 月 - 更新以支持 VS2010 Express Edition
  • 2011 年 2 月 - 更新以修复包括内存泄漏问题在内的多个 bug
  • 2011 年 7 月 - 更新以修复多个 bug
© . All rights reserved.