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

将 MVC 提升到 .NET 的新水平

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.62/5 (11投票s)

2013 年 3 月 30 日

GPL3

15分钟阅读

viewsIcon

74457

downloadIcon

907

如何使用强大的 Xomega Framework 和最佳 MVVM 原则,快速构建可重用且灵活的 WPF、Silverlight 或 ASP.NET 应用程序。

介绍 

模型-视图-视图模型 (MVVM) 是一种设计模式,它是 展示模型 模式的一种变体,该模式最初由 Martin Fowler 描述。自 Microsoft 在 WPF 的第一个版本中引入 MVVM 以来,MVVM 在 WPF 和 Silverlight 开发者中越来越受欢迎,现在有许多 MVVM 框架可供选择。

与先前的设计模式 MVC(模型-视图-控制器)和 MVP(模型-视图-演示器)一样,MVVM 模式的关键原则之一是关注点分离 (SoC),它允许清晰地分离应用程序的数据、展示和行为方面。

在本文中,我们将展示 Xomega Framework 如何将 MVC 和 MVVM 的基本原则提升到新的水平,使您能够以极低的学习曲线,快速构建高度灵活和可重用的 .Net 应用程序。请继续阅读,了解如何使用 Xomega Framework 快速轻松地开发应用程序。

关于 Xomega Framework 

Xomega Framework 是一个强大的开源框架,它建立在超过 10 年的经验之上,并基于在许多大型应用程序中已得到证明的方法。该框架的主要目标是

  • 实现快速应用程序开发。
  • 促进重用,从而显著降低维护成本。
  • 确保一致性,以提供最佳用户体验。

为了有效地演示 Xomega Framework 旨在解决的一个问题,让我们考虑以下示例。假设您的应用程序中有一个常见的字段“客户”,它几乎出现在您应用程序的每个屏幕上。该字段的要求如下。

  1. 如果客户在该屏幕上不可编辑,则应显示为 [客户编号] – [客户姓名],其中客户编号是客户的整数键,应格式化为六位数字字符串,前面填充零,例如 001234 – Joe Doe
  2. 在客户可编辑的屏幕中,应从下拉列表中选择客户,其中每个项目都显示为客户编号 – 客户姓名,如上一个要求所示。如果需要选择多个客户(例如用于搜索条件),则使用列表框。
  3. 在某些屏幕中,当高级用户能熟记客户编号并需要快速输入客户时,将使用文本框,该文本框接受不带前导零的整数客户编号。如果需要指定多个客户,则在文本框中接受逗号分隔的客户编号列表。
  4. 客户字段仅在用户具有“查看客户”特权时才可见。
  5. 客户字段仅在用户具有“编辑客户”特权时才可编辑。

xomfwk/CustomerExample.png

在典型的 WPF 应用程序中,您可能会定义多个可重用方法和类来检索客户列表,验证单次输入和多次输入的输入,并格式化要在屏幕上显示的输出。然后,对于每个屏幕,您都需要为客户字段设置绑定以使用这些方法和类,或者以其他方式将其连接起来。

软件开发的基本原则之一是 DRY:Don’t Repeat Yourself(不要重复自己)。与其在每个地方都连接字段,不如创建一个封装所有此类行为的单一可重用属性,您可以轻松地将其绑定到各种 UI 控件:只读标签、下拉列表、文本框、列表框等?然后,您可以轻松地将此类属性添加到您视图模型的数据对象中,这样您就可以在极短的时间内构建您的展示模型。

请继续阅读,了解 Xomega Framework 如何让实现此类需求变得轻而易举。

Xomega 数据属性

Xomega Framework 对展示模型支持的核心是数据属性的概念,它可以存储任何类型的值——字符串、基元、带时间的日期或带有附加属性的复杂对象,Xomega 还为此提供了一个方便的通用类 Header。每个属性都可以提供将值转换为不同格式及其与不同格式之间转换的方法。默认情况下,Xomega 定义了以下格式

  1. Internal 格式用于在内部存储值,通常是基元、日期/时间等。当您设置属性值时,它将始终尝试将其转换为内部格式。如果无法将值转换为内部格式,它将按原样存储该值(通常是字符串),以免丢失输入,但该属性将验证失败。
  2. DisplayString 格式用于在属性不可编辑时将值作为字符串显示给用户。这可以是格式化的数字或日期/时间、带有货币符号的金额,或者用于显示的任何其他值表示形式。
  3. EditString 格式用于在用户在文本框中编辑值时将值表示为字符串。此格式旨在方便输入,默认情况下与 DisplayString 相同。
  4. Transport 格式用于将内部值转换为发送到服务层并最终存储在数据库中的值。例如,当内部值为一个存储了数字 ID 和显示文本的对象时,传输值将是该数字 ID。

