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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.05/5 (7投票s)

2009年9月23日

CPOL

5分钟阅读

viewsIcon

54896

downloadIcon

3665

一种在子表中创建主从详细信息网格并显示父列的方法。

引言

这是一个在同一窗体上实现主从详细信息DataGridView的工作示例。有三个实体处于父子关系中:部门 (Department)、组 (Group)、学生 (Student)。部门包含组,组包含学生。组名在部门范围内是唯一的,但在整个大学范围内不是唯一的。有时,学生可能会从一个组转移到另一个组,甚至从一个部门转移到另一个部门。因此,如果管理员能够在更改学生的组时,明确指定每个部门的组,那将非常方便。我决定将组名(代码)扩展为显式组名 = 部门缩写 + 组名(代码)。当学生按当前组过滤并在学生网格中显示时,以及按当前部门过滤组并在组网格中显示时,对用户来说也会很方便。用户可以使用网格中的组合框更改组的部门和学生的组。所有这些点都已在本文中涵盖。

背景

我们的方法使用了 ADO.NET 对象:DataSetDataTableDataRelationDataColumnDataRow。以及这些 WinForms 对象:DataGridViewDataGridViewTextBoxColumnDataGridViewComboBoxColumn。还使用了 BindingSource

Using the Code

首先,我们在窗体上创建三个数据网格以及相应的绑定源(请参见下图)。

Design mode. Form with three DataGridView objects on it.

我们分别将它们命名为 gridDepartmentsgridGroupsgridStudents。然后,我们创建一些存储来保存窗体数据。出于这些目的,我更喜欢 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 日:初版。
同一窗体上的三个主从详细信息相关网格 - CodeProject - 代码之家
© . All rights reserved.