Silverlight 与 MVVM 和 WCF
本文旨在帮助您以更易于管理的方式,使用 Silverlight、MVVM 和 WCF 服务来开发业务应用程序。
引言
本文的目的是解释使用 Silverlight、MVVM 和 WCF 开发业务应用程序的众多方法之一。我将尝试通过为一个虚构的公司 XYZ 开发一个小型的 Silverlight 导航应用程序,来解释实现这一目标所需的各种组件及其相互关系。
前提条件
要打开和运行源代码,您需要
- Visual Studio 2010 SP1 或 2012(如果您没有 VS 2010 SP1,可以在这里获取 VS 2010 SP1)
- Silverlight 4 Toolkit - 2010 年 4 月(在此处下载)
- ……以及 Silverlight、MVVM 和 WCF 的基础知识。为什么不现在学习它们呢?
解决方案结构
这是一个 Silverlight 导航应用程序项目,解决方案资源管理器在不同的解决方案文件夹下组织了 12 个项目,以便于理解和维护。
但是,如果您想自己创建一个解决方案结构,可以按照下图所示创建新的 Silverlight 项目。
项目与连接
此应用程序提供以下功能
- 显示员工
- 创建新员工
- 更新员工
啊!很简单,但为什么要在一个解决方案中有 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
- 每个方法的返回类型始终是 `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 日。