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

Xomega 框架中的计算属性

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2020年12月7日

CPOL

9分钟阅读

viewsIcon

5665

如何编写多平台 UI 框架无关的表示逻辑

引言

在本文中,您将熟悉 Xomega Framework,并将了解如何为您的应用程序编写可测试且可重用的多平台表示逻辑。具体来说,您将学习如何使用传统的 MVVM 方法和一种新的、简单但强大的基于表达式的方法来实现计算属性。

什么是 Xomega 框架

对于不熟悉 Xomega Framework 的朋友来说,它是一个强大的框架,用于使用 MVVM 模式和清晰的层分离在 .NET 中构建 Web 和桌面应用程序。它允许您以与平台无关的方式轻松编写大部分表示逻辑,这不仅使其具有极强的可测试性,而且还允许您将其与完全不同的 UI 层(如 WPF 和 Blazor,甚至较旧的 WebForms 框架)一起使用(和重用)。

这样,您就可以在 Web、桌面甚至移动应用程序的不同 UI 技术之间共享您的表示逻辑,然后在新的 UI 框架可用时轻松升级 UI 层,从而使您的应用程序面向未来。

让我们来看看 Xomega 框架的一些主要部分。

数据对象

数据对象在 MVVM 模式中代表 ViewModel 的一部分,其中包含可观察的数据,视图可以绑定到这些数据。数据对象由特殊的数据属性以及其他子数据对象组成。

一种特殊类型的数据对象是列表对象,其中数据存储在数据行中,数据属性代表表中的列。

数据属性

与常规对象属性不同,Xomega 框架数据属性不仅封装了数据值或多个值,还封装了动态属性元数据,例如可编辑性、可见性、安全访问、属性是否必需、可能值的列表、修改状态以及值的格式、验证和转换规则。

该框架具有可扩展的数据属性层次结构,该层次结构表示具有特定转换和格式化规则的各种数据类型。

以下是 `AdventureWorks` 示例数据库中 `SalesOrderDetail` 表的数据对象及其部分属性的外观。

public class SalesOrderDetailObject : DataObject
{
    public EnumIntProperty ProductIdProperty { get; private set; }
    public SmallIntegerProperty OrderQtyProperty { get; private set; }
    public EnumIntProperty SpecialOfferIdProperty { get; private set; }
    public MoneyProperty UnitPriceProperty { get; private set; }
    public PercentFractionProperty UnitPriceDiscountProperty { get; private set; }
    public MoneyProperty LineTotalProperty { get; private set; }
}

属性更改事件

与经典的 MVVM 框架类似,Xomega 数据属性允许您订阅属性更改事件。但是,与仅通知值更改(如 `INotifyPropertyChange` 接口)不同,数据属性还可以通知您其元数据的更改,例如属性何时变得可编辑或必需,或者属性的可能值列表何时发生了更改。

一次属性更改事件可以针对数据属性的值或其任何元数据的任何更改触发,也可以针对这些更改的任何组合触发。因此,一个事件可以一次性通知您所有属性状态的变化,这使得侦听器(例如属性绑定的控件)可以同时更新其所有状态。

这也意味着,当只侦听属性值更改时,例如,您需要检查属性更改是否包括值更改,以避免不必要地执行回调。

绑定到 UI 控件

属性更改事件的主要用途之一是支持将属性绑定到 UI 控件。这将自动包括将控件的状态绑定到属性元数据,这使得此绑定非常简单易用。

一旦 UI 控件绑定到属性,您可以将属性设置为可编辑、必需或不可见,并且更改将自动反映在 UI 控件中,而无需单独绑定这些内容。

根据您使用的 UI 框架,可能有两种不同的方法可以将 UI 控件绑定到数据属性,如下所述。

控件属性绑定

对于允许您将附加属性与控件关联的 UI 框架(如 WPF 中的附加属性,或 WebForms 控件中的自定义属性),您可以在此类控件上设置数据属性名称,并通过单独的绑定将这些控件绑定到数据属性。

此方法允许您将任何自定义或第三方控件(派生自通用 UI 控件)绑定到数据属性。Xomega Framework 为 WPF 和 WebForms 控件提供了可扩展的属性绑定层次结构。

这是一个 WPF `ComboBox` 控件的示例,它使用 `static` 常量作为属性名绑定到 `ProductId` 属性,以提供编译器安全性。

<ComboBox xmlns:xom="clr-namespace:Xomega.Framework;
 assembly=Xomega.Framework.Wpf" xom:Property.Label="{Binding ElementName=lblProductId}" 
 xom:Property.Name="{x:Static o:SalesOrderDetailObject.ProductId}" />

`WebForm` 中相同的控件如下所示

