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

MVC 方式的级联选择。

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2013 年 3 月 30 日

CPOL

12分钟阅读

viewsIcon

26789

downloadIcon

603

如何轻松实现 MVC 模式下的级联选择并利用缓存的静态数据。

引言

级联选择列表是一种常见的 UI 设计模式,它允许你根据从其他列表中选择的值来缩小某个列表的选择范围。例如,你可以选择一个制造商,这将缩小可选产品的列表。接着选择一个产品,又可以从更小的列表中选择该产品的特定型号。当将这些用作筛选条件时,你可能还希望允许选择多个制造商,这将显示所有制造商产品的合并列表,依此类推。

在典型的 UI 应用程序中,开发人员通常在 View/Controller 层手动实现此行为,通过监听相应 UI 控件上的选择更改事件并相应地刷新依赖的选择列表。如果相同的级联选择列表足够常见,可以在多个屏幕中使用,那么开发人员最好创建一些通用的可重用函数或自定义控件,最坏的情况下就是直接在多个屏幕中复制相同的代码。

在本文中,我们将展示如何在 Model 层实现此行为,这允许以一种通用的方式进行,并可在多个屏幕之间甚至与不同的表示层技术(如 WPF、Silverlight 或 ASP.NET)重用。我们将使用 开源 Xomega Framework 来演示这种方法。 

Xomega Framework 概述

为了帮助您更好地理解我们正在讨论的方法,让我们快速回顾一下 Xomega Framework 本身。如果您想更深入地了解该框架的强大功能,可以阅读我们列在本篇文章末尾的其他文章。

Xomega Framework 中的 View Model 由一个 Data Object 组成,其中包含一个命名 Data Properties 列表。除了实际数据(值)之外,数据属性还包含有关属性的附加元信息,例如它是否可编辑、可见、必填等。它们还提供必要的功能来将值从一种格式转换为另一种格式、验证值以及提供属性可能假设的可能值的列表。此外,它们在属性值或关于属性的任何其他信息发生更改时支持属性更改通知。

数据属性可以轻松地绑定到标准的 UI 或 Web 控件,这些控件将保持控件的值和控件的状态与底层数据属性同步。当选择控件(如下拉列表或列表框)绑定到数据属性时,它将自动从该属性的可能值列表中填充,并且如果属性不是必填项,它还可能根据需要添加一个额外的空白选项。

每当属性发生任何更改时(例如,其值或可编辑状态),绑定的控件都会自动刷新以反映这些更改。您也可以通过触发适当的属性更改事件来手动触发刷新。

实现级联选择

既然您已经了解了 Xomega Framework 的基本原理,那么理解如何在 Model 中实现级联选择应该相当容易了。为了演示这一点,让我们创建一个简单的 Car 数据对象,它有两个属性:Make(品牌)和 Model(型号)。这两个属性都将具有相关的可选值列表,但 Model 的列表将取决于当前 Make 属性的选择值。

首先,让我们创建一个扩展基类 Xomega DataObject 的类 CarObject。为了方便起见,我们将为两个数据属性名称声明常量字符串,以便在绑定到 UI 控件时可以使用它们,以及实际的数据属性引用的成员,并提供公共 getter 和私有 setter。从技术上讲,您始终可以使用索引器通过其名称访问数据对象的属性,例如 carObject[CarObject.Make],但这将允许更方便地访问属性。

接下来,我们将重写 Initialize 方法,在该方法中,我们将构造、初始化和配置所有属性以及数据对象本身。Make 属性将只是一个文本属性的实例,我们将设置 ItemsProvider 委托以返回各种汽车品牌的列表。Model 属性的配置方式相同,除了 ItemsProvider 委托将使用 Make 属性的当前值来过滤汽车型号列表。

最后,我们将向 Make 属性添加一个更改侦听器,该侦听器检查属性值是否已更改,然后重置 Model 属性的值,并使其触发属性更改事件,指示可能值的列表已更改。这将自动刷新绑定到 Model 属性的任何选择控件。 

以下代码演示了这种简单的方法。

using System.Linq;

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

class CarObject : DataObject
{
    // declare property names that can be used for binding to controls
    public const string Make = "Make";
    public const string Model = "Model";

    // declare property instances to allow easy access to properties
    public TextProperty MakeProperty { get; private set; }
    public TextProperty ModelProperty { get; private set; }

