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

数据库表编辑器框架

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.43/5 (4投票s)

2009年9月23日

CPOL

6分钟阅读

viewsIcon

29270

downloadIcon

738

本文包含一个数据层和用于编辑数据库表的窗体。

引言

本文讨论的是如何将数据从数据库检索到应用程序,并将其呈现给用户进行查看和修改,然后将修改后的数据从应用程序写回数据库。数据处理的通用流程包含以下几个部分(此划分是相对的)

  • 存储过程。服务器端逻辑。这是一组标准的存储过程,用于将数据作为表从数据库中选择,进行新行的插入,更新已更正字段的更改,以及从表中删除行。如果实体是只读的,则只需要一个用于行选择的存储过程。
  • 实体编辑器窗体。图形用户界面。一个带有网格的窗体,允许以表格形式显示数据并进行修改。该窗体必须支持“脏”标志、刷新和保存按钮、异常处理,并告知用户有关更改的信息。任何时候必须只有一个窗体正在编辑数据,以避免冲突。因此,每个实体都必须实现一个单例窗体。
  • 数据层。支持连接挂载、实体修改管理以及内存中数据存储的对象。这是物理数据存储和 Windows 窗体表示之间的中间层。

每个开发人员都有自己的处理方式。一个足够大的项目包含了所有这些。它必须易于理解,并且能够灵活地应用于不同情况。这是我处理数据库实体的一种通用逻辑。

使用代码

简单查看器

您可以仅使用此工具来查看数据库表在特定时间的状态。要向应用程序添加简单的实体查看器,请执行以下步骤:

  1. 创建一个用于选择的存储过程
  2. 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
  3. 创建一个实现 ISelectCommandProvider 接口的类。它初始化实体选择的命令。设置存储过程名称和命令类型
  4. 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;
            }
        }
    }
  5. 创建一个派生窗口窗体,其中包含一个 DataGridView 和一个按钮。网格用于数据可视化,按钮用于从数据源更新数据。在窗体底部放置一个状态栏。您将看到类似下图的内容
  6. Form with Grid and Refesh button on it

    然后,选择“查看代码”并添加几行代码

    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 
            }
        }
    }
  7. 在某处添加一个对我们窗体创建器的调用
  8. /// <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,则需要这样做。

实体编辑窗体

创建实体编辑窗体并不难。步骤与创建查看器类似

  1. 这里是用于 Select、Insert、Update 和 Delete 的四个存储过程
  2. 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
  3. 创建一个 SqlCommandsProviderBase 类的派生类,该类实现 ISIUDCommandsProvider 接口的通用逻辑。在派生类中,您必须实现默认构造函数,该构造函数调用基类的构造函数,并将实体名称作为参数传递。基类构造函数将前缀“Select”连接到实体名称以创建 SELECT 存储过程,将前缀“Insert”连接到实体名称以创建 INSERT 存储过程,将前缀“Update”用于 UPDATE,将“Delete”用于 DELETE。例如,如果您的实体名称是Subject,则数据库中必须有以下存储过程:SelectSubjectInsertSubjectUpdateSubjectDeleteSubject。您还必须实现一个方法。这是一个受保护的重写方法,它接受 DbParameterCollection 作为参数,并根据实体字段对其进行初始化(您可以调用基类方法来初始化 ID 字段)。下面是一个示例
  4. 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"));
            }
    
        }
  5. 然后,您创建一个用于实体修改的 GUI。这是一个带有 DataGridView 和用于保存更改以及从数据库刷新新数据的 Buttons 的窗体。它与之前的窗体类似,只是多了一个 Save 按钮

    Form with DataGridView on it and 'Save' button and 'Refresh' button

  6. 接下来,我们需要为创建的窗体添加代码。这是构造函数,在其中初始化 EntityEditorForm 属性,重写的方法 InitGrid() 初始化网格(网格数据绑定和添加网格列)
  7. 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);
            }
        }
    }
  8. 现在,在您的代码中添加一个对窗体创建的调用。这与查看器的情况类似
  9. /// <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

此序列中的每个方法都执行其加载例程的相应部分。如果您想更改某些内容,可以简单地覆盖相应的方法。

我还需要描述另一个特性。所有方法 SaveFormDataLoadFormDataLoadFormDataSchema 本身都会处理它们内部发生的异常,除非它们在成对调用的 BeginLongOperationEndLongOperation 中,在这种情况下,异常会继续传播。我这样做的目的是允许这些方法显示错误消息,然后终止加载过程。当需要保存两个或更多实体时(窗体上有三个网格的示例,请参阅源代码),可以利用此功能

/// <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。实体编辑窗体的基窗体。此窗体包含执行加载过程、处理保存和刷新数据的通用逻辑。它处理窗体关闭,在更改后正确提示用户保存。它提供窗体数据上下文以及 LoadFormDataSaveFormData 方法的实现。以及许多其他功能。
  • SqlCommandsProviderBase。实现所有基于存储过程的命令提供者的通用逻辑的基类。
  • SqlDataSource。SQL 数据源。它实现了 IDataTableLoader/IDataTableSaver。它包含数据适配器逻辑:创建、填充和更新数据库表。

历史

  • 2009/09/22 - 首次创建。
© . All rights reserved.