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

演示模型实战

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (29投票s)

2008 年 1 月 31 日

CPOL

14分钟阅读

viewsIcon

128352

downloadIcon

1660

在 ASP.NET 网站、Windows Forms 和 WPF 中使用 Presentation Model 模式

引言

著名的 Model-View-Controller (MVC) 设计模式将表示层与域/数据访问层分离。此外,MVC 还将表示逻辑分解为视图和控制器。

Presentation ModelModel View Presenter (MVP) 是 Model-View-Controller (MVC) 设计模式的变体,其中控制器变成 Presentation Model 和 Presenter,并以不同的方式组织状态、逻辑和引用。

Presentation Model 的主要特点

  • 状态位于 Presentation Model 中
  • 逻辑位于 Presentation Model 中
  • 视图观察模型并据此更新
  • 视图“了解”Presentation Model
  • Presentation Model 不“了解”视图

Model View Presenter 有两种形式,分别称为 Supervising ControllerPassive View。它们共享相同的关键特征。

Model View Presenter 的主要特点

  • 状态位于视图中
  • 逻辑位于 Presenter 中
  • Presenter 观察视图
  • Presenter 更新视图
  • Presenter “了解”视图
  • 视图不“了解”Presenter

Presentation Model 是一种将表示行为从视图中提取出来的模式。它提供了一个集中的位置来存储状态/数据,以及一个完全独立于用于显示的视图的集中式事件源。

在实际项目中,我更喜欢使用 Presentation Model。本文将展示一个用于管理客户业务实体的设计过程,并说明 Presentation Model 如何应用于 ASP.NET 网站、Windows Forms 应用程序和 WPF 应用程序。

让我们开始分析一个虚构的业务实体——客户管理应用程序的需求。

需求分析

许多业务管理流程都可以抽象为以下场景:

  • 用户搜索业务实体
  • 用户查看实体列表
  • 用户从列表中选择一个实体
  • 用户查看所选实体的详细信息
  • 用户操作所选实体,例如修改、删除等。

我们的故事是关于管理客户这个示例业务实体的。用例可以描述如下:

用例名称

搜索和查看客户信息

目标

用户搜索客户并查看所选客户的详细信息。

先决条件

用户启动应用程序

事件流程

  1. 系统在列表中显示所有客户及其数量
  2. 用户通过键入搜索文本来搜索客户
  3. 系统将搜索文本与客户的名字、姓氏和地址进行匹配
    如果搜索文本为空,系统将显示所有客户
  4. 系统显示与搜索文本匹配的客户列表
  5. 系统根据搜索结果显示客户数量
  6. 用户选择一个客户
  7. 系统显示所选客户的详细信息
  8. 用户可以重复搜索

设计考虑因素

该应用程序可以设计为 ASP.NET 网站、Windows Forms 应用程序或 WPF 应用程序。本文将使用 Presentation Model 展示所有这三种应用程序形式。为了开始设计,首先来看一下网站上的一些用户界面选项。

在网站上,通常有三种实现此客户管理的方法:

选项 1:列表页和弹出详细信息页

此选项显示客户列表,每行都有一个超链接。单击超链接时,将显示一个新的弹出窗口,其中包含客户的详细信息。

Option1.JPG

选项 2:列表页和切换到详细信息页

虽然第一种选项是许多早期 Web 应用程序中常用的模式,但较新的 Web 应用程序会在同一窗口中显示详细信息,以避免弹出窗口。

Option2.JPG

选项 3:列表页和嵌入式详细信息页

与选项 2 类似,不会有弹出窗口,而且此选项在显示详细信息时也不会隐藏列表。它将列表和详细信息一起显示。

Option3.JPG

用户体验设计师通常会决定在最终产品中使用哪种选项。这个决定基于用户的反馈以及 UI 布局和外观设计。

这个决定可能会在项目中间发生变化。例如,最初选择了选项 1。然后用户报告说他们屏蔽了弹出窗口,因此必须改为选项 2。最后,由于布局和字体使屏幕有很大的空间,设计师可能会问我们是否可以同时显示列表和详细信息。然后就变成了选项 3。

架构设计应预见潜在的需求变更,并在发生变更时对代码的影响降到最低。换句话说,代码应该尽可能地可重用。

好消息是我们可以使用用户控件。用户控件是实现可重用的绝佳技术。它们被称为应用程序构建块。它创建了一个客户列表用户控件和一个客户详细信息用户控件。所有这三个选项都可以基于它们构建。

在选项 1 中,一个页面托管客户列表用户控件。一个弹出页面托管客户详细信息用户控件。

