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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (4投票s)

2009 年 3 月 12 日

CPOL

15分钟阅读

viewsIcon

21360

CslaGen 代码生成和生成业务对象的自定义的最佳保留秘密。

摘要

本项目展示了如何使用 **CSLA** DynamicRootList(或 EditableRootListBase)作为主列表对象来拥有一个主/明细 DataGridView。如果您将 DynamicRootList 用于主列表,则自动保存是一项标准功能。本项目还展示了如何在明细列表中实现自动保存。作为额外奖励,您可以对这两个列表进行排序。

在第一部分中,我们解释了问题,讨论了一些背景,分析了用例,并概述了数据库和业务对象设计。

在第二部分中,我们将首先详细介绍 **CslaGen** 中对象的规范。然后,我们将看到必须对生成的文件进行的更改,以便拥有正确的 DynamicRootListDynamicRoot 对象类型。最后,我们将讨论业务对象的自定义。

如果您不关心 CslaGen 的生成,只想了解 DataGridView 的细节,请跳过第二部分,直接到 第三部分 - 处理 DataGridView。否则,我将假定您对 CslaGen 有基本了解。

7. CslaGen 代码生成

首先,值得一提的是,这本来是一个非常大的章节,充满了冗余信息,因为有所有 CslaGen 对象每个细节的截图。写起来很痛苦,读起来也很无聊。另一方面,由于 **CslaGen** 文档的缺乏,新人确实需要尽可能多的细节。因此,我将截图的完整集合移至附录,仅在主文章中保留非冗余和重要信息。

关于条件(criteria)的一句话:这些是用于 CRUD 操作的参数。我使用三种类型

  • 条件 (Criteria) - 用于获取对象或集合。对于可编辑对象,它也用于更新和删除。对于根集合,通常没有任何属性,因此它将获取集合中的所有对象。在这种情况下,将使用 FilteredCriteria 来允许过滤。它们使用由 FilteredCriteria 生成的相同存储过程。通常的属性是

    • 根集合 - 没有属性
    • 根对象 - 对象 ID 和行版本
    • 子集合 - 父对象 ID
    • 子对象 - 对象 ID 和行版本
  • 条件新建 (CriteriaNew) - 仅用于创建对象,在集合中不存在。通常,它没有任何属性,对象以默认值创建。
  • 过滤条件 (FilteredCriteria) - 用于获取部分集合。过滤属性可以是对象的名称、某个日期或日期范围等。

7.1. BrandColl

BrandCollBrand 对象的集合。它是一个 EditableRootCollection(这是 CslaGen 中 EditableRootList 的名称)。DynamicRootList 的“不使用”名称是 EditableRootListBase 而不是 EditableRootList;名称不同,对象类型也不同。在 **8.1. 对象类型转换** 中,我们将把这个对象转换为 DynamicRootList

集合没有值属性,因为这些属性是在集合所组成的对象的。所以,我们将从条件部分开始。

7.1.1. 条件部分

为了获取集合,您必须定义一个 Criteria。尽管存储过程不会生成(Procedure 设置为 False),但生成的代码将使用存储过程,所以您必须指定其名称。请注意,所有 CreateOptionsDeleteOptions 都设置为 False 或为空。

Fig. 3 - Criteria for BrandColl

图 3 - BrandColl 的条件

还应该生成一个 FilteredCriteria。在这个特定的项目中,它没有任何用途,但在其他情况下可能很有用。请注意,过程名称相同。该过程的生成方式使其能够使用或不使用过滤参数。FilteredCriteria 的定义与 Criteria 相同。唯一的区别是

  • Procedure 设置为 True,因此生成了存储过程。
  • FilteredCriteria 确实有属性。

这些属性用于指定要使用的过滤器。在此示例中,我们将仅使用品牌名称作为过滤器。

Fig. 4 - Properties of FilteredCriteria for BrandColl

图 4 - BrandColl 的 FilteredCriteria 属性

7.1.2. 其他属性部分

BrandColl 的其他属性包括 **Item Type**,设置为 Brand。这会将 Brand 对象绑定到 BrandColl 集合。反之,在 Brand 中,**Parent Type** 属性设置为 BrandColl(图 10)。

Fig. 5 - Other properties for BrandColl

图 5 - 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** 中,您会指定其所有子对象。ModelCollBrand 的子对象,您在 **TypeName** 中指定它。请注意这些属性

  • LazyLoad (惰性加载)
  • LoadingScheme (加载方案)
  • LoadParameters (加载参数)

当您加载根集合 BrandColl 时,您必须加载所有 Brand 对象。在同一时间,您还可以加载所有子对象和孙对象(对于每个 Brand,加载其 ModelColl 以及集合中的所有 Model)。这是一个很大的对象要加载,而将 LazyLoad 设置为 True 可以避免加载所有这些对象。