Xomega Framework 的一个关键区别在于其对多值属性的集成支持。您只需将属性的 IsMultiValued 标志设置为 true,该属性将对每个单独的值应用相同的转换和验证规则,并且还可以将值列表显示为逗号(或其他分隔符)分隔的字符串,以及从该字符串解析值。绑定到多值属性与绑定到单值属性相同。因此,例如,如果您允许按单个值进行搜索,那么您可以轻松地使搜索条件字段成为多值字段。

Xomega Framework 最值得注意的特性是它将属性的状态(例如属性是否可编辑、是否必需、是否可见等)直接存储在属性本身中。将属性绑定到控件不仅会将控件值绑定到属性值,还会将控件状态绑定到属性状态,而无需额外工作。它还允许您以清晰且可重用的方式在展示模型中编写行为逻辑。例如,您的视图模型可能会监听一个属性值的变化,并因此使其他属性成为必需的,或更新它们的编辑性或可见性。

在典型的 MVVM 实现中,您需要在视图模型上定义额外的标志来存储属性是否可见或可编辑,并手动将控件的可见性和编辑性绑定到这些标志。

如果您尝试构建标准的 MVVM 应用程序,并且厌倦了在视图模型中为实现 INotifyPropertyChangeIDataErrorInfo 接口而进行的繁琐工作(这些接口分别由标准的 WPF 双向绑定和验证所必需),那么您一定会欣赏 Xomega Framework 避免了所有这些工作。

每当属性发生任何变化时(无论是值还是任何元数据),Xomega 数据属性都会触发自定义属性更改事件,您可以监听该事件,然后在视图模型中执行任何行为逻辑。该事件有一个 PropertyChange 属性,指示属性的哪些属性已更改,因此无需订阅多个事件。

绑定到数据属性的控件会自动监听属性更改事件并相应地更新其状态。如果属性不可编辑,则相应的控件将被禁用或设为只读。同样,如果属性不可见(例如由于安全限制),则绑定的控件将与关联的标签一起隐藏。类似地,绑定到必需属性的控件可以通过加粗标签等方式自动突出显示。

数据属性还可以实现一个返回属性可能值列表的方法。如果将选择列表控件(如下拉列表或列表框)绑定到这样的数据属性,则选择项列表将自动填充并与属性的可能值列表保持同步。对于像下拉列表这样不允许清除选中项的控件,如果该属性不是必需的,则会添加一个特殊的空白项。您还可以配置您的属性以在值为空时显示特定文本,例如 <无><无客户> 等。

正如我们之前提到的,您无需费心在视图模型中实现 IDataErrorInfo 接口来验证单个属性。您在一个属性中编写的(或者很多时候根本不需要编写,而是从基类继承)验证逻辑将自动应用于该属性被使用的任何地方,从而实现 Don’t Repeat Yourself。

xomfwk/ErrorField.png

当数据属性无效时,绑定的控件将被突出显示并显示验证错误消息。与其他生成“无效数字”之类的通用错误消息且不提及属性名称的可重用验证函数(如果未显示在相应字段旁边,则几乎无用)不同,Xomega 数据属性生成的错误消息包含属性名称,甚至包含屏幕上的实际标签。这不仅允许您在相应字段上显示错误消息,还可以在屏幕的某个位置或弹出对话框中输出错误消息摘要,用户将能够识别需要更正的字段。

xomfwk/ErrorSummary.png

回到我们之前提到的客户属性示例,该属性实现如下。

using System;
using System.Collections.Generic;
using System.ServiceModel;
using Xomega.Framework;
using Xomega.Framework.Lookup;
using Xomega.Framework.Properties;

public partial class CustomerProperty : EnumIntProperty 
{
    /// <summary>
    /// A constant used for the cached lookup table name
    /// </summary>
    public const string EnumTypeCustomer = "customer";

    /// <summary>
    /// Additional attribute for each value to store the formatted id
    /// </summary>
    public const string AttributeFormattedCustomerId = "formatted customer id";