在选项 2 中,一个页面托管客户列表用户控件和客户详细信息用户控件。选择客户时,隐藏客户列表用户控件并显示客户详细信息用户控件。

在选项 3 中,一个页面托管客户列表用户控件和客户详细信息用户控件。选择客户时,显示客户详细信息用户控件。有时可能需要滚动屏幕到客户详细信息区域的顶部。

再次的好消息是,无论屏幕布局或窗口排列如何变化,后台的数据模型实际上是相同的。这是一个客户列表和一个选定的客户。

我们需要一个类来保存客户列表和一个选定的客户。如果由于用户输入搜索文本而导致客户列表发生更改,它将发送一个“客户列表已更改”的事件。如果用户选择了一个客户,它将发送一个“选定客户已更改”的事件。

用户控件连接到此类并拉取客户列表或选定的客户以数据绑定到 UI 元素,例如网格视图或表单视图。

用户控件还监听此类发出的事件。如果客户列表已更改,则重新绑定网格视图。或者,如果选定的客户已更改,则重新绑定表单视图。

很自然地,Presentation Model 模式就派上用场了。上面描述的类正是 Presentation Model,我们将其命名为 `CustomerPresentationModel`。

`CustomerPresentationModel` 类具有用于保存客户列表和选定客户数据的属性。

public interface ICustomerPresentation : INotifyPropertyChanged
{
    IEnumerable<Customer> Items { get; set; }
    Customer SelectedItem { get; set; }
    int ItemCount { get; set; }
}

为了发送事件,我们可以定义一个自定义事件处理程序。但是,由于 Windows Forms 数据绑定和 WPF 数据绑定依赖于 `INotifyPropertyChanged` 接口,因此我们使用此 `INotifyPropertyChanged` 接口

`CustomerPresentationModel` 类是通用 Presentation Model 类 `PresentationModel<T>` 的子类。

public class CustomerPresentationModel :
    PresentationModel<Customer>, ICustomerPresentation
{
    //...
}

public abstract class PresentationModel<T> : INotifyPropertyChanged
{
    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;
    protected void NotifyPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    #endregion

    protected int itemCount;
    public int ItemCount
    {
        get { return itemCount; }
        set { itemCount = value; NotifyPropertyChanged("ItemCount"); }
    }

    protected IEnumerable items;
    public virtual IEnumerable Items
    {
        get { return GetItems(); }
        set { items = value; NotifyPropertyChanged("Items"); }
    }

    protected abstract IEnumerable GetItems();

    private T selectedItem;
    public virtual T SelectedItem
    {
        get { return selectedItem; }
        set { selectedItem = value; NotifyPropertyChanged("SelectedItem"); }
    }

    protected void Reset()
    {
        Items = null;
        SelectedItem = default(T);
    }
}

`PresentationModel<T>` 类是使用泛型的基类。有了这个类,我们可以将 Presentation Model 设计模式应用于将来各种业务实体,例如订单、产品等。

现在,我们将只关注 `CustomerPresentationModel` 及其用于构建三种类型应用程序的使用。

构建 ASP.NET 网站

Option3.JPG

构建 ASP.NET 网站的步骤如下:

p1.JPG

  • 创建用户控件 CustomerList.ascxCustomerEdit.ascx
  • 创建 `CustomPresentationModel` 的子类
  • 数据绑定到 `CustomPresentationModel`
  • 创建 Web 窗体来托管用户控件
    • Customer1.aspx 托管 CustomerList.ascx
    • Customer2.aspx 托管 CustomerList.ascxCustomerEdit.ascx
    • Customer3.aspx 托管 CustomerList.ascxCustomerEdit.ascx
    • CustomerDetails.aspx 托管 CustomerEdit.ascx

这些步骤都很直接,但有一些值得注意的地方。

ASP.NET 网站可以是无状态的,这意味着对象通常在每次请求服务时创建和销毁。在这种情况下,由于 Presentation Model 被多个用户控件访问,因此它应该在请求开始时创建,并在请求结束前一直存在。但是,虽然 ASP.NET Framework 提供了应用程序范围的存储和会话存储来存储自定义对象,但与 JSP 不同,它没有请求范围的存储。因此,我们将 `CustomerPresentationModel` 存储在会话中。

public static CustomerPresentationModel Instance
{
    get
    {
        CustomerPresentationModel instance = HttpContext.Current.Session["_c_"]
        as CustomerPresentationModel;
        if (instance == null)
        {
            instance = new CustomerPresentationModel();
            HttpContext.Current.Session["_c_"] = instance;
        }
        return instance;
    }
}

