The strProperty and dblProperty properties will accept any value, as long as the type is correct and the value of the numeric property is within the acceptable range. But what if the generic properties were meaningful entities, such as email addresses, ages, or zip codes? We should be able to invoke some code to validate the values assigned to each property. To do so, we implement each property as a special type of procedure: the so-called Property procedure.
Properties are implemented with a special type of procedure that contains a Get and a Set section (frequently referred to as the property’s getter and setter, respectively). The Set section of the procedure is invoked when the application attempts to set the property’s value; the Get section is invoked when the application requests the property’s value. The value passed to the property is usually validated in the Set section and, if valid, is stored to a local variable. The same local variable’s value is returned to the application when it requests the property’s value, from the property’s Get section. Listing 6.3 shows what the implementation of an Age property would look like.
Listing 6.3: Implementing Properties with Property Procedures
Private m_Age As Integer Property Age() As Integer Get Age = m_Age End Get Set (ByVal value As Integer) If value < 0 Or value >= 100 Then MsgBox("Age must be positive and less than 100") Else m_Age = value End If End Set End PropertyCode language: VB.NET (vbnet)
m_Age is the local variable where the age is stored. When a statement such as the following is executed in the application that uses your class, the Set section of the Property procedure is invoked:
obj.Age = 39Code language: VB.NET (vbnet)
Because the property value is valid, it is stored in the m_Age local variable. Likewise, when a statement such as the following one is executed, the Get section of the Property procedure is invoked, and the value 39 is returned to the application:
Debug.WriteLine(obj.Age)Code language: VB.NET (vbnet)
The value argument of the Set procedure represents the actual value that the calling code is attempting to assign to the property. The m_Age variable is declared as private because we don’t want any code outside the class to access it directly. The Age property is, of course, Public, so that other applications can set it.
Fields versus Properties
Technically, any variables that are declared as Public in a class are called fields. Fields behave just like properties in the sense that you can assign values to them and read their values, but there’s a critical distinction between fields and properties: When a value is assigned to a field, you can’t validate the value from within your code. If the value is of the wrong type, an exception will occur. Properties should be implemented with a Property procedure, so you can validate their values, as you saw in the preceding example. Not only that, but you can set other values from within your code. Consider a class that represents a contract with a starting and ending date. Every time the user changes the starting date, the code can adjust the ending date accordingly (which is something you can’t do with fields). If the two dates were implemented as fields, users of the class could potentially specify an ending date prior to the starting date.
Enter the Property procedure for the Age property in the Minimal class and then switch to the form to test it. Open the button’s Click event handler and add the following lines to the existing ones:
obj.Age = 39 Debug.WriteLine("after setting the age to 39, age is " & _ obj.Age.ToString) obj.Age = 199 Debug.WriteLine("after setting the age to 199, age is " & _ obj.Age.ToString)Code language: VB.NET (vbnet)
The value 39 will appear twice in the Output window, which means that the class accepts the value 39. When the third statement is executed, a message box will appear with the error’s description:
Age must be positive and less than 100Code language: VB.NET (vbnet)
The value 39 will appear in the Output window again. The attempt to set the age to 199 failed, so the property retains its previous value.
Our error-trapping code works fine, but what good is a message box displayed from within a class? As a developer using the Minimal class in your code, you’d rather receive an exception and handle it from within your code. So let’s change the implementation of the Age property a little. The Property procedure for the Age property (Listing 6.4) throws an InvalidArgument exception if an attempt is made to assign an invalid value to it. The InvalidArgument exception is one of the existing exceptions, and you can reuse it in your code. Later in this chapter, you’ll learn how to create and use custom exceptions.
Listing 6.4: Throwing an Exception from within a Property Procedure
Private m_Age As Integer Property Age() As Integer Get Age = m_Age End Get Set (ByVal value As Integer) If value < 0 Or value >= 100 Then Dim AgeException As New ArgumentException() Throw AgeException Else M_Age = value End If End Set End PropertyCode language: VB.NET (vbnet)
You can test the revised property definition in your application; switch to the test form, and enter the statements of Listing 6.5 in a new button’s Click event handler. (This is the code behind the Handle Exceptions button on the test form.)
Listing 6.5: Catching the Age Property’s Exception
Dim obj As New Minimal Dim userAge as Integer UserAge = InputBox("Please enter your age") Try obj.Age = userAge Catch exc as ArgumentException MsgBox("Can't accept your value, " & userAge.ToString & VbCrLf & _ "Will continue with default value of 30") obj.Age = 30 End TryCode language: VB.NET (vbnet)
This is a much better technique for handling errors in your class. The exceptions can be intercepted by the calling application, and developers using your class can write robust applications by handling the exceptions in their code. When you develop custom classes, keep in mind that you can’t handle most errors from within your class because you don’t know how other developers will use your class.
Handling Errors in a Class
When you design classes, keep in mind that you don’t know how another developer may use them. In fact, you may have to use your own classes in a way that you didn’t consider when you designed the class. A typical example is using an existing class with a web application. If your class displays a message box, it will work fine as part of a Windows Forms application. In the context of a web application, however, the message box will be displayed on the monitor of the server that hosts the application, and no one will see it. As a result, the application will keep waiting for a response to a message box before it continues; however, there’s no user to click the OK button in the message box, because the code is executing on a server. Even if you don’t plan to use a custom class with a web application, never interact with the user from within the class’s code. Make your code as robust as you can, but don’t hesitate to throw exceptions for all conditions you can’t handle from within your code. In general, a class’s code should detect abnormal conditions, but it shouldn’t attempt to remedy them.
The application that uses your class may inform the user about an error condition and give the user a chance to correct the error by entering new data, disable some options on the interface, and so on. As a class developer, you can’t make this decision — another developer might prompt the user for another value, and a sloppy developer might let his or her application crash (but this isn’t your problem). To throw an exception from within your class’s code, call the Throw statement with an Exception object as an argument. To play well with the Framework, you should try to use one of the existing exceptions (and the Framework provides quite a few of them). You can also throw custom exceptions by using a statement such as the following:
Throw New Exception("your exception's description")
Implementing Read-Only Properties
Let’s make our class a little more complicated. Age is not usually requested on official documents, because it’s valid only for a year after filling out a questionnaire. Instead, you must furnish your date of birth, from which your current age can be calculated at any time. We’ll add a BDate property in our class and make Age a read-only property.
To make a property read-only, you simply declare it as ReadOnly and supply the code for the Get procedure only. Revise the Age property’s code in the Minimal class, as seen in Listing 6.6. Then enter the Property procedure from Listing 6.7 for the BDate property.
Listing 6.6: Implementing a Read-Only Property
Private m_Age As Integer ReadOnly Property Age() As Integer Get Age = m_Age End Get End PropertyCode language: VB.NET (vbnet)
Listing 6.7: The BDate Property
Private m_BDate As DateTime Private m_Age As Integer Property BDate() As DateTime Get BDate = m_BDate End Get Set(ByVal value As Date) If Not IsDate(value) Then Dim DataTypeException As _ New Exception("Invalid date value") Throw DataTypeException End If If value > Now() Or _ DateDiff(DateInterval.Year, value, Now()) >= 100 Then Dim AgeException As New Exception _ ("Can’t accept the birth date you specified") Throw AgeException Else m_BDate = value m_Age = DateDiff(DateInterval.Year, value, Now()) End If End Set End PropertyCode language: VB.NET (vbnet)
As soon as you enter the code for the revised Age property, two error messages will appear in the Error List window. The code in the application’s form is attempting to set the value of a read-only property, and the editor produces the following error message twice: Property ‘Age’ is ‘ReadOnly.’ As you probably figured out, we must set the BDate property in the code, instead of the Age property. The two errors are the same, but they refer to two different statements that attempt to set the Age property.
There are two types of errors that can occur while setting the BDate property: an invalid date or a date that yields an unreasonable age. First, the code of the BDate property makes sure that the value passed by the calling application is a valid date. If not, it throws an exception. If the value variable is a valid date, the code calls the DateDiff() function, which returns the difference between two dates in a specified interval — in our case, years. The expression DateInterval.Year is the name of a constant, which tells the DateDiff() function to calculate the difference between the two dates in years. You don’t have to memorize the constant names — you simply select them from a list as you type.
So, the code checks the number of years between the date of birth and the current date. If it’s negative (which means that the person hasn’t been born yet) or more than 100 years (we’ll assume that people over 100 will be treated as being 100 years old), it rejects the value. Otherwise, it sets the value of the m_BDate local variable and calculates the value of the m_Age local variable.
Calculating Property Values on the Fly
There’s still a serious flaw in the implementation of the Age property. Can you see it? The person’s age is up-to-date the moment the birth date is entered, but what if we read it back from a file or database three years later? It will still return the original value, which is no longer the correct age. The Age property’s value shouldn’t be stored anywhere; it should be calculated from the person’s birth date as needed. If we avoid storing the age to a local variable and calculate it on the fly, users will always see the correct age. Revise the Age property’s code to match Listing 6.8, which calculates the difference between the date of birth and the current date, and returns the correct person’s age every time it’s called.
Listing 6.8: A Calculated Property
ReadOnly Property Age() As Integer Get Age = Convert.ToInt32(DateDiff(DateInterval.Year, m_BDate , Now())) End Get End PropertyCode language: VB.NET (vbnet)
Notice also that you no longer need the m_Age local variable because the age is calculated on the fly when requested, so remove its declaration from the class. As you can see, you don’t always have to store property values to local variables. A property that returns the number of files in a directory, for example, also doesn’t store its value in a local variable. It retrieves the requested information on the fly and furnishes it to the calling application. By the way, the calculations might still return a negative value if the user has changed the system’s date, but this is a rather far-fetched scenario.
You can implement write-only properties with the WriteOnly keyword and a Set section only, but write-only properties are rarely used (in my experience, only for storing passwords). Our Minimal class is no longer so minimal. It exposes some functionality, and you can easily add more. Add properties for name, profession, and income, and add methods to calculate insurance rates based on a person’s age and anything you can think of. Experiment with a few custom members, add the necessary validation code in your Property procedures, and you’ll soon find out that building and reusing custom classes is a simple and straightforward process. Of course, there’s a lot more to learn about classes, but you already have a good understanding of the way classes combine code with data.