<asp:DropDownList LabelID="lblProductId" 
 Property="<%# SalesOrderDetailObject.ProductId %>" AutoPostBack="true" runat="server" />

属性绑定的控件

在不支持使用单独属性绑定或存在问题的其他 UI 框架中,您可以开发和使用可以绑定到 Xomega 数据属性的自定义 UI 控件。

Xomega Framework 提供了一组通用的 Blazor 组件,您可以直接将它们绑定到数据属性。

例如,一个绑定到我们的 `SalesOrderDetailObject` 属性的 Blazor 组件视图的外观如下

<div>
    <XLabel Property="@VM?.MainObj?.ProductIdProperty" Text="Product:" />
    <XSelect Property="@VM?.MainObj?.ProductIdProperty" />
</div>
<div>
    <XLabel Property="@VM?.MainObj?.OrderQtyProperty" Text="Order Qty:" />
    <XInputText Property="@VM?.MainObj?.OrderQtyProperty" />
</div>
<div>
    <XLabel Property="@VM?.MainObj?.SpecialOfferIdProperty" Text="Special Offer:" />
    <XSelect Property="@VM?.MainObj?.SpecialOfferIdProperty" />
</div>
<div>
    <XLabel Property="@VM?.MainObj?.UnitPriceProperty" Text="Unit Price:" />
    <XDataLabel Property="@VM?.MainObj?.UnitPriceProperty" />
</div>
<div>
    <XLabel Property="@VM?.MainObj?.UnitPriceDiscountProperty" Text="Unit Price Discount:" />
    <XDataLabel Property="@VM?.MainObj?.UnitPriceDiscountProperty" />
</div>
<div>
    <XLabel Property="@VM?.MainObj?.LineTotalProperty" Text="Line Total:" />
    <XDataLabel Property="@VM?.MainObj?.LineTotalProperty" />
</div>

运行应用程序并打开该视图时,它的外观如下所示

我们为用户可以输入的最后三个字段使用了适当的编辑控件,例如下拉列表和文本框。最后三个字段基于用户选择的前三个字段的值计算得出,用户无法直接输入。因此,我们为它们使用了只读的 `XDataLabel` 组件,该组件也绑定到我们的属性。

另请注意,属性会自动以适当的格式显示,例如货币或百分比。

计算属性

如前所述,视图中的计算只读字段绑定到数据属性,其中值不由用户输入,而是从其他数据属性的值计算得出。

具体来说,单价是根据选定的产品计算的,折扣是根据选定的特价商品计算的。这些选定的值由 Xomega Framework 类 `Header` 表示,该类存储内部 ID、显示文本以及任意数量的命名属性。单价和折扣的值存储为这些选定的特殊属性,如下所示。

行总计属性是根据指定的数量以及计算出的单价和折扣计算的。让我们来看看如何使用 Xomega Framework 实现此类计算属性。

通过事件计算属性

实现计算属性最直接的方法是向其依赖的任何其他数据属性添加属性更改侦听器,然后在任何这些数据属性的值更改时重新计算计算值。

为了实现单价,我们可以重写数据对象上的 `OnInitialized` 方法,并向 `ProductIdProperty` 添加侦听器,这将按如下方式更新 `UnitPriceProperty`

protected override void OnInitialized()
{
    base.OnInitialized();
 
    ProductIdProperty.Change += (sender, e) =>
    {
        if (e.Change.IncludesValue())
        {
            UnitPriceProperty.SetValue(ProductIdProperty.IsNull() ?
                null : ProductIdProperty.Value["list price"]);
        }
    };
}

注意我们如何检查更改是否包含值,因为数据属性更改可能是由其他元数据更改触发的。

为了实现单价折扣的计算属性,我们将添加一个类似的侦听器到 `SpecialOfferIdProperty`,并将折扣值从选定的特价商品的属性中设置如下

    SpecialOfferIdProperty.Change += (sender, e) =>
    {
        if (e.Change.IncludesValue())
        {
            UnitPriceDiscountProperty.SetValue(SpecialOfferIdProperty.IsNull() ? 
                null : SpecialOfferIdProperty.Value["discount"]);

            UnitPriceDiscountProperty.Visible = !UnitPriceDiscountProperty.IsNull() &&
                                                 UnitPriceDiscountProperty.Value > 0;
        }
    };

在同一个侦听器中,我们更新折扣属性的可见性,以便在没有折扣时隐藏该字段。

最后,为了实现计算的行总计属性,我们将首先添加两个辅助函数:`GetLineTotal`(计算给定可空值的行总计)和 `UpdateLineTotal` 属性更改侦听器,它使用该函数从其他属性的值设置 `LineTotalPoperty` 的值,如下所示

private decimal GetLineTotal(decimal? price, decimal? discount, int? qty) =>
    (price ?? 0) * (1 - (discount ?? 0)) * (qty ?? 0);
 