数据绑定到 Presentation Model 实例

ASP.NET 具有出色的声明性数据绑定模型,可以绑定到普通 .NET 对象。这是通过 ASP.NET `ObjectDataSource` 控件完成的,该控件将数据绑定控件(如 `GridView`、`FormView` 或 `DetailsView` 控件)连接到对象。

<asp:GridView .... DataSourceID="CustomerListDataSource" DataKeyNames="Id">
    <Columns>
    ...
    </Columns>
</asp:GridView>

<asp:ObjectDataSource ... DataObjectTypeName="Demo.DataModel.Customer"
TypeName="CustomerPresentationModel" SelectMethod="GetCustomerList"
OnObjectCreating="CustomerListDataSource_ObjectCreating" />

`ObjectDataSource` 暴露一个 `TypeName` 属性,该属性指定用于执行数据操作的对象类型(类名)。在我们的例子中,它是 `CustomerPresentationModel`。

默认情况下,`ObjectDataSource` 将通过默认构造函数(无参数)实例化 `CustomerPresentationModel` 实例,但我们已经在会话中有了 `CustomerPresentationModel` 对象。幸运的是,我们可以处理 `ObjectCreating` 事件,将 `CustomerPresentationModel` 对象从会话分配给 `ObjectDataSource` 的 `ObjectInstance` 属性。

protected void CustomerDataSource_ObjectCreating
        (object sender, ObjectDataSourceEventArgs e)
{
    e.ObjectInstance = CustomerPresentationModel.Instance;
}

当数据绑定控件需要数据时,`ObjectDataSource` 会调用 `CustomerPresentationModel` 实例的 `GetCustomerList` 方法(在 `ObjectDataSource` 的 `SelectMethod` 属性中声明)。该方法应返回任何 `Object` 或 `IEnumerable` 列表、集合或数组。

`CustomerPresentationModel` 以属性而非方法的形式暴露客户列表和选定的客户,因此这里我们需要一些包装才能与 `ObjectDataSource` 一起使用。

public IEnumerable<Customer> GetCustomerList()
{
    return Items;
}

public Customer GetCustomer()
{
    return SelectedItem;
}

监听事件

用户控件监听 Presentation Model 的事件。客户列表用户控件监视“项目已更改”事件以重新绑定网格视图。客户详细信息用户控件同样监视“选定项目已更改”事件。这不仅是为了刷新表单视图,也是为了在 `CustomerPresentationModel` 中选定的项目为 `null` 时隐藏自身。

CustomerEdit.ascx.cs 中,

void pm_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName == "SelectedItem")
    {
        if (pm.GetCustomer() != null)
        {
            CustomerFormView.DataBind();
            Visible = true;
        }
        else
        {
            Visible = false;
        }
    }
}

事件驱动使得选项 2 和选项 3 非常容易实现。在选项 2 中,逻辑是如果没有选择客户,则显示列表并隐藏详细信息。在选项 3 中,不需要额外的逻辑。

当用户搜索客户列表时,`CustomerPresentationModel` 发出“项目已更改”事件,通知客户列表用户控件相应地刷新。

Customer2.aspx.cs 中,

void pm_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName == "SelectedItem")
    {
        this.CustomerList1.Visible = pm.GetCustomer() == null;
    }
}

这种事件驱动机制的好处是,可能有多个用户控件需要通知和刷新。它还提供了一种同步不同用户控件状态的解决方案。

需要依赖注入框架

查看 CustomerList.ascx.csCustomerEdit.ascx.cs 的代码,您会发现重复的代码,例如:

CustomerPresentationModel pm;

protected void Page_Load(object sender, EventArgs e)
{
    pm = CustomerPresentationModel.Instance;
    System.Diagnostics.Debug.Assert(pm != null);
    if (pm != null)
    {
        pm.PropertyChanged += new PropertyChangedEventHandler(pm_PropertyChanged);
    }
}

protected void Page_Unload(object sender, EventArgs e)
{
    System.Diagnostics.Debug.Assert(pm != null);
    if (pm != null)
    {
        pm.PropertyChanged -= new PropertyChangedEventHandler(pm_PropertyChanged);
    }
}

void pm_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
}

如果有一个 Dependency Injection 框架,那么上面的代码可以简化为类似这样的内容:

[PresentationMode(Scope= Session)]
CustomerPresentationModel pm;

[PropertyChangedEventSubscription]
void pm_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
}

我正在寻找一个可以用于 Web 应用程序的轻量级依赖注入框架。

