数据库表编辑器框架






4.43/5 (4投票s)
本文包含一个数据层和用于编辑数据库表的窗体。

引言
本文讨论的是如何将数据从数据库检索到应用程序,并将其呈现给用户进行查看和修改,然后将修改后的数据从应用程序写回数据库。数据处理的通用流程包含以下几个部分(此划分是相对的)
- 存储过程。服务器端逻辑。这是一组标准的存储过程,用于将数据作为表从数据库中选择,进行新行的插入,更新已更正字段的更改,以及从表中删除行。如果实体是只读的,则只需要一个用于行选择的存储过程。
- 实体编辑器窗体。图形用户界面。一个带有网格的窗体,允许以表格形式显示数据并进行修改。该窗体必须支持“脏”标志、刷新和保存按钮、异常处理,并告知用户有关更改的信息。任何时候必须只有一个窗体正在编辑数据,以避免冲突。因此,每个实体都必须实现一个单例窗体。
- 数据层。支持连接挂载、实体修改管理以及内存中数据存储的对象。这是物理数据存储和 Windows 窗体表示之间的中间层。
每个开发人员都有自己的处理方式。一个足够大的项目包含了所有这些。它必须易于理解,并且能够灵活地应用于不同情况。这是我处理数据库实体的一种通用逻辑。
使用代码
简单查看器
您可以仅使用此工具来查看数据库表在特定时间的状态。要向应用程序添加简单的实体查看器,请执行以下步骤:
- 创建一个用于选择的存储过程
- 创建一个实现 ISelectCommandProvider接口的类。它初始化实体选择的命令。设置存储过程名称和命令类型
- 创建一个派生窗口窗体,其中包含一个 DataGridView和一个按钮。网格用于数据可视化,按钮用于从数据源更新数据。在窗体底部放置一个状态栏。您将看到类似下图的内容
- 在某处添加一个对我们窗体创建器的调用
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:        Major League
-- Create date: 14/09/2009
-- Description:    gets sequence number for table
-- =============================================
IF EXISTS (SELECT * FROM dbo.sysobjects WHERE 
   id = object_id(N'[dbo].[SelectSequence]') AND 
   OBJECTPROPERTY(id, N'IsProcedure') = 1)
DROP PROCEDURE [dbo].[SelectSequence]
RETURN
GO
CREATE 
--ALTER
PROCEDURE [dbo].[SelectSequence] 
AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;
    --select sequence
    SELECT 
        table_name AS [TableName],
        sequence_value AS [SequenceValue]
    FROM
        dbo.sequence
END
GO
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DataModifier.DataLayer;    //ISelectCommandProvider
using System.Data.Common;        //DbCommand
namespace DataModifier.CommandProviders
{
    /// <summary>
    /// Commands provider for Sequence number
    /// </summary>
    public class SequenceNumberCmdsProvider : ISelectCommandProvider
    {
        /// <summary>
        /// Inits select command
        /// </summary>
        /// <param name="command">command to init</param>
        public void InitSelectCommand(DbCommand command)
        {
            command.CommandText = "SelectSequence";
            command.CommandType = System.Data.CommandType.StoredProcedure;
        }
    }
}

