2007年3月15日星期四

MVP实践之:Winform应用

上面的界面显示了程序最终运行的状态,主要完成两个功能:
  1. 输入一个构件的ID号,根据Id号到ftp服务器上查找该构件所有相关的图片,并且将这些图片的名称显示在一个ListBox中。
  2. 改变选中的图片(列表中选图片名)时将图片显示在右边的PictureBox中。
上面说的那两个功能,基本上可以说是这个程序的需求了,根据要求先画出界面来。注意这里画界面仅仅只是为了明确所需要的功能,既然是MVP模式的实践,我们就必须摆脱双击控件然后直接编写代码的习惯。将MVP的三要素从这个程序剥离出来,分析清楚它们各自的角色和职责,以及如何进行交互才是最首要的工作。
三要素:
View:是一个Form,包含了一些控件
Model:这里的功能就是访问ftp取得图片信息
Presenter:从界面获得构件id,根据id从model取得图片名称列表,然后传给界面显示;从界面获得图片名,根据图片名从model取得图片,将图片传给界面显示。

右边的图是实现该程序的类图,有两点需要先说明一下:
  1. Model在哪里?
    前面的文章“MVP vs MVC”中讲到过,MVP中的Model并非DomainModel,其中甚至包含了业务逻辑,就如在本程序中充当Model角色的是一个Service类,它负责从数据源取得数据并向外提供,需要的时候会先对数据做一定的处理。这里的数据源是一个ftp服务器。
  2. IView***是干什么的?
    使用MVP的一个很重要的原因是为了方便测试,将界面与数据和控制逻辑分离之后,可以在不依赖于具体的界面的情况下对View以下的逻辑层(V、P层)的代码进行测试。但是如果没有了具体的View,presenter与View的通信如何完成呢?引入“契约”可以解决这个问题。契约的基本思想是:一个对象它保证为它的使用者(调用它的对象)提供它所承诺的功能,而承诺的功能就是契约中所规定的功能。比如有这样一个规定:
    string SelectedPictName { get;}
    意思是所有的使用者都能够得到一个PictName,而所有遵守这个契约的对象都保证提供一个PictName。
    那么,本例中的IViewListDescPict就是这样一个契约,它规定了所有实现了这一接口的View都必须保证提供接口中所规定的功能(类实现接口,就必须实现接口中所有的功能)。这样presenter就不需要一个具体的View了,它只需要一个view的契约,通过调用契约中的功能来帮助自己完成任务。
以根据id查找构件所有的图片为例,看一下是如何工作的:

步骤1-4都是一些初始化工作,窗体作为视图负责初始化自己,然后创建一个它需要使用的presenter并且将自己与presenter邦定(实际上就是获得一个对presenter的引用)。
private void FormListDescPict_Load(object sender, EventArgs e)
{
presenter =new ListDescPictPresenter(this);
AttachPresenter(presenter);//this.presenter=presenter;
}
步骤5中view接受一个用户响应,但是它自己并不做处理而是在步骤6中将处理委托给presenter。
//register the presenter as a observer
btnSearch.Click +=presenter.SearchPictOfComp_EventHandler;
步骤6-11是一套控制逻辑,全部在presenter内部处理。presenter监视view中的按钮(观察者模式),当检测到用户点击事件之后,先从view取得componentid,从service取得数据,然后刷新view的数据显示。
private void SearchPictOfComp(Object sender,EventArgs e)
{
try
{
IList descPicts = pictService.GetPictsOfComp(view.ComponentId);
view.SetPictListDataSource(descPicts);
}
catch (Exception ex)
{
//View will show the error msg to the end user
view.Message = ex.Message;
}
}
我们可以看到view也就是这里的窗体form仅仅只是将用户的输入向下层传递,它甚至不需要知道自己什么时候该刷新,只是定义了刷新的方法,真正的刷新命令由presenter发出。view中没有逻辑处理,这样可以使得view尽量的简化。复杂的控制逻辑都在presenter层处理,因此这些控制也就变得可以复用了,意味着我们可以将这一套逻辑处理用于多个view,理想的情况下可以同时支持winform的和asp.net的程序。

最后要提一下异常处理问题。由于view没有处理逻辑,使得我们在view中没有捕获异常的机会,因此只能在presenter层捕捉了。那么如何将捕获的异常消息通知view以显示给用户呢?本例中是在IView这个契约里定义了一个Message属性,presenter将捕获的异常消息写入这个属性中,只要实现IView接口的view都可以获取这个消息,并进行处理。
public string Message
{
get
{
return msg;
}
set
{
if (!string.IsNullOrEmpty(value))
{
msg = value;
//you can use the msg in the way you like
MessageBox.Show(msg);
}
}
}
上面的方法虽然是对于错误处理提出来的,但是同样可以用于其他的用途,比如我希望返回一个操作成功的提示消息。

没有评论: