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

使用 CSLA DynamicRootList 创建主/详细 DataGridView - 第三部分

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.86/5 (5投票s)

2009 年 3 月 12 日

CPOL

9分钟阅读

viewsIcon

28850

downloadIcon

1

处理 DataGridView:排序、详细 DataGridView 的自动保存等。

摘要

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

在第一部分中,我们解释了问题,讨论了一些背景知识,分析了用例并概述了数据库和业务对象设计。在第二部分中,我们了解了 CslaGen 代码生成的细节,父子对象绑定的秘诀以及生成代码所需进行的更改。

9. 处理 DataGridView

需要明确的是,如果没有 DataGridView FAQ 的帮助,我认为我无法完成这个项目。这个 FAQ 是由 DataGridView 项目经理 Mark Rideout 指出的。在他的 MSDN 博客上,您可以找到大量关于 DGV 的有用信息和示例。

9.1. 粘滞模式:粘滞还是置顶?

Fig. 13 - Stick elements

图 13 - 粘滞元素

在 UI 中,有一个组合框,用于选择粘滞模式:粘滞置顶置顶意味着,每当您切换到另一个品牌时,当前的模型行将重置到左侧列的顶行。粘滞意味着,当您切换到另一个品牌时,当前的模型行和列将保持不变,前提是新品牌有足够的模型。

Stick 属性仅在 masterDGV_RowEnter 事件处理程序中处理。在加载新品牌之前,在粘滞模式下,详细信息 DGV 的当前列号会被存储。在显示新品牌模型后,CurrentCell 会被设置为存储的列。在置顶模式下,CurrentCell 会被设置为顶行第一列的可见单元格。请注意,DGV 的内部代码会负责保留当前行值,或者在当前行数大于总行数时将其减小。

粘滞模式支持应该扩展到 detailDGV_RowsRemoved,因为目前它总是将列重置为第一列可见。当执行此事件处理程序时,当前单元格为 null。因此,无法将当前列值复制到新选定的单元格。

9.2. 纯主/详细模式、排序和私有字段

纯主/详细 DataGridView 不需要太多代码。您为主列表指定一个 DataSource,它负责获取数据。对于详细列表 DataSource,您指定主绑定源并指定适当的 DataMember

private void MasterDetail_Load(object sender, EventArgs e)
{
    // Bind the DataGridView controls to the BindingSource
    masterDGV.DataSource = masterBindingSource;
    detailDGV.DataSource = detailBindingSource;

    // Get the data for the master DataSource from BrandColl
    masterBindingSource.DataSource = BrandColl.GetBrandColl();

    // Bind the detail DataSource to the master DataSource
    // using the DataMember "ModelColl"
    detailBindingSource.DataSource = masterBindingSource;
    detailBindingSource.DataMember = "ModelColl";

    // Hide some columns on masterDGV
    masterDGV.Columns[0].Visible = false;
    masterDGV.Columns[2].Visible = false; // RowVersion must be hidden

    // Hide some columns on detailDGV
    detailDGV.Columns[0].Visible = false;
    detailDGV.Columns[3].Visible = false; // RowVersion must be hidden
}

如果您只需要对主 DGV 进行排序,只需将此行替换为

masterBindingSource.DataSource = BrandColl.GetBrandColl();

改为以下内容

var sortedList = new SortedBindingList<Brand>(BrandColl.GetBrandColl());
sortedList.ApplySort("BrandName", ListSortDirection.Ascending);
masterBindingSource.DataSource = sortedList;

完整的代码包含在 CslaERLB1.zip 中供您参考。

当您还想对详细 DGV 进行排序时,真正的问题就开始了。我找不到使用数据绑定来做到这一点的方法,不得不手动完成。当然,还有很多其他小问题……我猜 DataGridView 确实是为使用数据表而不是业务对象而优化的。

Fig. 14 - Private fields

图 14 - 私有字段

上面的私有字段是解决方案的一部分:_thisMasterBrandColl 类型,而 _currentMasterItemBrand 类型。我发现最大的问题之一是当我们围绕主 DataGridView 移动光标时,如何使 _currentMasterItem 与当前行保持同步。

9.3. 绑定部分

Fig. 15 - Binding parts

图 15 - 绑定部分

所有处理绑定源的代码都在 DisplaySortedMasterDisplaySortedDetail 中。这些方法非常相似:

  • 关闭尽可能多的事件处理
  • 取消绑定源
  • 获取数据并进行排序
  • 将排序后的数据分配给绑定源并重置绑定
  • 开启正常的事件处理

UnbindBindingSource 方法是一个重要的辅助方法,您可能从 ProjectTracker 中知道它。CreateMasterItem 是一个用于创建新的主对象(带有空的详细列表)并显示一个空的 detailDGV 的方法。

