同一表单上的三个主/明细相关网格






4.05/5 (7投票s)
一种在子表中创建主从详细信息网格并显示父列的方法。
引言
这是一个在同一窗体上实现主从详细信息DataGridView
的工作示例。有三个实体处于父子关系中:部门 (Department)、组 (Group)、学生 (Student)。部门包含组,组包含学生。组名在部门范围内是唯一的,但在整个大学范围内不是唯一的。有时,学生可能会从一个组转移到另一个组,甚至从一个部门转移到另一个部门。因此,如果管理员能够在更改学生的组时,明确指定每个部门的组,那将非常方便。我决定将组名(代码)扩展为显式组名 = 部门缩写 + 组名(代码)。当学生按当前组过滤并在学生网格中显示时,以及按当前部门过滤组并在组网格中显示时,对用户来说也会很方便。用户可以使用网格中的组合框更改组的部门和学生的组。所有这些点都已在本文中涵盖。
背景
我们的方法使用了 ADO.NET 对象:DataSet
、DataTable
、DataRelation
、DataColumn
和 DataRow
。以及这些 WinForms 对象:DataGridView
、DataGridViewTextBoxColumn
、DataGridViewComboBoxColumn
。还使用了 BindingSource
。
Using the Code
首先,我们在窗体上创建三个数据网格以及相应的绑定源(请参见下图)。
我们分别将它们命名为 gridDepartments
、gridGroups
和 gridStudents
。然后,我们创建一些存储来保存窗体数据。出于这些目的,我更喜欢 System.Data.DataSet
而不是任何其他对象,因为它文档齐全且我非常熟悉。而且,更重要的是,它具有关系性。因此,我们使用 System.Data
命名空间中的内容,创建了一个存储三个实体和它们之间两个关系的数据结构。
这是声明
/// <summary>
/// Data set that collect all form data tables, relations
/// </summary>
private DataSet FormData = new DataSet();
/// <summary>
/// Relation that links department (parent) and group (child) entities
/// </summary>
private DataRelation DepartmentGroupRelation;
/// <summary>
/// Relation that links group (parent) and student (child) entities
/// </summary>
private DataRelation GroupStudentRelation;
/// <summary>
/// Department entity
/// </summary>
private DataTable DepartmentTable = new DataTable(DEPARTMENT_TABLE);
/// <summary>
/// Group entity
/// </summary>
private DataTable GroupTable = new DataTable(GROUP_TABLE);
/// <summary>
/// Student entity
/// </summary>
private DataTable StudentTable = new DataTable(STUDENT_TABLE);
这是实现
//load tables' schema from DataBase, this stuff just calls
//DataAdapter.FillSchema method with target DataTable
//and SchemaType.Source paremeters
base.LoadFormDataSchema(DepartmentTable, DepartmentCmds);
base.LoadFormDataSchema(GroupTable, GroupCmds);
base.LoadFormDataSchema(StudentTable, StudentCmds);
//add tables to data set
FormData.Tables.AddRange(new DataTable[] { DepartmentTable,
GroupTable, StudentTable });
//add relations between tables
DepartmentGroupRelation = FormData.Relations.Add(DEPARTMENT_GROUP_RELATION,
DepartmentTable.Columns[DEPARTMENT_ID],
GroupTable.Columns[GROUP_DEPARTMENT], true);
GroupStudentRelation = FormData.Relations.Add(GROUP_STUDENT_RELATION,
GroupTable.Columns[GROUP_ID],
StudentTable.Columns[STUDENT_GROUP], false);
要了解有关 EntityEditorForm.LoadFormDataSchema(DataTable, ISelectCommandProvider)
的更多信息,您可以参考我关于数据表编辑框架的文章。
下一行代码解决了组的“宽名称”问题。我们使用一个由组名和部门缩写组成的虚拟列。部门缩写是从父部门表中获取的,使用的是 DEPARTMENT_GROUP_RELATION
关系。我更喜欢这种解决方案,而不是使用额外的表字段。因为额外的字段会引起数据完整性问题(例如,组缩写可能会被更改)。
//add virtual columns
DataColumn wideName = GroupTable.Columns.Add(GROUP_WIDENAME, typeof(string),
"Parent(" + DEPARTMENT_GROUP_RELATION + ")." +
DEPARTMENT_ABBREVIATION + " + ' ' + " + GROUP_CODE);
wideName.ColumnMapping = MappingType.Hidden;
接下来,我们进行网格初始化。它包括数据绑定和网格列安装。这是主要技巧所在。BindingSource
对象实现了当前表行管理器,并且可以分配给 DataGridView.DataSource
。此外,DataGridView.DataMember
属性可以设置为关系名称。结果是,网格显示了绑定源中当前行的所有子项,使用了数据成员关系。每次 BindingSource
中的当前行更改时,网格就会显示下一个父项的子项。以下代码实现了此功能
// data binding.
//department binding data source
bsrcDepartments.DataSource = FormData;
bsrcDepartments.DataMember = DEPARTMENT_TABLE;
gridDepartments.DataSource = bsrcDepartments;
// We set bsrcDepartment (bounding source object) as data source to make
// groups list filtered by current department in gridGroups
bsrcGroups.DataSource = bsrcDepartments;
bsrcGroups.DataMember = DEPARTMENT_GROUP_RELATION;
gridGroups.DataSource = bsrcGroups;
// Again we set bsrcGroup (bounding source object) as data source to make
// students list filtered by current group in gridGroups
bsrcStudents.DataSource = bsrcGroups;
bsrcStudents.DataMember = GROUP_STUDENT_RELATION;
gridStudents.DataSource = bsrcStudents;
现在是时候向我们的网格添加列了。但在数据绑定之前,我们必须禁用网格列的自动生成。因此,在 //data binding
行之前插入以下代码行
gridDepartments.AutoGenerateColumns =
gridGroups.AutoGenerateColumns =
gridStudents.AutoGenerateColumns = false;
我们向每个网格添加列(您可以在文章源代码中找到完整代码)。
//add columns manually
AddDepartmentGridColumns(gridDepartments);
AddGroupGridColumns(gridGroups);
AddStudentGridColumns(gridStudents);
在这里,我们只显示这些方法的分离部分。这是 ID 列的添加
DataGridViewTextBoxColumn idColumn = new DataGridViewTextBoxColumn();
idColumn.Name = idColumn.DataPropertyName = DEPARTMENT_ID;
idColumn.HeaderText = DEPARTMENT_ID; // [TEXT]
idColumn.ValueType = typeof(Int64);
idColumn.Frozen = true;
idColumn.Visible = true;
idColumn.ReadOnly = true;
grid.Columns.Add(idColumn);
使用 idColumn.ReadOnly = true;
代码行将其设置为只读。然后,这是带有组合框的列,允许选择一个特定的组。组合框使用 GroupTable
数据表的表达式列来显示用于选择的“宽”组名。
// this is combobox column for wide group name changing. Wide group name consists
// of department abbreviation and group title to distinguish groups with same title
// ("first group", for instance) of different departments. In that way user takes
// possibility to change department of certain student, not group only, in more
// understandable manner.
DataGridViewComboBoxColumn groupColumn = new DataGridViewComboBoxColumn();
groupColumn.Name = groupColumn.DataPropertyName = STUDENT_GROUP;
groupColumn.HeaderText = STUDENT_GROUP; // [text]
groupColumn.DataSource = GroupTable;
groupColumn.DisplayMember = GROUP_WIDENAME;
groupColumn.ValueMember = GROUP_ID;
groupColumn.Name = GROUP_WIDENAME;
gridStudents.Columns.Add(groupColumn);
让我们详细解析这段代码块。
groupColumn.DataPropertyName = STUDENT_GROUP;
:列的值取决于STUDENT_GROUP
表字段。反之,所有组合框值的更改都会反映在STUDENT_GROUP
字段上(列名为“Group”,Int64
类型,是GroupTable
中行的 ID 引用)。groupColumn.DataSource = GroupTable;
:此属性指示组合框要显示的字符串的来源。groupColumn.ValueMember = GROUP_ID;
:这指示数据源中将用于与DataPropertyName
字段值进行比较以查找对应关系的键列名。groupColumn.DisplayMember = GROUP_WIDENAME;
:它指示组合框将从数据源中获取列名,以呈现由ValueMember
属性确定的键值的显示字符串。
关于行 ID。主从详细信息关系中的链接由一对键确保:主键(父实体)和外键(子实体)。在编辑模式下(当数据仅存在于单个工作站的内存中时),使用负的自增数字作为 ID 是一种常见做法。这确保了编辑模式下新行的 ID 的原创性,并可以避免过多的服务器调用。并且,当行插入数据库时,行会获得一个真实的 ID。也就是说,行键会改变。但是,这对于父子关系是不可容忍的。您是否使用 GUID ID 并不重要,但我偏爱传统的数字 ID 而非 GUID。基于数字 ID 的数据库比基于 GUID 的数据库(GUID 大小为 16 字节,而数字为 4 字节)大四倍。因此,我们在创建行时为其赋予真实值。
//add originality support
//we have to do this to support master-details relations
//between three tables.
DepartmentTable.TableNewRow +=
new DataTableNewRowEventHandler(SomeTable_TableNewRow);
GroupTable.TableNewRow +=
new DataTableNewRowEventHandler(SomeTable_TableNewRow);
StudentTable.TableNewRow +=
new DataTableNewRowEventHandler(SomeTable_TableNewRow);
...
/// <summary>
/// Auto assignment of new row's fields
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void SomeTable_TableNewRow(object sender, DataTableNewRowEventArgs e)
{
DetermineRowID(e.Row, 0, ((DataTable)sender).TableName);
}
/// <summary>
/// gives id for row
/// </summary>
/// <param name="row">row that requires id</param>
/// <param name="idColumnIndex">id column index</param>
/// <param name="entityName">entity name</param>
/// <remarks>
/// It determines undetermined rows only skipping determined
/// (id is equal or more than zero or is null)
/// </remarks>
protected static void DetermineRowID(DataRow row,
int idColumnIndex, string tableName)
{
Int64 idCurrent = (DBNull.Value == row[idColumnIndex]) ? -1 :
Convert.ToInt64(row[idColumnIndex]);
if (idCurrent < 0)
//it must be 'undefined'. use SetupVirtualIDColumn
{
row[idColumnIndex] = SequenceNumberManager.GetNextID(
tableName,
DataLayer.DataLayer.DefaultDataSource);
}
}
SequenceNumberManeger.GetNextID
使用默认数据源和其中的存储过程来获取实体的空闲 ID。这个存储过程通过每次调用增加实体当前的 ID 来返回一个绝对唯一的 ID。
IF EXISTS (SELECT * FROM dbo.sysobjects
WHERE id = object_id(N'[dbo].[GetNextID]') AND
OBJECTPROPERTY(id, N'IsProcedure') = 1)
DROP PROCEDURE [dbo].[GetNextID]
RETURN
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author: Major League
-- Create date: 14/09/2009
-- Description: gets sequence number for table
-- =============================================
CREATE
--ALTER
PROCEDURE [dbo].[GetNextID]
-- Table name
@TableName varchar(50)
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Declare the return variable here
DECLARE @NextID bigint; --Return value
--if table does not exist we just return NULL. It must exist.
IF EXISTS(SELECT * FROM dbo.sequence WHERE
table_name = @TableName)
BEGIN
-- get next sequence number
SELECT @NextID = (sequence_value + 1) FROM
dbo.sequence WHERE table_name = @TableName;
UPDATE dbo.sequence SET sequence_value = @NextID
WHERE table_name = @TableName;
END
ELSE
BEGIN
-- set next sequence to first number
SET @NextID = 1;
INSERT INTO sequence(table_name, sequence_value)
VALUES(@TableName, @NextID);
END
-- Return the result of the function
SELECT @NextID
END
GO
至此,我们有了一个具有三个主从详细信息相关网格的窗体。
结论
欢迎对文章主题发表任何评论、意见和建议!
历史
- 2009 年 9 月 23 日:初版。