Business Logic Framework - Tutorial 21:
In this tutorial we will see how to implement a disconnect mode with a later save/sync capability. The disconnected mode can be a very useful feature in case you are dealing with any of the following scenarios:
- Pass data to/from remote client using Web Services or Remoting and Synchronize changes made by user as needed.
- Retain user changes in memory until they press a certain button.
- Save changes/data in an Xml file for over a period of time allowing the user to make as many changes as they want and synchronizing on demand.
The disconnected mode is a powerful feature that allows you to make changes (inserts/updates/deletes) to multiple related objects and it can synchronize them later. During synchronization/save process the all the relationships and keys are automatically handled simplifying the entire process. In prior versions we had great support for building web services, however you were restricted to using the bare bones entities that were generated by the wsdl. Although this capability remains the same, you now can leverage the full power of the business objects on the client by simply passing the data between the client and the web service.
This tutorial will cover the scenario where the user is given the capability to make changes to data locally and the user can press Save or Cancel buttons. When the user presses the Save button, the business object will then try to save the entire set of changes (including any made to related tables). If the user presses the Cancel button, all changes can be simply discarded without ever sending anything to the database.
| // The purpose of this tutorial is to demonstrate the business object's capability to bind | |
| // to various controls and also be able to work in a disconnected mode where multiple changes are | |
| // saved together. | |
| // We create a property called Customer that will hold an instance of Customers class. | |
| // This instance will be used to load the user specified customer object and also bind to | |
| // the text boxes on the screen. | |
| private Customers _customer = null; | |
| public Customers Customer | |
| { | |
| get | |
| { | |
| // We just want to ensure that if _customer object is null we create a new instance | |
| // and return it. | |
| if (_customer == null) | |
| { | |
| _customer = new Customers(); | |
| } | |
| return _customer; | |
| } | |
| set | |
| { | |
| _customer = value; | |
| } | |
| } | |
| public Tutorial21() | |
| { | |
| InitializeComponent(); | |
| } | |
| private void Button_LoadCustomer_Click(object sender, EventArgs e) | |
| { | |
| // Check if the user specified a value for Customer ID | |
| if (this.TextBox_CustomerID.Text.Trim().Length > 0) | |
| { | |
| // Here we simply parse the user specified value into the CustomerID field. | |
| // The value specified must be a valid integer other wise the Parse method will throw an exception. | |
| this.Customer.CustomerID.Parse(this.TextBox_CustomerID.Text); | |
| // Create a join with Orders object. | |
| this.Customer.Join_CustomerID_Orders_Child(); | |
| // Load method expects that you specify the PrimaryKey value(s) before calling Load, and since we have already | |
| // specified the CustomerID value we can go ahead and call the Load method. | |
| // Since we have created a Join with a Child object, the Load method will automatically load all (orders) child records | |
| // for the specified customer. | |
| if (this.Customer.Load()) | |
| { | |
| // Now that the Customer and Order data has been loaded, we can change the ObjectState to Disconnected. | |
| // Disconnected state allows the objects to retain all user's changes locally, and they are not sent to the database | |
| // unless the ObjectState changes or the Save method is called. | |
| // Note: This is new property introduced in version 3.x | |
| this.Customer.ObjectState = ObjectStates.Disconnected; | |
| // Setting the ObjectMode of the Customer to Save will ensure that anytime | |
| // a value for FirstName or LastName is changed in the user interface their UseInSave | |
| // property values are changed to true. | |
| this.Customer.ObjectMode = ObjectModes.Save; | |
| // We dropped a BindingSource component to the form and set the binding between | |
| // TextBox_FirstName.Text and Customer.FirstName | |
| // TextBox_LastName.Text and Customer.LastName | |
| // It is time to supply a value for the DataSource. | |
| this.customersBindingSource.DataSource = this.Customer; | |
| // Since the Customer has been loaded we can enable the Save/Cancel buttons | |
| this.Button_Cancel.Enabled = true; | |
| this.Button_Save.Enabled = true; | |
| // Disable the TextBox for entering CustomerID until the Cancel button is clicked. | |
| this.TextBox_CustomerID.Enabled = false; | |
| // During the Load process for Customer if any orders were found they will be in a separate | |
| // table in the ResultSet property of the Customer object. | |
| // So we can simply assign the ResultSet property value to the OrderGrid's DataSource | |
| this.OrdersGrid.DataSource = this.Customer.ResultSet; | |
| // Since, the ResultSet will contain more than one table, we need to set the DataMember | |
| // property of the OrdersGrid with the table name for Orders data. | |
| // This can be easily achieved by calling the GetResultSetName() method of the | |
| // Orders object instance that is joined to Customer. Calling GetResultSetName | |
| this.OrdersGrid.DataMember = this.Customer.CustomerID_Orders_Child.GetResultSetName(); | |
| // If there is at least one row in the Grid then we'll set the Last row's CustomerID cell's value to the current Customer's CustomerID value. | |
| if (this.OrdersGrid.Rows.Count > 0) | |
| { | |
| this.OrdersGrid.Rows[this.OrdersGrid.Rows.Count - 1].Cells["CustomerID"].Value = this.Customer.CustomerID.Value; | |
| } | |
| this.OrdersGrid.Enabled = true; | |
| this.Label_Message.Text = "Customer " + this.Customer.FirstName.Value + " " + this.Customer.LastName.Value + " loaded.."; | |
| } | |
| else | |
| { | |
| this.Label_Message.Text = "Invalid Customer ID!"; | |
| this.OrdersGrid.Enabled = false; | |
| } | |
| } | |
| } | |
| private void Button_Cancel_Click(object sender, EventArgs e) | |
| { | |
| if (this.Customer != null) | |
| { | |
| this.Customer.Dispose(); | |
| this.Customer = null; | |
| } | |
| this.Label_Message.Text = ""; | |
| this.TextBox_CustomerID.Text = ""; | |
| this.Button_Save.Enabled = false; | |
| this.Button_Cancel.Enabled = false; | |
| this.TextBox_FirstName.Text = ""; | |
| this.TextBox_LastName.Text = ""; | |
| this.OrdersGrid.DataSource = null; | |
| this.OrdersGrid.Enabled = false; | |
| this.TextBox_CustomerID.Enabled = true; | |
| } | |
| private void Button_Save_Click(object sender, EventArgs e) | |
| { | |
| // Check to see if the user modified either the Orders data or the Customer information. | |
| // Customer's ResultSet property also contains the table that has all the order data. | |
| if (this.Customer.ResultSet.HasChanges() || this.Customer.IsDirty) | |
| { | |
| bool isvalid = false; | |
| // IsDirty flag on a business object can be used to determine if any of the property values have changed | |
| // since last time the object was loaded. | |
| // If the user did not make any changes, then there is no need to update such values. | |
| if (this.Customer.IsDirty) | |
| { | |
| // IsValid property returns true if all validators for all the fields are valid. | |
| // We have customized the "Customers" class, please refer to "CustomizedCustomers.cs" file | |
| // in the BusinessLogicLayer project to see that we have added RequiredFieldValidators | |
| // to FirstName and LastName fields. | |
| if (this.Customer.IsValid) | |
| { | |
| // The current state of the Customer object is "Disconnected" and hence the Update method | |
| // simply retain the changes in the ResultSet and will not be sending changes to the Database. | |
| isvalid = this.Customer.Update(); | |
| } | |
| else | |
| { | |
| // ErrorString property of the business objects can be used to retreive each failed validator's error message. | |
| // Error messages from the failed validators are separated by line breaks. | |
| MessageBox.Show(this.Customer.ErrorString); | |
| } | |
| } | |
| // We are setting the ObjectMode to Save on the "CustomerID_Orders_Child" object of type "Orders" | |
| // This will ensure that when we call the Save method all the order records modified by the user will get saved. | |
| this.Customer.CustomerID_Orders_Child.ObjectMode = ObjectModes.Save; | |
| // We want to ensure that either all changes are saved or nothing is saved so we can indicate to the business object | |
| // that it should run all the database commands in a transaction. | |
| this.Customer.UseTransaction = true; | |
| // Starting version 3.x a new feature called Disconnected Mode was introduced. | |
| // When the ObjectState property is set to Disconnected all Insert/Update/Delete commands affect the local ResultSet only. | |
| // By calling the Save method these changes can be sent to the database. | |
| // In addition, even if you don't use the Disconnected mode, a DataSet can be easily synchronized by calling the Save method. | |
| // The save method will automatically save all changes, retreive (auto generated) primary key values as well as save all the "joined" | |
| // object's changes as well. | |
| if (this.Customer.Save()) | |
| { | |
| this.Label_Message.Text = "All Changes Saved!"; | |
| } | |
| else | |
| { | |
| this.Label_Message.Text = this.Customer.ErrorString; | |
| } | |
| } | |
| } | |
| private void OrdersGrid_RowLeave(object sender, DataGridViewCellEventArgs e) | |
| { | |
| // The following code will ensure that the current CustomerID value is always populated into any new rows that are created. | |
| // This is basically how we can ensure that every now row will automatically receive the current CustomerID value automatically. | |
| if (this.OrdersGrid.Columns.Contains("CustomerID") | |
| && | |
| (this.OrdersGrid.Rows[e.RowIndex].Cells["CustomerID"].Value == null | |
| || | |
| (String.IsNullOrEmpty(this.OrdersGrid.Rows[e.RowIndex].Cells["CustomerID"].Value.ToString())) | |
| ) | |
| ) | |
| { | |
| this.OrdersGrid.Rows[e.RowIndex].Cells["CustomerID"].Value = this.Customer.CustomerID.Value; | |
| } | |
| } | |
| ' The purpose of this tutorial is to demonstrate the business object's capability to bind | |
| ' to various controls and also be able to work in a disconnected mode where multiple changes are | |
| ' saved together. | |
| ' We create a property called Customer that will hold an instance of Customers class. | |
| ' This instance will be used to load the user specified customer object and also bind to | |
| ' the text boxes on the screen. | |
| Private _customer As Customers = Nothing | |
| Public Property Customer() As Customers | |
| Get | |
| ' We just want to ensure that if _customer object is null we create a new instance | |
| ' and return it. | |
| If _customer Is Nothing Then | |
| _customer = New Customers() | |
| End If | |
| Return _customer | |
| End Get | |
| Set(ByVal value As Customers) | |
| _customer = value | |
| End Set | |
| End Property | |
| Public Sub New() | |
| InitializeComponent() | |
| End Sub | |
| Private Sub Button_LoadCustomer_Click(ByVal sender As Object, ByVal e As EventArgs) Handles Button_LoadCustomer.Click | |
| ' Check if the user specified a value for Customer ID | |
| If Me.TextBox_CustomerID.Text.Trim().Length > 0 Then | |
| ' Here we simply parse the user specified value into the CustomerID field. | |
| ' The value specified must be a valid integer other wise the Parse method will throw an exception. | |
| Me.Customer.CustomerID.Parse(Me.TextBox_CustomerID.Text) | |
| ' Create a join with Orders object. | |
| Me.Customer.Join_CustomerID_Orders_Child() | |
| ' Load method expects that you specify the PrimaryKey value(s) before calling Load, and since we have already | |
| ' specified the CustomerID value we can go ahead and call the Load method. | |
| ' Since we have created a Join with a Child object, the Load method will automatically load all (orders) child records | |
| ' for the specified customer. | |
| If Me.Customer.Load() Then | |
| ' Now that the Customer and Order data has been loaded, we can change the ObjectState to Disconnected. | |
| ' Disconnected state allows the objects to retain all user's changes locally, and they are not sent to the database | |
| ' unless the ObjectState changes or the Save method is called. | |
| Me.Customer.ObjectState = ObjectStates.Disconnected | |
| ' We dropped a BindingSource component to the form and set the binding between | |
| ' TextBox_FirstName.Text and Customer.FirstName | |
| ' TextBox_LastName.Text and Customer.LastName | |
| ' It is time to supply a value for the DataSource. | |
| Me.CustomersBindingSource.DataSource = Me.Customer | |
| ' Setting the ObjectMode of the Customer to Save will ensure that anytime | |
| ' a value for FirstName or LastName is changed in the user interface their UseInSave | |
| ' property values are changed to true. | |
| Me.Customer.ObjectMode = ObjectModes.Save | |
| ' Since the Customer has been loaded we can enable the Save/Cancel buttons | |
| Me.Button_Cancel.Enabled = True | |
| Me.Button_Save.Enabled = True | |
| ' Disable the TextBox for entering CustomerID until the Cancel button is clicked. | |
| Me.TextBox_CustomerID.Enabled = False | |
| ' During the Load process for Customer if any orders were found they will be in a separate | |
| ' table in the ResultSet property of the Customer object. | |
| ' So we can simply assign the ResultSet property value to the OrderGrid's DataSource | |
| Me.OrdersGrid.DataSource = Me.Customer.ResultSet | |
| ' Since, the ResultSet will contain more than one table, we need to set the DataMember | |
| ' property of the OrdersGrid with the table name for Orders data. | |
| ' This can be easily achieved by calling the GetResultSetName() method of the | |
| ' Orders object instance that is joined to Customer. Calling GetResultSetName | |
| Me.OrdersGrid.DataMember = Me.Customer.CustomerID_Orders_Child.GetResultSetName() | |
| ' If there is at least one row in the Grid then we'll set the Last row's CustomerID cell's value to the current Customer's CustomerID value. | |
| If Me.OrdersGrid.Rows.Count > 0 Then | |
| Me.OrdersGrid.Rows(Me.OrdersGrid.Rows.Count - 1).Cells("CustomerID").Value = Me.Customer.CustomerID.Value | |
| End If | |
| Me.OrdersGrid.Enabled = True | |
| Me.toolStripLabel1.Text = "Customer " + Me.Customer.FirstName.Value + " " + Me.Customer.LastName.Value + " loaded.." | |
| Else | |
| Me.toolStripLabel1.Text = "Invalid Customer ID!" | |
| Me.OrdersGrid.Enabled = False | |
| End If | |
| End If | |
| End Sub | |
| Private Sub Button_Cancel_Click(ByVal sender As Object, ByVal e As EventArgs) Handles Button_Cancel.Click | |
| If Me.Customer IsNot Nothing Then | |
| Me.Customer.Dispose() | |
| ' Somehow VB.NET does not unbind even after a CustomersBindingSource has been set to Nothing, so setting the fields to Nothing removes the binding. | |
| ' Note: This behavior is different from C#, in C# you don't need to set the fields to null as setting the CustomersBindingSource to null is sufficient. | |
| Me.Customer.FirstName = Nothing | |
| Me.Customer.LastName = Nothing | |
| Me.Customer = Nothing | |
| End If | |
| Me.toolStripLabel1.Text = "" | |
| Me.TextBox_CustomerID.Text = "" | |
| Me.Button_Save.Enabled = False | |
| Me.Button_Cancel.Enabled = False | |
| Me.TextBox_FirstName.Text = "" | |
| Me.TextBox_LastName.Text = "" | |
| Me.OrdersGrid.DataSource = Nothing | |
| Me.OrdersGrid.Enabled = False | |
| Me.TextBox_CustomerID.Enabled = True | |
| End Sub | |
| Private Sub Button_Save_Click(ByVal sender As Object, ByVal e As EventArgs) Handles Button_Save.Click | |
| 'Forces current control to perform databinding as if it lost focus. | |
| 'Without the following line WinForms's databinding won't take affect until the TextBox loses focus. | |
| Me.Validate() | |
| ' Check to see if the user modified either the Orders data or the Customer information. | |
| ' Customer's ResultSet property also contains the table that has all the order data. | |
| If (Me.Customer.ResultSet.HasChanges() Or Me.Customer.IsDirty) Then | |
| Dim isvalid As Boolean = False | |
| ' IsDirty flag on a business object can be used to determine if any of the property values have changed | |
| ' since last time the object was loaded. | |
| ' If the user did not make any changes, then there is no need to update such values. | |
| If (Me.Customer.IsDirty) Then | |
| ' IsValid property returns true if all validators for all the fields are valid. | |
| ' We have customized the "Customers" class, please refer to "CustomizedCustomers.cs" file | |
| ' in the BusinessLogicLayer project to see that we have added RequiredFieldValidators | |
| ' to FirstName and LastName fields. | |
| If (Me.Customer.IsValid) Then | |
| ' The current state of the Customer object is "Disconnected" and hence the Update method | |
| ' simply retain the changes in the ResultSet and will not be sending changes to the Database. | |
| isvalid = Me.Customer.Update() | |
| Else | |
| ' ErrorString property of the business objects can be used to retreive each failed validator's error message. | |
| ' Error messages from the failed validators are separated by line breaks. | |
| MessageBox.Show(Me.Customer.ErrorString) | |
| End If | |
| End If | |
| ' We are setting the ObjectMode to Save on the "CustomerID_Orders_Child" object of type "Orders" | |
| ' This will ensure that when we call the Save method all the order records modified by the user will get saved. | |
| Me.Customer.CustomerID_Orders_Child.ObjectMode = ObjectModes.Save | |
| ' We want to ensure that either all changes are saved or nothing is saved so we can indicate to the business object | |
| ' that it should run all the database commands in a transaction. | |
| Me.Customer.UseTransaction = True | |
| ' Starting version 3.x a new feature called Disconnected Mode was introduced. | |
| ' When the ObjectState property is set to Disconnected all Insert/Update/Delete commands affect the local ResultSet only. | |
| ' By calling the Save method these changes can be sent to the database. | |
| ' In addition, even if you don't use the Disconnected mode, a DataSet can be easily synchronized by calling the Save method. | |
| ' The save method will automatically save all changes, retreive (auto generated) primary key values as well as save all the "joined" | |
| ' object's changes as well. | |
| If (Me.Customer.Save()) Then | |
| Me.toolStripLabel1.Text = "All Changes Saved!" | |
| Else | |
| Me.toolStripLabel1.Text = Me.Customer.ErrorString | |
| End If | |
| End If | |
| End Sub | |
| Private Sub OrdersGrid_RowLeave(ByVal sender As Object, ByVal e As DataGridViewCellEventArgs) Handles OrdersGrid.RowLeave | |
| ' The following code will ensure that the current CustomerID value is always populated into any new rows that are created. | |
| ' This is basically how we can ensure that every now row will automatically receive the current CustomerID value automatically. | |
| If Me.OrdersGrid.Columns.Contains("CustomerID") AndAlso (Me.OrdersGrid.Rows(e.RowIndex).Cells("CustomerID").Value Is Nothing OrElse ([String].IsNullOrEmpty(Me.OrdersGrid.Rows(e.RowIndex).Cells("CustomerID").Value.ToString()))) Then | |
| Me.OrdersGrid.Rows(e.RowIndex).Cells("CustomerID").Value = Me.Customer.CustomerID.Value | |
| End If | |
| End Sub | |
The Customers class has been customized by specifying validation criteria on various fields.
| public partial class Customers : Customers_Base | |
| { | |
| // **************************************** | |
| // NOTE: We are using a feature called "partial classes" only available in from .NET 2.0 onwards | |
| // for this sample. If you would like to implement this in .NET 1.1 you can use the generated | |
| // Customers class under the "Custom" folder. In that case so it would be important for you to | |
| // NOT regenerate the "Custom" classes, or your changes will get overwritten. | |
| // Normally it is not necessary to regenerate "Custom" classes and it is only necessary add/remove | |
| // tables from the underlying schema. If you have customized code, please make sure to create a backup | |
| // before regenerating. | |
| // **************************************** | |
| // Override the OnObjectInitialized method and it can be used | |
| // to further customize your business object after the object has been initialized. | |
| // For example: Below we will add validation | |
| // to the FirstName and LastName fields. | |
| protected override void OnObjectInitialized(ObjectEventArgs e) | |
| { | |
| // Make sure to call the base OnObjectInitialized method else the ObjectInitialized event handlers will not be fired. | |
| base.OnObjectInitialized(e); | |
| // Starting v3.x you are able to leverage the power of built in validation and | |
| // the following just shows a simple view of it. | |
| // We are going to add a RequiredFieldValidator. This field validator | |
| // is not dependent on the IsNull property of the field, and hence you can | |
| // mark any field as required even if the field in the database allows nulls. | |
| // There are multiple overloads available for each validator, lets review two of them. | |
| RequiredFieldValidator fnValidator = new RequiredFieldValidator(); | |
| // We must specify which field will be validated by this Validator. | |
| fnValidator.FieldToValidate = this.FirstName; | |
| // Now we need to add the validator to the Field's Validators collection. | |
| // Note: It is possible to apply a validator to one field while it is going to validate the value | |
| // of a different field. This is by design to ensure utmost control and flexibility over validation process. | |
| this.FirstName.Validators.Add(fnValidator); | |
| // The following overload can used to directly set the FirstToValidate property as well | |
| // as add the validator into the field's Validators collection. | |
| new RequiredFieldValidator(this.LastName, true); | |
| } | |
| } | |
| Partial Public Class Customers | |
| Inherits Customers_Base | |
| ' **************************************** | |
| ' NOTE: We are using a feature called "partial classes" only available in from .NET 2.0 onwards | |
| ' for this sample. If you would like to implement this in .NET 1.1 you can use the generated | |
| ' Customers class under the "Custom" folder. In that case so it would be important for you to | |
| ' NOT regenerate the "Custom" classes, or your changes will get overwritten. | |
| ' Normally it is not necessary to regenerate "Custom" classes and it is only necessary add/remove | |
| ' tables from the underlying schema. If you have customized code, please make sure to create a backup | |
| ' before regenerating. | |
| ' **************************************** | |
| ' Override the OnObjectInitialized method and it can be used | |
| ' to further customize your business object after the object has been initialized. | |
| ' For example: Below we will add validation | |
| ' to the FirstName and LastName fields. | |
| Protected Sub OnObjectInitialized(ByVal e As ObjectEventArgs) | |
| ' Make sure to call the base OnObjectInitialized method else the ObjectInitialized event handlers will not be fired. | |
| MyBase.OnObjectInitialized(e) | |
| ' Starting v3.x you are able to leverage the power of built in validation and | |
| ' the following just shows a simple view of it. | |
| ' We are going to add a RequiredFieldValidator. This field validator | |
| ' is not dependent on the IsNull property of the field, and hence you can | |
| ' mark any field as required even if the field in the database allows nulls. | |
| ' There are multiple overloads available for each validator, lets review two of them. | |
| Dim fnValidator As RequiredFieldValidator = New RequiredFieldValidator() | |
| ' We must specify which field will be validated by this Validator. | |
| fnValidator.FieldToValidate = Me.FirstName | |
| ' Now we need to add the validator to the Field's Validators collection. | |
| ' Note: It is possible to apply a validator to o |
