You can write the code for a class as a single project file. However, writing the code this way is a pain because you still need a test program to exercise, test, and debug the class code. Therefore, the easiest way to start writing a class is to create a new project and make the class code part of the project. (You will see how to put the new class into a library file later in this chapter, in the section "Creating Your Own Class Library.")
First, you need to create a new project called TestProgram . Once you have done that, you should select Project, Add Class to add a class to the project. Name the new class CPassword.vb , as shown in Figure 15.2.
Figure 15.2. Adding the CPassword class to a project.
After you click the Open button, Visual Basic .NET automatically adds the class to your project and opens a new Code window, as shown in Figure 15.3.
Figure 15.3. The Code window for the CPassword class.
You need to write the new code for the class between the Public Class CPassword and End Class statements in the file. Later in this chapter, you'll also write code to test the class. You are now ready to start adding code to your new class.
Adding Class Members to a Class
Listing 15.1 shows the first section of code that you need to add to the CPassword class.
Listing 15.1 The Class Members for the CPassword Class
Option Strict On Public Class CPassword '============= Symbolic Constants ====================== Private Const MINLENGTH As Integer = 4 ' Password must have 4-30 characters Private Const MAXLENGTH As Integer = 30 Private Const MINSECURITY As Integer = 1 ' Ten levels of security clearance Private Const MAXSECURITY As Integer = 10 '======================================================== '================= Private Data ======================== Private Structure Security ' Hold relevant information in a structure Dim User As String ' A list of the users for this program Dim Pass As String ' The password Dim SecurityLevel As Integer ' Each user has a security level End Structure Private Shared mUserCount As Integer ' How many users Private Shared mUserList() As Security Private Check As Integer Private EncodedPW As String Private DecodedPW As String
Listing 15.1 begins with the definition of several symbolic constants. The first two constants define the minimum and maximum length for a password. Listing 15.1 arbitrarily sets these limits to 4 and 30 characters. The second two constants define the minimum and maximum values for the security levels for the system. You can assume that the lowest security level for the system is 1 and the highest is 10. (In a more sophisticated system, you could have these values stored in the Registry or in an .ini file.)
Next, you declare a Structure that groups the relevant data for a user into one convenient data item, named Security . Note that 15.1 declares it as a Private structure because there is no need for the outside world to have direct access to it.
Notice that each member of the Structure is declared with the Dim keyword. You might think that each of these structure members should also be declared Private , as in this example:
Private Structure Security Private User As String ' THIS IS THE WRONG WAY TO DECLARE MEMBERS!!! Private Pass As String ' ALSO WRONG!!! Private SecurityLevel As Integer ' STILL WRONG!!! End Structure
If you try to declare the members by using this syntax, you get the following error message whenever you try to access the members:
'TestPassword.CPassword.Security.User' is not accessible in _ this context because it is 'Private'.
This error message says that because you have declared the structure members as Private to the Security Structure , they cannot be accessed outside the Security Structure ”and that is not a very useful structure. You should use only the Dim keyword for declaring the structure members.
The code then declares the mUserCount and mUserList() variables . These data items are not only Private to the class, but they're Shared as well, for the reasons discussed earlier in this chapter. The last three statements simply declare three working variables that are used inside the class.
Notice that this chapter is very careful in using the words declare and define. In a strict sense, nothing in the class exists until an instance of the object for the class is defined, using the New keyword. After an instance of the object is defined, all the data items within the class are defined, too.
Adding Class Constructors to a Class
Any class you create automatically has a class constructor associated with it. A class constructor is a subroutine that is executed each time a new instance of the class is defined. The default constructor for every class is named New() . The code for this constructor is shown in Listing 15.2.
Listing 15.2 Code for the New() Class Constructor
Shared Sub New() ' If this was being used in a real program, this ' code would probably read the user data from a ' database file. We just fake it here. ReDim mUserList(100) With mUserList(0) .User = "Debbie Plesluska" .Pass = "Diamonds" .SecurityLevel = 8 End With With mUserList(1) .User = "Jay Crannell" .Pass = "Whiff" .SecurityLevel = 8 End With With mUserList(2) .User = "Jim McAllister" .Pass = "Slice" .SecurityLevel = 6 End With With mUserList(3) .User = "Jack" .Pass = "Joyce1" .SecurityLevel = 10 End With mUserCount = 4 End Sub
The constructor code begins with the keyword Shared . This marks the constructor as one that executes only once during the life of the program: It executes when the first CPassword object is instantiated . Any subsequent CPassword objects that may be defined do not call this New() constructor. This is a perfect situation for initializing any Shared data that is used in the class, which is exactly the situation with the CPassword class.
If this is the first time the constructor is called, a series of With-With End statements are executed to fill in several test users for the class. Near the bottom of the constructor, mUserCount is set to 4 . If a second object of the CPassword class is created, the Shared access specifier prevents the New() constructor code from being executed a second time; after all, there's no need to execute the code again.
Notice that the constructor serves the same purpose as the Initialization step discussed in Chapter 3. The purpose of the constructor is to perform any tasks (often data initialization) that need to be performed before the class object is used. In Listing 15.2, the constructor simply initializes the mUserList() and mUserCount data items.
In some programs, the constructor might need to be more complex than the simple constructor you've been working with in this chapter. In fact, you might want an object to be initialized in different ways, depending on the current state of the program. Indeed, if you want subsequent CPassword objects to be initialized using code that is different from the code for the first constructor (which in Listing 15.2 is Shared ), you can overload the New() constructor. Because of these varying needs, Visual Basic .NET allows you to have multiple constructors. However, each additional constructor still has the same name ”in this case, New() .
Wait a minute! If there are multiple constructors, but they all have the same name, how does Visual Basic .NET know which one to use? Easy. Visual Basic .NET looks at the parameter list for each constructor and decides which constructor to execute. Let's look at an example to see how this works.
Suppose you have an array of data that needs to be sorted. However, the data in the array may vary according to the point in the program at which the object is created. You might see something like the following code fragment:
Public Sub New(ByVal nums() As Double) ' code to sort doubles End Sub Public Sub New(ByVal nums() As long) ' code to sort longs End Sub Public Sub New(ByVal nums() As Integer) ' code to sort integers End Sub Public Sub New(ByVal str() As String) ' code to sort strings End Sub
Assume that the constructors in the code fragment are for the Widget class. Further suppose that you create an instance of the Widget object as follows :
Dim MyNums(100) As Double, MyNames(100) As String ' some code that does something Dim MyWidget as New Widget(MyNums) Dim AnotherWidget as New Widget(MyNames)
Because you are defining the objects with different data types as their parameters, Visual Basic .NET can determine which New() constructor to use when the code executes. Obviously, when MyWidget is defined, the New() constructor that accepts a Double data type as its parameter is executed. Likewise, when AnotherWidget is defined, the New() constructor using the String data type as its parameter is executed.
Do you see what you gain by being able to overload the constructor? You can create a Widget object that uses different data types as the need arises. Even better, the syntax structure to create the object doesn't change, regardless of data type. Although the example here is fairly trivial, being able to overload the constructor makes a class more flexible because it can initialize different data types without changing anything in the Dim statement. In other words, you can move the complexity of handling different data types for the Widget class from outside the class to inside the class, where it belongs.
Another nice thing about constructors is that with a constructor, you, the programmer, don't have to remember to explicitly initialize a data item. If you place the responsibility for initializing the data item in the constructor, the object is automatically initialized properly whenever an object is created.
You can have as many constructors as you need for a class. The only requirement is that the parameters passed to the different constructors must have different parameter lists. That is, each constructor must have a parameter list that uses different data types, a different parameter count, or both. For example, the following code causes no problems for Visual Basic .NET because each constructor has a different parameter list:
Public Sub New(ByVal MyInt As Integer) Public Sub New(ByVal MyStr As String) Public Sub New(ByVal MyInt As Integer, ByVal Range as Integer) Public Sub New(ByVal MyInt As Integer, ByVal Range as Double) Public Sub New(ByVal MyStr As String, ByVal Range as Integer)
Adding a New User to a Class
After the CPassword object has been created, all the procedures associated with that class are available through the CPassword object. One of the first things the user of your class might do is add a new user to the existing list of users. The code for doing this is shown in Listing 15.3.
Listing 15.3 Adding a New User
Public Function AddNewUser(ByVal User As String, ByVal Password As String, _ ByVal SecurityLevel As Integer) As Integer ' Purpose This function is used to add a new user to the system. The ' code checks the length of the password and then check to ' see if the name entered is already in the list. If either ' check fails, logic False is returned. ' ' Argument list: ' User The name of the user ' Password A string that contains the password ' SecurityLevel The clearance for this user ' ' Return Value: ' integer -1 if set OK; 0 otherwise ' Dim i, length As Integer Dim buff As String ' See if password length OK If Password.Length < MINLENGTH OrElse Password.Length > MAXLENGTH Then Return 0 End If ' Check security level If SecurityLevel < MINSECURITY OrElse SecurityLevel > MAXSECURITY Then Return 0 End If ' See if the name is already in use. For i = 0 To mUserCount - 1 If UCase(mUserList(i).User) = UCase(User) Then Return 0 End If Next mUserCount += 1 ' Add one to the list If mUserCount >= mUserList.GetUpperBound(0) Then ' See if we need to grow ReDim Preserve mUserList(mUserCount + 10) ' the user list. End If ' If we get here, it's ok to add them: With mUserList(mUserCount) .User = User .Pass = Password .SecurityLevel = SecurityLevel End With Return -1 ' Everything's fine End Function
In Listing 15.3, all three data items (that is, username, password, and security level) associated with a new user are passed to the AddNewUser() function. The code first checks to make sure the password falls between the minimum and maximum password lengths (that is, 4 and 30). Next, the code checks to make sure the security level also falls within the allowable range (that is, 1 to 10). Finally, the code checks to make sure the user's name is not already in the list of current users.
If any of these checks fail, the function returns logic False to the caller. The caller can then display an error message and take the appropriate corrective action.
If the checks are passed successfully, the code increments mUserCount and then checks whether the mUserList() array is large enough to accept another user. If it is not, the array is redimensioned to a larger value. A With-With End block then adds the new user to the list. The value logic True is returned to the caller to inform the caller that the user has been added to the list successfully.
Public Procedures and the Class Interface
Thus far in the chapter, the procedures (subroutines and functions) have been defined with the Public access specifier. You know this means that you can access the procedures from outside the class. Indeed, the Public procedures define the interface for the CPassword class. The interface of a class dictates how the outside world must interact with the class. Only through the class interface can you gain access to any of the private data contained within the class itself.
One of your goals in creating programs is to make their interfaces as easy to use as possible. Sometimes it's difficult to remember it, but the code you write today might end up being used by someone else tomorrow. You should always attempt to write clear, consistent, and easily understood code, regardless of who might see it in the future. If nothing else, you will likely need to review the code sometime, and you'd be amazed at how quickly you forget what you wrote just a few weeks ago.
There are a number of things you can do to make using the class easier: You can document procedures, provide cautionary notes, and be consistent.
You should document every subroutine or function you ever write. You can see an example of my documentation preference in Listing 15.3. You can begin with a section titled Purpose , which is a statement of what the procedure should do. In some cases, this is a verbalization of the algorithm used in the procedure. In other cases, the entire procedure may be nothing more than a series of procedure calls to other functions and subroutines. In either case, you should describe the starting state of the data, the transformations made to the data, and the ending state of the data. The user should be able to read this description and understand what the procedure does.
After the procedure description, you should have an Argument list section. (You can instead call it a Parameter list section, if you like; you can use the terms argument and parameter interchangeably. My preference is to use argument list, probably because it is a holdover from when I used other programming languages years ago.) This section describes any data that has been passed to the procedure. You should always use descriptive names for the arguments that are passed to a procedure. Listing 15.3 could have used U , P , and S for the arguments that are being passed into the function, and it would make absolutely no difference in the efficiency of the procedure. However, using User , Password , and SecurityLevel sure makes it easier for humans to read and understand the code.
The last section of the comment, titled Return Value: , tells what data type is returned by the procedure call. Obviously, this section would not apply to subroutines because subroutines cannot return values. However, it's a good idea to always have this section present in the description, even in subroutines. First, always having a Return Value: section present adds consistency to the documentation. For a subroutine, you simply use N/A (not applicable ) as the return value. Second, you might be surprised at how often you start out with something being a subroutine and then decide later that it needs to return a value, forcing you to change it to a function with a return value anyway. Such changes don't necessarily mean you did a bad job designing the class. When you get into the code, things you might not have foreseen may force you to make changes. Programs continue to evolve over time.
Providing Cautionary Notes
Although a Caution section does not appear in any of the code presented so far in this chapter, you might want to end the comment section with such a section. In some special cases, it might be that a certain program state, or conditions, must be present in order for the function to work properly, even though the code doesn't necessarily show it. For example, say that a procedure writes data to a data file. The code might assume that the file is already open and that the user has access to the file. In such a case, you could add a cautionary note that tells the reader that the code assumes that such-and-such data file is open for writing.
As a general rule, you use cautionary notes to warn yourself and other programmers about things the code cannot check itself. That is, you use cautionary notes when the procedure does something that is based on a state of the program that is dictated by data that is not in scope.
If you look at the interface for the CPassword class, you'll see that the procedures that have similar arguments are always placed in the same sequence in the argument list. Here's an example:
Public Function GetPassword(ByVal User As String, _ ByVal Password As String) As String Public Function CheckPassword(ByVal User As String, _ ByVal Password As String) As Integer Public Function SetPassword(ByVal User As String, _ ByVal Password As String) As String
All these procedures use User and Password as function arguments. Notice that this example is consistent in placing User first and then Password . Doing this makes it easier to use the CPassword class interface because you know you can expect functions that use these two variables to always place them in the same order. It's a minor thing, but a nice minor thing.
Again, consistency has absolutely no impact on the efficiency of the program. It might, however, increase your efficiency and that of other programmers because it relieves programmers from having to look up the argument order for the functions that use User and Password .
Class Helper Procedures
It is not uncommon to have a number of procedures that simply do subtasks for a class. These are called helper procedures. A helper procedure is a subroutine or function that is never part of the class interface. As such, it always uses a Private access specifier and, hence, has its visibility limited to the class itself. A helper procedure cannot be called from outside the class. Listing 15.4 shows a helper function from the CPassword class.
Listing 15.4 The CreateChecksum() Helper Function
Private Function CreateChecksum(ByVal User As String) As Integer ' Purpose This routine is used to create a checksum for a password. ' This is done by taking the numeric value of each character ' in the user's name and adding it up. The stored value ' is the sum modulus 26. ' ' Argument list: ' User The username for this password ' ' Return Value: ' integer The checksum ' Dim i As Integer, sum As Long sum = 0 For i = 0 To Len(User) - 1 sum += Asc(User.Substring(i, 1)) Next i CreateChecksum = sum Mod 26 End Function
The Purpose section of Listing 15.4 tells you that the CreateChecksum() function's only purpose is to create a checksum number from the user's name. Other functions in the class (for example, GetPassword() ) use this function for their own purposes. However, by giving it a Private access specifier, you prevent the function from being called outside the CPassword class. By doing this, you are explicitly preventing anyone else from using this function.
The sole purpose of CreateChecksum() is to help the GetPassword() and SetPassword() functions do their jobs. Even so, you still need to follow the documentation and consistency standards for helper procedures. It just makes sense to do so.
Adding the Rest of the CPassword Class Code
Listing 15.5 shows the rest of the code for the CPassword class. The listing includes only procedures that have not already been discussed.
Listing 15.5 The Remainder of the CPassword Class Code
Public Function SetPassword(ByVal User As String, _ ByVal Password As String) As String ' Purpose This function is used to encode the password string entered ' by the user. It does this by adding four numeric characters ' to the front of the string followed by the password charac- ' ters themselves. Each character is calculated as the char- ' acter modulo 26 plus the length of the password. ' ' Argument list: ' Password A string that contains the password ' User The name of the user ' ' Return Value: ' string The encoded password ' Check = CreateChecksum(User) EncodedPW = EncodePassword(Password, Check) SetPassword = EncodedPW End Function Public Function GetSecurityLevel(ByVal User As String) As Integer ' Purpose This routine is used to retrieve the security level of the ' user. ' ' Argument list: ' User The user for this password ' ' Return Value: ' integer The security level of the user. It returns 0 ' if the user is not found ' Dim i As Integer For i = 0 To mUserCount - 1 If UCase(mUserList(i).User) = UCase(User) Then Return mUserList(i).SecurityLevel End If Next i Return 0 End Function Public Function CheckPassword(ByVal User As String, _ ByVal Password As String) As Integer ' Purpose This routine is used to check for a password match for a ' user. ' ' Argument list: ' User The user for this password ' Password A string that contains the password ' ' Return Value: ' integer -1 if set if the correct password is given, ' 0 otherwise ' Dim i As Integer For i = 0 To mUserCount - 1 If UCase(mUserList(i).User) = UCase(User) Then If UCase(mUserList(i).Pass) = UCase(Password) Then Return -1 ' Match End If End If Next i Return 0 ' No match End Function Public Function GetPassword(ByVal User As String, _ ByVal Password As String) As String ' Purpose This routine is used to check for a password match for a ' user. ' ********** Right now it is just a way to check the encoding and ' ********** decoding. ' ' Argument list: ' User The user for this password ' Password A string that contains the password ' ' Return Value: ' string the decoded password ' Empty string otherwise ' Dim Temp As String, OriginalPW As String OriginalPW = Password Check = CreateChecksum(User) EncodedPW = EncodePassword(Password, Check) Temp = DecodePassword(EncodedPW, Check) If UCase(Temp) = UCase(OriginalPW) Then Return Temp ' Match Else Return "" ' No match End If End Function '================= Helper Functions ====================== Private Function DecodePassword(ByVal password As String, _ ByVal Check As Integer) As String ' Purpose This routine is used to decode a password. _ It reverses the encode procedure. ' ' Argument list: ' password the password ' Check the checksum for the password ' ' Return Value: ' string the decoded password ' Dim i As Integer Dim length As Integer Dim s As String, w As String For i = 0 To 3 s &= Chr(Asc(password.Substring(i, 1)) - 26) Next i length = Val(s.Substring(0, 2)) ' The password length is first 2 chars Check = Val(s.Substring(2, 2)) ' Next two chars are checksum For i = 4 To length + 3 w = w + Chr(Asc(password.Substring(i, 1)) - Check) Next i DecodePassword = w End Function Private Function EncodePassword(ByVal Password As String, _ ByVal Check As Integer) As String ' Purpose: This routine is used to encode the password string entered ' by the user. It does this by adding two numeric characters ' to the front of the string followed by the password charac- ' ters themselves. Each character is calculated as the char- ' acter modulo 26 plus the length of the password. ' ' Parameters: ' Password the password to encode ' Check the checksum for the password ' ' Return value: ' string the encoded password ' Dim s, cs, d, buff As String Dim Temp, i, length As Integer d = Format$(Len(Password), "00") s = Format$(Check, "00") buff = d & s cs = "" For i = 0 To 3 ' Format length and checksum into password Temp = Asc(buff.Substring(i, 1)) + 26 cs &= Format$(Chr(Temp)) Next i d = "" For i = 0 To Len(Password) - 1 ' Encode the password itself using checksum Temp = Asc(Pass.Substring(i, 1)) + Check d = d & Chr(Temp) Next i s = d Randomize() For i = Len(d) To MAXLENGTH 4 ' Fill in the rest with random chars Temp = 0 Do While Temp < 32 Or Temp > 127 Temp = Rnd() * 127 Loop s = s & Chr(Temp) Next i Password = cs & s ' Put the pieces together EncodePassword = Password End Function
It would be worth your time to read through the code in Listing 15.5 so that you understand how the code works. You will also need to refer to this listing a little later in this chapter.