9.4. 主 DataGridView 事件处理

Fig. 16 - Master DGV handlers

图 16 - 主 DataGridView 处理程序

9.4.1. 数据源数据提交失败:masterDGV_DataError

根据 VS 2008 文档,DataError 事件“当外部数据解析或验证操作引发异常时,或当尝试将数据提交到数据源失败时发生。”请注意,这是一个相当广泛的范围:数据解析、验证或提交。当其中一个条件发生时,您会得到一个不太友好的 MessageBox,提示您处理该事件;您不需要捕获任何异常,只需创建一个事件处理程序。当用户编辑品牌名称并将其留空时,如果您不处理此事件,您将收到提到的 MessageBox,说发生了一个 System.NullReferenceException。单击确定后,旧值将被恢复。如果您处理该事件,最终结果是相同的,但用户看不到错误 MessageBox

9.4.2. 如果数据有效,您可以离开该行:masterDGV_RowValidating

每次您尝试离开该行时,DGV 都会尝试验证该行,并触发 RowValidating 事件。事件处理程序的目标是确保您不输入无效数据:不允许空品牌名称和重复项。如果是一行新数据,则不进行任何检查。如果您正在离开一个未保存的空行,那么您的情况是您的单元格光标位于底部的插入行上,并且您没有插入新的品牌。DGV 的内部代码会处理这个问题,但您实际上不想对这一行做任何事情。如果当前行已存在且有未提交的更改,事件处理程序将

  • 检查是否存在底层品牌对象
  • 修剪品牌名称
  • 检查底层品牌对象是否有效
if (masterDGV.IsCurrentRowDirty)
{
    if (masterDGV.Rows[e.RowIndex].DataBoundItem != null)
    {
        masterDGV[1, e.RowIndex].Value = 
		masterDGV[1, e.RowIndex].Value.ToString().Trim();
        var master = (Brand) masterDGV.Rows[e.RowIndex].DataBoundItem;
        if (!master.IsValid)
        {
            // it's invalid; wait for correction
            e.Cancel = true;

            // disable buttons on master navigator
            masterNavDelete.Enabled = false;
            masterNavMoveFirst.Enabled = false;
            masterNavMovePrevious.Enabled = false;
            masterNavMoveNext.Enabled = false;
            masterNavMoveLast.Enabled = false;
        }
    }
}

e.Cancel = true; 负责在用户使对象有效或按Esc取消更改之前,阻止光标留在当前行。禁用主导航器上的所有按钮是一种视觉选项,旨在让用户清楚地知道他必须先纠正问题,然后才能执行其他操作。顺便说一句,禁用详细导航器上的按钮是徒劳的,因为它们仍然会显示出来。

9.4.3. 不同的品牌需要更新模型列表:masterDGV_RowEnter

当您更改当前行时,即当您进入另一行时,会发生 RowEnter 事件。事件处理程序的主要功能是用相应的排序后的详细列表更新详细 DGV。如果是一行新数据,您正处于插入行,并且处理程序将

  • 创建一个新的品牌对象,其中包含一个空的模型集合,并显示一个完全空的详细 DGV
  • 设置一些按钮(您不能删除尚不存在的品牌,也不能为不存在的品牌创建模型)
if (masterDGV.Rows[e.RowIndex].IsNewRow)
{
    // this is the insert row (not saved)

    // make a new master object and display a blank (empty) detail collection
    CreateMasterItem();

    // disable delete button
    masterNavDelete.Enabled = false;

    // prevent users from adding detail rows
    detailDGV.AllowUserToAddRows = false;
}

如果它是一行旧数据

  • 检查是否存在底层品牌对象
  • 显示当前品牌的排序后的详细列表
  • 如果存在任何详细行(模型),则根据 Stick 状态处理当前行和列的位置(参见 **9.1. 粘滞模式:粘滞还是置顶?**)
  • 设置一些按钮(品牌已存在,您可以为其创建模型)
else
{
    // not a new master row
    if (masterDGV.Rows[e.RowIndex].DataBoundItem != null)
    {
        // get the underlying master object and display the detail collection
        _currentMasterItem = (Brand) masterDGV.Rows[e.RowIndex].DataBoundItem;
        DisplaySortedDetail();

        if (detailDGV.Rows.Count > 0)
        {
            // detail collection isn't empty
            if (Stick)
            {
                if (detailDGV.CurrentRow != null)
                {
                    // set current column to the stored column
                    detailDGV.CurrentCell =
                        detailDGV.Rows[detailDGV.CurrentRow.Index].Cells
						[currentDetailColumn];
                }
            }
        }
        detailDGV.AllowUserToAddRows = true;
    }
}

