Business Logic Framework - Tutorial 11:
Our eleventh tutorial shows you how to update a single record to the database and prevent any accidental overwriting of underlying data that might have been changed.
Problem:
In today's environment there are more and more applications that work in disconnected mode, meaning that they allow the user to manipulate data while the connection to the database is no longer open. This paradigm creates a very fundamental problem in multi user environment i.e. the ability for more than one person to be modifying the same data while they are disconnected. Hence one user may change data and update the database while the other user may still be working with the old data. In this situation when the second user issues an update command on the same record that was already updated by the first user be default the update command will overwrite first user's changes. This may be acceptable in some application scenarios but at the same time lot of scenarios require that a user may only update the data if no one else has modified the underlying record and may not overwrite someone else's changes.
Solution:
Industry standard practice is to use the original values that were retrieved and construct a where clause with those values and use it in the UPDATE or DELETE statement. This can require you to retain the old values while the user modifies them and result in lot of extra code and effort to ensure this technique is applied to each update or delete statement.
QuickObjects Business Logic Framework on the other hand provides a very easy solution to implement this technique. All that needs to be done is to set a property called ConcurrencyMode to ConcurrencyModes.DetectChanges. This ensures that any original values that were loaded are used to check the underlying row hasn't changed during an update or delete command.
Here is the example code that performs concurrency check for Update and Delete:
| private Customers customer = new Customers(); | |
| private void Button_LoadCustomer_Click(object sender, EventArgs e) | |
| { | |
| if (this.TextBox_CustomerID.Text.Trim().Length > 0) | |
| { | |
| // The following line will indicate to the customer object that concurrency check should | |
| // be performed during an update or delete operation. By default this check is not performed | |
| // and the row is simply updated or deleted. By enabling this feature of the business object | |
| // you ensure that if the underlying data in the database has not changed after the original | |
| // record was fetched. If the underlying record has changed then the Update or Delete operation will | |
| // not update or delete the record respectively. This mode does not provide any feedback in form of | |
| // throwing an exception or providing an error if it fails to update or delete the underlying database | |
| // record. Hence you can use the AffectedRecords property or the return | |
| // value of the Delete method to ensure weather the delete was successful or not. | |
| customer.ConcurrencyMode = Akal.QuickObjects.ObjectBase.ConcurrencyModes.DetectChanges; | |
| // the following line displays a very useful method on each field type object | |
| // Parse method takes any string value and converts it to the correct type | |
| // and assigns the converted value into the Value property. | |
| customer.CustomerID.Parse(this.TextBox_CustomerID.Text); | |
| // Since we do not want to modify the Photo we don't have to load it. | |
| customer.Photo.Visible = false; | |
| // Load method expects that the primary key field(s) of the business object | |
| // have already been assigned values. Once the value is assigned | |
| // to the primary key fields, calling the Load method will load all field values | |
| // into the business object. | |
| if (customer.Load()) | |
| { | |
| // Since we have already called the Load method and it was successful | |
| // we can now access the values of all the fields. | |
| this.TextBox_FirstName.Text = customer.FirstName.Value; | |
| this.TextBox_LastName.Text = customer.LastName.Value; | |
| // Just for the visual reference sake we will now display the loaded values. | |
| // These values will not change on Update but will be used by the concurrency manager | |
| // to perform a check and ensure that the underlying row is still the same as the | |
| // one we loaded into the business object. | |
| this.TextBox_OFirstName.Text = customer.FirstName.OriginalValue.Value.ToString(); | |
| this.TextBox_OLastName.Text = customer.LastName.OriginalValue.Value.ToString(); | |
| } | |
| else | |
| { | |
| this.ResetTextBoxes(); | |
| } | |
| } | |
| else | |
| { | |
| this.ResetTextBoxes(); | |
| } | |
| } | |
| private void ResetTextBoxes() | |
| { | |
| this.TextBox_CustomerID.Enabled = true; | |
| this.Button_Save.Enabled = false; | |
| this.TextBox_FirstName.Text = ""; | |
| this.TextBox_LastName.Text = ""; | |
| } | |
| private void Button_Save_Click(object sender, EventArgs e) | |
| { | |
| if (this.TextBox_FirstName.Text.Trim().Length > 0 | |
| && this.TextBox_LastName.Text.Trim().Length > 0) | |
| { | |
| // Setting the ObjectMode property of an object allow the object to | |
| // know what it is supposed to do with the data you are supplying it with. | |
| // As shown in the example below, once the ObjectMode is set to Save | |
| // each value that you assign to this business object's (customer) properties | |
| // will be used to update the underlying data base record for this object. | |
| customer.ObjectMode = Akal.QuickObjects.ObjectBase.ObjectModes.Save; | |
| // Assigning the value to the Primary Key field(s) is required for the object | |
| // to be able to update. | |
| customer.CustomerID.Parse(this.TextBox_CustomerID.Text); | |
| customer.FirstName.Value = this.TextBox_FirstName.Text; | |
| customer.LastName.Value = this.TextBox_LastName.Text; | |
| // Calling Update method is all you need after assigning the values that need to be updated. | |
| // since the concurrency mode is already set to DetectChanges calling the Update method | |
| // will automatically utilize the concurrency manager to detect underlying row changes before | |
| // performing the update. | |
| if (customer.Update()) | |
| { | |
| MessageBox.Show("Customer Saved!"); | |
| } | |
| else | |
| { | |
| if (customer.ErrorString == null) | |
| { | |
| MessageBox.Show("The underlying row was modified by a different user or a process and your changes were not saved.\nPlease reload the object and save your changes again."); | |
| } | |
| else | |
| { | |
| MessageBox.Show(customer.ErrorString); | |
| } | |
| } | |
| } | |
| else | |
| { | |
| MessageBox.Show("First Name and Last Name must be specified"); | |
| } | |
| } | |
| private void Button_Delete_Click(object sender, EventArgs e) | |
| { | |
| // Since the ConcurrencyMode is already set to DetectChanges calling the Delete | |
| // method will only work if the underlying record in the database still has the same values | |
| // as the ones that were fetched earlier by the business object in the Load method call. | |
| if (customer.Delete()) | |
| { | |
| MessageBox.Show("Customer Deleted!"); | |
| this.ResetTextBoxes(); | |
| } | |
| else | |
| { | |
| if (customer.ErrorString == null) | |
| { | |
| MessageBox.Show("The underlying row was modified by a different user or a process and the business object did not delete your record.\nPlease reload the object and then try delete again."); | |
| } | |
| else | |
| { | |
| MessageBox.Show(customer.ErrorString); | |
| } | |
| } | |
| } | |
| Private customer As Customers = New Customers() | |
| Private Sub Button_LoadCustomer_Click(ByVal sender As Object, ByVal e As EventArgs) Handles Button_LoadCustomer.Click | |
| If Me.TextBox_CustomerID.Text.Trim().Length > 0 Then | |
| ' The following line will indicate to the customer object that concurrency check should | |
| ' be performed during an update or delete operation. By default this check is not performed | |
| ' and the row is simply updated or deleted. By enabling this feature of the business object | |
| ' you ensure that if the underlying data in the database has not changed after the original | |
| ' record was fetched. If the underlying record has changed then the Update or Delete operation will | |
| ' not update or delete the record respectively. This mode does not provide any feedback in form of | |
| ' throwing an exception or providing an error if it fails to update or delete the underlying database | |
| ' record. Hence you can use the AffectedRecords property or the return | |
| ' value of the Delete method to ensure weather the delete was successful or not. | |
| customer.ConcurrencyMode = Akal.QuickObjects.ObjectBase.ConcurrencyModes.DetectChanges | |
| ' the following line displays a very useful method on each field type object | |
| ' Parse method takes any string value and converts it to the correct type | |
| ' and assigns the converted value into the Value property. | |
| customer.CustomerID.Parse(Me.TextBox_CustomerID.Text) | |
| ' Since we do not want to modify the Photo we don't have to load it. | |
| customer.Photo.Visible = False | |
| ' Load method expects that the primary key field(s) of the business object | |
| ' have already been assigned values. Once the value is assigned | |
| ' to the primary key fields, calling the Load method will load all field values | |
| ' into the business object. | |
| If customer.Load() Then | |
| ' Since we have already called the Load method and it was successful | |
| ' we can now access the values of all the fields. | |
| Me.TextBox_FirstName.Text = customer.FirstName.Value | |
| Me.TextBox_LastName.Text = customer.LastName.Value | |
| ' Just for the visual reference sake we will now display the loaded values. | |
| ' These values will not change on Update but will be used by the concurrency manager | |
| ' to perform a check and ensure that the underlying row is still the same as the | |
| ' one we loaded into the business object. | |
| Me.TextBox_OFirstName.Text = customer.FirstName.OriginalValue.Value.ToString() | |
| Me.TextBox_OLastName.Text = customer.LastName.OriginalValue.Value.ToString() | |
| Else | |
| Me.ResetTextBoxes() | |
| End If | |
| Else | |
| Me.ResetTextBoxes() | |
| End If | |
| End Sub | |
| Private Sub ResetTextBoxes() | |
| Me.TextBox_CustomerID.Enabled = True | |
| Me.Button_Save.Enabled = False | |
| Me.TextBox_FirstName.Text = "" | |
| Me.TextBox_LastName.Text = "" | |
| End Sub | |
| Private Sub Button_Save_Click(ByVal sender As Object, ByVal e As EventArgs) Handles Button_Save.Click | |
| If (Me.TextBox_FirstName.Text.Trim().Length > 0 And Me.TextBox_LastName.Text.Trim().Length > 0) Then | |
| ' Setting the ObjectMode property of an object allow the object to | |
| ' know what it is supposed to do with the data you are supplying it with. | |
| ' As shown in the example below, once the ObjectMode is set to Save | |
| ' each value that you assign to this business object's (customer) properties | |
| ' will be used to update the underlying data base record for this object. | |
| customer.ObjectMode = Akal.QuickObjects.ObjectBase.ObjectModes.Save | |
| ' Assigning the value to the Primary Key field(s) is required for the object | |
| ' to be able to update. | |
| customer.CustomerID.Parse(Me.TextBox_CustomerID.Text) | |
| customer.FirstName.Value = Me.TextBox_FirstName.Text | |
| customer.LastName.Value = Me.TextBox_LastName.Text | |
| ' Calling Update method is all you need after assigning the values that need to be updated. | |
| ' since the concurrency mode is already set to DetectChanges calling the Update method | |
| ' will automatically utilize the concurrency manager to detect underlying row changes before | |
| ' performing the update. | |
| If customer.Update() Then | |
| MessageBox.Show("Customer Saved!") | |
| Else | |
| If customer.ErrorString Is Nothing Then | |
| MessageBox.Show("The underlying row was modified by a different user or a process and your changes were not saved.\nPlease reload the object and save your changes again.") | |
| Else | |
| MessageBox.Show(customer.ErrorString) | |
| End If | |
| End If | |
| Else | |
| MessageBox.Show("First Name and Last Name must be specified") | |
| End If | |
| End Sub | |
| Private Sub Button_Delete_Click(ByVal sender As Object, ByVal e As EventArgs) Handles Button_Delete.Click | |
| ' Since the ConcurrencyMode is already set to DetectChanges calling the Delete | |
| ' method will only work if the underlying record in the database still has the same values | |
| ' as the ones that were fetched earlier by the business object in the Load method call. | |
| If customer.Delete() Then | |
| MessageBox.Show("Customer Deleted!") | |
| Me.ResetTextBoxes() | |
| Else | |
| If customer.ErrorString Is Nothing Then | |
| MessageBox.Show("The underlying row was modified by a different user or a process and the business object did not delete your record.\nPlease reload the object and then try delete again.") | |
| Else | |
| MessageBox.Show(customer.ErrorString) | |
| End If | |
| End If | |
| End Sub | |
Enter a value for Customer ID

Now Click on the "Load Customer" button
You will now see that the Customer Details have been loaded and the First and Last Names of the customer will appear in the text boxes.
Now modify these values and click on the Save button. Since the underlying row in the database hasn't changed the Update will be successful.

Now Click Save button again, and since the underlying row is different from the one originally loaded in the business object, the Update operation will not work.

Now lets enter a different Customer ID and click on the Load Customer button.

Once the customer details are loaded we can click on the Delete button.

To simulate a more real life scenario, simply update the underlying customer row in the database before clicking on the Delete button. You will see that the business object will not delete the customer row since it has already changed in the database.
