No matter what kind of form you've got, after you've created it, you need to get data into it and out of it. Although it is possible for a form to update an application's data directly when the user presses OK or Apply, this is generally considered bad practice for anything except the main form of your application. The problem is that changes in one part of the application might adversely affect your code. For this reason, forms should be as stand-alone as possible. This means that forms will have a set of properties that they manage, letting the client of the form populate the initial values of the properties and pulling out the final values as appropriate, just as you saw earlier in the typical usage of ColorDialog. Because most properties managed by a form are actually properties on the controls that make up the form, you may be tempted to make the control fields in your form public, letting the client of the form do this: Dim dlg As LoanApplicationDialog = New LoanApplicationDialog() dlg.applicantNameTextBox.Text = "Joe Borrower" ' DON'T Dim res As DialogResult = dlg.ShowDialog() The problem with this approach is the same problem you'll encounter when making any field public: If LoanApplicationDialog wants to change the way the applicant 's name is displayed, such as from a TextBox control to a Label control, all users of the LoanApplicationDialog class must now be updated. To avoid this problem, the general practice is to expose public custom form properties that get and set the form's child control properties: [1]
Public Property ApplicantName() As String Get Return applicantNameTextBox.Text End Get Set(ByVal Value As String) applicantNameTextBox.Text = Value End Set End Property Properties are a means of exposing data fields to the users of a class without giving those users direct access to the data itself. The data fields of a class (in this case, the dialog) should remain private . This is one of the principles of encapsulation . Properties look exactly like data fields to users of the class, and thus programming against them is simple and natural. However, properties allow you to validate the access to the data fields. When users request access to the data, they are actually executing code in the Get or Set methods of the property. By eliminating either the Get or Set methods , you can make a property write- or read-only, respectively. In addition, the code that gets can make decisions about whom to grant access to, and also whether to accept values during Set operations. Finally, properties result in a simpler usage model for the form clients , because they no longer need to concern themselves with the implementation details best left to the form: Dim dlg As LoanApplicationDialog = New LoanApplicationDialog() dlg.ApplicantName = "Joe Borrower" Dim res As DialogResult = dlg.ShowDialog() Handling OK and CancelBefore data can be retrieved from the property of a modal form, ShowDialog must first return, and this means that the form must be closed. One way to do that is by calling the Form.Close function inside each button's Click event handler: Sub okButton_Click(sender As Object, e As EventArgs) _ Handles okButton.Click Me.Close() End Sub Sub cancelButton_Click(sender As Object, e As EventArgs) _ Handles cancelButton.Click Me.Close() End Sub Unfortunately, by default, calling Close will return DialogResult.Cancel from the ShowDialog method. For other buttons , we'd like to be able to return other members of the DialogResult enumeration: Enum DialogResult Abort Cancel ' result of calling Form.Close() Ignore No None ' default OK Retry Yes End Enum The Abort, Ignore, No, and Retry values are used mostly by MessageBox.Show, [2] but you should feel free to use them for your own custom forms. The one we want to return from ShowDialog when the OK button is pressed is, of course, OK. Checking the return value from ShowDialog is a shortcut for checking the DialogResult property of the form itself, something you can do instead:
dlg.ShowDialog() Dim res As DialogResult = dlg.DialogResult If res = DialogResult.OK ' user pressed OK By default, the DialogResult property of any form is None. To return something other than Cancel from ShowDialog, you set the form's DialogResult property before closing the form: Sub okButton_Click(sender As Object, e As EventArgs) _ Handles okButton.Click Me.DialogResult = DialogResult.OK Me.Close() End Sub Sub cancelButton_Click(sender As Object, e As EventArgs) _ Handles cancelButton.Click ' Close will set the DialogResult to Cancel, so setting ' it explicitly is just good manners Me.DialogResult = DialogResult.Cancel Me.Close() End Sub When you set the form's DialogResult to something other than None, a modal form interprets that to mean that it should close. Calling Close in this case isn't even necessary, reducing our code to the following for modal forms: Sub okButton_Click(sender As Object, e As EventArgs) _ Handles okButton.Click Me.DialogResult = DialogResult.OK End Sub Sub cancelButton_Click(sender As Object, e As EventArgs) _ Handles cancelButton.Click Me.DialogResult = DialogResult.Cancel End Sub With this code in place, clicking on the OK or Cancel button dismisses a form such as the one shown in Figure 3.4. This action returns the correct result and, using properties, exposes whatever information the user entered during the lifetime of the form. Figure 3.4. A Sample Form Used as a Dialog (See Plate 6)
Unfortunately, we don't have quite all the behavior we need from our OK and Cancel buttons. In Figure 3.4 notice that the OK button is not drawn as the default button. The default button is the one invoked when the Enter key is pressed, and it's typically drawn with a thicker border than nondefault buttons. In addition, although you can't see this in a picture, the Cancel button isn't invoked when the ESC key is pressed. Enabling this behavior is a matter of designating in the form itself which buttons should be invoked when Enter and ESC are pressed. You do this by setting the form's AcceptButton and CancelButton properties: Sub InitializeComponent() ... Me.AcceptButton = Me.okButton Me.CancelButton = Me.cancelButton ... End Sub Sub okButton_Click(sender As Object, e As EventArgs) _ Handles okButton.Click Me.DialogResult = DialogResult.OK End Sub Sub cancelButton_Click(sender As Object, e As EventArgs) _ Handles cancelButton.Click Me.DialogResult = DialogResult.Cancel End Sub Notice that we used the Designer to set these two properties. This is handy because the Property Browser shows a drop-down list of all the buttons currently on the form to choose from. As it turns out, after you've set the form's CancelButton property, you don't need to set the DialogResult property in the Cancel button's Click event handler, making the Cancel button's Click event handler itself unnecessary. This works because when you set the form's CancelButton property, the Designer sets the DialogResult property on the Cancel button itself. Because it's normally a button that dismisses a form, the Button class provides a DialogResult property to designate the result that pressing this button will have on the form. By default, the value of this property is DialogResult.None, but the Designer sets to Cancel the DialogResult property of the button designated as the CancelButton on the form. However, the Designer does not set the form's AcceptButton DialogResult property in the same manner. [3] Luckily, if you set the DialogResult property of the OK button to DialogResult.OK yourself, you can dismiss the form without having the OK or Cancel button Click event handler at all:
Sub InitializeComponent() ... Me.okButton.DialogResult = DialogResult.OK Me.cancelButton.DialogResult = DialogResult.Cancel ... Me.AcceptButton = Me.okButton Me.CancelButton = Me.cancelButton ... End Sub 'okButton_Click handler not needed 'cancelButton_Click handler not needed So even though it's possible to implement the client event handlers for the OK and Cancel buttons, often you can get away with simply setting the form's AcceptButton and CancelButton properties and setting the DialogResult property of the OK button. This technique gives you all the data exchange behavior you'll need in a modal form (except data validation, which we cover later in this chapter). Modeless Form DataModeless forms require a different strategy than modal forms to communicate user-updated data to the form's client. For one thing, setting the DialogResult property of a modeless form doesn't automatically dismiss it as it does for a modal form. For another thing, because Show returns immediately, the client usage model is different. Finally, modeless forms acting as dialogs usually have Apply and Close buttons, so data entered into the form can be used before the modeless form even goes away. What's needed is a way to notify a client of a modeless form when the Accept button is pressed. Luckily, standard .NET events [4] can be used for this purpose:
Public Class PropertiesDialog Inherits Form ' Event to fire when Accept is pressed Public Event Accept As EventHandler Sub acceptButton_Click(sender As Object, e As EventArgs) _ Handles acceptButton.Click RaiseEvent Accept(Me, EventArgs.Empty) End Sub End Class In this example, notice that PropertiesDialog exposes a public event called Accept using the standard EventHandler delegate signature. When the Accept button is pressed, the form fires the Accept event to notify interested parties. We use the AddHandler method to register a function to be called when the event is fired , passing in the location of the function via AddressOf: Private WithEvents dlg As PropertiesDialog Sub showProperties_Click(sender As Object, e As EventArgs) _ Handles showProperties.Click Dim dlg As PropertiesDialog = New PropertiesDialog() AddHandler dlg.Accept, AddressOf Properties_Accept dlg.Show() End Sub Sub dlg_Accept(sender As Object, e As EventArgs) Handles dlg.Accept Dim dlg As PropertiesDialog = CType(sender, PropertiesDialog) MessageBox.Show(dlg.SomeProperty) End Sub When the dialog is created, notice that it is declared at class scope using the WithEvents keyword. This notifies the compiler, and VS.NET, that we wish to bind to events the class might throw. A method is created that handles the dialog's Accept event. By convention, the dialog passes itself to the event handler when it fires so that the receiver of the event can get back the reference to the dialog using a simple cast operation. The only thing left to do is to make the modeless dialog's Close button call Form.Close, and you've got yourself a modeless dialog. See Appendix B: Delegates and Events for more on this topic. |