    public CustomerProperty(DataObject parent, string name)
        : base(parent, name)
    {
        EnumType = EnumTypeCustomer;
            
        // display value as 001234 - Joe Doe
        DisplayFormat = String.Format("{0} - {1}",
            string.Format(Header.AttrPattern, AttributeFormattedCustomerId),
            Header.FieldText);

        // add custom validation rule
        Validator += ValidateCustomer;

        bool canUserViewCustomers = true; // TODO: retrieve View Customer privilege
        bool canUserEditCustomers = true; // TODO: retrieve Edit Customer privilege
        AccessLevel = canUserEditCustomers ? AccessLevel.Full :
            canUserViewCustomers ? AccessLevel.ReadOnly : AccessLevel.None;
    }

    // override to fine-tune the value conversion rules
    protected override object ConvertValue(object value, ValueFormat format)
    {
        int id;
        if (format == ValueFormat.Internal && Int32.TryParse("" + value, out id))
        {
            // try parsing the id to remove leading zeros, etc.
            return base.ConvertValue(id, format);
        }
        return base.ConvertValue(value, format);
    }

    // checks if the value has been resolved to a valid customer in the lookup table
    public static void ValidateCustomer(DataProperty dp, object value)
    {
        Header cust = value as Header;
        if (value != null && (cust == null || cust.Type != EnumTypeCustomer))
            dp.ValidationErrors.AddError("{0} has an invalid customer {1}.", dp, value);
    }

    // Returns possible customer values from the lookup cache.
    // If the values are not yet in the cache, reads them
    // from service layer and stores in the cache.
    protected override LookupTable GetLookupTable()
    {
        LookupTable res = base.GetLookupTable();
        if (res == null)
        {
            List<Header> data = new List<Header>();
            foreach (Store_ReadListOutput row in ReadList())
            {
                Header h = new Header(EnumType, "" + row.CustomerId, row.Name);
                // store the formatted id as a separate attribute
                h[AttributeFormattedCustomerId] = row.CustomerId.ToString("000000");
                data.Add(h);
            }
            res = new LookupTable(EnumType, data, true);
            LookupCache cache = LookupCache.Get(CacheType);
            if (cache != null) cache.CacheLookupTable(res);
        }
        return res;
    }

    // reads the possible customer values from the service layer
    protected virtual IEnumerable<Store_ReadListOutput> ReadList()
    {
        ChannelFactory<IStoreService> factory = new ChannelFactory<IStoreService>("IStoreService");
        IStoreService svc = factory.CreateChannel();
        IEnumerable<Store_ReadListOutput> output = svc.ReadList(new Store_ReadListInput());
        factory.Close();

        return output;
    }
}

Xomega 数据对象作为视图模型

Xomega Framework 提供了一套丰富的基类属性,它们实现了大多数常见类型的验证和值转换。如您所见,您还可以定义自己的通用或应用程序特定的 Xomega 数据属性,它们功能强大且高度可重用。

一旦您拥有了应用程序使用的数据属性集,构建视图模型就变得轻而易举。您只需要创建一个 Xomega DataObject 的子类,在数据对象初始化期间简单地创建和配置命名属性实例。我们强烈建议您还为您的属性名称定义字符串常量,您应该使用这些常量来引用属性。这将使重构更加容易,并且还允许在 XAML 中进行一些编译时检查(我们将在下面进行描述)。我们也建议您提供公共的属性 getter 以方便使用。以下是一个这样的数据对象的示例

using System;
using Xomega.Framework;
using Xomega.Framework.Properties;

public partial class StoreCriteria : DataObject
{
    #region Constants

    public const string CreatedDateFrom = "CreatedDateFrom";
    public const string CreatedDateTo = "CreatedDateTo";
    public const string Customer = "Customer";

    #endregion
        
    #region Properties

    public DateProperty CreatedDateFromProperty { get; private set; }
    public DateProperty CreatedDateToProperty { get; private set; }
    public CustomerProperty CustomerProperty { get; private set; }

    #endregion

    #region Construction

    protected override void Initialize()
    {
        CreatedDateFromProperty = new DateProperty(this, CreatedDateFrom);
        CreatedDateToProperty = new DateProperty(this, CreatedDateTo);
        CustomerProperty = new CustomerProperty(this, Customer);
        CustomerProperty.Required = true;
        CustomerProperty.IsMultiValued = true;
    }

    #endregion
}

Xomega Framework 支持在父数据对象中嵌套子对象和对象列表。这将允许您构建复杂视图的视图模型,并更好地控制视图行为,如下面所示。

与数据属性一样,Xomega 数据对象具有一个 Editable 标志,该标志控制数据对象是否可编辑。当它不可编辑时,其所有属性和子对象都将不可编辑,这将自动使绑定到它们的所有控件禁用或只读。如果用户可以打开您的表单进行查看,并且需要单击“编辑”来启用控件,那么您所要做的就是将底层数据对象的 editable 标志设置为 true。您不再需要编写冗长的方法来启用或禁用视图中的所有控件。

