Let's modify the spec to say that the Suppliers form will come up in display mode (with editing disabled), and the user will have the ability to make a menu choice to edit the form, and then save or cancel the edits. Figure 2-25. Customer Details updatedTo accomplish this, you'll want to add a menu to the form, and an indication (perhaps in the form title bar) as to which mode you are in: Read, Edit, or Unsaved. In Read mode, the text boxes and grid will be disabled. In Edit mode the controls will be enabled. Once you've made changes to the form, but not yet saved them, you'll be in Unsaved mode. The advantage of distinguishing between Edit and Unsaved mode is that if Cancel is selected or there is an attempt to close the form, you can put up a reminder that the changes have not been saved. To begin, add a menu strip control to frmSuppliers.vb, as shown in Figure 2-26. Figure 2-26. Add Editing Menu to frmSuppliersThe code in the frmSuppliers_Load event handler, as it now stands, loads the data from the database. You need to change it to first disable the text boxes and the datagrid, and then add event handlers to detect when the user makes changes. The new implementation of frmSuppliers_Load is shown in Example 2-9. Example 2-9. New Suppliers form Load event handlerPrivate Sub frmSuppliers_Load( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load Me.SuppliersTableAdapter.FillByCompanyName( _ Me.NorthwindDataSet.Suppliers, Me.m_CompanyNameParameter) Me.ProductsTableAdapter.Fill(Me.NorthwindDataSet.Products) Dim ctrl As Control Dim txtbox As TextBox = Nothing Dim dgv As DataGridView = Nothing For Each ctrl In Me.Controls If TypeOf ctrl Is TextBox Then txtbox = CType(ctrl, TextBox) txtbox.Enabled = False AddHandler txtbox.ModifiedChanged, AddressOf TextBoxChanged ElseIf TypeOf ctrl Is DataGridView Then dgv = CType(ctrl, DataGridView) dgv.Enabled = False AddHandler dgv.CellValueChanged, AddressOf DataGridChanged End If Next Me.Text = formName + " Read only" End Sub
Add a class member named formName and set that to the invariant text for the form title. Public Class frmSuppliers Private ReadOnly formName As String = "Suppliers" The last line of Example 2-9 sets the form's title to Suppliers Read only. When the mode changes, you'll modify this string to keep the user up to date on the current mode. Let's examine the For Each loop in Example 2-9 a bit more closely. You start by iterating through all of the controls in the form's Controls collection. You can't know what type of control you have, so you define your variable to be of type Control: Dim ctrl As Control You are looking for TextBox and DataGridView controls (the two types of controls you want to modify) so you create references to those types, which you will use if you determine that the actual type of the control is one of these two types: Dim txtbox As TextBox Dim dgv As DataGridView As you examine each control in turn, you check to see if it is of type Textbox If so, it is safe to cast that object to a TextBox and then set the Enabled property. If TypeOf ctrl Is TextBox Then txtbox = CType(ctrl, TextBox) txtbox.Enabled = False The next step is to set the method that you want all text boxes to invoke when their contents are changed: AddHandler txtbox.ModifiedChanged, AddressOf TextBoxChanged AddHandler adds an event handler to the text box. It takes two arguments: the event you want to handle (in this case, ModifiedChanged which is fired whenever the modified state of the text box is changed) and the address of the method to invoke. The net effect is that whenever the text box is changed, then the TextBoxChanged method will be called. If the control is not a Text box, you test to see if it is a DataGridView, and if so, you disable it and set its event handler: ElseIf TypeOf ctrl Is DataGridView Then dgv = CType(ctrl, DataGridView) dgv.Enabled = False AddHandler dgv.CellValueChanged, AddressOf DataGridChanged End If Notice that with a DataGridView you are responding to a different event: CellValueChanged. You'll come back to these methods in a moment.
Add the Click event handler for the Edit menu item. To do so, click on Edit, then in the Properties window, click on the lightening bolt and then double-click next to the Click event. Visual Studio 2005 will create a skeleton for your event handler that you will fill in, as shown in Example 2-10. Example 2-10. Edit item Click event handlerPrivate Sub EditToolStripMenuItemEdit_Click( _ ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles EditToolStripMenuItemEdit.Click Dim ctrl As Control For Each ctrl In Me.Controls ctrl.Enabled = True Next Me.Text = formName + " Ready to edit" End Sub When the user clicks Editing Edit, the event handler iterates through the controls and enables every control. This is safe, because you dont mind enabling the menu items and labels and other controls you previously ignored. You also change the title of the form to "Ready to edit." When the user makes a change to any of the text boxes, the ModifiedChanged event fires, and as you saw earlier, the TextBoxChanged method is invoked. The job of this method is to keep track of changes in the data, and to set the title to "Edited, not saved." To track whether any value has been changed, create a member variable: Private m_Dirty As Boolean = False This will be useful later, when the user clicks Cancel: you can test if any values have been changed just by checking this one Boolean value. Next, create event handlers so that when either the TextBoxChanged or the DataGridChanged events fire, a helper method DataChanged, will be called that sets the m_Dirty flag to true, and sets the text for the form to "Edited, not saved." You need to type in the code for these, as shown in Example 2-11, rather than using Visual Basic to create event handlers. You associate them with the event at runtime by calling AddHandler, as you saw above. Example 2-11. DataGrid and TextBox changed event handlers and the DataChanged helper method'event handler Private Sub DataGridChanged( _ ByVal sender As System.Object, _ ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) DataChanged( ) End Sub 'event handler Private Sub TextBoxChanged( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) DataChanged( ) End Sub 'helper method Private Sub DataChanged( ) Me.m_Dirty = True Me.Text = formName + " Edited, not saved." End Sub Using the DataChanged helper method "factors out" common code from both event handlers, so that you do not have the same code in two places. This makes maintaining the program much easier.
All that is left is to handle the Save and Cancel events. If the user clicks Cancel, you want to check and see if the m_Dirty flag has been set true (indicating that the user has made some changes that might be lost). If so, you'll show a warning dialog box. If the user insists on the Cancel (saying yes at the warning), then you'll reload the original data (just as you did in Form_Load), disable the appropriate controls, and set the form title back to read only (ReadOnly). Because resetting the data is done both in Form_Load and in Cancel, it's useful to factor that code out to a common method that can be called from either event handler, as shown in Example 2-12. Example 2-12. LoadFromDB helper methodPrivate Sub LoadFromDB( ) Me.SuppliersTableAdapter.FillByCompanyName( _ Me.NorthwindDataSet.Suppliers, Me.m_CompanyNameParameter) Me.ProductsTableAdapter.Fill(Me.NorthwindDataSet.Products) End Sub Thus, the start of the frmSuppliers_Load event handler goes from: Private Sub frmSuppliers_Load( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load Me.SuppliersTableAdapter.FillByCompanyName( _ Me.NorthwindDataSet.Suppliers, Me.m_CompanyNameParameter) Me.ProductsTableAdapter.Fill(Me.NorthwindDataSet.Products) to the simpler: Private Sub frmSuppliers_Load( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load LoadFromDB( ) Because you want to take the same action of disabling the controls and setting the form title back to read only whether the user clicks Cancel or Save, you'll factor that work out to a common method as well, as shown in Example 2-13. Example 2-13. StopEditing helper methodPrivate Sub StopEditing( ) Dim ctrl As Control For Each ctrl In Me.Controls If TypeOf ctrl Is DataGridView Or TypeOf ctrl Is TextBox Then ctrl.Enabled = False End If Next Me.Text = formName + " Read only" End Sub Example 2-14 shows the the source code for the Cancel button event handler. Example 2-14. Cancel button Click event handlerPrivate Sub CancelToolStripMenuItemCancel_Click( _ ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles CancelToolStripMenuItemCancel.Click Dim doCancel As Boolean = True If Me.m_Dirty = True Then Dim result As DialogResult = _ MessageBox.Show( _ "You have unsaved work. Are you sure you want to cancel?", _ "Risk of losing unsaved changes", _ MessageBoxButtons.YesNo, _ MessageBoxIcon.Warning) If result = DialogResult.No Then doCancel = False End If End If If doCancel = True Then LoadFromDB( ) StopEditing( ) m_Dirty = False End If End Sub You first test to see if the m_Dirty flag is true. If so, you show a message box with the warning text and the Yes and No buttons. The result is stored in a DialogResult variable, which you can then test against the enumerated constant DialogResult.No. Assuming this test fails (and the user clicked Yes), or if the m_Dirty flag was not true, you are now ready to reload the original data (by calling LoadFromDB) and disable the controls (by calling StopEditing). Finally, if the user clicks Save, you will call EndEdit on the DataConnector, and Update on the TableAdapter (as shown earlier in this chapter). Finally, you will call StopEditing to return to read only mode. Code for handling the Save item on the menu is shown in Example 2-15. Example 2-15. Save menu item Click event handlerPrivate Sub SaveToolStripMenuItem_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles SaveToolStripMenuItem.Click Me.SuppliersBindingSource.EndEdit( ) If m_Dirty = True Then Dim tbChanges As Data.DataTable = _ Me.NorthwindDataSet.Suppliers.GetChanges( ) If Not tbChanges Is Nothing Then Me.SuppliersTableAdapter.Update(tbChanges) End If tbChanges = Me.NorthwindDataSet.Products.GetChanges( ) If Not tbChanges Is Nothing Then Me.ProductsTableAdapter.Update(tbChanges) End If End If StopEditing( ) End Sub The complete source for this form is shown in Example 2-16. Example 2-16. Complete source code for frmSuppliersPublic Class frmSuppliers Private m_CompanyNameParameter As String Private m_Dirty As Boolean = False Private ReadOnly formName As String = "Suppliers" Public WriteOnly Property CompanyNameParameter( ) As String Set(ByVal value As String) m_CompanyNameParameter = value End Set End Property Private Sub frmSuppliers_Load( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load LoadFromDB( ) Dim ctrl As Control Dim txtbox As TextBox Dim dgv As DataGridView For Each ctrl In Me.Controls If TypeOf ctrl Is TextBox Then txtbox = CType(ctrl, TextBox) txtbox.Enabled = False AddHandler txtbox.ModifiedChanged, AddressOf TextBoxChanged ElseIf TypeOf ctrl Is DataGridView Then dgv = CType(ctrl, DataGridView) dgv.Enabled = False AddHandler dgv.CellValueChanged, AddressOf DataGridChanged End If Next Me.Text = formName + " Read only" End Sub Private Sub StopEditing( ) Dim ctrl As Control For Each ctrl In Me.Controls If TypeOf ctrl Is DataGridView Or TypeOf ctrl Is TextBox Then ctrl.Enabled = False End If Next Me.Text = formName + " Read only" End Sub Private Sub LoadFromDB( ) Me.SuppliersTableAdapter.FillByCompanyName( _ Me.NorthwindDataSet.Suppliers, Me.m_CompanyNameParameter) Me.ProductsTableAdapter.Fill(Me.NorthwindDataSet.Products) End Sub Private Sub EditToolStripMenuItem_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles EditToolStripMenuItem.Click Dim ctrl As Control For Each ctrl In Me.Controls ctrl.Enabled = True Next Me.Text = formName + " Ready to edit" End Sub 'event handler Private Sub DataGridChanged( _ ByVal sender As System.Object, _ ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) DataChanged( ) End Sub 'event handler Private Sub TextBoxChanged( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) DataChanged( ) End Sub 'helper method Private Sub DataChanged( ) Me.m_Dirty = True Me.Text = formName + " Edited, not saved." End Sub Private Sub CancelToolStripMenuItem_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles CancelToolStripMenuItem.Click Dim doCancel As Boolean = True If Me.m_Dirty = True Then Dim result As DialogResult = _ MessageBox.Show( _ "You have unsaved work. Are you sure you want to cancel?", _ "Risk of losing unsaved changes", _ MessageBoxButtons.YesNo, _ MessageBoxIcon.Warning) If result = DialogResult.No Then doCancel = False End If End If If doCancel = True Then LoadFromDB( ) StopEditing( ) m_Dirty = False End If End Sub Private Sub SaveToolStripMenuItem_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles SaveToolStripMenuItem.Click Me.SuppliersBindingSource.EndEdit( ) If m_Dirty = True Then Dim tbChanges As Data.DataTable = _ Me.NorthwindDataSet.Suppliers.GetChanges( ) If Not tbChanges Is Nothing Then Me.SuppliersTableAdapter.Update(tbChanges) End If tbChanges = Me.NorthwindDataSet.Products.GetChanges( ) If Not tbChanges Is Nothing Then Me.ProductsTableAdapter.Update(tbChanges) End If End If StopEditing( ) End Sub End Class When you run this application, and find a Supplier, the controls are initially disabled, as shown in Figure 2-27. If you click on Editing Edit, you enter Edit mode, and the title changes to "Ready to edit," as shown in Figure 2-28. If you change any field the Title changes again to "Edited, not saved," as shown in Figure 2-29. If you click on Cancel, because you have made changes, the warning dialog will come up, as shown in Figure 2-30. Figure 2-27. Suppliers form opens in read only modeFigure 2-28. Edit modeFigure 2-29. Edited, not savedFigure 2-30. Warning dialog for canceling with unsaved workIf you click Yes here, the changes will be undone and you'll be returned to the original read only mode. If you click No, you'll remain in "Edited, not saved" mode. Finally, if you click Save, the changes are saved, the database is updated, and you are returned to read only mode, with the new data reflected. Mission accomplished. |