Windows Forms 应用程序

WindowsFormsApp.JPG

我们已经创建了 ASP.NET 网站。现在是时候创建一个 Windows Forms 应用程序了。

在 ASP.NET 网站中使用的步骤也适用于此处,包括:

p2.JPG

  • 创建用户控件 CustomerList.csCustomerEdit.cs
  • 创建 `CustomPresentationModel` 的子类并使其成为单例
  • 数据绑定到 `CustomPresentationModel`

在 Windows Forms 应用程序中,`CustomPresentationModel` 可以作为单例存在。通过在 Visual Studio 中创建 `BindingSource`,数据绑定也非常方便。

但重要的是要记住,Windows Forms 数据绑定是由更改通知驱动的。这意味着只有当数据源通知数据已更改(通过提供通知事件)时,Windows Forms 才会更新用户界面元素。

在我们的例子中,数据源是 `CustomPresentationModel` 实例。当客户列表重新加载时,它会发出两个 `PropertyChanged` 事件。一个属性名更改为 `Items`,另一个属性名更改为 `ItemCount`。

绑定到 `ItemCount` 属性的标签自动更新,但网格似乎没有响应 `Items` 更改事件。为什么?

对于简单的属性到属性绑定,数据源需要通过提供“PropertyName 更改事件”或实现 `INotifyPropertyChanged` 接口来提供属性更改通知。

当数据源是列表时,数据源需要提供列表更改通知,该通知用于在列表中的项被添加、删除或删除时通知用户界面元素,通常通过 `IBindingList` 接口

也就是说,要与 Windows Forms 数据绑定完全集成,规则是:在业务实体类中实现 `INotifyPropertyChanged` 接口,并为业务实体集合使用 `IBindingList` 接口。

我喜欢数据绑定,但犹豫是否要用它来污染我的实体和实体集合。我宁愿使用匿名方法通过代码刷新网格视图。

private void CustomerList_Load(object sender, EventArgs e)
{
    customerPresentationModelBindingSource.DataSource =
        CustomerPresentationModel.Instance;
    customerBindingSource.DataSource =
        CustomerPresentationModel.Instance.Items;

    CustomerPresentationModel.Instance.PropertyChanged +=
        delegate(object s, PropertyChangedEventArgs ev)
    {
       if (ev.PropertyName == "Items")
              customerBindingSource.DataSource =
              CustomerPresentationModel.Instance.Items;
        };
    }
}

Composite UI Application Block 中,有作为依赖注入容器的 Work Items,以及提供多对多、松耦合事件系统机制的 Event Broker。这个应用程序块是使用 Presentation Model 模式构建应用程序的理想平台。

WPF 应用程序

WPFApp2.JPG

正如预期的那样,步骤与 ASP.NET 网站和 Windows Forms 应用程序中所使用的相同。

p3.JPG
  • 创建用户控件 CustomerList.xamlCustomerEdit.xaml
  • 创建 `CustomPresentationModel` 的子类并使其成为单例
  • 数据绑定到 `CustomPresentationModel`

在这里,`CustomPresentationModel` 像在 Windows Forms 应用程序中一样,以单例的形式存在。

绑定到客户列表的列表视图会随着 `Items` 更改事件而更新。除此之外,WPF 数据绑定还有其他一些很棒的功能。

在 WPF 中,我们可以直接连接到对象,而无需像 ASP.NET 中的 `ObjectDataSource` 和 Windows Forms 中的 `BindingSource` 这样的辅助工具。通常,我定义一个 `static` 资源来引用 `CustomPresentationModel` 实例。

<UserControl x:Class="Demo.WpfApp.CustomerEdit"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Width="550"
    xmlns:local="clr-namespace:Demo.WpfApp">

<UserControl.Resources>
    <ObjectDataProvider x:Key="controller"
        ObjectType="{x:Type local:CustomerPresentationModel}"
    MethodName="get_Instance" />
</UserControl.Resources>

还可以将数据绑定到 UI 元素的 `DataContext` 属性,并允许子元素继承数据源信息,从而简化绑定语法。这在 CustomerEdit.asmx 用户控件中得到了很好的演示,其中顶层网格的 `DataContext` 被绑定到选定的客户对象。网格内的元素随后仅使用路径绑定到客户对象的属性。

<UserControl x:Class="Demo.WpfApp.CustomerEdit"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Width="550"
    xmlns:local="clr-namespace:Demo.WpfApp">