数据对象的另一个有用功能是支持修改跟踪。通常,您需要编写大量自定义逻辑来检测用户是否进行了任何更改,以便在用户关闭视图时提示未保存的更改。使用 Xomega 数据对象,这会自动发生,您只需检查 DataObjectModified 标志即可。

正如我们之前提到的,每个数据属性都封装了验证其值的逻辑。但是,如果您需要执行任何跨字段验证,例如确保开始日期不晚于结束日期,那么您可以在相应的数据对象中这样做

// overridden to perform cross-field validation
public override void Validate(bool force)
{
    base.Validate(force);

    if (CreatedDateFromProperty.Value != null && CreatedDateToProperty.Value != null &&
        CreatedDateToProperty.Value < CreatedDateFromProperty.Value)
    {
        validationErrorList.AddError("{0} cannot be later than {1}",
            CreatedDateFromProperty, CreatedDateToProperty);
    }
}

在调用数据对象的 Validate 方法(这将验证所有属性和嵌套的子对象)后,您将能够调用 GetValidationErrors 方法来获取验证期间生成的错误列表。如果列表包含任何错误,则可以显示给用户并终止保存操作,如下所示。

criteria.Validate(true);
ErrorList valErr = criteria.GetValidationErrors();
if (valErr.HasErrors())
{
    Errors.Show(valErr);
    return;
}

您可能知道,标准的 MVVM 模式在很大程度上仅限于 WPF 和 Silverlight 开发。Xomega 视图模型数据对象的出色之处在于,您可以以平台无关的方式在这些对象中定义展示逻辑,然后轻松地将其绑定到**任何**视图:WPF、Silverlight、ASP.NET 或 WinForms(如果需要)。请继续阅读,了解 Xomega Framework 如何支持绑定到前三种类型的视图。

绑定到 WPF、Silverlight 和 ASP.NET 视图

WPF 和 Silverlight 中 XAML 的引入使得声明式地定义视图变得更加容易。理想情况下,您希望让经验丰富的 UI 设计师在表单上布局和样式化您的控件,并且让经验丰富的开发人员编写您可以轻松与视图连接的视图模型。不幸的是,即使使用标准的 WPF 和 Silverlight,您仍然需要付出大量努力才能将视图与底层数据模型连接起来。您需要了解值转换器和验证器,将控件状态(如 Enabled 属性)绑定到相应的视图模型标志,并将选择项列表绑定到适当的值列表。

Xomega Framework 使其变得如此简单,您只需要指定要绑定控件的属性名称,并将您的数据对象设置为控件的 DataContext,其余的它都会处理。以下是一个如何在 WPF 中将客户列表框绑定到客户属性的示例

<Label Name="lblCustomer">Customer:</Label>
<ListBox xmlns:xom="clr-namespace:Xomega.Framework;assembly=Xomega.Framework"
         xmlns:l="clr-namespace:MyProject.Objects;assembly=MyProject.Objects"
         xom:Property.Name="{x:Static l:StoreCriteria.Customer}"
         xom:Property.Label="{Binding ElementName=lblCustomer}"
         Name="ctlCustomer"/>

将文本框或组合框绑定到客户属性的语法完全相同,这使得在属性的控件类型之间切换变得极其简单。

请注意,我们使用了我们定义的静态常量 StoreCriteria.Customer 作为属性名。这将确保如果我们重构视图模型以重命名属性,只要我们更改常量的值,应用程序仍然可以工作,并且如果我们实际更改了重构期间的常量名称,我们将得到一个编译时错误。请注意,如果您使用标准的 WPF 绑定路径,则在更改属性名称时不会收到编译错误,而是运行时错误。

在 XAML 中绑定到 Xomega 数据属性在 Silverlight 中的情况与 WPF 几乎相同,但您无法使用常量,因为 Silverlight 不支持 x:Static 构造。

<TextBlock Name="lblCustomer" Text="Customer:" />
<ListBox xmlns:xom="clr-namespace:Xomega.Framework;assembly=Xomega.Framework"
         xmlns:l="clr-namespace:MyProject.Objects;assembly=MyProject.Objects"
         xom:Property.Name="Customer"
         xom:Property.Label="{Binding ElementName=lblCustomer}"
         Name="ctlCustomer"/>

