Building a Conflict Resolution Screen

 

Building a Conflict Resolution Screen

The rest of this chapter focuses on building a conflict resolution screen that allows the user to resolve a conflict by selecting the current user value, the original database value, the current database value, or a typed-in value. To drive this conflict resolution screen, the application will display only the customer names and will allow the user to update the list. If a DbConcurrency exception occurs, the conflict resolution screen will be displayed.

Creating the Project

First we create the project for demonstrating conflict resolution when a DbConcurrency exception occurs after an update of the database server from disconnected data.

  1. Create a Microsoft Windows application using the appropriate programming language.

  2. Add a MenuStrip control to the form. Add the following menu items.

    image from book

    MenuItem List

    &File    &Sync With Database    E&xit &Concurrency    &Resolve Concurrency Errors 
image from book

  • Add a status bar with a single status label called status.

  • Add a new database file to the application by right-clicking the project, choosing Add, choosing New Item, and then choosing SQL Database. Name the database Customer.mdf, and then click the Add button. This launches the Data Source Configuration Wizard. You haven't created any tables yet, so click Finished to add the empty customer DataSet to your project.

  • Add the tables as shown earlier in Figure 5-1 or, at a minimum, add a table called TblCustomer that has a column named Id that is a uniqueidentifier primary key and a column named Name that is a varchar(50) data type. Neither column should allow a null value. (You can edit the tables by right-clicking the database file and clicking Open.)

  • Open the DataSources window by choosing Data and then choosing Show Data Sources. The CustomerDataSet class is visible in this window, but it needs to be updated to show tables that you have added. Right-click the CustomerDataSet and choose Configure DataSet With Wizard, which will launch the Data Source Configuration Wizard. Select all the tables you have created in the database, as shown in Figure 5-6.

  • In the Data Sources window, drag and drop TblCustomer onto the form. This adds TblCustomerDataGridView to the form. Set the TblCustomerDataGridView object's Dock property to Fill. Set the AutoSizeMode of the Name column to Fill. Also note that Visual Studio automatically added CustomerDataSet, TblCustomerBindingSource, TblCustomer-TableAdapter, and TblCustomerBindingNavigator to the designer tray (Figure 5-7).

  • image from book
    Figure 5-6: Adding tables to the CustomerDataSet using the Data Source Configuration Wizard

    image from book
    Figure 5-7: Adding TblCustomer to the form adds the necessary objects to the designer tray automatically.

    Extending the Typed DataSet (CustomerDataSet) Class

    We must extend this CustomerDataSet class so that there is one place to automatically generate new GUIDs for our primary key columns. Then we can forget about generating new GUIDs in the application.

    1. Right-click the CustomerDataSet.xsd file and choose View Code. This adds a new file called CustomerDataSetExtension.vb or CustomerDataSetExtension.cs under the CustomerDataSet.xsd node.

    2. Add the following code to this file.

      image from book

      Visual Basic

      Imports System.Data Partial Public Class CustomerDataSet    Private createDefaultGuidForNewRows As Boolean = False    Public Sub CreateDefaultGuids()       If (createDefaultGuidForNewRows) Then Exit Sub       createDefaultGuidForNewRows = True       For Each dt As DataTable In Me.Tables          If (Not dt.Columns("Id") Is Nothing) Then             AddHandler dt.TableNewRow, AddressOf TableNewRow          End If       Next    End Sub    Private Sub TableNewRow(ByVal sender As Object, _          ByVal e As DataTableNewRowEventArgs)       If (TypeOf e.Row("Id") Is DBNull) Then          e.Row("Id") = Guid.NewGuid()       End If    End Sub End Class 
    image from book

    image from book

    C#

    using System; using System.Data; namespace ConflictResolution {    public partial class CustomerDataSet    {       private bool createDefaultGuids = false;       public void CreateDefaultGuids()       {          if (createDefaultGuids) return;          createDefaultGuids = true;          foreach (DataTable dt in this.Tables)          {             if (dt.Columns["Id"] != null)                dt.TableNewRow += new DataTableNewRowEventHandler(TableNewRow);          }       }       private void TableNewRow(object sender, DataTableNewRowEventArgs e)       {          if (e.Row["Id"] is DBNull)             e.Row["Id"] = Guid.NewGuid();       }    } } 
    image from book

  • This code needs to be called once from the application, so add the following code into the form object's Load method. You can then run the application and add new customers to the table.

    image from book

    Visual Basic

    CustomerDataSet.CreateDefaultGuids() 
    image from book

    image from book

    C#

    customerDataSet.CreateDefaultGuids(); 
    image from book

  • Extending the TableAdapter (TblCustomerTableAdapter) Class to Expose the ContinueUpdateOnError Property

    The DataAdapter object contains a property called ContinueUpdateOnError. If this property is set to true, multi-row updates continue when concurrency errors occur. The TableAdapter objects expose only the minimally required properties, and this application requires the ContinueUpdateOnError property of the underlying TableAdapter object's DataAdapter to be set to true. This means the TblCustomerTableAdapter must be extended.

    1. In the file called CustomerDataSetExtension.vb or CustomerDataSetExtension.cs that you just added when you extended the CustomerDataSet class, add the following code to expose the ContinueUpdateOnError property. (Unfortunately, this code cannot easily be added in a single place for all TableAdapter objects. If you have additional TableAdapter objects, add code to extend each of them in a similar fashion.)

      image from book

      Visual Basic

      Namespace CustomerDataSetTableAdapters    Partial Public Class TblCustomerTableAdapter       Public Property ContinueUpdateOnError() As Boolean          Get             Return Adapter.ContinueUpdateOnError          End Get          Set(ByVal value As Boolean)             Adapter.ContinueUpdateOnError = Value          End Set       End Property    End Class End Namespace 
      image from book

      image from book

      C#

      namespace ConflictResolution.CustomerDataSetTableAdapters {    public partial class TblCustomerTableAdapter    {       public bool ContinueUpdateOnError       {          get { return Adapter.ContinueUpdateOnError; }          set { Adapter.ContinueUpdateOnError = value; }       }    } } 
      image from book

    2. Add the following code to the form's Load method to set the ContinueUpdateOnError property of each TableAdapter object to true.

      image from book

      Visual Basic

      TblCustomerTableAdapter.ContinueUpdateOnError=True 
      image from book

      image from book

      C#

      tblCustomerTableAdapter.ContinueUpdateOnError = true; 
      image from book

    Synchronizing the Disconnected DataSet with the Database Server

    The application can now be run, but without any synchronization of the disconnected CustomerDataSet with the database server. The following step adds synchronization to your code.

    Creating the Conflict Resolution Screen

    The primary purpose of the conflict resolution screen is to allow you to select the final data values when a concurrency error occurs. It displays your current values, the original database values, and the current database values. The screen is somewhat generic, and you will probably want to tweak it to suit your needs. For reference, the form that you create is shown in Figure 5-8.

    image from book
    Figure 5-8: The completed conflict resolution screen

    1. Add a new form to your project called frmConflict.vb or frmConflict.cs.

    2. Add a Panel control to the form, and set its Dock property to Bottom.

    3. Add two Button controls to the Panel control. Assign the Name properties, btnAccept and btnCancel, and set their Text property to "Accept Final Value" and "Cancel". Set the Anchor property on both buttons to Top.

    4. Add a SplitContainer control to the form. Set its Dock property to Fill, and set its Orientation property to Horizontal.

    5. Add a TabControl to the top of the SplitContainer. Create three tab pages, with the Name property set to tabCurrent, tabOriginal, and tabCurrentDb. Set the Text property to Your Current Value, Original Database Value, and Current Database Value.

    6. Add a TabControl to the bottom of the SplitContainer. Create a single tab page, with the Name property set to tabFinal. Set the Text property to Final Value.

    7. The conflict resolution screen is created by passing two DataRow objects into a new constructor. The first DataRow object contains your current data values in the Current DataRowVersion and the original database data values in the Original DataRowVersion.

      The second DataRow object initially contains the current database data value in the Original and Current DataRowVersion. The Current DataRowVersion will contain the final result of your conflict resolution. These DataRow objects are stored in properties. Here is the code to accomplish this.

      image from book

      Visual Basic

      Public Sub New(ByVal currentDataRow As DataRow, _       ByVal currentDatabaseDataRow As DataRow)    MyBase.New()    InitializeComponent()    Me.CurrentDataRow = currentDataRow    FinalDatabaseDataRow = currentDatabaseDataRow End Sub Public Property FinalDatabaseDataRow() As DataRow    Get       Return m_finalDatabaseDataRow    End Get    Set(ByVal value As DataRow)       m_finalDatabaseDataRow = value    End Set End Property Private m_finalDatabaseDataRow As DataRow = Nothing Public Property CurrentDataRow() As DataRow    Get       Return m_currentDataRow    End Get    Set(ByVal value As DataRow)       m_currentDataRow = value    End Set End Property Private m_currentDataRow As DataRow = Nothing 
      image from book

      image from book

      C#

      public frmConflict(DataRow currentDataRow, DataRow currentDatabaseDataRow) {    InitializeComponent();    CurrentDataRow = currentDataRow;    FinalDatabaseDataRow = currentDatabaseDataRow; } public DataRow FinalDatabaseDataRow {    get { return m_finalDatabaseDataRow; }    set { m_finalDatabaseDataRow = value; } } private DataRow m_finalDatabaseDataRow = null; public DataRow CurrentDataRow {    get { return m_currentDataRow; }    set { m_currentDataRow = value; } } private DataRow m_currentDataRow = null; 
      image from book

    8. Create the PopulateTab and CopyToFinal methods. For each column in the DataRow object, the PopulateTab populates a tab page with a Label that shows the column name, a TextBox that shows the value of the column based on the DataRowVersion, and a Button that lets you copy the selected value to the final value by executing the CopyToFinal method. Here is the code for these two methods.

      image from book

      Visual Basic

      Public Sub PopulateTab(ByVal tab As TabPage, ByVal dataRow As DataRow, _       ByVal dataRowVersion As DataRowVersion, ByVal m_ReadOnly As Boolean)    Const verticalSpacing As Integer = 30    Const labelWidth As Integer = 50    Const horizontalSpacing As Integer = 10    Const buttonWidth As Integer = 100    Const buttonHeight As Integer = 20    For col As Integer = 0 To dataRow.ItemArray.Length - 1       Dim val As Object = dataRow(col, dataRowVersion)       Dim label As New Label()       tab.Controls.Add(label)       label.Text = dataRow.Table.Columns(col).ColumnName       label.Top = (col + 1) * verticalSpacing       label.Left = horizontalSpacing       label.Width = labelWidth       label.Visible = True       Dim textBox As New TextBox()       tab.Controls.Add(textBox)       textBox.Text = val.ToString()       textBox.Top = (col + 1) * verticalSpacing       textBox.Left = (horizontalSpacing * 2) + labelWidth       textBox.Width = tab.Width textBox.Left _          - buttonWidth - (horizontalSpacing * 2)       textBox.Name = tab.Name + label.Text       textBox.ReadOnly = m_ReadOnly       textBox.Visible = True       textBox.Anchor = AnchorStyles.Left _          Or AnchorStyles.Top Or AnchorStyles.Right       If (tab.Name = "tabFinal") Then Continue For       Dim btn As New Button()       tab.Controls.Add(btn)       btn.Text = "Copy to Final"       btn.Left = textBox.Left + textBox.Width _          + horizontalSpacing       btn.Top = (col + 1) * verticalSpacing       btn.Height = buttonHeight       btn.Visible = True    btn.Anchor = AnchorStyles.Top OR AnchorStyles.Right       AddHandler btn.Click, AddressOf CopyToFinal       Dim propertyBag As New ArrayList()       propertyBag.Add(dataRow.Table.Columns(col))       propertyBag.Add(textBox)       btn.Tag = propertyBag    Next End Sub Private Sub CopyToFinal(ByVal sender As Object, ByVal e As EventArgs)    Dim btn As Button = CType(sender, Button)    Dim propertyBag As ArrayList = CType(btn.Tag, ArrayList)    Dim dc As DataColumn = CType(propertyBag(0), DataColumn)    Dim textBox As TextBox = CType(propertyBag(1), TextBox)    tabFinal.Controls(tabFinal.Name + dc.ColumnName).Text = textBox.Text End Sub 
      image from book

      image from book

      C#

      public void PopulateTab(TabPage tab, DataRow dataRow,       DataRowVersion dataRowVersion, bool readOnly) {    const int verticalSpacing = 30;    const int labelWidth = 50;    const int horizontalSpacing = 10;    const int buttonWidth = 100;    const int buttonHeight = 20;    for (int col = 0; col < dataRow.ItemArray.Length; col++)    {       object val = dataRow[col, dataRowVersion];       Label label = new Label();       tab.Controls.Add(label);       label.Text = dataRow.Table.Columns[col].ColumnName;       label.Top = (col + 1) * verticalSpacing;       label.Left = horizontalSpacing;       label.Width = labelWidth;       label.Visible = true;       TextBox textBox = new TextBox();       tab.Controls.Add(textBox);       textBox.Text = val.ToString();       textBox.Top = (col + 1) * verticalSpacing;       textBox.Left = (horizontalSpacing * 2) + labelWidth;       textBox.Width = tab.Width - textBox.Left          - buttonWidth - (horizontalSpacing * 2);       textBox.Name = tab.Name + label.Text;       textBox.ReadOnly = readOnly;       textBox.Visible = true;       textBox.Anchor = AnchorStyles.Left | AnchorStyles.Top          | AnchorStyles.Right;       if (tab.Name == "tabFinal") continue;       Button btn = new Button();       tab.Controls.Add(btn);       btn.Text = "Copy to Final";       btn.Left = textBox.Left + textBox.Width + horizontalSpacing;       btn.Top = (col + 1) * verticalSpacing;       btn.Height = buttonHeight;       btn.Visible = true;       btn.Anchor = AnchorStyles.Top | AnchorStyles.Right;       btn.Click += new EventHandler(CopyToFinal);       ArrayList propertyBag = new ArrayList();       propertyBag.Add(dataRow.Table.Columns[col]);       propertyBag.Add(textBox);       btn.Tag = propertyBag;    } } 
      image from book

    9. Add code to call the PopulateTab method for each tab page when the form is loaded. The code is as follows.

      image from book

      Visual Basic

      Private Sub frmConflict_Load( ByVal sender As System.Object, _       ByVal e As System.EventArgs) Handles MyBase.Load    PopulateTab(tabCurrent, CurrentDataRow, _       DataRowVersion.Current, true)    PopulateTab(tabOriginal, CurrentDataRow, _       DataRowVersion.Original, true)    PopulateTab(tabCurrentDb, FinalDatabaseDataRow, _       DataRowVersion.Original, true)    PopulateTab(tabFinal, FinalDatabaseDataRow, _       DataRowVersion.Current, false) End Sub 
      image from book

      image from book

      C#

      private void frmConflict_Load(object sender, EventArgs e) {    PopulateTab(tabCurrent, CurrentDataRow,        DataRowVersion.Current, true);    PopulateTab(tabOriginal, CurrentDataRow,        DataRowVersion.Original, true);    PopulateTab(tabCurrentDb, FinalDatabaseDataRow,        DataRowVersion.Original, true);    PopulateTab(tabFinal, FinalDatabaseDataRow,        DataRowVersion.Current, false); } 
      image from book

    10. Add code to collect the final data values from the TextBox objects in tabFinal and place the values into the FinalDatabaseRow, which will be retrieved by the calling form. This code will be placed into the Click event handler of btnAccept as follows.

      image from book

      Visual Basic

      Private Sub btnAccept_Click(ByVal sender As System.Object, _       ByVal e As System.EventArgs) Handles btnAccept.Click    for each dc as DataColumn in FinalDatabaseDataRow.Table.Columns       FinalDatabaseDataRow(dc) = _          tabFinal.Controls(tabFinal.Name + dc.ColumnName).Text    next End Sub 
      image from book

      image from book

      C#

      private void btnAccept_Click(object sender, EventArgs e) {    foreach (DataColumn dc in FinalDatabaseDataRow.Table.Columns)    {        FinalDatabaseDataRow[dc] =            tabFinal.Controls[tabFinal.Name + dc.ColumnName].Text;    } } 
      image from book

    11. The final step is to configure the buttons. Set the DialogResult of btnAccept to OK, and set the DialogResult of btnCancel to Cancel. On the form's property screen, set the Accept-Button property to btnAccept and set the CancelButton property to btnCancel.

    Calling the Conflict Resolution Screen

    The conflict resolution screen deals with conflicts only on a DataRow-by-DataRow basis. This means you must loop through your concurrency errors, displaying the conflict resolution screen for each conflict.

    Correcting Concurrency Errors with the Conflict Resolution Screen

    When it's time to test the conflict resolution screen, you must have two instances of the application running. When each instance starts, it loads a copy of the data from the database. If changes are made in both instances, the first instance that synchronizes with the database will succeed and the second instance will fail for each DataRow object that was also updated by the first instance.

    Follow these steps to create and resolve concurrency errors.

    1. Start two instances of the application. (Navigate to the ConflictResolution.exe file and double-click it twice, or right-click the project in the Solution Explorer window and choose Debug | Start New Instance twice.) You will see the contents of the TblCustomer table.

    2. The first time the application is executed there is no data, so add data to both instances, as shown in Figure 5-9.

    3. Synchronize with the database. When the first instance is synchronized, the only noticeable change is that the status bar displays a message indicating that the data is synchronized. When the second instance is synchronized, the same message is displayed, but the grid displays the data that was input into the first instance. Notice that both instances can synchronize with the new data because there are no conflicts. If the first instance is synchronized again, it will contain the data from the second instance, as shown in Figure 5-10.

    4. Make changes to the same name in both instances. For example, you can change the name 234 to Joe in one instance and to Mary in the other instance. Synchronize the instance with Joe as the name, and the synchronization will succeed. Next, synchronize the instance with Mary as the name; a concurrency error occurs because the data in the database was changed between the time this instance was previously synchronized and this attempt to synchronize. The grid displays the error, as shown in Figure 5-11.

    5. Finally, test the conflict resolution screen. Click the Resolve Concurrency Errors menu item to launch the conflict resolution screen, as shown in Figure 5-12. The tab pages are populated with data. Move between the tab pages to see the current disconnected data values, the original database values, and the current database values. Note that the final data values are displayed at the bottom, and notice the button beside each TextBox that allows you to copy the value in the TextBox into the final data value.

    image from book
    Figure 5-9: Different data has been entered into each instance of the application.

    image from book
    Figure 5-10: The two instances are synchronized with the database.

    image from book
    Figure 5-11: The concurrency error is displayed in the grid because the underlying data changed since we read the data.

    image from book
    Figure 5-12: The conflict resolution screen allows you to resolve a concurrency error by selecting the current disconnected data values, the original database values, or the current database values.

    Building a Better Conflict Resolution Screen

    The conflict resolution screen was designed to let you generically resolve conflicts from concurrency errors. Most of the code can be reused in your application, but you might want to make some changes to make the screen more appealing to your users and to further encapsulate your code. Here are some changes to consider.

     


    Programming Microsoft ADO. NET 2.0 Applications. Advanced Topics
    Linux Application Development (2nd Edition)
    ISBN: 735621411
    EAN: 2147483647
    Year: 2004
    Pages: 85
    Authors: Michael K. Johnson, Erik W. Troan
    BUY ON AMAZON

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net