<UserControl.Resources>
    <ObjectDataProvider x:Key="controller"
        ObjectType="{x:Type local:CustomerPresentationModel}"
    MethodName="get_Instance" />
    <SolidColorBrush x:Key="LabelForegroundBrush" Color="#FFFFFFFF"/>
</UserControl.Resources>

<Grid DataContext="{Binding Source={StaticResource controller},
        Path=SelectedItem}" Background="#FF595959">
<Grid.ColumnDefinitions>
    <ColumnDefinition Width="Auto" />
    <ColumnDefinition Width="Auto" />
    <ColumnDefinition Width="Auto" />
    <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>

<Grid.RowDefinitions>
    <RowDefinition Height="Auto" />
    <RowDefinition Height="Auto" />
    <RowDefinition Height="*" />
</Grid.RowDefinitions>

<Label Grid.Row="0" Foreground="{DynamicResource LabelForegroundBrush}">
        First Name</Label>
<TextBox Grid.Row="0" Grid.Column="1" Width="150"
         HorizontalAlignment="Left" VerticalAlignment="Center"
         Text="{Binding Path=FirstName}">
</TextBox>

<Label Grid.Row="0" Grid.Column="2"
        Foreground="{DynamicResource LabelForegroundBrush}">Last Name</Label>
<TextBox Grid.Row="0" Grid.Column="3" Width="150"
         HorizontalAlignment="Left" VerticalAlignment="Center"
         Text="{Binding Path=LastName}"/>

<Label Grid.Row="1" Foreground="{DynamicResource LabelForegroundBrush}">Address</Label>
<TextBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="3"
         HorizontalAlignment="Stretch" VerticalAlignment="Center"
         Text="{Binding Path=Address}"/>

<Label Grid.Row="2" Foreground="{DynamicResource LabelForegroundBrush}">Comments</Label>
<TextBox Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="3"
         HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
         MinHeight="50"
         Text="{Binding Path=Comments}" TextWrapping="Wrap" />
</Grid>

</UserControl>

关于 WPF 的一些思考。WPF 旨在带来应用程序的“差异化用户体验”或“差异化 UI”。在我们的例子中,它可能是在“选定项”更改事件发生时,在应用程序的底部区域激活模拟 3D 效果或翻页效果的 storyboard 或 timeline。

差异化 UI 对 LOB(线业务)应用程序意味着什么?可能不仅仅是翻页。我正在等待看到 WPF Composite Client Application Block 将如何定义它。

评估

到目前为止,我们已经展示了如何在 ASP.NET 网站、Windows Forms 应用程序和 WPF 应用程序中使用 Presentation Model。尽管这里使用的 `Customer` 类是虚构的,但网站 UI 选项的设计实践实际上是一个真实的故事。直到后来找到以下参考文献,我们才意识到 Presentation Model 正在发挥作用:

看起来,由于 Presentation Model 起源于 Smalltalk、Java 世界、FLEX 世界以及 .NET 世界,它们都在尝试和使用这种模式。这鼓励我们继续在这条道路上进行探索。

Microsoft Web Client Software FactoryMicrosoft Smart Client Software Factory 使用 MVP 模式。有几点不如 Presentation Model 好。例如,每个视图,通常是一个用户控件,都需要一个 Presenter 类添加到现有的代码隐藏类/代码旁边类中。我觉得这增加了代码维护的难度。不如上面展示的 Presentation Model 清晰。

Microsoft Smart Client Software Factory 基于 Composite UI Application Block (CAB),它提供了许多出色的功能,例如依赖注入 (IoC) 框架、事件代理。使用 CAB 和 Presentation Model 构建应用程序既简单又有趣。

在 WPF 界,WPF 团队一直在推广 Model-View-ViewModel 模式。本质上,Model-View-ViewModel 就是 Presentation Model。它们是同一事物的两个标签。

在即将推出的 WPF Composite Client Application Block 中,看到 Microsoft 的 Patterns & Practices 团队将如何使用 Presentation Model 模式或 MVP 将非常有趣。依赖注入将如何工作?如何将 WPF 命令/事件处理扩展到 AOP 风格?

结论

正如本文所示,Presentation Model 被证明是一种有效的实践模式。尽管 ASP.NET、Windows Forms 和 WPF 是完全不同的技术,但它们可以共享同一个 Presentation Model 类。该模式带来了解耦数据模型和表示层的价值。它还可以与许多 .NET 技术集成并利用它们,例如用户控件和数据绑定。

希望您在阅读本文后开始喜欢 Presentation Model,如果您还没有。为了使其更具说服力,我还有另一篇文章展示了如何在 SharePoint 中使用 Presentation Model

历史

© . All rights reserved.