动态生成用户定义界面(第 2 部分)






4.83/5 (13投票s)
进一步解释了我们如何解决允许用户定义自己的数据结构以及定义自己的用户界面来编辑这些数据的难题。
引言
在 上一篇文章 中,我描述了我要动态生成用户界面的问题,即如何允许用户创建自己的“类型”以及如何在屏幕上表示这些类型。本文更侧重于将动态用户界面层映射到域模型所涉及的概念和代码,或者更具体地说,创建与动态用户界面相匹配的动态域模型,以便用户界面可以轻松地映射到它。我建议在阅读本文之前先阅读那篇文章,因为我将继续从上次中断的地方讲起。
回顾示例项目中的类图
在编译时,我们不知道用户将创建哪些机器类型,也不知道这些机器类型有哪些字段。毕竟,我们创建了这种结构。我们可以选择像图中所展示的那样对 Machine
类的行为进行建模,这种结构很容易映射到数据库,但映射到用户界面并将 Machine
视为特定机器类型虽然可行,但有些棘手,而且工作量很大。从概念上讲,我希望 Machine
是一个通用类型,并且拥有诸如 Machine
of Roller
和 Machine
of Painter
之类的类型,或者更通用地说,Machine
of T
,我可以处理它们,其中 Roller
、Painter
或 T
是用户在运行时定义的机器类型。
为了实现这种效果,我需要以某种方式以对象形式定义一个 Machine 域类型。一种建模方式如下:
在此示例中,对于域模型中的每种类型,我都将有一个 ClassDef
对象,其中包含一个或多个 PropDef
对象,这些对象定义了类的属性。其想法是,当我创建一个域模型对象(如 MachineType
)时,该对象将获得一组基于为 MachineType
配置的 ClassDef
的 PropDef
的属性。
在我们的示例中,MachineType
是一个继承自 BusinessObject
的类。因此,当实例化 MachineType
对象时,它会收到一组 BOProp
对象,这些对象由 MachineType
的 ClassDef
定义。然后,这些对象成为域模型对象的真正属性。我们可以像下面这样为我们编译时已知的属性编写 C# 属性:
public class MachineType : BusinessObject {
public virtual String Name
{
get
{
return ((String)(base.GetPropertyValue("Name")));
}
set
{
base.SetPropertyValue("Name", value);
}
}
//...
}
上面的 C# 强类型属性使用 BusinessObject
的受保护的 GetPropertyValue()
和 SetPropertyValue()
方法来检索和设置动态属性值。拥有这些使对象更容易使用(Intellisense!),并且表现得与普通域模型对象(即没有动态创建属性的对象)完全一样。
这种整体结构使我们能够动态更改属性集,同时为编译时已知的属性提供类型安全性。
现在,为了创建映射到此对象的用户界面,我们必须创建一组 UIFormField
来定义我们的表单(请参阅 第 1 部分 结尾附近的简要讨论),并将它们链接到 ClassDef
对象。
ClassDef
具有一组 UIForm
,允许对同一类型进行不同的视图——一个视图可能只显示几个属性,另一个视图可能显示所有属性。每个 UIForm
包含一组 UIFormField
,每个字段都对应一个 PropDef
(链接是通过 PropertyName
字段建立的)。这些 UIFormField
描述了如何显示实例化的 BOProp
。通过这种结构,可以为 BusinessObject
的任何子类型动态创建表单,并将控件链接到其底层对象。
但是,这并没有解决想要拥有多个“参数化”的 Machine 类型的问题,因为每个类型只有一个 ClassDef
。因此,我们所做的是将结构从 ClassDef
和 Type
之间的一对一映射更改为一对多映射,如下所示:
在 ClassDef
上包含 TypeParameter
属性允许我们区分一个 Type
的多个 ClassDef
。现在,在我们的 Machine 示例中,通过实例化具有适当 ClassDef
对象的 Machine,我们可以有效地获得运行时“类型”。在示例程序中,您可以选择一个 MachineType
并选择创建一个该类型的新 Machine。此时,系统会向 MachineType
对象请求一个新的 Machine
,如下所示:
public partial class MachineType
{
public Machine CreateMachine()
{
ClassDef machineClassDef = GetMachineClassDef();
var machine = new Machine(machineClassDef) {MachineType = this};
foreach (MachinePropertyDef machinePropertyDef in MachinePropertyDefs)
{
MachineProperty property = machine.MachineProperties.CreateBusinessObject();
property.MachinePropertyDef = machinePropertyDef;
}
return machine;
}
CreateMachine()
的第一行获取与正在创建的 Machine
的 MachineType
相对应的 Machine ClassDef
对象(接下来我们将看 GetMachineClassDef()
)。下一行创建 Machine
对象,为其提供适当的 ClassDef
对象,以便它获得正确的属性。在此之后,有一些代码用于支持持久化和加载到数据库结构,但这将在第 3 部分讨论,因此我在这里省略了它。最后,我们返回新的 Machine
对象,该对象具有适合此 MachineType
的正确属性(BOProp
)。换句话说,它是一个 Machine
of T
!
继续 MachineType
类,我们继续讲解 GetMachineClassDef()
方法:
public ClassDef GetMachineClassDef()
{
string machineTypeClassDefName = "Machine_" + Name;
ClassDef machineClassDef;
string machineAssemblyName = "MachineExample.BO";
if (ClassDef.ClassDefs.Contains(machineAssemblyName, machineTypeClassDefName))
{
machineClassDef =
ClassDef.ClassDefs[machineAssemblyName, machineTypeClassDefName];
ClassDef.ClassDefs.Remove(machineClassDef);
}
machineClassDef = CreateNewMachineClassDef();
ClassDef.ClassDefs.Add(machineClassDef);
return machineClassDef;
}
此方法检索与 Machine
of T
对应的 ClassDef
对象。在搜索 ClassDefs
字典时,我们为其提供一个参数化名称,例如“Machine_Roller”或“Machine_Painter”,ClassDefs
集合会检索匹配的 ClassDef
对象。
一个重要的考虑因素是,即使它找到了匹配的 ClassDef
,它也会将其删除并创建一个新的。这是因为 ClassDef
的定义本身可能已被用户自 ClassDef
构建以来执行的操作所更改,例如,通过添加或更改该 MachineType
的 MachinePropertyDef
。为了确保正在创建的 Machine
具有其类型的最新定义,我们每次都创建 ClassDef
。当然,这是简单的方法——一个更好的方法是在 MachineType
更改时并且仅在更改时替换或更新 ClassDef
,并且这就是我们在生产项目中实现它的方式。
最后要看的是 ClassDef
的创建过程,因为正是在这个过程中,UI 表单定义也被构建起来了:
private ClassDef CreateNewMachineClassDef()
{
ClassDef baseMachineClassDef = ClassDef.ClassDefs[typeof (Machine)];
ClassDef machineClassDef = baseMachineClassDef.Clone();
machineClassDef.TypeParameter = Name;
在这里,我们获取基本的 Machine ClassDef
,克隆它,然后应用类型参数。如果我们获取的 ClassDef
是针对名为“Roller
”的 MachineType
,那么 TypeParameter
属性将被设置为“Roller”,从概念上讲,我们现在拥有了一个 Machine
of Roller
的 ClassDef
。
UIDef uiDef = machineClassDef.UIDefCol["default"];
UIGrid uiGrid = uiDef.UIGrid;
UIForm form = uiDef.UIForm;
form.Title = "Add/Edit a Machine";
UIFormTab tab = form[0];
UIFormColumn column = tab[0];
这段代码只是获取包含所有 UIFormField
的 UIFormColumn
对象。我之前在图表中省略了 UIFormTab
和 UIFormColumn
,因为它们只是用于布局的组织结构,不会影响我们如何将表单视为仅包含一组字段的概念。您会看到也检索了一个 UIGrid
——这是为了我们可以设置显示此 Machine Type 的网格的列(网格的建模方式与表单相同,但结构略有不同)。
foreach (MachinePropertyDef machinePropertyDef in MachinePropertyDefs)
{
var machinePropertyPropDef =
new PropDef(machinePropertyDef.PropertyName, "System",
machinePropertyDef.PropertyType,
PropReadWriteRule.ReadWrite, "", null,
machinePropertyDef.IsCompulsory.Value, false);
machinePropertyPropDef.Persistable = false;
machineClassDef.PropDefcol.Add(machinePropertyPropDef);
在这里,我们使用 MachineType
的 MachinePropertyDef
定义来设置 Machine
of X
ClassDef
的所有属性。它们被标记为不可持久化,因为它们的持久化是通过 MachineProperty
对象单独处理的。目前,我忽略了持久化——它将在第 3 部分介绍。请注意,在创建 PropDef
对象时,会为其提供 Machine Property Definition 的 PropertyType
和 IsCompulsory
值,以便在保存前可以进行验证。
var uiProperty =
new UIFormField(null, machinePropertyDef.PropertyName,
"TextBox", "System.Windows.Forms", "", "",
true, "", new Hashtable(), null);
column.Add(uiProperty);
这会为我们正在处理的 MachinePropertyDef
添加一个表单字段,该字段将显示在 Machine
of T
的表单上。
uiGrid.Add(
new UIGridColumn(machinePropertyDef.PropertyName,
machinePropertyDef.PropertyName, "", "",
false, 100, UIGridColumn.PropAlignment.left,
new Hashtable()));
同样,我们也添加了一个网格列。在示例应用程序中,您可以看到这一点:当您在“数据 | Machine”屏幕中选择不同的 Machine Type 时,网格列会根据所选项而变化。网格在运行时使用此行代码添加的列进行配置。
}
return machineClassDef;
}
}
最后,我们返回新创建和配置好的 Machine
of T
的 ClassDef
。
MachineType
类上的这三个方法已完全配置了您可以想象的任何 Machine Type 的动态创建、查看和编辑。无需进一步的自定义——当编辑 Machine
时,UI 生成类只需使用与该 Machine
of T
关联的 ClassDef
来创建表单或网格。
最后要看的是 Machine
的动态属性如何从数据库持久化和加载。请留意几天后发布的第 3 部分!
链接
在创建示例代码时使用了 Habanero Enterprise Framework。
历史
- 2008 年 11 月 27 日:添加了文章。