使用 CSLA DynamicRootList 构建主/明细 DataGridView - 第二部分






4.67/5 (4投票s)
CslaGen 代码生成和生成业务对象的自定义的最佳保留秘密。
摘要
本项目展示了如何使用 **CSLA** DynamicRootList
(或 EditableRootListBase
)作为主列表对象来拥有一个主/明细 DataGridView
。如果您将 DynamicRootList
用于主列表,则自动保存是一项标准功能。本项目还展示了如何在明细列表中实现自动保存。作为额外奖励,您可以对这两个列表进行排序。
在第一部分中,我们解释了问题,讨论了一些背景,分析了用例,并概述了数据库和业务对象设计。
在第二部分中,我们将首先详细介绍 **CslaGen** 中对象的规范。然后,我们将看到必须对生成的文件进行的更改,以便拥有正确的 DynamicRootList
和 DynamicRoot
对象类型。最后,我们将讨论业务对象的自定义。
如果您不关心 CslaGen 的生成,只想了解 DataGridView
的细节,请跳过第二部分,直接到 第三部分 - 处理 DataGridView。否则,我将假定您对 CslaGen 有基本了解。
7. CslaGen 代码生成
首先,值得一提的是,这本来是一个非常大的章节,充满了冗余信息,因为有所有 CslaGen 对象每个细节的截图。写起来很痛苦,读起来也很无聊。另一方面,由于 **CslaGen** 文档的缺乏,新人确实需要尽可能多的细节。因此,我将截图的完整集合移至附录,仅在主文章中保留非冗余和重要信息。
关于条件(criteria)的一句话:这些是用于 CRUD 操作的参数。我使用三种类型
- 条件 (Criteria) - 用于获取对象或集合。对于可编辑对象,它也用于更新和删除。对于根集合,通常没有任何属性,因此它将获取集合中的所有对象。在这种情况下,将使用
FilteredCriteria
来允许过滤。它们使用由FilteredCriteria
生成的相同存储过程。通常的属性是- 根集合 - 没有属性
- 根对象 - 对象 ID 和行版本
- 子集合 - 父对象 ID
- 子对象 - 对象 ID 和行版本
- 条件新建 (CriteriaNew) - 仅用于创建对象,在集合中不存在。通常,它没有任何属性,对象以默认值创建。
- 过滤条件 (FilteredCriteria) - 用于获取部分集合。过滤属性可以是对象的名称、某个日期或日期范围等。
7.1. BrandColl
BrandColl
是 Brand
对象的集合。它是一个 EditableRootCollection
(这是 CslaGen 中 EditableRootList
的名称)。DynamicRootList
的“不使用”名称是 EditableRootListBase
而不是 EditableRootList
;名称不同,对象类型也不同。在 **8.1. 对象类型转换** 中,我们将把这个对象转换为 DynamicRootList
。
集合没有值属性,因为这些属性是在集合所组成的对象的。所以,我们将从条件部分开始。
7.1.1. 条件部分
为了获取集合,您必须定义一个 Criteria
。尽管存储过程不会生成(Procedure
设置为 False
),但生成的代码将使用存储过程,所以您必须指定其名称。请注意,所有 CreateOptions
和 DeleteOptions
都设置为 False
或为空。
还应该生成一个 FilteredCriteria
。在这个特定的项目中,它没有任何用途,但在其他情况下可能很有用。请注意,过程名称相同。该过程的生成方式使其能够使用或不使用过滤参数。FilteredCriteria
的定义与 Criteria
相同。唯一的区别是
Procedure
设置为True
,因此生成了存储过程。FilteredCriteria
确实有属性。
这些属性用于指定要使用的过滤器。在此示例中,我们将仅使用品牌名称作为过滤器。
7.1.2. 其他属性部分
BrandColl
的其他属性包括 **Item Type**,设置为 Brand
。这会将 Brand
对象绑定到 BrandColl
集合。反之,在 Brand
中,**Parent Type** 属性设置为 BrandColl
(图 10)。
BrandColl
的其他属性7.2. Brand
Brand
是一个 EditableSwitchable
对象。在 **8.1. 对象类型转换** 中,我们将把这个对象转换为 DynamicRoot
。
EditableSwitchable
对象有时可能是根对象,有时可能是子对象。另一方面,Brand
也是 ModelColl
的父对象。下一节是关于 **Child Collection Properties**,即 Brand
的子对象,也就是 ModelColl
。稍后,我们将讨论 Brand
作为子对象的属性。这些属性显示在 **Csla Object Info** 窗格下的 **04. Child Object Options** 中。
7.2.1. 子集合部分
在 Brand
的 **Child Collection Properties** 中,您会指定其所有子对象。ModelColl
是 Brand
的子对象,您在 **TypeName** 中指定它。请注意这些属性
LazyLoad (惰性加载)
LoadingScheme (加载方案)
LoadParameters (加载参数)
当您加载根集合 BrandColl
时,您必须加载所有 Brand
对象。在同一时间,您还可以加载所有子对象和孙对象(对于每个 Brand
,加载其 ModelColl
以及集合中的所有 Model
)。这是一个很大的对象要加载,而将 LazyLoad
设置为 True
可以避免加载所有这些对象。
LoadingScheme
属性可以设置为 SelfLoad
、ParentLoad
或 None
。尽管我将其设置为 SelfLoad
,您也可以将其设置为 ParentLoad
,因为生成的代码完全相同 - 至少在这种情况下。
LoadParameters
属性指定加载 ModelColl
时必须传递什么参数。这会引用 Brand
的一个条件,可能会令人困惑。父条件的属性用作子条件加载它的参数。这意味着您必须同时在父和子对象上拥有条件,并且至少有一个共同的属性,在本例中,该属性是 BrandID
。
7.2.2. 值属性部分
Brand
对象具有以下值属性
BrandID
BrandName
RowVersion
请注意,第一个和最后一个属性是只读的,因为您不应该更改数据库层提供的值。
BrandID
属性使用 System.Threading.Interlocked.Decrement
获取一个(临时)默认值。静态字段 _lastID
存储最后使用的 ID。这在 为对象生成临时唯一 ID 值 中有详细讨论。
BrandID
的 `PrimaryKey` 属性类型为 DBProvidedPK
,因此 `ReadOnly` 属性设置为 True
也就不足为奇了。正如我之前所说,我使用的是 CslaGen 的一个稍有不同的版本,您可能会注意到 `FKConstraint` 属性。此功能在此示例中未使用。
BrandName
值属性与 BrandID
类似,但有一些区别
PrimaryKey
设置为Default
DefaultValue
设置为空白PropertyType
设置为String
ReadOnly
设置为False
最后,RowVersion
值属性与 BrandName
类似,但有一些区别
PropertyType
设置为ByteArray
ReadOnly
设置为True
7.2.3. 条件部分
Brand
的 Criteria
也非常标准。它用于获取、更新和删除 Brand
对象,但不负责创建它们,因为所有 CreateOption
s 都设置为 False
或为空。
Brand
的 Criteria
使用两个属性:BrandID
和 RowVersion
。这意味着您必须同时提供这两个参数才能获取、更新或删除 Brand
对象。回到图 6 的注释,这两个属性中至少有一个也必须用作子对象的 Criteria
。BrandID
属性满足此要求。
Brand
的 CriteriaNew
仅用于创建 Brand
对象,除此之外别无他用。所有 DeleteOption
s 和 GetOption
s 都设置为 False
或为空。当然,它不使用任何属性。由于对象尚不存在,因此没有 ID 或行版本。
7.2.4. 其他属性部分
Brand
的其他属性如下所示。通常,所有 **04. Child Object Options** 都非常重要,才能使其正常工作。如前所述,它们引用 Brand
作为子对象的属性。
- Lazy Load 设置为
False
。 - Parent Insert Only 设置为
False
。 - Parent Properties 为空。
- **Parent Type** 属性是
BrandColl
中 **Item Type** 属性(图 5)的补充。
这可能会让您感到困惑(我知道它让我感到困惑!),但实际上,这是正确的。这指的是 Brand
对象本身的属性,而这个对象不需要惰性加载(否则,DataGridView
中将没有数据可显示)。我们之前在 Brand
的 **Child Collection Properties**(图 6)中说过,Brand
的子对象是惰性加载的。这意味着 ModelColl
是惰性加载的。
7.3. ModelColl
ModelColl
是 Model
对象的集合。它是一个 EditableChildCollection
(这是 CslaGen 中 EditableChildList
的名称)。同样,我们将从条件部分开始,因为集合没有值属性。
7.3.1. 条件部分
ModelColl
必须有一个 Criteria
,我们在其中指定它如何被其 Brand
父对象获取。仅设置了 GetOption
s,并且所有 CreateOption
s 和 DeleteOptions
都设置为 False
或为空。与图 3 中显示的内容唯一的区别是 Procedure
设置为 True
,因此生成了存储过程。当然,您也可以使用 FilteredCriteria
,就像我们对 BrandColl
所做的那样。
BrandID
属性用于获取 ModelColl
,如 CriteriaProperties
中所指定。回到图 6 的注释,BrandID
属性也必须用作父对象上的 Criteria
。
7.3.2. 其他属性部分
ModelColl
的其他属性如下所示。一如既往,所有 **04. Child Object Options** 都非常重要,才能使其正常工作。
- Lazy Load 属性与
Brand
的 **Child Collection Properties**(图 6)中的LazyLoad
属性类似。我们希望ModelColl
被惰性加载(请重读图 10 中的注释)。 - **Parent Properties** 属性设置为
BrandID
,是Brand
的 **Child Collection Properties**(图 6)中LoadParameters
属性的补充。后者设置为Criteria.BrandID
(此处Criteria
指的是Brand
的条件)。此属性说明如何将ModelColl
集合绑定到父对象。 - **Parent Type** 属性是
Brand
的 **Child Collection Properties**(图 6)中TypeName
属性的补充。此属性说明ModelColl
的父对象是谁。 - Add Parent Reference 设置为
False
。我猜如果需要在ModelColl
中拥有对Brand
父对象的引用,则可以将其设置为True
。 - Item Type 设置为
Model
。
7.4. Model
Model
是一个 EditableChild
对象,没有子对象。我们将从值属性开始。
7.4.1. 值属性部分
Model
对象具有以下值属性
ModelID
(类似于Brand
中的BrandID
)ModelName
(类似于Brand
中的BrandName
)价格
RowVersion
(类似于Brand
中的RowVersion
)
Price
属性的数据类型在 SQL Server 中定义为 money
,而在 .NET Framework 中,“翻译”为 Decimal
。
您可能会发现很难理解为什么 BrandID
属性不包含在 **Values Properties** 集合中。没有必要这样做,因为 **Csla** 框架会在默认构造函数中调用 MarkAsChild
时处理添加 Parent
属性。
BrandID
在 Models
表中如何存储?如您在图 12 的 **04. Child Object Options** 中所见,**Parent Properties** 引用了 BrandID
。而且,**CslaGen** 会相应地生成存储过程。
如果您将 BrandID
属性添加到 **Values Properties** 集合中,您将最终得到重复项。为了避免这些重复项,我尝试了一个聪明的办法:我将 BrandID
属性添加到了 **Values Properties** 集合中,并将 **Parent Properties** 设置为空白。问题是 BrandID
的值始终是默认值,而不是我期望的父 Brand
的正确 ID。
7.4.2. 条件部分
此部分与 Brand
条件非常相似,您可以参考 **7.2.3. 条件部分**。
Model
的 Criteria
用于获取、更新和删除 Model
对象,但不负责创建它们,因为所有 CreateOption
s 都设置为 False
或为空。Model
的 Criteria
使用两个属性:ModelID
和 RowVersion
。这意味着您必须同时提供这两个参数才能获取、更新或删除 Model
对象。
Model
的 CriteriaNew
仅用于创建 Model
对象,除此之外别无他用。您可以看到所有 DeleteOption
s 和 GetOption
s 都设置为 False
或为空。当然,它不使用任何属性。由于对象尚不存在,因此没有 ID 或行版本。
7.4.3. 其他属性部分
Model
的其他属性如下所示。如我之前所说,所有 **04. Child Object Options** 都非常重要,才能使其正常工作。
- **Lazy Load** 属性设置为
False
,否则详细的DataGridView
中将没有数据可显示。 - 关于 **Parent Properties**,请参阅 **7.4.1. 值属性部分** 中的最后注释。也请参考图 11 中的注释。简而言之,此属性设置为
BrandID
。它通过ModelColl
将子对象Model
绑定到祖父对象Brand
。 - **Parent Type** 属性是
ModelColl
中 **Item Type** 属性(图 11)的补充。
Model
(Parent Properties = BrandID
) -> ModelColl
(Parent Properties = BrandID
) -> Brand
(BrandID
))
8. 对生成代码的更改
在 CslaGen 的 **Project Properties** 选项卡中,您必须勾选 **Use ".Designer" in file names**。这样,**CslaGen** 将为每个对象生成两个文件(都是 public partial class
文件)
- <Object Name>.Designer.cs 文件始终生成
- <Object Name>.cs 文件仅在不存在时生成
预期的用途是让您在非 .Designer 文件中进行所有自定义,这样每次更改任何对象并需要重新生成项目时,自定义就不会丢失。
8.1. 对象类型转换
为了将 CslaGen 对象 EditableRootCollection
+ EditableSwitchable
转换为更方便的 DynamicRootList
+ DynamicRoot
(**CSLA** 模板命名),可以进行两个级别的更改。最少的更改包括
- 更改
BrandColl
的基类 - 确保
Brand
永远不是子对象 - 向
BrandColl
添加AddNewCore
方法
而完整的更改则包括清理无用的代码。
完整的更改意味着清理 Brand
对象“可切换性”的所有痕迹,包括一对 if
语句,并删除不需要的类
- Brand.Designer.cs 中的
GetBrand
的重载之一。 - Brand.Designer.cs 中的
DeleteBrand
。 - Brand.Designer.cs 中的
GetBrandChild
。 - Brand.Designer.cs 中的
NewBrandChild
。 - BrandColl.Designer.cs 中的
DataPortal_Update
。 - BrandColl.Designer.cs 中的
DataPortal_Delete
。
我们不再进一步分析这些更改。未修改的生成文件包含在 CslaERLB1.zip 中,以及 Brand.Designer.cs.minimal
,以便您可以比较文件以找出确切的差异。
从最少更改开始,在 BrandColl.Designer.cs 中,您需要将基类从 BusinessListBase<BrandColl, Brand>
更改为 EditableRootListBase<Brand>
并注释掉一些行,如下所示。
8.1.1. 对 BrandColl.Designer.cs 的更改
// public partial class BrandColl : BusinessListBase<BrandColl, Brand>
public partial class BrandColl : EditableRootListBase<Brand>
(...)
public void Add()
{
// Brand brand = Brand.NewBrandChild();
Brand brand = Brand.NewBrand();
Add(brand);
}
*/
8.1.2. 对 Brand.Designer.cs 的更改
在 Brand.Designer.cs 中,您只需要注释掉一行。
internal static Brand GetBrand(SafeDataReader dr)
{
Brand obj = new Brand();
// obj.MarkAsChild();
obj.Fetch(dr);
obj.MarkOld();
obj.ValidationRules.CheckRules();
return obj;
}
8.1.3. 对 BrandColl.cs 的更改
在 BrandColl.cs 中,您需要
- 添加
AddNewCore
方法,因为DataGridView
需要它。 - 注释掉
Initialize
方法,因为DynamicRootList
基类未定义此方法。
所以,没有什么需要重写的。
#region Factory Methods
///<summary>Adds a new item to the end of the collection.</summary>
///<returns>The item that was added to the collection.</returns>
///<exception cref="T:System.InvalidCastException">The new item
///is not the same type as the objects contained in the
//<see cref="T:System.ComponentModel.BindingList`1" />.</exception>
protected override object AddNewCore()
{
Brand item = Brand.NewBrand();
Add(item);
return item;
}
#endregion
(...)
/*
protected override void Initialize()
{
//this.FetchPre += new EventHandler(OnFetchPre);
//this.FetchPost += new EventHandler(OnFetchPost);
}
*/
8.2. 业务对象自定义
8.2.1. 对 Brand.cs 的更改
这些更改仅涉及验证规则。**CSLA** 2.1 在此领域引入了多项改进,特别是 RuleSeverity
。关于 DataGridView
的警告:它使用自己的内部错误提供程序,而不是标准的 ErrorProvider
。有一个增强的 ErrorWarnInfoProvider
,它利用此功能并根据严重性显示不同的图标,但您不能将其与 DataGridView
一起使用。
自 **CSLA** 3.0 起,您可以指定 PropertyFriendlyName
,而 brandNameRules
就是为此目的而使用的。
最好有一个 CommonRules
用于 NoDuplicates
。这是可能的,因为每个可编辑的子对象都有对其父集合的引用(一旦您将其 Add
到集合中)。请注意使用 ToUpperInvariant
来确保匹配不区分大小写。
#region Validation Rules
protected override void AddBusinessRules()
{
var brandNameRules = new RuleArgs("BrandName", "Brand name");
ValidationRules.AddRule(CommonRules.StringRequired, brandNameRules);
ValidationRules.AddRule(CommonRules.StringMaxLength,
new CommonRules.MaxLengthRuleArgs(brandNameRules.PropertyName,
brandNameRules.PropertyFriendlyName, 20));
ValidationRules.AddRule<Brand, RuleArgs>(NoDuplicates, brandNameRules);
}
private static bool NoDuplicates<T>(T target, RuleArgs e) where T : Brand
{
// no two brands can have the same name; match is case insensitive
var parent = (BrandColl)target.Parent;
if (parent != null)
{
foreach (var item in parent)
if (item.BrandName.ToUpperInvariant() ==
target.BrandName.ToUpperInvariant() &&
!ReferenceEquals(item, target))
{
e.Description = e.PropertyFriendlyName + " must be unique.";
return false;
}
}
return true;
}
#endregion
8.2.2. 对 ModelColl.cs 的更改
在此文件中,您需要做的就是
- 添加一个
AddNewCore
方法,以便DataGridView
可以在您离开行后立即保存该行。
涉及的代码与 **8.1.3. 对 BrandColl.cs 的更改** 中提供的代码基本相同。
8.2.3. 对 Model.cs 的更改
在此文件中,您需要添加验证规则部分
- 添加一个
AddBusinessRules
方法 - 添加一个
NoDuplicates
方法
涉及的代码与 **8.2.1. 对 Brand.cs 的更改** 中提供的代码基本相同。请注意,模型名称的唯一性是针对父 Brand
检查的,正如用例所指定的那样。
8.3 存储过程更改
生成的存储过程代码几乎是正确的,只是它应该在删除品牌之前先删除品牌的所有模型。要更正这一点,请编辑 Brand.sql,并在其之前
/* Delete object from Brands */
添加这些行
/* Delete child object from Models */
DELETE FROM [Models]
WHERE [BrandID] = @BrandID
未修改的生成代码包含在 CslaERLB1.zip 中。
这是 ProjectTracker 示例中采用的解决方案。如果外键规范是 ON DELETE CASCADE
,则可以避免这种情况。我们可以就每种解决方案的速度进行争论。我更喜欢在存储过程中进行明确的声明,因为它更透明,也更具可移植性。**CslaGen** 不会尝试解决这个问题,您必须自己解决。
一个值得注意的有趣点是为什么会存在这个问题。当您删除一个对象时,**CSLA** 首先从数据库中删除它,然后删除业务对象本身及其所有子对象。
本文的其他部分
历史
- 文档版本 1:2009 年 3 月 12 日。