Catel(翻译)-为什么选择Catel
1. 介绍
这篇文章主要是为了说明,我们为什么要使用Catel框架作为开发WPF,Silverlight,和Windows phone7应用程序的开发框架。
2. 通用功能
2.1. 这是你的选择
针对需对开发者,再使用架构的时候是希望有很大的自由度的,但是大部分框架则需要开发者完全遵循他的规则,要么都使用这个框架,要么就不能使用。而catel不是,Catel包含很多的功能:日志,诊断,反射,MVVM,用户控件,窗体,这些所有的功能都是与其他功能互补的,二针对Catel则是你可以决定使用这些功能中的一个,或多个,或全部。而对于大部分应用程序都会有一个启动项-bootstrapper,通过这个来决定你的架构,当这个决定后,好多东西都是定好的,比如说Views的名字必须这样,控件的名字必须那样,而Catel可以让你自由设置。还有一点最重要Catel可以和其他框架一起使用,这样开发者可以很好的利用其他框架来补充应用程序的每个方面来完成一个最好的框架。
2.2 Data handing
有一件很重要的事情是大部分开发者来写用了很多精力来实现对象的序列号。序列号是一种专业的技术,尽管少数的开发者能够掌握序列化方法(考虑程序集版本的改变,类的改变(增加或减少属性)等等).而大部分开发者所认为的,掌握的序列号技术,只是创建一个BinaryFormatter对象,如下面代码所示:
BinaryFormatter serializer = new BinaryFormatter(); var myObject = (MyObject)serializer.Deserialize(stream);
大部分开发者都不知道当出现如下情况是反射会出现问题:
- 1,你更改了assembly的版本
- 2,你增加或删除了一个属性
- 3,你增加或删除了一个事件
- 甚至在你已经掌握了一些知识准备去处理这些问题,和每个开放者一样,我也遇到过这种情况,也是先过代码来处理,我实现了一个DataObjectBase类,这个能够用于作为所有数据类的基类存放在内存中,也可以序列化到磁盘上(比如stean,或者XML或者….)
2.2.1 DataObjectBase 类
使用这个类是非常简单的,你只需要定义一个新的类,继承与DataObjectBase就可以了。
/// <summary> /// MyObject Data object class which fully supports serialization, /// property changed notifications, /// backwards compatibility and error checking. /// </summary> #if !SILVERLIGHT [Serializable] #endif public class MyObject : DataObjectBase<MyObject> { /// <summary> /// Initializes a new object from scratch. /// </summary> public MyObject() { } #if !SILVERLIGHT /// <summary> /// Initializes a new object based on <see cref="SerializationInfo"/>. /// </summary> /// <param name="info"><see cref="SerializationInfo"/> // that contains the information.</param> /// <param name="context"><see cref="StreamingContext"/>.</param> protected MyObject(SerializationInfo info, StreamingContext context) : base(info, context) { } #endif }
正如你在上面代码中所示,MyObject类继承于DataObjetBase,提供一个空的钩子函数,但也有一个构造函数用于二进制序列号,上面的代码看起来复杂,但是可以由一个dataobject代码段来构造,你只需要写类的名字。
2.2.2 定义属性
为类定义属性是非常简单的,属性与dependcy 属性像是,使用这种方式来定义属性的优点如下:
- 1,这样定义属性能自动包括序列化,不需要制定复杂的数据接口
- 2在当类还没有被构造或者属性没有在序列号中赋值时,你可以为属性指定默认值(这种情况下,属性被加到一个存在的类中)
- 3,propertyData对象能够用于获取属性,以至于编译器来检查错误
- 4,你可以直接订阅来更改通知,所有的属性都支持了 NodifyPropertyChanged
- 如下的代码定义了一个string类型的Name属性。
/// <summary> /// Gets or sets the name. /// </summary> public string Name { get { return GetValue<string>(NameProperty); } set { SetValue(NameProperty, value); } } /// <summary> /// Register the Name property so it is known in the class. /// </summary> public static readonly PropertyData NameProperty = RegisterProperty("Name", typeof(string), string.Empty);
- 如果需要一个注册的属性可以再序列化的时候排除,当对象反序列化时,默认值将可以使用。
2.2.3 序列化
我已经提到序列化好多次了,让我看来看看如何简单序列化你的程序,而不考虑assembly版本,首先,将原来继承于DataObjectBase的对象,改为继承SavableDataObjectBase.依赖目标框架,许多属性作为序列化模式
- 1,二进制
- 2,XML
- 3,DataContract
- 4,JSON
下面的代码显示了如何存储一个对象(如果可以,也可以存储一个嵌套的对象)
var myObject = new MyObject(); myObject.Save(@"C:\myobject.dob");
看起来非常的简单,但是这确实是你唯一需要做的事情,你也能够通过重新调用重载的方法,来指定序列化模式。
载入也是很简单的,你可以通过如下的代码
var myObject = MyObject.Load(@"C:\myobject.dob");
2.2.4 提供的功能
DataObjectBase提供了一些功能,如下
- INotifyPropertyChanged
- 所有通过RegisterProperty注册的属性都会关注通知信息。
IDataError
非常容易设置字段或业务的错误,通过SetFieldError和SetBussinessError方法,这些方法能够通过改写ValidateFiled和ValidateBussinessRules的方法实现
IEditableObject
数据对象能够自动创建一个内部备份和存储它,如果需要,使用IEditableobject接口
序列化
如说过好多次的,使用SavableDataObjectBase对象,你能够存储你的文件到流(如在disk中,stream在内存中等等)注意这个类并不适合数据库通讯,比较好的办法是转换他(ORM 匹配的方式,通过Entity Framework,NHibernate,LLBLGen Pro 等等)
这个API是位于Catel.Core程序集中,这个可以不只使用WPF(也可以用于ASP.NET,Windows Forms)等等。
2.3 MVVM基础
在近几年,MVVM已经变成写WPF,Sliverlight和Windows Phone应用程序的一种常用模式,模式本身是非常简单的,但MVVM本身有许多的缺陷很问题:
1,如何在View-Model中显示模型对话框或者是消息框。
2,如何在View-Model中执行进程
3,如何让用户在View-Model中选择一个文件。
在我看来,有许多好的架构将其划分出来,例如,人们直接在View-Model中调用MessageBox.show,如果你也是使用这种方式,问问你自己:谁会在一个单元测试中点击按钮。
在我们真正开始用Catel进行开发前,我们需要通过调查来确定MVVM模式在Line of Business(LoB)是真正有用的。基于这个调整和研究,我们创建了一个稳固的MVVM模型来解决MVVM模式中已知的这些问题 。
2.3.1 ViewModelBase
如大多数MVVM框架一样,所有View-Models都使用ViewModelBase作为基类,这个基类继承与DataObjectBase类(在文章的前门提到过这个类),这个将会有如下的优点:
- 1,类似于Dependency属性的方式
- 2,自动监控更改
- 3,支持字段和业务错误
- 因为当前类继承与DataObjectBase,你可以增加字段或者是业务错误,这些将自动反应到UI上么,编写View-Models是如此的简单。
- 2.3.2 Model 映射
- 在使用MVVM模式时,我们注意到,很多的开发者都有一个Model,并且将这个Model的属性匹配到View-Model上,当UI关闭后,用户又将这些属性重新放回到Model中, 在使用Catel的时候,则不需要实现这些代码。
- 在Catel中,我们创建特性,运行你定义属性作为一个Model,Model是作为View-Model表现效果的一个部分呈现给用户的。View-Model可以实现多个模型的组合。
- 顶一个模型是非常简单的,你只需要定义Model中的特性.
/// <summary> /// Gets or sets the shop. /// </summary> [Model] public IShop Shop { get { return GetValue<IShop>(ShopProperty); } private set { SetValue(ShopProperty, value); } } /// <summary> /// Register the Shop property so it is known in the class. /// </summary> public static readonly PropertyData ShopProperty = RegisterProperty("Shop", typeof(IShop));
使用Model特性是非常强大的,基本上这是在View-Model中的扩展,如果模型支持IEditableObject,BaseEdit将自动在View-Model的Initialize中被调用。当前View-Model被Cancel,则CancelEdit也会被调用保证更改被回退掉。而当模型被定义时,也可以使用ViewToModel特性,如你下面的代码所看到的
/// <summary> /// Gets or sets the name of the shop. /// </summary> [ViewModelToModel("Shop")] public string Name { get { return GetValue<string>(NameProperty); } set { SetValue(NameProperty, value); } } /// <summary> /// Register the Name property so it is known in the class. /// </summary> public static readonly PropertyData NameProperty = RegisterProperty("Name", typeof(string));
在上面代码中的ViewModel特性会自动匹配View-Model 中的Name属性到Shop.Name属性中,使用这种方式,你不需要手工去编写这些代码来从模型中获取值,另一个很好的效果是View-Model将自动验证所有定义在Model特性中的对象,所有的字段和业务的错误将自动匹配到View-Model上
总的来说,Model和ViewModelToModel特性将确定不需要重复验证或者人工匹配。
2.2.3 Commands
Commanding 在Catel能被很好的支持,Catel支持Command类,这些在其他框架中被称之为RelayCommand或者是DelegateCommand,在View-Model中定义一个Command是已经非常容易的时候,你可以在下面的代码中看到。
// TODO: Move code below to constructor Edit = new Command<object, object>(OnEditExecute, OnEditCanExecute); // TODO: Move code above to constructor /// <summary> /// Gets the Edit command. /// </summary> public Command<object, object> Edit { get; private set; } /// <summary> /// Method to check whether the Edit command can be executed. /// </summary> /// <param name="parameter">The parameter of the command.</param> private bool OnEditCanExecute(object parameter) { return true; } /// <summary> /// Method to invoke when the Edit command is executed. /// </summary> /// <param name="parameter">The parameter of the command.</param> private void OnEditExecute(object parameter) { // TODO: Handle command logic here }
有一些人并不喜欢ICommand实现,例如Caliburn(Micro)使用约定,而并不需要创建Command,这种做法有些缺陷。如下
- 1,这个需要你确保控件名与方法名一致
- 2,如果你不是完全熟悉约定,你将不好分辨出是否是一个Command.
- 3,这个方法是一个Public(否则,你将在UnitTest中无法调用Commands),这些看起来很自由,但其实并不是我们所看到的那样。
- 4,你将在Excute中一直调用CanExecute,因为你不能保证Excute在约束匹配的时候就执行。
- 5,没有办法人工的刷新CanExcute状态
2.3.4 Services
服务室Catel解决需要将消息传递给用户的问题,让用户开始一个进程,ViewModelBase有一个GetServcie方法来通过一个IOC容器获得获得实际实现的一个接口,对于WPF和Silverlight,Catel使用Unity,对于Windows Phone7,IOC是通过定制实现的。
在Catel中,服务最重要的效果是,默认情况下,真正的实现是使用Unity容器注册过的,通过Ioc容器,你可以注册测试实现,例如,MessageBox Click实际,在这种情况下,你能够模仿用户点击OK,如果你想,你也可以模仿Cancel,这样你可以通过单元测试用户在View-Model中执行的所有可能的操作。
使用services时非常容易的,如前面所提到的,GetServcie方法获取service的真正执行,例如,显示一个消息给用户,你可以使用如下的代码
var messageService = GetService<IMessageService>(); messageService.ShowError("An error occurred");
所有的服务都能被简单的使用,Catel支持一些services,如下是services能够支持的目标框架
服务名称和描述 | WPF | Sliverlight | WP7 |
ILogger -确保写消息日志 |
|||
ILocationService -返回地理位置 |
|||
IMessageService -显示弹出框 |
|||
INavigationService - 导航服务 |
|||
IOpenFileService -让用户选择文件并打开 |
|||
IPleaseWaitService – 在UI线程上启动一个等待标记 |
|||
IProcessService - 执行进程 |
|||
ISaveFileService - 让用户选择一个文件来保存 |
|||
IUIVisualizerService - 显示模态窗口 |
当然你也可以设计你自己的服务
2.3.5与其他View-Models交互
大部分的框架需要设置复杂的消息系统或者其他的技术来在View-Models直接进行通信,这个方法的缺点在于一旦View-Model是指在ModuleX中实现,并且你关注View-Model,ModelX的开发者必须关注其他的ViewModel,而现在你唯一需要做的事情是设置一个InterestedIn特性,如下代码所示。
[InterestedIn(typeof(FamilyViewModel))] public class PersonViewModel : ViewModelBase
那么,在PersionViewModel中(这个关注FamiliyViewModel的更改),你只则只需要修改OnViewModelPropertyChanged事件方法,即可。
/// <summary> /// Called when a property has changed for a view model type /// that the current view model is interested in. This can /// be accomplished by decorating the view model with the <see cref="InterestedInAttribute"/>. /// </summary> /// <param name="viewModel">The view model.</param> /// <param name="propertyName">Name of the property.</param> protected override void OnViewModelPropertyChanged(IViewModel viewModel, string propertyName) { // You can now do something with the changed property }
也有可能关注多个View-Models,从View-Model被传递到OnViewModelPropertyChanged中,非常容易检查View-Model类型。
2.3.6 嵌套用户控件问题
在MVVM中,有一个复杂架构问题,称之为”嵌套控件问题“,直到现在,我们没有发现其他框架能够使用一个清晰的方式来解决这个问题。这个问题是嵌套的控件应该很容易获取自己所在的View-Model,但是,如何去管理这个,我们看到了许多的解决方案,比如:
1,在另一个View-Model中定义嵌套的View-Models.
2,在最上层的View-Model上定义嵌套用户控件,在用户控件中显示这些属性
下面是这个问题的图片显示
通常的MVVM格式
Catel中的MVVM
-
2.7 诊断和日志
如上面的图片所示,Catel解决这个问题的方法是更加的专业,这个有如下理由说明:
1,分割关注点(每个控件都只有一个View-Model来包含它自己的信息,而并不关系其子节点)
2,用户控件可以被重用
UserControl<TViewModel)能够基于一个实际的用户控件的datacontext来构造一个ViewModel,例如,当你定义了嵌套的用户控件,你唯一需要做的事情就是确认用户控件的datacontext要有一个对象被注入到属于用户控件的View-Model中。
例如,我们有一个persion控件,这个persion控件,只能使用一个有效的IPerson实例(View-Model仅有的构造函数),然后将以如下的方式定义在XAML中。
<Controls:PersonControl DataContext=”{Binding Person}” />
用户控件将关注datacontext的改变,并且尝试去使用正确的datacontext来构建PersonViewModel,在我们的例子中,IPerson接口的一个实例,将自动将IPersion对象转换成PersionViewModel并且控件将有自己的View-Model.
2.4 用户接口
我前面已经提到过很多次-Catel比其他的MVVM模式,提供了更多的UI Elemenet,下面将是最受欢迎的几个。
2.4.1. InfoBarMessageControl
InfoBarMessageControl能够通过IDataWariningInfo和IDataErrorInfo接口显示警告和错误给用户,这个控件在以统一的方式显示当前窗体或控件的当前状态给用户的功能上是非常有用的。
2.4.2 DataWindow
当使用WPF开发时,我们一直需要如下的窗体:
1,确定/取消 按钮的数据窗体
2,确定/取消/接受按钮的应用程序设置/选项
3,在一个窗体上的关闭按钮
创建这些窗体的步骤一直是相同的:
1,先在窗体的底部创建一个WrapPanel
2,一边又一遍使用相同的RoutedUICommand对象增加按钮。
DataWindow类使得非常容易的去创建一个基本窗体,简单的给窗体指定一个模式,通过使用这个窗体,你能够将精力集中在实际业务的实现,而不需要担心按钮本身的实现,这个可以节省你的实际,在如下的例子中,是在XAML中编写XMAL到实际的输出控件上。
2.4.3 PleaseWaitWindow
PleaseWaitWindow是一个在长时间操作中显示等待的窗体,这个也有一个PleaseWaitHelper的类让你更加容易的使用PleaseWaitWindow.
2.5 IO
Catel也针对IO提供了很多扩展API,可能你会问为什么,下面就是显示IO框架
1,支持文件盒文件夹名超过255个字符
2,Combine文件路径或者URL的,比如
Path.Combine(“C:”, “Windows”, “Temp”);.
这些API贴近于System.IO的设计,以便于你很自然地使用它
这个API在Catel.Core程序集中,你可以使用在除WPF以外的地方
2.6 反射
在内部,Catel使用了很多反射技术来实现功能,这章将解释这些反射实现的优点,并不是全部替换了.NET本身实现的反射类,Catel反射类是对默认情况的一个扩展,这个程序及扩展更多的是针对Catel不同的扩展框架来做的一个相同的行为,例如,在WPF中,我们能够使用当前的AppDomain来载入程序集,然而,在Silverlight中,则需要我们来查询当前的Deployment对象来处理,在WP7中,则很难获得载入对象,通过一个独立的类来实现,通过这种方式对所有的不同框架提供统一的接口
这个API在Catel.Core程序集中,你可以使用在除WPF以外的地方
Catel使用log4net来作为日志处理的基础,它也提供了额外的功能呢和扩展方法来确保很容易的去记录异常和通用信息,因为Sliverlight和WP7并没有日志能力(客户端),ILog实现一个虚拟的接口。
这个API在Catel.Core程序集中,你可以使用在除WPF以外的地方
2.8 性能
每一个框架都被要求有比较好的性能,我们需要比他们更好,因此,有450个单元测试时用来确保Catel的性能和同步的。这些单元测试在WPF和Sliverlight之间共享,来确保这些行为时一样的,通过这种方式,我们尝试确保在Silverlight在相同的框架下没有性能损失。
3,当前框架的特定功能
如你现在所知道的,Catel支持Sliverlight实现,sliverlight是一个非常受欢迎的平台,但是他缺少已经在WPF中有效的特定,然后使用Catel的开发者,我们尝试在Catel让他们都起到效果。
我们也尝试在Windows Phone 7上实现相同的方式,但是有些不同的东西针对WP7开发,这个是由于不同的UI和状态管理决定的。
3.1 在sliverlight中的自动刷新命令
In Silverlight, commands are not re-evaluated automatically because there is no CommandManager
that calls theCanExecute
on every routed event. If the user solves an error in the window by, for example, adding a value in an empty field, the CanExecute
state of the OK or Save command is not updated (and still is disabled). Other frameworks require the developer to manually refresh the commands.
Catel offers a clean way of providing the same behavior of WPF. Thanks to the ViewModelBase
propertyInvalidateCommandsOnPropertyChanged
(which is enabled by default for Silverlight and Windows Phone 7), all the ICommand
implementations on the View-Model are automatically re-evaluated for you. This way, the user will immediately see the OK or Save button become enabled after setting the value.
3.2.在Sliverlight和WP7中序列化和存储数据
When developing software for Silverlight or Windows Phone 7, you have access to isolated storage.SavableDataObjectBase
supports saving and loading of complex graph objects to isolated storage out of the box. To save an object (including all the child objects), you can use the code below:
using (var isolatedStorageFile = IsolatedStorageFile.GetUserStoreForApplication()) { using (var isolatedStorageFileStream = isolatedStorageFile.OpenFile("UserData.dob", FileMode.Create, FileAccess.ReadWrite)) { myObject.Save(isolatedStorageFileStream); } }
using (var isolatedStorageFile = IsolatedStorageFile.GetUserStoreForApplication()) { if (isolatedStorageFile.FileExists("UserData.dob")) { using (var isolatedStorageFileStream = isolatedStorageFile.OpenFile("UserData.dob", FileMode.Open, FileAccess.Read)) { return MyObject.Load(isolatedStorageFileStream); } } }
3.3在WP7中的导航
Navigation on Windows Phone 7 is very important. The navigation works the same like the web, thus with request variables. In Catel, it is very easy to navigate to another View (Model) using INavigationService
.
To navigate to another View directly, simply use this code inside your View-Model:
var navigationService = GetService<INavigationService>(); navigationService.Navigate("/UI/Pages/MyPage.xaml");
var parameters = new Dictionary<string, object>(); parameters.Add("MyObjectID", 1); var navigationService = GetService<INavigationService>(); navigationService.Navigate("/UI/Pages/MyPage.xaml", parameters);
The navigation service automatically converts this into the following URL:
/UI/Pages/MyPage.xaml?MyObjectID=1
3.4 Retrieving and simulating geographical locations on Windows Phone 7
The Windows Phone 7 device has a great feature: it can retrieve the current geographical location via GPS. However, how can we implement this correctly in MVVM? In Catel, a separate service is available to get the geographical location. The usage is very simple:
var locationService = GetService<ILocationService>(); locationService.LocationChanged += OnCurrentLocationChanged; locationService.Start();
It is very important to stop the service when you no longer need it (think about the battery of the user):
var locationService = GetService<ILocationService>(); locationService.LocationChanged -= OnCurrentLocationChanged; locationService.Stop();
In the OnCurrentLocationChanged
event, you can simply query the location from the EventArgs
:
private void OnCurrentLocationChanged(object sender, LocationChangedEventArgs e) { if (e.Location != null) { MapCenter = new GeoCoordinate(e.Location.Latitude, e.Location.Longitude, e.Location.Altitude); } }
4,使用MVVM来写一个N层架构
我们收到了许多用户的关于MVVM的如何实现一个N层架构的问题,大部分的用户认为MVVM来替代业务层,但并不是这种情况,这个章节将解释如何使用MVVM来写一个LOB应用程序的可能,这个将是一个简单或者是复杂的你所想要到东东。
依赖于终端用户的复杂性的需求,你可能决定在你的应用程序中使用一个多层架构,我并不相信一个单一的标准,但是层的一个实际方案决定了每个项目不能被超越创建??,但是没有层将被遗忘。
这章中以图片的形式显示N层架构,MVVM如何被使用在架构中,很久之前,N层架构是被以一种关注点分离的方式来开发的,近来,数据访问层(DAL)本身是一个复杂的东西,现在,有了许多的ORM技术来为你生成DAL,一些ORM技术,如LLBLGen Pro,允许你在DAL中增加你自己的验证和DAL与业务功能合起来。
4.1 2层应用系统
写两层架构师非常简单的,非常简单的应用程序并不需要任何强类型或者是业务规则,下面是2层架构使用MVVM的展示图。
正如你所看到的,使用3层架构的是DAL并没有参与到MVVM中,业务逻辑层使用DTO对象作为模型,并且将DTO对象转换成DAL能够处理的Entties,我们看到许多用户从UI层开始引用DAL,但是这个是错误的,下面是3层架构中显示的好的或坏的处理方法。
图中的箭头表示使用。
好的解决方案
在好的解决方案中,我们看到UI层使用BL层,然后BL层使用DAL层,上一次能一直使用一个较低的”down-the-hill”,下面的层应该不要使用上面的层(在非常错误的解决方案中有显示)
因为DAL并没有被引用,所以需要创建DTO对象来在DAL和BL直接传递数据。DTO在UI层也是有效的。
错误的解决方案
错误的解决方案显示,我们看到开发者直接使用了UI层来访问了DAL,这种方案中,他们不需要创造Custom DTO对象。
如果你想写Custom DTO对象,唯一的选择你需要写个横断关注,可以被所有的层使用,横断层中,你可以针对所有的DAL的实体编写接口,并且在UI和BL层中使用它。
现在,大部分的人们使用ORM映射方式,ORM映射可以使用生成DAL代码,你不需要花费时间自己写,ORM模板是你的选择,你可以生成DTO对象。
非常错误的解决方案
非常错误的解决方案显示用户底层使用了上层的方法,这个事非常错误的,破坏了整个的关注点分离的原则,如果你认为这正确地,那么请你回答,你的BL和UI在WPF中,那么你如何使用相同的BL到WP7或者是ASP.NET中。
4.3 MVVM如何与RIA服务和Silverlight结合
大部分的开发者发现很难去找到一个好的方式使用MVVM结合到Silverlight和RIA Service上,从过一个架构的观点来看,RIA服务只是起到DAO的效果,然后Sliverlight中的RIA服务必须使用相同的办法在DTO中。