然后,选择“查看代码”并添加几行代码
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using DataModifier.CommandProviders;   //SequenceNumberCmdsProvider
namespace DataModifier.Forms
{
    /// <summary>
    /// Form that shows last values of sequence numbers
    /// </summary>
    public partial class SequenceForm : EntityEditorForm
    {
        /// <summary>
        /// Default .ctor
        /// </summary>
        public SequenceForm()
        {
            InitializeComponent();
            //set refresh button
            RefreshButton = btnRefresh;
            // initialze data objects
            currContext.DataTable = new DataTable();
            currContext.SelectCommandProvider = new SequenceNumberCmdsProvider();
        }
        /// <summary>
        /// Inits grid
        /// </summary>
        protected override void InitGrid()
        {
            gridSequence.AutoGenerateColumns = true;
            gridSequence.ReadOnly = true;
            bsrcSequence.DataSource = currContext.DataTable;
            gridSequence.AllowUserToAddRows = false;    //hide new row line
            gridSequence.AllowUserToDeleteRows = false;    //disable row deleting 
        }
    }
}
/// <summary>
/// Opens form with current sequence numbers
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnShowIdentifiers_Click(object sender, EventArgs e)
{
    //be only one
    SequenceForm.OpenForm(typeof(SequenceForm), this);
}
实际上就这些了。现在编译代码,按下按钮,然后享受。
还有一件事。我们必须禁用列 ID 的初始化,因为“sequence”实体没有 ID。我们可以通过覆盖初始化方法来做到这一点
/// <summary>
/// Setups virtual id column.
/// </summary>
/// <param name="column">column to make id column</param>
/// <remarks>calls from LoadDataSchema()</remarks>
protected override void SetupVirtualIDColumn(DataColumn column)
{
    //disable virtual id column initialization
    //because sequence table does not contain it.
}
如果您的实体没有整数列 ID,则需要这样做。
实体编辑窗体
创建实体编辑窗体并不难。步骤与创建查看器类似
- 这里是用于 Select、Insert、Update 和 Delete 的四个存储过程
- 创建一个 SqlCommandsProviderBase类的派生类,该类实现ISIUDCommandsProvider接口的通用逻辑。在派生类中,您必须实现默认构造函数,该构造函数调用基类的构造函数,并将实体名称作为参数传递。基类构造函数将前缀“Select”连接到实体名称以创建SELECT存储过程,将前缀“Insert”连接到实体名称以创建INSERT存储过程,将前缀“Update”用于UPDATE,将“Delete”用于DELETE。例如,如果您的实体名称是Subject,则数据库中必须有以下存储过程:SelectSubject、InsertSubject、UpdateSubject、DeleteSubject。您还必须实现一个方法。这是一个受保护的重写方法,它接受DbParameterCollection作为参数,并根据实体字段对其进行初始化(您可以调用基类方法来初始化 ID 字段)。下面是一个示例
- 然后,您创建一个用于实体修改的 GUI。这是一个带有 DataGridView和用于保存更改以及从数据库刷新新数据的Buttons 的窗体。它与之前的窗体类似,只是多了一个 Save 按钮 
- 接下来,我们需要为创建的窗体添加代码。这是构造函数,在其中初始化 EntityEditorForm属性,重写的方法InitGrid()初始化网格(网格数据绑定和添加网格列)
- 现在,在您的代码中添加一个对窗体创建的调用。这与查看器的情况类似
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:        Major League
-- Create date: 19/09/2009
-- Description:    gets subjects
-- =============================================
IF EXISTS (SELECT * FROM dbo.sysobjects WHERE 
   id = object_id(N'[dbo].[SelectSubject]') AND 
   OBJECTPROPERTY(id, N'IsProcedure') = 1)
DROP PROCEDURE [dbo].[SelectSubject]
RETURN
GO
CREATE 
--ALTER
PROCEDURE [dbo].[SelectSubject] 
AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;
    --select subjects
    SELECT 
        subject_id AS [ID],
        subject_name AS [Name],
        subject_available AS [Available]
    FROM
        dbo.[subject]
END
GO
-- =============================================
-- Author:        Major League
-- Create date: 19/09/2009
-- Description:    updates subjects
-- =============================================
IF EXISTS (SELECT * FROM dbo.sysobjects 
WHERE id = object_id(N'[dbo].[UpdateSubject]') AND 
      OBJECTPROPERTY(id, N'IsProcedure') = 1)
DROP PROCEDURE [dbo].[UpdateSubject]
RETURN
GO
CREATE 
--ALTER
PROCEDURE [dbo].[UpdateSubject] 
    @ID bigint,
    @Name varchar(50),
    @Available bit,
    @Original_ID bigint
AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;
    UPDATE dbo.[subject]
    SET
        subject_id = @ID,
        subject_name = @Name,
        subject_available = @Available
    WHERE
        subject_id = @Original_ID
END
GO
-- =============================================
-- Author:        Major League
-- Create date: 19/09/2009
-- Description:    inserts subjects
-- =============================================
IF EXISTS (SELECT * FROM dbo.sysobjects 
WHERE id = object_id(N'[dbo].[InsertSubject]') AND 
      OBJECTPROPERTY(id, N'IsProcedure') = 1)
