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

AoB:一个用于显示、编辑和自动更新多对多关系的基类Windows窗体 - 第3部分

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (1投票)

2005年10月21日

13分钟阅读

viewsIcon

46631

downloadIcon

836

在这一部分,我将解释如何处理并发问题。

引言

在这一部分,我将解释如何处理并发问题。

尽管这个窗体能够进行一些编辑和数据控制,但它控制得并不好,很容易导致并发冲突,导致数据未正确链接或保存。

首先,重要的是要获取可用的ID值,因为这些ID用于链接A和B数据。

我们有CurrentMaxAIDCurrentMaxBIDCurrentMaxABID。我们需要找到存在的最大值,而不仅仅是计数 - 有些可能已被删除。

Int32 CurrentMaxAID = 0;
Int32 CurrentMaxBID = 0;
Int32 CurrentMaxABID = 0;
object ob;

public void LoadDatabase()
{

    ...

    //Get current maximum value of AID
    CurrentMaxAID = 0;
    ob = ds.Tables["A"].Compute("Max(AID)", "");
    if (ob.ToString() != "")
        CurrentMaxAID = Int32.Parse(ob.ToString());
    CurrentMaxAID++;

    //Get current maximum value of BID
    CurrentMaxBID = 0;
    ob = ds.Tables["B"].Compute("Max(BID)", "");
    if (ob.ToString() != "") CurrentMaxBID = Int32.Parse(ob.ToString());
    CurrentMaxBID++;

    //Get current maximum value of ABID  //could use BA
    CurrentMaxABID = 0;
    ob = ds.Tables["AB"].Compute("Max(ABID)", "");
    if (ob.ToString() != "") CurrentMaxABID = Int32.Parse(ob.ToString());
    CurrentMaxABID++;

    DatabaseLoaded = true;
}

现在,如果用户进入A DataGridView的“添加新行”功能,我们需要更新AID字段。我们可以使用“用户添加行”事件来添加下一个CurrentMaxAID编号。

private void dgvA_UserAddedRow(object sender, DataGridViewRowEventArgs e)
{
  dgvA.CurrentRow.Cells[0].Value = CurrentMaxAID.ToString();
  CurrentMaxAID++;
}

B网格类似。

请注意,在AName字段中输入第一个字符之前,该数字实际上不会被添加。

对我来说,这似乎比原始DataGrid有了很大的设计改进,在原始DataGrid中,我使用了列更改事件,该事件显然在每次列更改时都会触发。

//set up some event handlers for when editing(adding) data 
//adds a unique AID number to new rows
DS.Tables["A"].ColumnChanged += new 
  DataColumnChangeEventHandler(this.ATableDataColumnChanged);

private void ATableDataColumnChanged(object sender, 
             System.Data.DataColumnChangeEventArgs e) 
{
    //This code adds a (hopefully) unique AID to the A table.
    if ((e.Row["AID"].ToString() == "") || 
               (int.Parse(e.Row["AID"].ToString()) == 0))
        e.Row["AID"] = CurrentMaxAID++;
}

现在需要做出一个设计决策。在AB DataGridView中添加B记录的代码似乎很显而易见。当然,我们只需要添加现有的(新的)AID、下一个ABID、下一个BID以及BName。然而,虽然这对AB表有效,但B表却没有得到更新。需要添加代码来使用BIDBName更新B表。

另一种观点是,也许只应该允许现有的B条目添加到新的A行中。因此,应该先创建新的B条目,然后进行选择。这是我在最初设计时采用的路线。我将阻止用户直接向选项卡的一侧(AB)添加条目,只允许用户从现有的BName条目中选择。这将允许我演示一些新技术。(我实际上已经为第一个想法编写了代码,并在下面进行了描述。)

我并不是说我的决定是最好的,它可能不是,我只是说这是我最初做出的决定,而且由于我正在将一个现有应用程序转换为v2.0,我想暂时保持这种方式。

我将使用A DataGridView上的上下文菜单来显示所有现有B行的列表。然后,用户可以选择要添加的B行,可以是范围,也可以是单独选择,然后通过再次右键单击并选择“将选定的行添加到A”来添加它们。

首先,我将通过设计器(属性)禁用向AB DataGridView添加新行的能力。将AllowUserToAddRows设置为false。(当然,如果您使用了下面描述的附加代码,请不要这样做。)