LoadingScheme 属性可以设置为 SelfLoadParentLoadNone。尽管我将其设置为 SelfLoad,您也可以将其设置为 ParentLoad,因为生成的代码完全相同 - 至少在这种情况下。

LoadParameters 属性指定加载 ModelColl 时必须传递什么参数。这会引用 Brand 的一个条件,可能会令人困惑。父条件的属性用作子条件加载它的参数。这意味着您必须同时在父和子对象上拥有条件,并且至少有一个共同的属性,在本例中,该属性是 BrandID

Fig. 6 - Child Collection Properties for Brand

图 6 - Brand 的 Child Collection Properties

7.2.2. 值属性部分

Brand 对象具有以下值属性

  • BrandID
  • BrandName
  • RowVersion

请注意,第一个和最后一个属性是只读的,因为您不应该更改数据库层提供的值。

BrandID 属性使用 System.Threading.Interlocked.Decrement 获取一个(临时)默认值。静态字段 _lastID 存储最后使用的 ID。这在 为对象生成临时唯一 ID 值 中有详细讨论。

BrandID 的 `PrimaryKey` 属性类型为 DBProvidedPK,因此 `ReadOnly` 属性设置为 True 也就不足为奇了。正如我之前所说,我使用的是 CslaGen 的一个稍有不同的版本,您可能会注意到 `FKConstraint` 属性。此功能在此示例中未使用。

Fig. 7 - ValueProperties for BrandID (of Brand)

图 7 - BrandID(Brand 的)的值属性

BrandName 值属性与 BrandID 类似,但有一些区别

  • PrimaryKey 设置为 Default
  • DefaultValue 设置为空白
  • PropertyType 设置为 String
  • ReadOnly 设置为 False

最后,RowVersion 值属性与 BrandName 类似,但有一些区别

  • PropertyType 设置为 ByteArray
  • ReadOnly 设置为 True

7.2.3. 条件部分

BrandCriteria 也非常标准。它用于获取、更新和删除 Brand 对象,但不负责创建它们,因为所有 CreateOptions 都设置为 False 或为空。

Fig. 8 - Criteria for Brand

图 8 - Brand 的条件

BrandCriteria 使用两个属性:BrandIDRowVersion。这意味着您必须同时提供这两个参数才能获取、更新或删除 Brand 对象。回到图 6 的注释,这两个属性中至少有一个也必须用作子对象的 CriteriaBrandID 属性满足此要求。

BrandCriteriaNew 仅用于创建 Brand 对象,除此之外别无他用。所有 DeleteOptions 和 GetOptions 都设置为 False 或为空。当然,它不使用任何属性。由于对象尚不存在,因此没有 ID 或行版本。

Fig. 9 - CriteriaNew for Brand

图 9 - Brand 的 CriteriaNew

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 是惰性加载的。

Fig. 10 - Other Brand properties

图 10 - Brand 的其他属性

7.3. ModelColl

ModelCollModel 对象的集合。它是一个 EditableChildCollection(这是 CslaGen 中 EditableChildList 的名称)。同样,我们将从条件部分开始,因为集合没有值属性。

7.3.1. 条件部分

ModelColl 必须有一个 Criteria,我们在其中指定它如何被其 Brand 父对象获取。仅设置了 GetOptions,并且所有 CreateOptions 和 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

Fig. 11 - Other ModelColl properties

图 11 - ModelColl 的其他属性

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 属性。

BrandIDModels 表中如何存储?如您在图 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. 条件部分**。

ModelCriteria 用于获取、更新和删除 Model 对象,但不负责创建它们,因为所有 CreateOptions 都设置为 False 或为空。ModelCriteria 使用两个属性:ModelIDRowVersion。这意味着您必须同时提供这两个参数才能获取、更新或删除 Model 对象。

ModelCriteriaNew 仅用于创建 Model 对象,除此之外别无他用。您可以看到所有 DeleteOptions 和 GetOptions 都设置为 False 或为空。当然,它不使用任何属性。由于对象尚不存在,因此没有 ID 或行版本。

7.4.3. 其他属性部分

Model 的其他属性如下所示。如我之前所说,所有 **04. Child Object Options** 都非常重要,才能使其正常工作。

  • **Lazy Load** 属性设置为 False,否则详细的 DataGridView 中将没有数据可显示。
  • 关于 **Parent Properties**,请参阅 **7.4.1. 值属性部分** 中的最后注释。也请参考图 11 中的注释。简而言之,此属性设置为 BrandID。它通过 ModelColl 将子对象 Model 绑定到祖父对象 Brand
  • Model (Parent Properties = BrandID) -> ModelColl (Parent Properties = BrandID) -> Brand (BrandID))

  • **Parent Type** 属性是 ModelColl 中 **Item Type** 属性(图 11)的补充。

Fig. 12 - Other Model properties

图 12 - Model 的其他属性

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 日。
© . All rights reserved.