9.4.4. 当前单元格丢失 - 选择另一个:masterDGV_RowsRemoved

当您删除一行时,会发生 RowsRemoved 事件。事件处理程序只是尝试将单元格光标保持在最明显行(最下面一行)的第一列可见单元格上。不可能将光标保留在之前的同一列,因为在执行此事件处理程序时,当前单元格为 null。如果根本没有行

  • 创建一个新的品牌对象,其中包含一个空的模型集合,并显示一个完全空的详细 DGV
  • 在主 DGV 上,将单元格光标定位在左上角单元格并选择该单元格
  • 设置一些按钮(没有品牌可供删除;请注意,您需要强制更新按钮状态)
if (masterBindingSource.Count == 0)
{
    // master collection is empty

    // make a new master object and display a blank (empty) detail collection
    CreateMasterItem();

    // move cursor to top left cell
    masterDGV.CurrentCell = masterDGV.Rows[0].Cells[1];
    masterDGV.CurrentCell.Selected = true;

    // disable delete button
    masterNavDelete.Enabled = false;

    // force the update of the button status
    masterNav.Validate();
}

DGV 中有一些行。如果删除了最后一行,则将单元格光标定位在插入行上方、左下角的单元格并选择该单元格。

else if (e.RowIndex == masterDGV.RowCount - 1)
{
    // we are on the last data row (not the insert row)

    // previous last row deleted ; select the new last row
    masterDGV.CurrentCell = masterDGV.Rows[e.RowIndex - 1].Cells[1];
    masterDGV.CurrentCell.Selected = true;
}

9.5. 详细 DataGridView 事件处理

Fig. 17 - Detail DGV handlers

图 17 - 详细 DataGridView 处理程序

作为对抗无聊的斗争的一部分,我们将避免冗余代码,仅展示在主 DGV 的类似事件处理程序中不存在的相关代码。

9.5.1. 模型列表可能需要更新:detailDGV_UpdateModelListHelper

摘要中所述,自动保存是标准功能,但仅限于主对象。自动保存意味着行在您移动到另一行时立即保存。对于详细对象,您的代码必须使用事件处理程序来实现此行为。这必须在您离开某一行和删除某一行时完成。此辅助方法做什么?

  • 检查主对象是否需要保存
  • 保存主对象
  • 重新加载详细列表
if (_currentMasterItem.IsSavable)
{
    _thisMaster.SaveItem(_currentMasterItem);
    masterDGV_RowEnter(sender,
        new DataGridViewCellEventArgs(masterDGV.CurrentCell.ColumnIndex,
        masterDGV.CurrentRow.Index));
}

重新加载详细列表的操作使用了 masterDGV_RowEnter 事件处理程序,该处理程序在您移动到不同的主行时被调用。此事件处理程序会检查许多条件,重用它可避免编写重复的代码。

9.5.2. 数据源数据提交失败:detailDGV_DataError

此事件的功能已在前面解释过。对于详细行,您也需要它,因为 Price 的类型是 Decimal,可能存在转换问题。用户不会看到烦人的 MessageBox,而是会卡在价格单元格中,直到其输入值为有效的十进制值。还有其他可能的解决方案,如掩码输入,但它们完全超出了本项目的范围。当然,我更希望向用户显示一个错误图标,以便他明白为什么他无法离开单元格。但这并非选项。

9.5.3. 如果数据有效,您可以离开,但要保存该行:detailDGV_RowValidating

此事件处理程序与主对象的类似事件处理程序非常相似。唯一的区别是当对象有效时:在这种情况下,会调用 detailDGV_UpdateModelListHelper,以便立即将该行提交到数据库。

9.5.4. 您无法删除尚不存在的模型: detailDGV_RowEnter

当您处于插入行时,事件处理程序会禁用删除行的按钮。

9.5.5. 当前单元格丢失 - 保存更改并选择另一个:detailDGV_RowsRemoved

此事件处理程序也与主对象的类似事件处理程序非常相似。不过有两个区别:

  • 首先,通过调用 detailDGV_UpdateModelListHelper,该行会立即提交到数据库。
  • 由于这是详细集合,当它为空时,自然不会创建空的详细集合。

将单元格定位在插入行上方最左边的列,并选择当前单元格的代码就在这里,并且做的事情相同。唯一的区别是您有两列可见,而在品牌 DGV 中您只有一列可见。

本文的其他部分

历史

  • 文档版本 1:2009 年 3 月 12 日
  • 文档版本 2:2009 年 3 月 14 日 - 错误更正
  • 文档版本 3:2009 年 3 月 15 日 - 解释了 masterDGV_DataError
© . All rights reserved.