现在我需要向A DataGridView添加一个上下文菜单。创建一个上下文菜单,添加一个“从B添加行”项,一个“将选定的行添加到A”项和一个“取消”选项。双击这些项以创建事件处理程序骨架。在A DataGridView上,将上下文菜单分配给contextmenustrip属性。我也将相同的上下文菜单分配给AtoB DataGrid

我在应用程序中添加了一个小视觉线索,以帮助我看到正在发生的事情,即更改当前活动的每个DataGrid的标题背景色。这在删除时特别有用,可以让我看到实际上是从哪个网格视图删除的。在显示要选择的B行列表时,我将标题更改为另一种颜色,以便我可以看到那里的情况。这完全是非标准的,但我喜欢它,所以就这样了。唯一的问题是DataGridView现在没有标题 - 为什么?它有什么问题?实现向后兼容应该并不难吧。不过,我使用了一些标签来实现相同的效果。有一些代码利用鼠标按下事件来组织颜色。

private void dgMouseDown(object sender, 
           System.Windows.Forms.MouseEventArgs e)
{
    DataGridView myGrid = (DataGridView)sender;

    //If caption not coral, which indicates that the grid 
    //is displaying a list of all A or B for selectionand assignment
    //set all caption header to LightSteelBlue, then set active 
    //one to Goldenrod - because need to know from 
    //what grid what you are deleting!!!
    lbA.BackColor = Color.LightSteelBlue;
    lbB.BackColor = Color.LightSteelBlue;
    if (lbAtoB.BackColor != Color.Coral)
        lbAtoB.BackColor = Color.LightSteelBlue;
    if (lbBtoA.BackColor != Color.Coral)
        lbBtoA.BackColor = Color.LightSteelBlue;
    //which grid am I on and highlight it
    if (myGrid.Name.Trim() == "dgvA")
        lbA.BackColor = Color.Goldenrod;
    if (myGrid.Name.Trim() == "dgvB")
        lbB.BackColor = Color.Goldenrod;
    if (myGrid.Name.Trim() == "dgvAtoB")
        if (lbAtoB.BackColor != Color.Coral)
            lbAtoB.BackColor = Color.Goldenrod;
    if (myGrid.Name.Trim() == "dgvBtoA")
        if (lbBtoA.BackColor != Color.Coral)
            lbBtoA.BackColor = Color.Goldenrod;
}

如果您需要更多信息,例如在DataGridView的哪个位置单击了,请使用dgMouseDown类中的命中测试信息结构。

System.Windows.Forms.DataGridView.HitTestInfo hti;
hti = myGrid.HitTest(e.X, e.Y);
MessageBox.Show(hti.Type.ToString());

在上下文菜单事件类中,我添加了一些代码来隐藏在特定上下文中没有意义的选项,并组织我的标题颜色。我将数据集中的B表分配给AtoB DataGridView。这似乎效果很好,尽管我心中仍有些不确定,不知道像ABID等未使用的列是怎么回事。我假设DataGridView和列类知道如何处理 - 但如果有什么奇怪的错误,这可能是一个要查找的地方。另一种方法是正确地将列分配给网格,然后稍后重新分配旧的列,或者使用一个DataGridView,它位于设计器中的AtoB网格之上或之下,并在需要时使其活动和可见。我以前用过这个技术,效果很好,尽管在设计器视图中解决任何问题有点麻烦 - 使用下拉列表在属性弹出窗口中选择要处理的网格。

