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






3.86/5 (5投票s)
处理 DataGridView:排序、详细 DataGridView 的自动保存等。
摘要
本项目展示了如何使用 CSLA DynamicRootList
(或 EditableRootListBase
)作为主列表对象来创建一个主/详细 DataGridView
。如果您使用 DynamicRootList
作为主列表,则自动保存是标准功能。本项目还展示了如何在详细列表中实现自动保存。另外,您还可以对这两个列表进行排序。
在第一部分中,我们解释了问题,讨论了一些背景知识,分析了用例并概述了数据库和业务对象设计。在第二部分中,我们了解了 CslaGen 代码生成的细节,父子对象绑定的秘诀以及生成代码所需进行的更改。
9. 处理 DataGridView
需要明确的是,如果没有 DataGridView FAQ 的帮助,我认为我无法完成这个项目。这个 FAQ 是由 DataGridView
项目经理 Mark Rideout 指出的。在他的 MSDN 博客上,您可以找到大量关于 DGV
的有用信息和示例。
9.1. 粘滞模式:粘滞还是置顶?
在 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
确实是为使用数据表而不是业务对象而优化的。
上面的私有字段是解决方案的一部分:_thisMaster
是 BrandColl
类型,而 _currentMasterItem
是 Brand
类型。我发现最大的问题之一是当我们围绕主 DataGridView
移动光标时,如何使 _currentMasterItem
与当前行保持同步。
9.3. 绑定部分
所有处理绑定源的代码都在 DisplaySortedMaster
和 DisplaySortedDetail
中。这些方法非常相似:
- 关闭尽可能多的事件处理
- 取消绑定源
- 获取数据并进行排序
- 将排序后的数据分配给绑定源并重置绑定
- 开启正常的事件处理
UnbindBindingSource
方法是一个重要的辅助方法,您可能从 ProjectTracker 中知道它。CreateMasterItem
是一个用于创建新的主对象(带有空的详细列表)并显示一个空的 detailDGV
的方法。
9.4. 主 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 事件处理
作为对抗无聊的斗争的一部分,我们将避免冗余代码,仅展示在主 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