Let’s add a little more complexity to our class. Because we’re storing birth dates to our custom objects, we can classify persons according to their age. Most basic developers will see an opportunity to use constants here. Instead of using constants to describe the various age groups, we’ll use an enumeration with the following group names:
Public Enum AgeGroup Infant Child Teenager Adult Senior Overaged End EnumCode language: VB.NET (vbnet)
These statements must appear outside any procedure in the class, and we usually place them at the beginning of the file, right after the declaration of the class. Public is an access modifier (we want to be able to access this enumeration from within the application that uses the class). Enum is a keyword: It specifies the beginning of the declaration of an enumeration and it’s followed by the enumeration’s name. The enumeration itself is a list of integer values, each one mapped to a name. In our example, the name Infant corresponds to 0, the name Child corresponds to 1, and so on. The list of the enumeration’s members ends with the End Enum keyword. You don’t really care about the actual values of the names because the very reason for using enumerations is to replace numeric constants with more-meaningful names. You’ll see shortly how enumerations are used both in the class and the calling application.
As you already know, the Framework uses enumerations extensively, and this is how you can add an enumeration to your custom class. You should provide an enumeration for any property with a relatively small number of predetermined settings. The property’s type should be the name of the enumeration, and the editor will open a drop-down box with the property’s settings as needed.
Now add to the class the GetAgeGroup method (Listing 6.12), which returns the name of the age group to which the person represented by an instance of the Minimal class belongs. The name of the group is a member of the AgeGroup enumeration.
Listing 6.12: Using an Enumeration
Public Function GetAgeGroup() As AgeGroup Select Case m_Age Case Is < 3 : Return (AgeGroup.Infant) Case Is < 10 : Return (AgeGroup.Child) Case Is < 21 : Return (AgeGroup.Teenager) Case Is < 65 : Return (AgeGroup.Adult) Case Is < 100 : Return (AgeGroup.Senior) Case Else : Return (AgeGroup.Overaged) End Select End FunctionCode language: VB.NET (vbnet)
The GetAgeGroup method returns a value of the AgeGroup type. Because the AgeGroup enumeration was declared as Public, it’s exposed to any application that uses the Minimal class. Let’s see how we can use the same enumeration in our application. Switch to the form’s code window, add a new button, and enter the statements from Listing 6.13 in its event handler.
Listing 6.13: Using the Enumeration Exposed by the Class
Protected Sub Button1_Click(...) _ Handles Button1.Click Dim obj As Minimal obj = New Minimal() Try obj.BDate = InputBox("Please Enter your birthdate") Catch ex As ArgumentException MsgBox(ex.Message) Exit Sub End Try Debug.WriteLine(obj.Age) Dim discount As Single If obj.GetAgeGroup = Minimal.AgeGroup.Infant Or _ obj.GetAgeGroup = Minimal.AgeGroup.Child Then discount = 0.4 If obj.GetAgeGroup = Minimal.AgeGroup.Senior Then discount = 0.5 If obj.GetAgeGroup = Minimal.AgeGroup.Teenager Then discount = 0.25 MsgBox("You age is " & obj.Age.ToString & _ " and belong to the " & _ obj.GetAgeGroup.ToString & _ " group" & vbCrLf & "Your discount is " & _ Format(discount, "Percent")) End SubCode language: VB.NET (vbnet)
This routine calculates discounts based on the person’s age. Notice that we don’t use numeric constants in our code, just descriptive names. Moreover, the possible values of the enumeration are displayed in a drop-down list by the IntelliSense feature of the IDE as needed (Figure 6.3), and you don’t have to memorize them or look them up as you would with constants. I’ve used an implementation with multiple If statements in this example, but you can perform the same comparisons by using a Select Case statement.
Figure 6.3 – The members of an enumeration are displayed automatically in the IDE as you type
You could also call the GetAgeGroup method once, store its result to a variable, and then use this variable in the comparisons. This approach is slightly more efficient, because you don’t have to call the member of the class repeatedly. The variable, as you can guess, should be of the AgeGroup type. Here’s an alternate code of the statements of Listing 6.13 using a temporary variable, the grp variable, and a Select Case statement:
Dim grp As AgeGroup = obj.GetAgeGroup Select Case grp Case Minimal.AgeGroup.Infant, Minimal.AgeGroup.Child ... Case Minimal.AgeGroup.Teenager ... Case Minimal.AgeGroup.Senior ... Case Else End SelectCode language: VB.NET (vbnet)
You’ve seen the basics of working with custom classes in a VB application. Let’s switch to a practical example that demonstrates not only the use of a real-world class, but also how classes can simplify the development of a project.
The Contacts Project Example
In Chapter, “Working with Forms,” I discussed briefly the Contacts application. This application uses a custom Structure to store the contacts and provides four navigational buttons to allow users to move to the first, last, previous, and next contact. Now that you have learned how to program the ListBox control and how to use custom classes in your code, we’ll revise the Contacts application. First, we’ll implement a class to represent each contact. The fields of each contact (company and contact names, addresses, and so on) will be implemented as properties and they will be displayed in the TextBox controls on the form.
We’ll also improve the user interface of the application. Instead of the rather simplistic navigational buttons, we’ll place all the company names in a sorted ListBox control. The user can easily locate the desired company and select it from the list to view the fields of the selected company. The editing buttons at the bottom of the form work as usual, but we no longer need the navigational buttons. Figure 6.4 shows the revised Contacts application.
Figure 6.4 – The interface of the Contacts application is based on the ListBox control.
Name the new class Contact and enter the code from Listing 6.14 into it. The names of the private members of the class are the same as the actual property names, and they begin with an underscore. (This is a good convention that lets you easily distinguish whether a variable is private, and the property value it stores.) The implementation of the properties is trivial, so I’m not showing the code for all of them.
Listing 6.14: The Contact Class
<Serializable()> Public Class Contact Private _companyName As String Private _contactName As String Private _address1 As String Private _address2 As String Private _city As String Private _state As String Private _zip As String Private _tel As String Private _email As String Private _URL As String Property CompanyName() As String Get CompanyName = _companyName End Get Set(ByVal value As String) If value Is Nothing Or value = "" Then Throw New Exception("Company Name field can’t be empty") Exit Property End If _companyName = value End Set End Property Property ContactName() As String Get ContactName = _contactName End Get Set(ByVal value As String) _contactName = value End Set End Property Property Address1() As String Get Address1 = _address1 End Get Set(ByVal value As String) _address1 = value End Set End Property Property Address2() As String Get Address2 = _address2 End Get Set(ByVal value As String) _address2 = value End Set End Property Property City() As String Get City = _city End Get Set(ByVal value As String) _city = value End Set End Property Property State() As String Get State = _state End Get Set(ByVal value As String) _state = value End Set End Property Property ZIP() As String Get ZIP = _zip End Get Set(ByVal value As String) _zip = value End Set End Property Property tel() As String Get tel = _tel End Get Set(ByVal value As String) _tel = value End Set End Property Property EMail() As String Get EMail = _email End Get Set(ByVal value As String) If value.Contains("@") Or value.Trim.Length = 0 Then _email = Value Else Throw New Exception("Invalid e-mail address!") End If End Set End Property Property URL() As String Get URL = _URL End Get Set(ByVal value As String) _URL = value End Set End Property Overrides Function ToString() As String If _contactName = "" Then Return _companyName Else Return _companyName & vbTab & "(" & _contactName & ")" End If End Function Public Sub New() MyBase.New() End Sub Public Sub New(ByVal CompanyName As String, _ ByVal LastName As String, ByVal FirstName As String) MyBase.New() Me.ContactName = LastName & ", " & FirstName Me.CompanyName = CompanyName End Sub Public Sub New(ByVal CompanyName As String) MyBase.New() Me.CompanyName = CompanyName End Sub End ClassCode language: VB.NET (vbnet)
The first thing you’ll notice is that the class’s definition is prefixed by the <Serializable()> keyword. The topic of serialization is discussed in Chapter, “XML and Object Serialization,” but for now all you need to know is that the .NET Framework can convert objects to a text or binary format and then store them in files. Surprisingly, this process is quite simple; as you will see, we’ll be able to dump an entire collection of Contact objects to a file with a single statement. The <Serializable()> keyword is an attribute of the class, and (as you will see later in this book) there are more attributes you can use with your classes — or even with your methods. The most prominent method attribute is the <WebMethod> attribute, which turns a regular function into a web method.
The various fields of the Contact structure are now properties of the Contact class. The implementation of the properties is trivial except for the CompanyName and EMail properties, which contain some validation code. The Contact class requires that the CompanyName property have a value; if it doesn’t, the class throws an exception. Likewise, the EMail property must contain the symbol @. Finally, the class provides its own ToString method, which returns the name of the company followed by the contact name in parentheses.
The ListBox control, in which we’ll store all contacts, displays the value returned by the object’s ToString method, which is why you have to provide your own implementation of the method to describe each contact. The company name should be adequate, but if there are two companies by the same name, you can use another field to differentiate them. I used the contact name, but you can use any of the other properties (the URL would be a good choice).
The ListBox displays a string, but it stores the object itself. In essence, it’s used not only as a navigational tool, but also as a storage mechanism for our contacts. Now, we must change the code of the main form a little. Start by removing the navigational buttons; we no longer need them. Their function will be replaced by a few lines of code in the ListBox control’s SelectedIndexChanged event. Every time the user selects another item on the list, the statements shown in Listing 6.15 display the contact’s properties in the various TextBox controls on the form.
Listing 6.15: Displaying the Fields of the Selected Contact Object
Private Sub ListBox1 SelectedIndexChanged(...) _ Handles ListBox1.SelectedIndexChanged currentContact = ListBox1.SelectedIndex ShowContact() End SubCode language: VB.NET (vbnet)
The ShowContact() subroutine reads the object stored at the location specified by the currentContact variable and displays its properties in the various TextBox controls on the form. The TextBox controls are normally read-only, except when editing a contact. This action is signaled by clicking the Edit or the Add button on the form.
When a new contact is added, the code reads its fields from the controls on the form, creates a new Contact object, and adds it to the ListBox control. When a contact is edited, a new Contact object replaces the currently selected object on the control. The code is similar to the code of the Contacts application. I should mention that the ListBox control is locked while a contact is being added or edited, because it doesn’t make sense to select another contact at that time.
Adding, Editing, and Deleting Contacts
To delete a contact (Listing 6.16), we simply remove the currently selected object from the ListBox control. In addition, we must select the next contact on the list (or the first contact if the deleted one was last in the list).
Listing 6.16: Deleting an Object from the ListBox
Private Sub bttnDelete_Click(...) Handles bttnDelete.Click If currentContact > -1 Then ListBox1.Items.RemoveAt(currentContact) currentContact = ListBox1.Items.Count - 1 If currentContact = -1 Then ClearFields() MsgBox("There are no more contacts") Else ShowContact() End If Else MsgBox("No current contacts to delete") End If End SubCode language: VB.NET (vbnet)
When you add a new contact, the following code is executed in the Add button’s Click event handler:
Private Sub bttnAdd_Click(...) Handles bttnAdd.Click adding = True ClearFields() HideButtons() ListBox1.Enabled = False End SubCode language: VB.NET (vbnet)
The controls are cleared in anticipation of the new contact’s fields, and the adding variable is set to True. The OK button is clicked to end either the addition of a new record or an edit operation. The code behind the OK button is shown in Listing 6.17.
Listing 6.17: Committing a New or Edited Record
Private Sub bttnOK_Click(...) Handles bttnOK.Click If SaveContact() Then ListBox1.Enabled = True ShowButtons() End If End SubCode language: VB.NET (vbnet)
As you can see, the same subroutine handles both the insertion of a new record and the editing of an existing one. All the work is done by the SaveContact() subroutine, which is shown in Listing 6.18.
Listing 6.18: The SaveContact() Subroutine
Private Function SaveContact() As Boolean Dim contact As New Contact Try contact.CompanyName = txtCompany.Text contact.ContactName = txtContact.Text contact.Address1 = txtAddress1.Text contact.Address2 = txtAddress2.Text contact.City = txtCity.Text contact.State = txtState.Text contact.ZIP = txtZIP.Text contact.tel = txtTel.Text contact.EMail = txtEMail.Text contact.URL = txtURL.Text Catch ex As Exception MsgBox(ex.Message) Return False End Try If adding Then ListBox1.Items.Add(contact) Else ListBox1.Items(currentContact) = contact End If Return True End FunctionCode language: VB.NET (vbnet)
The SaveContact() function uses the adding variable to distinguish between an add and an edit operation, and either adds the new record to the ListBox control or replaces the current item in the ListBox with the values on the various controls. Because the ListBox is sorted, new contacts are automatically inserted in the correct order. If an error occurs during the operation, the SaveContact() function returns False to alert the calling code that the operation failed (most likely because one of the assignment operations caused a validation error in the class’s code).
The last operation of the application is the serialization and deserialization of the items in the ListBox control. Serialization is the process of converting an object to a stream of bytes for storing to a disk file, and deserialization is the opposite process. To serialize objects, we first store them into an ArrayList object, which is a dynamic array that stores objects and can be serialized as a whole. Likewise, the disk file is deserialized into an ArrayList to reload the persisted data back to the application; then each element of the ArrayList is moved to the Items collection of the ListBox control. ArrayLists and other Framework collections are discussed in Chapter, “Storing Data in Collections,” and object serialization is discussed in Chapter Serialization and XML. You can use these features to test the application and examine the corresponding code after you read about ArrayLists and serialization. I’ll discuss the code of the Load and Save operations of the Contacts sample project in Chapter “Serialization and XML”.
Making the Most of the ListBox Control
This section’s sample application demonstrates an interesting technique for handling a set of data at the client. We usually need an efficient mechanism to store data at the client, where all the processing takes place — even if the data comes from a database. In this example, we used the ListBox control, because each item of the control can be an arbitrary object. Because the control displays the string returned by the object’s ToString method, we’re able to customize the display by providing our own implementation of the ToString method. As a result, we’re able to use the ListBox control both as a data-storage mechanism and as a navigational tool. As long as the strings displayed on the control are meaningful descriptions of the corresponding objects and the control’s items are sorted, the ListBox control can be used as an effective navigational tool. If you have toomany items to display on the control, you should also provide a search tool to help users quickly locate an item in the list, without having to scroll up and down a long list of items. Review the ListBoxFind project of Chapter 6 for information on searching the contents of the ListBox control.
When data are being edited, you have to cope with another possible problem. The user may edit the data for hours and forget to save the edits every now and then. If the computer (or, even worse, the application) crashes, a lot of work will be wasted. Sure the application provides a Save command, but you should always try to protect users from their mistakes. It would be nice if you could save the data to a temporary file every time the user edits or adds an item to the list. This way, if the computer crashes, users won’t lose their edits. When the application starts, it should automatically detect the presence of the temporary file and reload it. Every time the user saves the data by using the application’s Save command, or terminates the application, the temporary file should be removed.