您可能已经注意到,Xomega Framework 允许您为每个属性绑定的控件关联一个标签,这有助于框架同时显示和隐藏控件和标签,并使用该标签的文本作为错误消息中的属性名称,以便用户在查看错误摘要时可以轻松识别出错的字段。它还允许您通过将相应标签加粗来突出显示必需字段。您可以通过定义全局标签样式来实现这一点。

<Style x:Key="LabelStyle" TargetType="{x:Type Label}"
       xmlns:xom="clr-namespace:Xomega.Framework;assembly=Xomega.Framework">
  <Style.Triggers>
    <Trigger Property="xom:Property.Required" Value="True">
        <Setter Property="FontWeight" Value="Bold"/>
    </Trigger>
  </Style.Triggers>
</Style>

将 ASP.NET 控件绑定到 Xomega 数据属性同样简单——您只需指定属性名,您可以使用对象中定义的常量。以下是相应的绑定外观

<asp:Label runat="server" ID="lblCustomer" Text="Customer:" />
<asp:ListBox runat="server" ID="ctlCustomer" LabelID="lblCustomer"
             Property="<%# StoreCriteria.Customer %>"  />

要轻松地将整个包含面板绑定到您的数据对象,您可以调用以下实用方法。

WebUtil.BindToObject(pnlCriteria, criteria); 

Xomega 中的任何属性级别验证错误都已集成到 WPF 验证机制中。因此,如果您想定义一个自定义验证模板,在无效字段旁边显示一个红色的星号和错误消息作为工具提示,您可以通过标准方式实现,如下所示。

<ControlTemplate x:Key="validationTemplate">
  <DockPanel>
    <TextBlock Foreground="Red" Text="*" FontSize="18" FontWeight="Bold"
               ToolTip="{Binding ElementName=adornerPlaceholder,
        Path=AdornedElement.(xom:Property.Validation).Errors.ErrorsText}" />
    <AdornedElementPlaceholder Name="adornerPlaceholder"/>
  </DockPanel>
</ControlTemplate>

在 WPF 中绑定数据网格也非常相似。您需要将数据对象列表设置为网格的 DataContext,并为每列中的单元格指定数据模板,如下所示。

<ListView Grid.Row="2" Name="gridResults">
  <ListView.View>
    <GridView>
        <GridView.Columns>
            <GridViewColumn Header="Customer Id">
              <GridViewColumn.CellTemplate>
                <DataTemplate>
                  <TextBlock xom:Property.Name="{x:Static l:CustomerObject.CustomerId}"/>
                </DataTemplate>
              </GridViewColumn.CellTemplate>
            </GridViewColumn>
            <GridViewColumn Header="Name">
              <GridViewColumn.CellTemplate>
                <DataTemplate>
                  <TextBlock xom:Property.Name="{x:Static l:CustomerObject.Name}"/>
                </DataTemplate>
              </GridViewColumn.CellTemplate>
            </GridViewColumn>
        </GridView.Columns>
    </GridView>
  </ListView.View>
</ListView>

这同样适用于 ASP.NET 网格,只是您将使用 Xomega 实用方法 WebUtil.BindToObject 来绑定数据对象列表。

<asp:GridView runat="server" ID="grdResults">
  <Columns>
    <asp:TemplateField HeaderText="Customer Id">
      <ItemTemplate>
        <asp:HyperLink runat="server" ID="fldCustomerId"
                       Property="<%# CustomerObject.CustomerId %>"
                       NavigateUrl="~/CustomerDetailsPage.aspx?CustomerId={value}" />
      </ItemTemplate>
    </asp:TemplateField>
    <asp:TemplateField HeaderText="Name">
      <ItemTemplate>
        <asp:Label runat="server" ID="fldName" Property="<%# CustomerObject.Name %>"/>
      </ItemTemplate>
    </asp:TemplateField>
  </Columns>
</asp:GridView>

如何开始使用 Xomega Framework

如果您想试用 Xomega Framework,看看它如何轻松构建 .Net 应用程序,以下是您可以尝试的几种方法

  • CodePlex 下载最新版本的 Xomega Framework。它包含一个基于 Microsoft AdventureWorks 数据库的示例应用程序,演示了框架的功能。
  • 如果您使用 NuGet,您也可以通过包管理器将 Xomega Framework 添加到您的项目中。只需搜索 Xomega 的包即可。

如果您喜欢这篇文章,或者有任何问题,或者想分享您使用该框架的经验,请投票并留言。

额外资源

历史

  • 2011/11/29:本文初版发布。
  • 2011/12/18:上传了包含当前示例的项目。 
© . All rights reserved.