    // construct the properties, initialize and configure
    protected override void Initialize()
    {
        // add a Make property and set up the item provider
        MakeProperty = new TextProperty(this, Make);
        MakeProperty.ItemsProvider = delegate(object input)
        {
            return MakeList;
        };

        // add a Model property and set up the item provider
        ModelProperty = new TextProperty(this, Model);
        ModelProperty.ItemsProvider = delegate(object intput)
        {
            // filter models by the currently selected Make
            var models = from m in ModelList
                         where m.Make == MakeProperty.Value
                         select m.Model;
            return models.ToList();
        };

        // set up the make property change listener to update the list of models
        MakeProperty.Change += delegate(object sender, PropertyChangeEventArgs args)
        {
            if (args.Change.IncludesValue()) // check if the make value was changed
            {
                ModelProperty.SetValue(null); // reset the model value
                ModelProperty.FirePropertyChange( // refresh the list of models
                    new PropertyChangeEventArgs(PropertyChange.Items, null, null));
            }
        };
    }

    // ========================================================
    // The following is some static sample data for the lists

    private static string[] MakeList = { "Acura", "BMW", "Lexus" };
    private static ModelItem[] ModelList = 
    {
        new ModelItem("Acura", "MDX"),
        new ModelItem("Acura", "RDX"),
        new ModelItem("BMW", "X5"),
        new ModelItem("BMW", "X3"),
        new ModelItem("Lexus", "LX 450"),
        new ModelItem("Lexus", "LX 570")
    };

    private class ModelItem
    {
        public string Model { get; private set; }
        public string Make { get; private set; }

        public ModelItem(string make, string model)
        {
            Make = make;
            Model = model;
        }
    }
}

现在,您只需在 XAML 或 ASPX 中将两个下拉列表控件绑定到这两个属性,它们就会自动填充正确的值。以下代码片段在 XAML 中对此进行了说明。

<Grid xmlns:xom="clr-namespace:Xomega.Framework;assembly=Xomega.Framework"
      xmlns:l="clr-namespace:MyNamespace;assembly=MyAssembly">
   <Label Name="lblMake">Make:</Label>
   <ComboBox Name="ctlMake" xom:Property.Name="{x:Static l:CarObject.Make}"
             xom:Property.Label="{Binding ElementName=lblMake}"/>

   <Label Name="lblModel">Model:</Label>
   <ComboBox Name="ctlModel" xom:Property.Name="{x:Static l:CarObject.Model}"
             xom:Property.Label="{Binding ElementName=lblModel}"/>
</Grid>

缓存静态数据的级联选择

在前面的示例中,我们使用了硬编码的数组来为数据属性提供可能值的列表。在大多数情况下,选择列表包含静态数据,这些数据很少甚至从不更改,因此可以针对整个应用程序、当前用户会话或甚至仅针对当前请求所需的持续时间进行缓存。

Xomega Framework 为静态数据缓存提供了强大而灵活的支持,这使得为选择控件提供可能值的列表、以自定义方式格式化值以及实现级联选择变得异常容易。让我们看看 Xomega Framework 缓存静态数据的基本组件。

Headers

用于存储静态数据项的主要类称为 Header,它定义了项所代表对象的 L 基本信息。它包括该项的字符串格式的内部 ID、面向用户的名称/描述以及任意数量的附加命名属性。可以按以下方式创建和配置项的 Header。 

// construct a new header of type car_models for LX 450 with an internal ID 123
Header model = new Header(“car_models”, “123”, “LX 450”);
model.addToAttribute(“make”, “Lexus”); // set the make attribute (or add to it)
model.DefaultFormat = Header.FieldText; // display item’s text vs. ID by default

Header 类提供了一种非常简单的方式来通过使用传递给 ToString 方法的特殊格式字符串来显示其 ID、文本或附加属性的自定义组合作为字符串。您可以设置 Header 的默认格式,以指示它默认应如何转换为字符串。以下代码片段演示了一些自定义格式的示例。 

// format to display the text followed by ID in parentheses, e.g. “LX 450 (123)”
String format = String.Format( "{0} ({1})", Header.FiedText, Header.FieldId );

// format to display the make followed by the model separated by a dash, e.g. “Lexus - LX 450”
format = String.Format( "{0} - {1}", String.Format(Header.AttrPattern, "make"), Header.FiedText );

