BindingSource、事务沙盒以及添加前的预添加和后添加模式





5.00/5 (9投票s)
对不同数据输入模式以及事务沙盒的需求进行调查。
引言
这是关于讨论 DataTable
事务的系列文章中的第三篇。前两篇是
在本文中,我想探讨 .NET 2.0 新的 BindingSource
类以及预/后添加操作之间的复杂性。但首先...
重构
- 修正了“未提交”的拼写错误。
- 向事务记录器添加了一个
RowAdded
事件,该事件在行实际添加到DataTable
的行集合时触发。 DataTableSynchronizationManager
现在会记住添加到事务日志中的第一个非同步记录。这可以防止对先前事务进行重新同步。SynchronizationManager
现在支持将主键值传递给重载的GetTransactions
方法。TransactionRecordPacket
可以使用外部提供的 PK 值进行初始化。
预添加
典型的 UI 有一个“添加”按钮,该按钮会打开一个 UI,允许用户输入各种字段。单击“确定”通常会调用 DataTable
的 AcceptChanges
方法,或者,如果使用 BindingSource
类,则调用 EndEdit
方法。如果用户单击“取消”,则可以调用 RejectChanges
或 CancelEdit
方法。例如,给定以下对象图初始化
一个 DataTable
<data:DataTable def:Name="dataTable" TableName="PersonInfo">
<Columns>
<data:DataColumn ColumnName="PK" DataType="System.Guid"/>
<data:DataColumn ColumnName="LastName" DataType="System.String"/>
<data:DataColumn ColumnName="FirstName" DataType="System.String"/>
<data:DataColumn ColumnName="Address" DataType="System.String"/>
<data:DataColumn ColumnName="City" DataType="System.String"/>
<data:DataColumn ColumnName="State" DataType="System.String"/>
<data:DataColumn ColumnName="Zip" DataType="System.String"/>
</Columns>
</data:DataTable>
以及代码(因为我很懒,不想编写扩展程序来处理 PrimaryKey
数组的烦恼,我希望 .NET 中集合的处理方式能更一致)
dataTable.PrimaryKey = new DataColumn[] { dataTable.Columns["PK"] };
DataView
<data:DataView def:Name="dataView" Table="{dataTable}"
Sort="LastName, FirstName"/>
BindingSource
<BindingSource def:Name="bindingSource" DataSource="{dataView}"/>
TransactionLogger
<cd:DataTableTransactionLog def:Name="dataLog"
SourceTable="{dataTable}"
TransactionAdded="{app.OnTransactionAdded}"
OnRowAdding="{app.OnRowAdding}"/>
以及具有必要数据绑定的输入窗体
<?xml version="1.0" encoding="utf-8"?>
<!-- (c) 2006 Marc Clifton All Rights Reserved -->
<MyXaml xmlns="System.Windows.Forms, System.Windows.Forms,
Version=2.0.0000.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
xmlns:data="System.Data, System.Data, Version=2.0.0000.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089"
xmlns:ctd="Clifton.Tools.Data, Clifton.Tools.Data"
xmlns:cwf="Clifton.Windows.Forms, Clifton.Windows.Forms"
xmlns:cd="Clifton.Data, Clifton.Data"
xmlns:def="Definition"
xmlns:ref="Reference">
<Form Name="PersonInfoDlg"
Text="Person Info"
ClientSize="570, 200"
MinimizeBox="false"
MaximizeBox="false"
StartPosition="CenterScreen"
AcceptButton="{btnOK}"
CancelButton="{btnCancel}">
<Controls>
<Label Location="10, 50" Size="100, 15" Text="Last Name:"/>
<TextBox Location="10, 65" Size="200, 20">
<DataBindings>
<cwf:DataBinding DataSource="{bindingSource}" PropertyName="Text"
DataMember="LastName"/>
</DataBindings>
</TextBox>
<Label Location="220, 50" Size="100, 15" Text="First Name:"/>
<TextBox Location="220, 65" Size="200, 20">
<DataBindings>
<cwf:DataBinding DataSource="{bindingSource}" PropertyName="Text"
DataMember="FirstName"/>
</DataBindings>
</TextBox>
<Label Location="10, 95" Size="100, 15" Text="Address:"/>
<TextBox Location="10, 110" Size="300, 20">
<DataBindings>
<cwf:DataBinding DataSource="{bindingSource}" PropertyName="Text"
DataMember="Address"/>
</DataBindings>
</TextBox>
<Label Location="10, 140" Size="100, 15" Text="City:"/>
<TextBox Location="10, 155" Size="100, 20">
<DataBindings>
<cwf:DataBinding DataSource="{bindingSource}" PropertyName="Text"
DataMember="City"/>
</DataBindings>
</TextBox>
<Label Location="120, 140" Size="60, 15" Text="State:"/>
<TextBox Location="120, 155" Size="60, 15">
<DataBindings>
<cwf:DataBinding DataSource="{bindingSource}" PropertyName="Text"
DataMember="State"/>
</DataBindings>
</TextBox>
<Label Location="200, 140" Size="60, 15" Text="Zip:"/>
<TextBox Location="200, 155" Size="60, 15">
<DataBindings>
<cwf:DataBinding DataSource="{bindingSource}"
PropertyName="Text" DataMember="Zip"/>
</DataBindings>
</TextBox>
<Button def:Name="btnOK" Location="450, 10" Size="80, 25"
Text="OK" Click="{app.OnOK}"/>
<Button def:Name="btnCancel" Location="450, 35" Size="80, 25"
Text="Cancel" Click="{app.OnCancel}"/>
</Controls>
</Form>
</MyXaml>
以下 C# 代码实例化对话框并处理对话框结果
protected void OnPreAdd(object sender, EventArgs e)
{
Parser p2 = new Parser();
p2.AddToCollection +=
new Parser.AddToCollectionDlgt(OnAddToCollection);
p2.AddOrUpdateReferences(p.References);
Form form = (Form)p2.Instantiate("personInfoDlg.myxaml", "*");
bindingSource.AddNew();
DialogResult res=form.ShowDialog();
if (res == DialogResult.OK)
{
bindingSource.EndEdit();
}
else
{
bindingSource.CancelEdit();
dataLog.CollectUncommittedRows();
// Refresh the transaction grid.
dgTransactions.DataSource = null;
dgTransactions.DataSource = dataLog.Log;
}
}
我们可以看到,在对话框中输入信息
会导致该行被添加到绑定到同一 BindingSource
的 DataGrid
中
预添加的关键在于这行代码
Form form = (Form)p2.Instantiate("personInfoDlg.myxaml", "*");
bindingSource.AddNew();
DialogResult res=form.ShowDialog();
幕后
- 在显示对话框之前添加了一个新行。
- 绑定到数据源字段的控件的属性会被清除。
DataTable
事务之所以发生,是因为控件的属性绑定到了DataTable
字段。- 绑定控件的更改会立即影响到绑定
DataTable
的相应字段。
奇怪的副作用
这是一个有趣的副作用:当我更改控件中的值时,底层 DataTable
会被更新,因此任何同样绑定到它的控件,例如网格,也会被更新。虽然这似乎并没有发生,但只需将编辑对话框移开它在主窗口中覆盖的数据网格即可。这会导致 DataGrid
刷新,然后,我正在编辑的数据就会突然出现!
这是一个问题。数据在实际“提交”之前就被更新到了主 DataTable
。如果您在例如搜索对话框中使用同一个 DataTable
,用户将看到他们正在更新的更改!如果您的应用程序支持无模式对话框,并且同样共享一个系统范围的 DataTable
,那么那些对话框将自动更新!这不是首选行为,肯定不是我喜欢的行为。
后添加
后添加操作将在数据输入*之后*添加行。通常,不使用数据绑定——没有 DataRow
实例供货币管理器使用,以便更新行的字段。相反,通常会创建一个行,用控件中的值填充它,然后将该行添加到 DataTable
实例。
我不想采用上述过程,而是想使用一种稍微不同的方法——使用我在前几篇文章中讨论过的 DataTableTransactionLog
。我不会将控件绑定到主 DataTable
,而是会克隆 DataTable
来获取结构,然后使用克隆的 DataTable
作为我的数据源。如果用户单击“确定”,则会同步事务。这在 OnPostAdd
事件中完成,并且需要对你在 OnPreAdd
事件中看到的内容进行一些小的修改。
protected void OnPostAdd(object sender, EventArgs e)
{
Parser p2 = new Parser();
p2.AddToCollection +=
newParser.AddToCollectionDlgt(OnAddToCollection);
// Clone the table structure.
DataTable dtTemp = dataTable.Clone();
// Create a new binding source.
BindingSource bsTemp = new BindingSource(dtTemp, null);
// Create a temp log for the temp table.
DataTableTransactionLog logTemp =
new DataTableTransactionLog(dtTemp);
// but really, we're still creating the row first!
DataRowView newRow=(DataRowView)bsTemp.AddNew();
// Initialize the PK.
newRow["PK"] = Guid.NewGuid();
// Some MyXaml stuff.
p2.References["app"] = this;
p2.References["bindingSource"] = bsTemp;
Form form =
(Form)p2.Instantiate("personInfoDlg.myxaml", "*");
DialogResult res = form.ShowDialog();
if (res == DialogResult.OK)
{
// Instantiate a syncMgr for the master table.
DataTableSynchronizationManager sync =
new DataTableSynchronizationManager(dataLog);
// Instantiate a syncMgr for the temp table.
DataTableSynchronizationManager syncTemp =
new DataTableSynchronizationManager(logTemp);
// Get the transactions.
List<TransactionRecordPacket> packets =
syncTemp.GetTransactions();
// Add them to the master log.
sync.AddTransactions(packets);
// Sync up.
sync.Sync();
// All done.
dataLog.AcceptChanges();
dgTransactions.DataSource = null;
dgTransactions.DataSource = dataLog.Log;
}
}
我们能够使用完全相同的对话框来处理两种配置,这很有趣。但请注意注释“实际上,我们仍然先创建行!”。在内部,我们先创建行,但从外部来看,对于绑定到表的应用程序的其他控件而言,我们只在用户实际单击“确定”时才添加行。
真正的后添加
但我想真正实现的是在此对话框中表达的
这里的想法,在标题“多重添加”中表达,将允许用户输入多个姓名和地址
- 无需在应用程序的某个地方单击“添加”按钮。
- 将保留上条记录的信息,以便重复使用。
为什么这样做?例如,我家里有三个人——我和我的女朋友,还有我的儿子。如果我只是为这三个人添加三个单独的记录,为什么要重新输入地址信息?因此,在冗长的介绍之后,我们终于可以切入本文的重点了——如何实现这一点,以便
DataTable
在用户单击“添加新记录”或“更新”之前实际上不会被更新(从而避免了副作用)。- 新行不会清除绑定到数据源的控件。
- 我们可以记录事务,以便用于同步镜像表,例如服务器上的表。
- 我们可以为用户提供对他们正在编辑的记录的撤销/重做功能(这是否很愚蠢?)。
显而易见的是,由于新行创建时间的限制,数据绑定不能直接用于上述对话框。在上述对话框中,当用户单击“添加新记录”时,新行才真正创建(或者不是吗?)。
但是……
在很多方面,最简单的答案是在 AddNew
事件处理程序中创建行,并手动初始化行的字段与控件值。坦率地说,如果我必须在我客户的应用程序中的一百个对话框中这样做,我会发疯的。ORM?不,我不想让客户除了绑定到服务器提供的一个 DataTable
之外,还与数据访问层相关联。我特别不希望创建与持久存储直接交互的客户端,或者拥有直接或间接特定于持久存储架构的类。目标是拥有一个通用的客户端,能够以这种方式处理数据,无论它管理的是人员、电影、书籍还是其他任何东西。至于仅仅使用 DataGrid
来完成所有事情,嗯,也许用户更喜欢上面这样的对话框,而不是 DataGrid
。DataGrid
使程序员的生活更轻松,但这是否是用户想要的?也许,也许不是。
RowTransactionSandbox
我们将添加到事务记录器和同步管理器中的类是一个行事务沙盒。这个类管理一个沙盒化的 DataTable
和 DataRow
,它与源 DataTable
隔离,直到通过特定方式(使用我们之前的类)与源表同步。
UML 图
以下图展示了沙盒如何融入前一篇文章的 UML 图中
UML 图说明了沙盒如何管理主(源)数据和沙盒化数据的事务日志。它还管理同步管理器,以便它可以将事务包从沙盒日志传输到主日志。
事件和数据流图
下图说明了事件和数据流
上图说明了沙盒是从“新行”或“现有行”事件初始化的。如果沙盒使用现有行进行初始化,那么数据必须来自主表中的一行。沙盒,因为它是为单行管理事务,所以包含该行的 PK 值集。最初,新行上的这些值是 null,或者在以现有行开始时被填充。此外,行字段绑定到对话框中的各种控件。
当用户添加记录时,应用程序必须创建新的 PK 值,无论它们是否存在。如果记录正在被更新,那么 PK 值就没有什么特别的处理——它们已经存在了。在这两种情况下,沙盒都会获取行事务,并使用事务集中的 PK 值。事务包会传递给主同步管理器,并更新主源。由于行现在存在,沙盒会使用现有行数据重新初始化,这为使用现有数据添加新记录设置了事务。此外,还有一个内部字段 lastOriginalIdx,它保存了多少事务记录由此初始事务列表组成。在进行更新时,所有这些事务都可以被丢弃!
状态图
最后,这张图说明了状态转换、撤销/重做以及日志管理
上图应该有助于理解从添加新行到更新现有行,以及在记录被清除时可能返回到“新”状态的状态转换。该图还说明,当添加记录时,“新行”事务是日志中的第一项,并且原始值事务加上更改事务是从同步管理器收集的。如果记录正在被更新,则只需要更改事务——其余的事务日志将被丢弃(我们将在代码中看到这一点)。
实现
对于所有这些图表,实现实际上非常简单。当一个复杂概念可以在没有大量代码的情况下实现时,这是很好的。
初始化
在 Initialize
方法中
public void Initialize()
{
if (sourceLogger == null)
{
throw new DataTableTransactionException(
"SourceLogger must be initialized.");
}
state = SandboxState.New;
sandboxTable = sourceLogger.SourceTable.Clone();
sandboxLogger = new DataTableTransactionLog(sandboxTable);
sourceSyncMgr = new DataTableSynchronizationManager(sourceLogger);
sandboxSyncMgr = new DataTableSynchronizationManager(sandboxLogger);
sandboxLogger.RowAdding += new DataTableTransactionLog.RowAddedDlgt(
OnRowAdding);
}
沙盒
- 克隆源
DataTable
,这会复制结构信息, - 初始化自己的事务记录器,
- 为沙盒和源记录器初始化同步管理器,
- 挂接
RowAdding
事件,以便在BindingSource
添加行后进行一些后期初始化。
在演示中,沙盒和 BindingSource
是通过声明式初始化的
<cd:RowTransactionSandbox def:Name="tSandbox"
SourceLogger="{dataLog}"/>
<BindingSource def:Name="tBindingSource"
DataSource="{tSandbox.SandboxTable}"/>
请注意,沙盒的克隆 DataTable
被用作 DataSource
。
RowAdding
处理程序会进行一些最终初始化,跟踪添加的行并设置状态。通常,BindingSource.AddNew
方法只在沙盒的整个生命周期中调用一次,因此此处理程序的主要目的是获取新行并保存它。
void OnRowAdding(object sender, RowAddedEventArgs e)
{
row = e.Record.Row;
State = SandboxState.New;
lastOriginalIdx = 0;
}
准备添加记录
一旦调用了 BindingSource.AddNew()
方法(在附加到沙盒的 DataTable
实例的 BindingSource
上),沙盒就可以开始跟踪新行的事务了。以下来自演示的代码显示了这一点
protected void OnMultiAdd(object sender, EventArgs e)
{
Parser p2 = new Parser();
p2.AddToCollection +=
new Parser.AddToCollectionDlgt(OnAddToCollection);
p2.AddOrUpdateReferences(p.References);
Form form =
(Form)p2.Instantiate("personInfoMultiAddDlg.myxaml", "*");
form.Tag = p2;
BindingSource bsTemp =
(BindingSource)p2.GetReference("tBindingSource");
bsTemp.AddNew(); // This is the important step!
form.ShowDialog();
}
准备更新记录
假设您有一条记录,您想先更新它,或者修改它以创建一条新记录。过程完全相同,只是调用了沙盒的 BeginEdit()
方法。此方法使用源行中的行初始化沙盒的行,并初始化将用于沙盒管理的整个事务“集”的 PK 值。
public void BeginEdit(DataRow srcRow)
{
if (row == null)
{
throw new DataTableTransactionException(
"Row not initialized. Call AddNew() on " +
"the binding source for the sandbox DataTabe first.");
}
foreach (DataColumn dc in sourceLogger.SourceTable.Columns)
{
row[dc.ColumnName] = srcRow[dc.ColumnName];
}
pkValues = new Dictionary<string, object>();
foreach (DataColumn dc in sourceLogger.SourceTable.PrimaryKey)
{
pkValues[dc.ColumnName] = row[dc.ColumnName];
}
lastOriginalIdx = sandboxLogger.Log.Count;
State = SandboxState.Existing;
}
上述代码中至关重要的是,此初始化会在源值复制到沙盒值时创建事务记录。因此,如果用户决定修改当前值(这也生成事务),然后将该记录添加为新记录,沙盒将拥有完整的信息集——所有现有事务以及已在沙盒日志中的任何新事务,已准备好同步到源事务日志。此外,还有一个内部字段 lastOriginalIdx,它保留了包含此初始事务列表的事务记录的数量。执行更新时,所有这些事务都可以被丢弃!
在演示中,“更新”菜单事件处理程序调用以下方法。请注意在调用 AddNew()
之后调用 BeginEdit()
。
protected void OnMultiUpdate(object sender, EventArgs e)
{
Parser p2 = new Parser();
p2.AddToCollection +=
new Parser.AddToCollectionDlgt(OnAddToCollection);
p2.AddOrUpdateReferences(p.References);
Form form =
(Form)p2.Instantiate("personInfoMultiAddDlg.myxaml", "*");
form.Tag = p2;
BindingSource bsTemp =
(BindingSource)p2.GetReference("tBindingSource");
RowTransactionSandbox ts =
(RowTransactionSandbox)p2.GetReference("tSandbox");
bsTemp.AddNew(); // Create the sandbox's row
ts.BeginEdit(((DataRowView)bindingSource.Current).Row);
form.ShowDialog();
}
添加、更新、清除和删除
当要添加一行(无论它是否存在于原始 DataTable
中)时,会调用沙盒的 Add
方法。此代码要求应用程序提供新的 PK 值。这些值保存在 PK 字段中,并且整个沙盒事务日志会被压缩,这意味着如果您将字段从 a 更改为 b,再更改为 c,则只保留从 a 到 c 的事务。(压缩器还会删除与删除行事务相关的任何事务。)获取事务包并更新源。
public void Add(Dictionary<string, object> pkValues)
{
if (row == null)
{
throw new DataTableTransactionException(
"Row not initialized. Call AddNew() on the binding " +
"source for the sandbox DataTabe first.");
}
this.pkValues = pkValues;
UpdateRowPKValues();
sandboxLogger.Compact();
List<TransactionRecordPacket> trpList;
trpList = sandboxSyncMgr.GetTransactions(pkValues);
UpdateSource(trpList);
}
更新过程类似,只是没有 PK 值需要初始化。此外,无法进行压缩,因为这会将更改的字段移到初始化事务(从源行创建)中,而这些事务将在 RemoveRange
调用中被丢弃。在某个时候,可以重构这个“问题”。这是代码
public void Update()
{
if (row == null)
{
throw new DataTableTransactionException(
"Row not initialized. Call AddNew() on the " +
"binding source for the sandbox DataTabe first.");
}
if (state != SandboxState.Existing)
{
throw new DataTableTransactionException(
"Can't update a new row.");
}
// Can't compact, as this changes the
// transaction list ordering.
// sandboxLogger.Compact();
List<TransactionRecordPacket> trpList;
trpList = sandboxSyncMgr.GetTransactions(pkValues);
trpList.RemoveRange(0, lastOriginalIdx);
UpdateSource(trpList);
}
删除过程直接操作事务日志,丢弃所有内容并注入一个 DeleteRow
事务。
public void Delete()
{
if (row == null)
{
throw new DataTableTransactionException(
"Row not initialized. Call AddNew() on the " +
"binding source for the sandbox DataTabe first.");
}
if (state != SandboxState.Existing)
{
throw new DataTableTransactionException(
"Can't delete a new row.");
}
sandboxLogger.ClearLog();
sandboxLogger.Log.Add(new DataTableTransactionRecord(0,
row, DataTableTransactionRecord.RecordType.DeleteRow));
List<TransactionRecordPacket> trpList;
trpList = sandboxSyncMgr.GetTransactions(pkValues);
UpdateSource(trpList);
Clear();
}
请记住,我特意要记录主表上发生的事务,以便本地表和远程表可以同步。因此,仅仅删除主表中的一行可能看起来很费力,但这一点可以通过这张截图来说明
如您所见,主表中有一个 DeleteRow
事务!
Clear
方法会清除所有事务并将沙盒置于“新行”状态。注意它如何注入一个“NewRow
”事务,以便我们准备好添加新行及其字段值。
public void Clear()
{
if (row == null)
{
throw new DataTableTransactionException(
"Row not initialized. Call AddNew() on the " +
"binding source for the sandbox DataTabe first.");
}
ClearAllFields();
// Clear the log, as the only transaction
// allowed now is add.
sandboxLogger.ClearLog();
// Setup for an "Add".
sandboxLogger.Log.Add(new DataTableTransactionRecord(0,
row, DataTableTransactionRecord.RecordType.NewRow));
State = SandboxState.New;
}
现在,真正的魔法发生在 UpdateSource
方法中。在这里,沙盒事务包被添加到源同步管理器中,并且源 DataTable
被同步。沙盒事务日志被清除,注入一个“新行”事务记录以处理可能使用一些现有字段值添加新行的情况,然后调用 BeginEdit
方法,设置所有初始值。沙盒现在处于“更新”状态。
protected void UpdateSource(
List<TransactionRecordPacket> trpList)
{
sourceSyncMgr.AddTransactions(trpList);
sourceSyncMgr.Sync();
sandboxLogger.ClearLog();
// Setup for an "Add".
sandboxLogger.Log.Add(new DataTableTransactionRecord(0,
row, DataTableTransactionRecord.RecordType.NewRow));
BeginEdit(row);
}
演示应用程序
演示应用程序通过声明式和命令式代码做了很多事情,并演示了预添加、后添加和多重添加/更新过程。所有事务都在演示中显示,因此您可以看到它们在创建事务和更新主数据方面的差异。请注意,在本文中我没有涵盖状态管理——多重添加器对话框上的菜单项状态和按钮状态。
结论
在某些方面,将 BindingSource
的管理包含在沙盒代码中会更容易,但是,BindingSource
类需要 System.Windows.Forms
,我想让沙盒不包含此要求。这意味着应用程序必须比最初期望的对与沙盒的接口承担更多的责任。
使用本文和前两篇文章中讨论的类,程序员现在可以根据用户需求,以各种模式创建数据输入屏幕。数据事务被记录下来,适合与远程源进行同步。