数据库表编辑器框架






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
和用于保存更改以及从数据库刷新新数据的Button
s 的窗体。它与之前的窗体类似,只是多了一个 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 - 首次创建。