// convert the model to a string using the specified format
String displayText = model.ToString(format);

查找表

在为静态数据构造了特定类型的 Header 列表后,您可以将其存储在自索引查找表中,该表允许您通过 ID、文本或任何附加属性的任何唯一组合来查找特定项。例如,如果为项存储了一个唯一的缩写作为附加属性,那么您将能够通过其缩写在表中查找它。您需要做的就是使用我们上面描述的所需格式调用 LookupByFormat 方法。LookupByID 方法是查找最常见情况(按 ID 查找项)的特定版本。您还可以执行区分大小写或不区分大小写的查找。 

以下是使用查找表的简短示例。 

// build a list of car models using headers
IEnumerable<Header> modelList = buildModelList();
// construct a case sensitive lookup table
LookupTable lkupTbl = new LookupTable("car_models", modelList, true);

// look up the model by ID
Header model = lkupTbl.LookupById("123");
// look up the model by text
model = lkupTbl.LookupByFormat(Header.FieldText, "LX 450");

查找缓存

不同类型的静态数据的查找表存储在 LookupCache 类中并从中访问。可能有各种类型的查找缓存,它们决定了静态数据的缓存方式和位置、缓存的生命周期、并发访问支持等。以下是潜在的缓存类型列表。

  • 全局缓存。 这是全局应用程序缓存的默认选项,它缓存整个应用程序的静态数据,因此所有应用程序用户共享。此类型由常量 LookupCache.Global 表示。 
  • 用户缓存。 这是仅为当前用户会话存储的缓存,在 Web 环境中与应用程序缓存不同。它可用于缓存特定于当前用户但受安全限制的静态数据。此类型由常量 LookupCache.User 表示。 
  • 请求缓存。 在服务器环境中,此类型的缓存可用于缓存请求持续时间的数据,此时处理请求需要读取某个列表,然后在多个位置从中查找项。 
  • 分布式缓存。 这是一个可以托管在多个服务器集群上的缓存,用于分发内存和其他资源的利用,并提供更好的可伸缩性和可用性。它可以建立在现有的分布式缓存实现(如 AppFabric、Coherence 等)之上。缓存的数据可以在此环境中跨多个应用程序共享。 

为给定类型提供适当的查找缓存由实现 ILookupCacheProvider 接口的类处理。Xomega Framework 包含一些默认的缓存提供程序,但您可以在应用程序设置中进行配置以使用自定义实现,如下所示。

<appSettings>
  <add key="Xomega.Framework.Lookup.ILookupCacheProvider" value="MyPackage.MyLookupCacheProvider"/>
</appSettings>

为了获取缓存的查找表,您首先需要获取正确类型的查找缓存,然后检索指定类型的查找表。查找表可能在第一次访问时加载到缓存中。有时,查找表将异步加载,例如当通过 Silverlight 的 WCF 服务调用进行加载时,这些调用始终是异步的。在这种情况下,您需要提供一个回调,以便在查找表可用后调用它。以下示例说明了如何获取缓存的 LookupTable

protected void WorkWithCarModels()
{
    // set up a delegate if the table is loaded asynchronously
    LookupCache.LookupTableReady onReady = delegate(string type)
    {
        if (!done) WorkWithCarModels();
    };
    // get the global lookup cache
    LookupCache cache = LookupCache.Get(LookupCache.Global);

    // retrieve the lookup table; pass null callback if loading is always synchronous
    LookupTable tblModels = cache.GetLookupTable("car_models", onReady);
    if (tblModels != null)
    {
        // work with the lookup table
        done = true;
    }
}

加载缓存

要填充缓存,您显然可以在应用程序、用户会话或请求开始时预加载每个 LookupCache(根据需要),方法是构造查找表并直接在查找缓存对象上调用 CacheLookupTable。 

然而,更好的方法是在应用程序启动期间注册一些缓存加载程序,这些加载程序可以按需动态加载一个或多个查找表。这将允许您避免加载和使用未使用的内存,并有助于更好地模块化加载不同类型静态数据的例程。 

要创建缓存加载程序,您需要定义一个实现 ILookupCacheLoader 接口的类。最简单的方法是继承抽象基类 LookupCacheLoader 并按如下方式实现其 LoadCache 方法。 