DROP PROCEDURE [dbo].[InsertSubject]
RETURN
GO
CREATE 
--ALTER
PROCEDURE [dbo].[InsertSubject] 
    @ID bigint,
    @Name varchar(50),
    @Available bit
AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;
    INSERT INTO dbo.[subject](
        subject_id,
        subject_name,
        subject_available
    )
    VAlUES(
        @ID,
        @Name,
        @Available
    )
END
GO
-- =============================================
-- Author:        Major League
-- Create date: 19/09/2009
-- Description:    deletes subjects
-- =============================================
IF EXISTS (SELECT * FROM dbo.sysobjects 
WHERE id = object_id(N'[dbo].[DeleteSubject]') AND 
      OBJECTPROPERTY(id, N'IsProcedure') = 1)
DROP PROCEDURE [dbo].[DeleteSubject]
RETURN
GO
CREATE 
--ALTER
PROCEDURE [dbo].[DeleteSubject] 
    @Original_ID bigint
AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;
    DELETE 
    FROM
        dbo.[subject]
    WHERE
        subject_id = @Original_ID
END
GO
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;        //SqlDbTypes
using System.Data.Common;        //DbParameterCollection
using System.Data.SqlClient;        //SqlParameter
namespace DataModifier.CommandProviders
{
    /// <summary>
    /// Provides commands to select, update, insert and delete 
    /// Subject entities
    /// </summary>
    public class SubjectCmdsProvider : SqlCommandsProviderBase
    {
        /// <summary>
        /// .ctor
        /// </summary>
        public SubjectCmdsProvider()
            : base ("Subject")
        {
        }
        /// <summary>
        /// Sets command parameters
        /// </summary>
        /// <param name="parameters">command parameters</param>
        protected override void AddAllColumns(DbParameterCollection parameters)
        {
            base.AddAllColumns(parameters);        // add ID column
            /*
            @Name varchar(50),
            @Available bit,
             */
            parameters.Add(new SqlParameter("Name", 
                           SqlDbType.VarChar, 50, "Name"));
            parameters.Add(new SqlParameter("Available", 
                           SqlDbType.Bit, 0, "Available"));
        }
    }
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using DataModifier.CommandProviders; //SubjectCmdsProvider
namespace DataModifier.Forms
{
    /// <summary>
    /// Form to edit subjects
    /// </summary>
    public partial class SubjectForm : EntityEditorForm
    {
        private const string SUBJECT_TABLE = "Subject";
        private const string SUBJECT_ID = "ID";
        private const string SUBJECT_NAME = "Name";
        private const string SUBJECT_AVAILABLE = "Available";
        /// <summary>
        /// Default .ctor
        /// </summary>
        public SubjectForm()
        {
            InitializeComponent();
            //set entity editor buttons
            SavingButton = btnSave;
            RefreshButton = btnRefresh;
            MessageArea = lblMessageArea;
            //set from data objects
            currContext.DataTable = new DataTable(SUBJECT_TABLE);
            currContext.CommandsProvider = new SubjectCmdsProvider();
        }
        /// <summary>
        /// Init grid to edit subjects
        /// </summary>
        protected override void InitGrid()
        {
            gridSubjects.AutoGenerateColumns = false;
            
            //data binding
            gridSubjects.DataSource = currContext.DataTable;
            //add columns to grid
            DataGridViewTextBoxColumn colmnID = new DataGridViewTextBoxColumn();
            colmnID.Name = colmnID.DataPropertyName = SUBJECT_ID;
            colmnID.HeaderText = SUBJECT_ID;        //[TEXT]
            colmnID.ValueType = typeof(Int64);
            colmnID.ReadOnly = true;
            gridSubjects.Columns.Add(colmnID);
            DataGridViewTextBoxColumn colmnName = new DataGridViewTextBoxColumn();
            colmnName.Name = colmnName.DataPropertyName = SUBJECT_NAME;
            colmnName.HeaderText = "Subject";        //[TEXT]
            colmnName.ValueType = typeof(string);
            gridSubjects.Columns.Add(colmnName);
            DataGridViewCheckBoxColumn colmnAvailable = new DataGridViewCheckBoxColumn();
            colmnAvailable.Name = colmnAvailable.DataPropertyName = SUBJECT_AVAILABLE;
            colmnAvailable.HeaderText = SUBJECT_AVAILABLE;        //[TEXT]
            colmnAvailable.ValueType = typeof(Boolean);
            gridSubjects.Columns.Add(colmnAvailable);
        }
    }
}
/// <summary>
/// Opens form to edit subjects
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnSubjects_Click(object sender, EventArgs e)
{
    //be only one
    SubjectForm.OpenForm(typeof(SubjectForm), this);
}
在同一个窗体中编辑两个或更多实体
当您打算在同一个窗体中编辑两个或更多实体时,您需要进行一些调整。除了为实体编辑窗体所做的一切之外,您还必须实现标准加载例程的每个步骤和 SaveFormData() 方法。为了说明我的意思,我引用了 ExecuteLoadingConsecution() 方法的主体,该方法在每次打开实体编辑器窗体时都会被调用
LoadFormDataSchema();
InitGrid(); //in general data displayed with aid of grid
LoadFormData();
SubscribeToEvents();
//do this at the end to avoid excessive events handling
此序列中的每个方法都执行其加载例程的相应部分。如果您想更改某些内容,可以简单地覆盖相应的方法。
我还需要描述另一个特性。所有方法 SaveFormData、LoadFormData 和 LoadFormDataSchema 本身都会处理它们内部发生的异常,除非它们在成对调用的 BeginLongOperation 和 EndLongOperation 中,在这种情况下,异常会继续传播。我这样做的目的是允许这些方法显示错误消息,然后终止加载过程。当需要保存两个或更多实体时(窗体上有三个网格的示例,请参阅源代码),可以利用此功能
/// <summary>
/// Saves form data
/// </summary>
protected override void SaveFormData()
{
    // We want to terminate saving if any exception occurs. This is 
    // possible with BeginLongOperation()/EndLongOperation logic.
    BeginLongOperation();
    try
    {
        base.SaveFormData(DepartmentTable, DepartmentCmds);
        base.SaveFormData(GroupTable, GroupCmds);
        base.SaveFormData(StudentTable, StudentCmds);
        Changed = false;
    }
    catch    //we want just to catch exception. All messages are displayed already 
    {
    }
    finally
    {
        EndLongOperation();
    }
}
如果任何表的保存失败,则保存过程将终止。
请注意,因为 LoadFormData() 而不是 SaveFormData(),它会单独调用,也会从 BeginLongOperation/EndLongOperation 对中调用。因此,它必须在发生异常时传播异常。我将通过一个方法示例来说明我的意思(来自同一个窗体上有三个网格的示例)
/// <summary>
/// Loads form data
/// </summary>
protected override void LoadFormData()
{
    BeginLongOperation();
    try
    {
        base.LoadFormData(DepartmentTable, DepartmentCmds);
        base.LoadFormData(GroupTable, GroupCmds);
        base.LoadFormData(StudentTable, StudentCmds);
        Changed = false;
        EndLongOperation();
    }
    catch (Exception ex)
    {
        EndLongOperation();            //terminate loading long operation first
        TransmitException(ex);
    }
}
有关如何使用此工具创建包含两个或更多网格的窗体的更多信息,请参阅源代码。
关键框架元素
本段包含允许轻松创建实体编辑窗体的元素描述。
- AppBaseForm。所有应用程序窗体的基窗体,包含 Windows 窗体单例逻辑。当您编辑实体时,为每个实体只有一个窗体很重要,以避免冲突。此逻辑实现了此原则。
- EntityEditorForm。实体编辑窗体的基窗体。此窗体包含执行加载过程、处理保存和刷新数据的通用逻辑。它处理窗体关闭,在更改后正确提示用户保存。它提供窗体数据上下文以及- LoadFormData和- SaveFormData方法的实现。以及许多其他功能。
- SqlCommandsProviderBase。实现所有基于存储过程的命令提供者的通用逻辑的基类。
- SqlDataSource。SQL 数据源。它实现了- IDataTableLoader/IDataTableSaver。它包含数据适配器逻辑:创建、填充和更新数据库表。
历史
- 2009/09/22 - 首次创建。