[说到这里,设计器是不是有个bug?当我点击设计器中的DataGridView,然后点击属性弹出标签时,我并不总是能得到属性或事件列表。我需要再次点击DataGridView吗?(Visual C# Express beta 2版本。)]

如果我右键单击了DataGridView并选择了“从B选择”选项,那么右侧网格上方的标题将变成珊瑚色,并将显示所有B的列表。我现在可以通过单击行标题来选择我想分配给A的B。我可以使用shift键进行范围选择,和/或control键进行单个多行选择。在旧的DataGrid中选择和滚动的一个问题似乎已经被修复了。(如果你需要,这里有一个修复补丁!)

选择行后,使用上下文菜单并选择“将选定的行添加到A”菜单选项(或取消!)。

在此事件中,我们遍历AtoB DataGridView的行,查看哪些行被选中。我使用两个网格的AID和BID来查看是否存在现有的AB记录。我不想重复。为了做到这一点,我使用了一个DataView来过滤和计数所有记录。如果计数为0,我将新记录添加到基础表中。数据集似乎会处理其余的事情 - 如果设置了正确的标志,例如ds.EnforceConstraints = truenr.SetParentRow(nr); - 我想?这一切都发生很久以前了,我已经把它都弄好了……

private void addthesetoAToolStripMenuItem_Click(object sender, EventArgs e)
{
    DataView dv;
    DataRow nr;
    
    //process selected rows
    foreach (DataGridViewRow row in dgvAtoB.Rows)
    {
        if (row.Selected)
        {
            //need to see if the AB row already exists. Dataview seems easiest?
            dv = ds.Tables["AB"].DefaultView;
            dv.RowFilter = "AID='" + 
               dgvA.CurrentRow.Cells["AID"].Value.ToString() 
               + "' and BID='" + 
               row.Cells["BID"].Value.ToString() + "'";
            if (dv.Count == 0)
            //no duplicates (I hope) 
            {
                //Update the AB table now
                nr = ds.Tables["AB"].NewRow();
                nr["ABID"] = CurrentMaxABID++;
                nr["AID"] = dgvA.CurrentRow.Cells["AID"].Value;
                nr["BID"] = row.Cells["BID"].Value;
                nr["BName"] = row.Cells["BName"].Value;
                nr.SetParentRow(nr);
                ds.Tables["AB"].Rows.Add(nr);
            }
        }
    }

现在可能有更好的方法来查看AB记录是否存在,请告诉我。当然,在原始版本中,我似乎不得不使用绑定管理器基类来访问为DataGrid提供数据的表。我不记得为什么会这样,但一定有什么障碍,因为谁会使用绑定管理器基类,除非他们不得不这样做!!!可能是因为关系被分配给了网格,我猜。

我对选项卡的另一边也做了同样的事情。

现在我已经重写了代码,我看不出有什么问题可以允许直接添加参与者。它将使用与上面相同的基本例程。唯一的区别是获取下一个最大BID,这很简单 - CurrMaxBID

唯一的问题是我应该在完成行编辑后才更新A表。所以,我需要知道什么时候离开了添加新数据行。

有很多事件正在发生。其中似乎最有用的是DataGridView.UserAddedRow事件。文档说,当用户完成在DataGridView中添加一行时,就会发生此事件。然后它会展示如何用它来更新运行总数。我不知道它怎么能做到这一点,因为它是在空白行中进行单元格输入时发生的第一个击键。当所有文本输入完成后,它不会触发。

事件的顺序是:用户点击进入新行(星号标记)。有RowLeaveRowValidatingRowValidatedRowEnter事件。图标变为右三角形图标。用户按下键盘上的一个键。有一个RowAdded事件,然后是UserRowAdded事件。图标变为编辑铅笔。用户现在可以继续输入。当焦点离开行时,会触发RowLeaveRowValidatingRowvValidatedRowEnter事件。图标变回右三角形图标,并且*添加新行*更改为星号(除非仍然在上面)。

我确定还有无数其他事件在触发,例如,单元格编辑结束?但是文档在哪里呢???微软似乎从不编写我们实际想要使用的示例代码;它总是显得那么晦涩难懂!

为了取得一些进展,我决定在我知道我添加了一个新行时设置一个标志,然后使用行验证事件来更新数据库。我期待听到这方面的最终答案:)

向AtoB网格添加行的代码现在是

private void dgvAtoB_UserAddedRow(object sender, DataGridViewRowEventArgs e)
{
    RowAddedToAtoB = true;
    //use this in the validated event to update database, 
    //needs to go here otherwise things out of sync 
    //when cell edit routine invoked???.

    dgvAtoB.CurrentRow.Cells["ABID"].Value = 
            CurrentMaxABID.ToString();
    CurrentMaxABID++;
    dgvAtoB.CurrentRow.Cells["BID"].Value = 
            CurrentMaxBID.ToString();
    CurrentMaxBID++;
    dgvAtoB.CurrentRow.Cells["AID"].Value = 
            dgvA.CurrentRow.Cells["AID"].Value;
}

private void dgvAtoB_RowValidated(object sender, 
                    DataGridViewCellEventArgs e)
{
    DataRow nr;

    if (RowAddedToAtoB)
    {
        nr = ds.Tables["B"].NewRow();
        nr["BID"] = dgvAtoB.CurrentRow.Cells["BID"].Value;
        nr["BName"] = dgvAtoB.CurrentRow.Cells["BName"].Value.ToString();
        ds.Tables["B"].Rows.Add(nr);
        //now need to make sure that the BA relation gets updated
        nr = null;
        nr = ds.Tables["BA"].NewRow();
        nr["ABID"] = dgvAtoB.CurrentRow.Cells["ABID"].Value;
        nr["BID"] = dgvAtoB.CurrentRow.Cells["BID"].Value;
        nr["AID"] = dgvAtoB.CurrentRow.Cells["AID"].Value;
        nr["AName"] = dgvA.CurrentRow.Cells["AName"].Value.ToString();
        ds.Tables["BA"].Rows.Add(nr);

        RowAddedToAtoB = false;
    }
}

(请注意,添加用户行会调用其他事件,例如CellValueChanged例程,这些调用似乎存在一些时序问题,因此此RowAddedToAtoB标志必须先设置,而不是在最后!!!这里有一些奇怪的异步事件,我需要调查!)

请注意,此方法不检查是否存在B记录(如果是在另一边,则不检查A记录)。因此,有可能添加两个或多个“Phil”。根据您的需求,这可能是好事或坏事。[所以,我仍然认为第二个设计决策是最好的。]

现在一个必须面对的主要问题变得清晰起来。如果我向A条目添加一些B条目,这将更新AtoB关系,但不会更新BtoA关系。这是一个主要的麻烦。这意味着,除非采取措施,否则关系的两边在视图中将不会匹配。更糟糕的是,由于我只从AtoB side更新实际的AB数据表,BtoA side上的数据集中的任何更改都不会更新DataTable。我不能简单地添加额外的更新代码,因为两边可能已经严重不同步。

我必须做的是,确保如果我对AtoB side进行更改,我也将其传递给BtoA side。这包括编辑、添加记录和删除。同样,对于关系另一侧的工作也是如此。这是一个主要的麻烦,需要添加几乎重复的代码,并处理额外的编辑和删除事件。

如果数据集中有一个自动选项可以同时更新两个关系,那会容易得多。

代码与上面的代码相似 - 有关详细信息,请参阅源代码。

除此之外,我们还需要检查对ANameBName的更改是否会更新AB或BA列表中的其他条目,以使一切都同步。为了做到这一点,我使用了CellValueChanged事件。

这处理了对AName(在第一个选项卡上)的更改。

private void dgvA_CellValueChanged(object sender, 
                     DataGridViewCellEventArgs e)
{
    //need to propogate any changes through to table BA
    //find the record(s) first, then change it(them).
    
    string s;
    s = "AID = '" + dgvA.CurrentRow.Cells["AID"].Value.ToString() + "'";
    DataRow[] dr = ds.Tables["BA"].Select(s, null, 
                    DataViewRowState.CurrentRows);
    foreach (DataRow r in dr)
    {
        if (e.ColumnIndex == 1)
            r["AName"] = dgvA.CurrentRow.Cells["AName"].Value.ToString();
    }
}

接下来的事件处理了在(第一个选项卡上的)右侧网格中BName的更改。然而,当添加新行时,此事件也会被触发。我在UserAddedRow例程中设置了一个标志,所以如果设置了该标志,我将从例程中返回。

private void dgvAtoB_CellValueChanged(object sender, DataGridViewCellEventArgs e)
{
    //need to propogate any changes through to table B
    //find the record(s) first, then change it(them).

    string s;
    DataRow[] dr;

    if (RowAddedToAtoB) return;
    //I'm adding a row so don't do anything

    s = "BID = '" + 
        dgvAtoB.CurrentRow.Cells["BID"].Value.ToString() 
        + "'";
    dr = ds.Tables["B"].Select(s, null, 
         DataViewRowState.CurrentRows);
    foreach (DataRow r in dr)
    {
        if (e.ColumnIndex == 3)
            r["BName"] = 
              dgvAtoB.CurrentRow.Cells["BName"].Value.ToString();
    }
    //Now propogate through to "all" in AB table
    dr = ds.Tables["AB"].Select(s, null, 
          DataViewRowState.CurrentRows);
    foreach (DataRow r in dr)
    {
        if (e.ColumnIndex == 3)
            r["BName"] = 
              dgvAtoB.CurrentRow.Cells["BName"].Value.ToString();
    }
}

在处理完这些部分之后,我更新例程中出现了一个bug。最终我跟踪到了我使用的参数,如果您已经使用了代码,您将需要更改它。

在数据库更新例程中,找到这里的一行

daAB.UpdateCommand.Parameters.Add("@BID", OleDbType.Integer, 10, "BID");

然后在这几行中,您会注意到ABID1读取daA.UpdateCommand……

它当然应该读取ABID1.dAB.UpdateCommand……(这真的很难追踪……)

所以这段代码应该读取

//OleDbParameter ABID1 = 
  daAB.UpdateCommand.Parameters.Add("@ABID", 
  OleDbType.Integer, 10, "ABID");
//ABID1.SourceVersion = DataRowVersion.Original;
OleDbParameter ABID2 = 
  daAB.UpdateCommand.Parameters.Add("@AID", 
  OleDbType.Integer, 10, "AID");
ABID2.SourceVersion = DataRowVersion.Original;
OleDbParameter ABID3 = 
  daAB.UpdateCommand.Parameters.Add("@BID", 
  OleDbType.Integer, 10, "BID");
ABID3.SourceVersion = DataRowVersion.Original;

最后的问题涉及行的删除。要删除的行被高亮显示,然后按下删除键。在删除行之前,应该询问用户是否确定,然后应该检查行对任何相关记录的影响。如果有关联,则在删除关系之前不应允许删除。当然,这需要在实际删除之前完成,所以使用行删除事件,而不是行已删除事件。使用e.Cancel将停止删除,并将行恢复到按下删除键之前的状态。

这是例程

private void dgvA_UserDeletingRow(object sender, 
             DataGridViewRowCancelEventArgs e)
{
    //this routine cannot cope with multiple selections??? 
    //The routine is looking at the current row 
    //indicated by the right arrow icon

    //Check to see if any relations before allowing delete. 
    //Note could have done a selection and delete
    if (MessageBox.Show("Do you want to delete this row?", 
        "", MessageBoxButtons.YesNo) == DialogResult.No)
    {
        e.Cancel = true;
        return;
    }
    //need to see if the AB row already exists. 
    //Dataview seems easiest?, is there a performance hit?
    
    DataView dv = ds.Tables["AB"].DefaultView;
    dv.RowFilter = "AID='" + dgvA.CurrentRow.Cells["AID"].Value.ToString() + "'";
    if (dv.Count != 0)
    {
        MessageBox.Show("Can't Delete this row as there" + 
           " are B entries depending on it - delete those first!");
        e.Cancel = true; 
    }
}

如果正在删除的行在AB部分,那么我还需要删除BA关系中的相应条目。

private void dgvAtoB_UserDeletingRow(object sender, 
                  DataGridViewRowCancelEventArgs e)
{
    //Check to see if the user want to delete this row.
    if (MessageBox.Show("Do you want to delete this row?", 
        "", MessageBoxButtons.YesNo) == DialogResult.No)
    {
        e.Cancel = true;
        return;
    }
    //Delete the row and delete any corresponding entries in BA
    DataRow[] dr = ds.Tables["BA"].Select("ABID = '" + 
                   dgvAtoB.CurrentRow.Cells["ABID"].Value + 
                   "'", null, DataViewRowState.CurrentRows);
    foreach (DataRow r in dr) r.Delete();
}

您会注意到,从注释中可以看出,此例程无法处理选择多行进行删除。它似乎只查看被高亮显示并显示右箭头图标的行。然后就出错了!

我认为我需要拦截删除键,然后检查我在哪个网格上,然后遍历行。是时候进一步调查了。

有人有其他方法吗?在我的.NET 1.1版本中,我继承并重写了DataGrid类。

就这样。希望您能从这个例程中获得一些价值,并能使用它或其中的一些代码片段。感谢所有帮助我解决不同问题并提供代码帮助我的人。

附注:如果您使用此例程编写了一个杀手级应用程序,请记住我……

(注意:Excel导入似乎存在一些问题。当我最初编写例程时,我使用的是Office 2000。现在我的新机器上有Office 2002,我似乎在COM方面遇到了一些问题。为了解决这个问题,我基本上引用了Zip中提供的原始DLL。同样,如果有人有明确的答案……)

© . All rights reserved.