private void UpdateLineTotal(object sender, PropertyChangeEventArgs e)
{
    if (e.Change.IncludesValue())
    {
        LineTotalProperty.SetValue(GetLineTotal(UnitPriceProperty.Value,
            UnitPriceDiscountProperty.Value, OrderQtyProperty.Value));
    }
}

现在,我们只需要更新 `OnInitialzied` 方法,并将 `UpdateLineTotal` 添加为它所依赖的所有属性的值的侦听器,如下所示

    ProductIdProperty.Change += UpdateLineTotal;
    SpecialOfferIdProperty.Change += UpdateLineTotal;
    OrderQtyProperty.Change += UpdateLineTotal;

请记住,所有这些表示逻辑完全包含在数据对象中,并且不依赖于实际的 UI 层,这意味着您可以将其与任何 UI 框架重用。

通过表达式计算属性

虽然如前所述,通过事件实现计算属性可能是最直接的方法,但它并非最简单或最自然的方法。添加所有这些可以重新计算其他属性值的属性侦听器可能会非常繁琐。即使添加了所有这些,您也很难理解每个计算属性是如何计算的。您还需要非常小心地更改任何计算,以确保为计算值使用的任何其他属性添加新的侦听器,并删除不再依赖的属性的侦听器。

定义计算属性的一种更自然的方法是仅表达计算值的公式,并让系统在其他属性更改时更新计算值。这类似于在 Excel 电子表格中为计算单元格定义此类公式。

为了支持这一点,Xomega Framework 添加了一个新功能,您可以定义一个表达式,该表达式可以接受任意数量的数据对象或数据属性并返回计算值(作为对象)。然后,您可以调用目标计算属性上的 `SetComputedValue` 方法,并将该表达式与表达式参数的实际对象引用一起传递给它。

通过这种方法,使用一个接受当前数据对象的表达式来设置计算的 `UnitPriceProperty` 将非常简单且简洁,如下所示

protected override void OnInitialized()
{
    base.OnInitialized();
 
    // computed property using the entire object
    Expression<Func<SalesOrderDetailObject, object>> xPrice = sod =>
        sod.ProductIdProperty.IsNull(null) ? null : sod.ProductIdProperty.Value["list price"];
    UnitPriceProperty.SetComputedValue(xPrice, this);
}

如果您希望它更简洁、更易于阅读,那么您可以使表达式基于特定数据属性而不是数据对象,如下面的代码片段所示,它根据特价商品设置价格折扣。

    // computed property using individual property
    Expression<Func<EnumProperty, object>> xDiscount = spOf =>
        spOf.IsNull(null) ? null : spOf.Value["discount"];
    UnitPriceDiscountProperty.SetComputedValue(xDiscount, SpecialOfferIdProperty);

如果您想根据其他属性的值或元数据来设置计算的元数据(例如可见性或可编辑性),那么您可以使用类似的返回 `bool` 的表达式。例如,以下代码显示了如何仅当价格折扣本身的值(折扣)大于零时才使其可见。

    // computed visible attribute based on discount value
    Expression<Func<PercentFractionProperty, bool>> xVisible = dp =>
        !dp.IsNull(null) && dp.Value > 0;
    UnitPriceDiscountProperty.SetComputedVisible(xVisible, UnitPriceDiscountProperty);

最后,为了配置更复杂的计算属性,例如我们的 `LineTotalProperty`,我们可以让表达式使用我们的辅助函数 `GetLineTotal`,并将各个数据属性的值传递给它,如下所示

    // computed total using a helper function
    Expression<Func<SalesOrderDetailObject, decimal>> xLineTotal = sod => GetLineTotal(
        sod.UnitPriceProperty.Value, sod.UnitPriceDiscountProperty.Value, 
        sod.OrderQtyProperty.Value);
    LineTotalProperty.SetComputedValue(xLineTotal, this);

使用这样的辅助函数将使表达式易于阅读和实现,因为您将不再受限于辅助函数中的表达式语法,并且可以在其中使用 C# 功能的全部范围。

结论

在本文中,您学习了 Xomega Framework 的基本原理,该框架使用可观察的数据属性和数据对象来实现应用程序的可重用表示逻辑,而独立于所使用的 UI 框架。

您看到了属性更改事件如何用于将数据属性绑定到 UI 控件以及实现计算属性。

最后,您了解了新的 Xomega Framework 功能如何允许您使用清晰简洁的表达式轻松设置计算属性,而无需管理任何属性侦听器的麻烦。

请随时尝试 Xomega Framework 的这些很酷的新功能,并告诉我们您的看法。

历史

  • 2020 年 12 月 7 日 - 本文的第一个版本
© . All rights reserved.