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

Silverlight 与 MVVM 和 WCF

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.69/5 (8投票s)

2013 年 7 月 23 日

CPOL

7分钟阅读

viewsIcon

30144

downloadIcon

1159

本文旨在帮助您以更易于管理的方式,使用 Silverlight、MVVM 和 WCF 服务来开发业务应用程序。

引言

本文的目的是解释使用 Silverlight、MVVM 和 WCF 开发业务应用程序的众多方法之一。我将尝试通过为一个虚构的公司 XYZ 开发一个小型的 Silverlight 导航应用程序,来解释实现这一目标所需的各种组件及其相互关系。

前提条件 

要打开和运行源代码,您需要

  1. Visual Studio 2010 SP1 或 2012(如果您没有 VS 2010 SP1,可以在这里获取 VS 2010 SP1
  2. Silverlight 4 Toolkit - 2010 年 4 月(在此处下载
  3. ……以及 Silverlight、MVVM 和 WCF 的基础知识。为什么不现在学习它们呢?

解决方案结构

 这是一个 Silverlight 导航应用程序项目,解决方案资源管理器在不同的解决方案文件夹下组织了 12 个项目,以便于理解和维护。

但是,如果您想自己创建一个解决方案结构,可以按照下图所示创建新的 Silverlight 项目。

项目与连接 

此应用程序提供以下功能

  1. 显示员工
  2. 创建新员工
  3. 更新员工

啊!很简单,但为什么要在一个解决方案中有 12 个项目呢?答案是关注点分离可测试性。前端不是重点(您以后可以使用此结构轻松添加更多页面和复杂设计),但架构是重点。

这是使用 VS 2010 的“体系结构”菜单创建的依赖关系图(注意:此菜单仅在 Ultimate 版本中可用)

因此,Web 项目托管 Silverlight 项目(**XYZ**),该项目使用 MVVM 模式和 WCF 服务来渲染屏幕和逻辑。让我们来详细了解一下这些主题。

MVVM 简介

本文的目的不是详细讲解 MVVM(您自己也能找到详细信息),但总而言之,它打破了用户界面和业务之间的联系。由于其丰富的绑定支持,MVVM 非常适合 Silverlight 和 WPF。

MVVM 代表 **M**odel、**V**iew、**V**iew**M**odel,它们之间的交互是

模型 - 包含所有与业务相关的内容。在我们的例子中,**XYZ.Model** 使用 WCF 服务来获取、添加和更新员工。

视图 - 负责所有屏幕和所有取悦用户的内容。在我们的例子中,这是 **XYZ** 项目。

视图模型 - 视图的单一联系点。视图不需要知道模型是否存在,但可以向视图模型询问它需要的任何东西。在我们的解决方案中,这是 **XYZ.ViewModel** 项目。

深入代码

服务合同

服务部分包含三个项目:**XYZ.Service**、**XYZ.Data** 和 **XYZ.Contract**。

服务契约和数据与服务实现分离,以便于维护和测试。您可以使用模拟实现编写测试方法。

这是服务接口。

[ServiceContract(Namespace = "http://XYZ")]
public interface IEmployeeService
{
    [OperationContract]
    MethodResult<List<Employee>> GetEmployees();
    [OperationContract]
    MethodResult<Employee> GetEmployee(int employeeId);
    [OperationContract]
    MethodResult<int> SaveEmployee(Employee employee);
    [OperationContract]
    MethodResult<int> DeleteEmployee(int? employeeId);
}

`MethodResult` 是一个泛型类,所有服务方法都应该返回此类型,并将返回值传递给泛型类型。例如,`GetEmployees` 方法返回员工列表。这种方法也有一些好处。

  • 每个方法的返回类型始终是 `MethodResult`,可以将其设置为开发团队遵循的标准。(我不知道有多少人问过自己:我如何创建一个不需要向调用者返回任何值的方法?太好了,答案是……我不知道。有人能给出答案吗?)
  • 异常不会抛给客户端,而是被其他属性包装。请注意,服务抛出的任何异常都会被浏览器转换为 404 文件未找到错误,您总是会收到

    但在此方法中,我们可以向用户显示正确的错误消息,例如

这是 `MethodResult`。

[DataContract]
public class MethodResult<T>
{
    [DataMember]
    public Status Status { get; set; }

    [DataMember]
    public string ErrorMessage { get; set; }

    [DataMember]
    public T Data { get; set; }

    public MethodResult()
    {
        Status = Status.Unknown;
    }
}

服务实现

`IEmployeeService` 由 `EmployeeService` 类实现,该类使用存储库模式作为数据存储。存储库 `XYZ.Repository` 项目封装了底层存储系统,可以是 LINQ to SQL、Entity Framework 或任何其他系统。这里的数据存储在内存中。
public class EmployeeService : IEmployeeService
{
    private readonly ILogService _logService;
    private readonly IEmployeeRepository _repository;


    public EmployeeService(IEmployeeRepository repository)
    {
        _repository = repository;
        _logService = new FileLogService(typeof(EmployeeService));
    }

    public EmployeeService()
        : this(new EmployeeRepository())
    {

    }
}

正如您在此处注意到的,构造函数接收存储库接口,这使得编写测试方法更容易,可以使用模拟类。您可以在 `XYZ.Service.Test` 项目中看到示例测试方法。此外,您还可以看到构造函数中初始化的 `ILogService` 接口,这有助于日志记录。这个 `XYZ.Log` 项目使用log4net v1.2.11 将信息写入日志文件。这个项目来自我一年前在 CodeProject 上撰写的一篇文章,标题为log4Net 日志记录框架

示例日志文件

这是服务方法的实现(除 `Getemployees` 方法外,其他方法都被省略了)。

public MethodResult<List<Employee>> GetEmployees()
{
    Enter("GetEmployees");
    var result = new MethodResult<List<Employee>>();

    try
    {
        result.Data = _repository.GetEmployees();
        result.Status = Status.Success;
    }
    catch (Exception ex)
    {
        result.ErrorMessage = ex.Message;
        result.Status = Status.Error;
        Error(ex);
    }

    Exit();
    return result;
}

前端

XYZ.Model

WCF 服务在 **XYZ.Model** 项目中被引用。它有自己的接口,以便视图模型能够与之交互。再次强调,这种接口方法将有助于我们编写测试方法。

public interface IEmployeeService
{
    event Action<string> GetEmployeesError;
    event Action<string> GetEmployeeError;
    event Action<string> SaveEmployeeError;

    event Action<ObservableCollection<Employee>> GetEmployeesCompleted;
    event Action<Employee> GetEmployeeCompleted;
    event Action<int> SaveEmployeeCompleted;

    void GetEmployees();
    void GetEmployee(int employeeId);
    void SaveEmployee(Employee employee);
}

正如您所见,这个接口提供了比方法多一倍的事件。即三个方法对应六个事件(每个方法都有一个用于完成,一个用于失败)。这些事件用于将方法的状��传达给视图模型,视图模型再通知视图。

XYZ.ViewModel

视图模型帮助视图获取数据以进行操作。所有视图模型都将继承自 `ViewModelBase` 类。这个类实现了 `INotifyPropertyChanged` 接口,以便在视图模型中的某些内容发生更改时通知视图。这有助于视图直接使用 XAML 或代码隐藏代码中的绑定来更新我们的视图模型数据。

public abstract class ViewModelBase : EventManager, INotifyPropertyChanged
{

    protected void RaisePropertyChanged(string property)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(property));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public virtual bool IsValid()
    {
        return true;
    }
}

正如您注意到的,它还继承自 `EventManager` 类。当您在类中定义事件时,编译器会为每个事件创建 add 和 remove 访问器,从而增加了代码大小,但调用者可能并非所有事件都同时使用。如果我们只为访问的事件创建访问器,就可以减少额外的代码大小。

`EventManager` 类公开了一个 `EventSet` 属性,该属性管理仅在有人对事件感兴趣时才创建那些访问器。例如,一个类公开了 10 个事件,但调用者只使用了其中的两个事件,那么编译器只创建这两个事件访问器,从而减小了代码大小。这个概念不是我的,而是《CLR via C#》的作者 Jeffrey Richter 的。事实上,`EventSet` 类就是从那本书中提取的,只是在 `Raise` 方法中做了一些轻微的修改(参见第 11 章:事件)。

public abstract class EventManager
{
    private readonly EventSet _eventSet = new EventSet();

    protected EventSet EventSet { get { return _eventSet; } }
}

public sealed class EventSet
{

    /// <summary>
    /// The private dictionary used to maintain EventKey -> Delegate mappings
    /// </summary>
    private readonly Dictionary<EventKey, Delegate> _mEvents = 
            new Dictionary<EventKey, Delegate>();
    
    /// <summary>
    /// Adds an EventKey -> Delegate mapping if it doesn't exist or
    /// combines a delegate to an existing EventKey
    /// </summary>
    /// <param name="eventKey"></param>
    /// <param name="handler"></param>
    public void Add(EventKey eventKey, Delegate handler)
    {
        Monitor.Enter(_mEvents);
        Delegate d;
        _mEvents.TryGetValue(eventKey, out d);
        _mEvents[eventKey] = Delegate.Combine(d, handler);
        Monitor.Exit(_mEvents);
    }
        
    /// <summary>
    /// Removes a delegate from an EventKey (if it exists) and
    /// removes the EventKey -> Delegate mapping the last delegate is removed
    /// </summary>
    /// <param name="eventKey"></param>
    /// <param name="handler"></param>
    public void Remove(EventKey eventKey, Delegate handler)
    {
        Monitor.Enter(_mEvents);
        // Call TryGetValue to ensure that an exception is not thrown if
        // attempting to remove a delegate from an EventKey not in the set
        Delegate d;
        if (_mEvents.TryGetValue(eventKey, out d))
        {
            d = Delegate.Remove(d, handler);
            // If a delegate remains, set the new head else remove the EventKey
            if (d != null) _mEvents[eventKey] = d;
            else _mEvents.Remove(eventKey);
        }
        Monitor.Exit(_mEvents);
    }
        
    /// <summary>
    ///  Raises the event for the indicated EventKey 
    /// </summary>
    /// <param name="eventKey"></param>
    /// <param name="values"> </param>
    public void Raise(EventKey eventKey, params object[] values)
    {
        // Don't throw an exception if the EventKey is not in the set
        Delegate d;
        Monitor.Enter(_mEvents);
        _mEvents.TryGetValue(eventKey, out d);
        Monitor.Exit(_mEvents);
        if (d != null)
        {
            // Because the dictionary can contain several different delegate types,
            // it is impossible to construct a type-safe call to the delegate at
            // compile time. So, I call the System.Delegate type's DynamicInvoke
            // method, passing it the callback method's parameters as an array of

            // objects. Internally, DynamicInvoke will check the type safety of the
            // parameters with the callback method being called and call the method.
            // If there is a type mismatch, then DynamicInvoke will throw an exception.
            d.DynamicInvoke(values);
        }
    }
}

使用此 EventManager 创建一个事件。

static readonly EventKey LoadEmployeesSuccessEventKey = new EventKey();
 
public event Action<ObservableCollection<EmployeeDto>> LoadEmployeesSuccess
{
    add { EventSet.Add(LoadEmployeesSuccessEventKey, value); }
    remove { EventSet.Remove(LoadEmployeesSuccessEventKey, value); }
}

void OnLoadEmployeesSuccess(ObservableCollection<EmployeeDto> employees)
{
    EventSet.Raise(LoadEmployeesSuccessEventKey, employees);
}

由于不能从 Silverlight 同步调用 WCF 服务,我们需要一种方法来向用户告知进度,而事件是最佳选择。这是在代码调用服务方法时显示进度消息的屏幕。

调用完成后

**XYZ.ViewModel** 调用 Model 来获取数据,它拥有供视图使用的各种方法和事件。按照惯例,每个视图都有一个视图模型,并命名为 [视图]ViewModel。例如,`EmployeesViewModel` 是为视图 `Employees` 准备的。但只要相关,一个视图模型可以被任意数量的视图使用。

XYZ

最后是视图部分。视图将使用视图模型来连接事件和将数据绑定到控件。以下是访问 `Employees.xaml` 文件时发生的情况。

public partial class Employees
{
    public Employees()
    {
        InitializeComponent();
    }

    // Executes when the user navigates to this page.
    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
        var vm = new EmployeesViewModel();
        vm.LoadEmployeesStarted += AppHelper.ShowBusy;

        vm.LoadEmployeesSuccess += employees =>
                                       {
                                           AppHelper.StopBusy();
                                           LayoutRoot.DataContext = vm;
                                           var view = new PagedCollectionView(employees);
                                           DataGridEmployees.ItemsSource = view;
                                       };

        vm.LoadEmployeesFailed += reason =>
                                      {
                                          AppHelper.StopBusy();
                                          AppHelper.ShowMessage(string.Empty, reason);
                                      };
        vm.LoadEmployees();
    }
}

`Employee` 视图使用 `EmployeeViewModel` 并将其 `SaveEmployeeCommand` 绑定到其保存按钮。

<Button Content="Save Employee" Width="100" 
  Command="{Binding SaveEmployeeCommand}" x:Name="ButtonSave" Margin="10"/>

点击按钮后,将调用 `EmployeeViewModel` 中的 `SaveEmployee` 方法。

void SaveEmployee()
{
    OnSaveEmployeeStarted();
    _service.SaveEmployee(CurrentEmployee.AsEmployee());
}

最后...

很高兴您能看到这里,谢谢!当然,我无法在这里解释解决方案中的所有内容,但我鼓励您下载代码并自行探索。如果您有任何不清楚的地方或遇到任何问题,请留言,我会尽快回复您。再次感谢您的阅读。

关注点

此解决方案不依赖任何第三方控件,除了 log4net,但您可以尝试使用 MVVM(例如:MVVM light)和测试的第三方库。

历史

  • 发布日期:2013 年 7 月 23 日。
© . All rights reserved.