public partial class CarModelsCacheLoader : LookupCacheLoader
{
    public CarModelsCacheLoader()
        : base(LookupCache.Global, false, "car_models")
    {
    }

    protected override void LoadCache(string tableType, CacheUpdater updateCache)
    {
        // build the lookup table
        LookupTable lkupTbl = buildLookupTable(type);

        // update cache by calling the provided function (possibly asynchronously)
        updateCache(lkupTbl);
    }
}

接下来,您只需在应用程序启动期间注册您的缓存加载程序,如下所示,这样就完成了。

private void Application_Startup(object sender, StartupEventArgs e)
{
    LookupCache.AddCacheLoader(new CarModelsCacheLoader());
}

整合

现在您已经了解了 Xomega Framework 为缓存静态数据提供的强大支持的所有组成部分,以及使用数据属性构建视图模型数据对象的 L 基本元素,让我们看看它在实现缓存静态数据的级联选择方面是多么轻松。 

为了封装缓存静态数据的所有 L 复杂工作,Xomega Framework 定义了一个特殊的 EnumProperty 数据属性,它使用查找表来提供可能值的列表,并查找和验证设置在属性上的值。

当您的属性基于可能的静态值列表时,您只需将其构建为 EnumProperty(或者在整数 ID 用于值时可选地构建为 EnumIntProperty),然后将 EnumType 设置为查找缓存中静态数据的类型字符串。您还可以额外配置其他参数,例如要使用的缓存类型、要显示的显示格式、手动输入值时使用的键格式等。以下代码片段演示了此属性配置。

EnumProperty ModelProperty = new EnumProperty(this, Model);
ModelProperty.EnumType = "car_models";
ModelProperty.CacheType = LookupCache.Global; // default
ModelProperty.DisplayFormat = Header.FieldText; //default
ModelProperty.KeyFormat = Header.FieldId; //default

最后,如果每个值都有一个附加属性来存储另一个属性的相应值,例如列表中每个汽车型号的品牌,那么您可以通过简单地调用 EnumProperty 上的 SetCascadingProperty 方法并传递附加属性的名称以及它所依赖的另一个属性来设置级联选择。以下代码显示了在使用缓存的静态数据时,如何大大简化前面提到的汽车品牌和型号级联选择的示例。 

class CarObject : DataObject
{
    // declare property instances to allow easy access to properties
    public EnumProperty MakeProperty { get; private set; }
    public EnumIntProperty ModelProperty { get; private set; }

    // construct the properties, initialize and configure
    protected override void Initialize()
    {
        // add a Make property and set up the enum type
        MakeProperty = new EnumProperty(this, "Make");
        MakeProperty.EnumType = "car_makes";

        // add a Model property and set up the enum type
        ModelProperty = new EnumIntProperty(this, "Model");
        ModelProperty.EnumType = "car_models";

        // set up cascading based on the make attribute
        // and the value of the MakeProperty
        ModelProperty.SetCascadingProperty("make", MakeProperty);
    }
}

结论

在本文中,您了解了 Xomega Framework 如何允许您将大多数表示层行为(如级联选择)封装在 MVC 模式的视图模型中,而不是在视图或控制器中,从而使其可以在多个表示层技术之间重用。 

您还发现了 Xomega Framework 对静态数据缓存的强大支持,并学习了其中涉及的基本组件 - 一个通用的 Header 类,具有灵活的格式来表示静态数据元素;一个自索引的 LookupTable,它通过静态数据属性的任何组合提供强大的查找;一个可自定义的 LookupCache,可以在多个级别上提供缓存;最后,也是最重要的,模块化的按需缓存加载程序,它们还支持异步缓存加载。 

最后,您能够看到使用特殊的 EnumPropertyEnumIntProperty 数据属性可以轻松地提供基于静态数据的可能值列表并设置级联选择。  

后续步骤  

我们重视您的反馈,因此如果您喜欢这篇文章或对改进它有任何建议,或者对框架有任何疑问,请对本文进行投票并留下您的评论。为了更好地理解框架的功能,您还可以阅读我们列在下一部分中的其他文章,这些文章涵盖了框架的其他各种方面。 

要亲身体验该框架,您可以 从 CodePlex 下载最新版本 或使用 NuGet 进行安装。 

额外资源

© . All rights reserved.