Programmer to ProgrammerTM | |||||
|
|
|
|
|
|
|
|
|
|
|
| |||||||||||||||||||
The ASPToday
Article February 14, 2001 |
Previous
article - February 13, 2001 |
||||||||||||||||||||||||||||||
| |||||||||||||||||||||||||||||||
ABSTRACT |
| ||||||||||||||||||||||||||||||
| |||||||||||||||||||||||||||||||
Article Discussion | Rate this article | Related Links | Index Entries | ||||||||
ARTICLE |
Class.Session variables are both helpful and hurtful. They let us maintain state, but they complicate our code and they complicate our lives. Suppose you are reviewing the code of one of your teammates, and you find Session("ID"). What does it mean? Does this hold the user's ID? An invoice ID? A client account ID? You can "read" the variable, of course, but is it safe to write to it? If you write to it, is it safe to write any data type, or just numeric or string data? And if when reviewing two other team mate's code you found Session("UserID") as well as Session("User_ID"), would you know what distinguished the two? As all Session variables are undeclared, more than a few of us have finally tracked that elusive bug to a simple misspelling in a Session variable.
My team and I, working on a healthcare web application for StatusOne Health Systems, were dealing with all these annoyances, and addressed them by wrapping our Session variables in VBscript classes. By collecting related Session variables into classes and accessing them through object properties, it is possible to reduce or eliminate most of these annoyances. Consider the benefits we have seen:
VBscript classes have been well covered in articles on this and other ASP reference sites, and these articles seem to fall into two categories. First, there are those that explain what they are and how to code a simple VBscript class (e.g. Using Classes within VBscript, Take Advantage of VBScript 5 Classes). Second, there are those that tackle an interesting but thorny problem, and use classes as part of the solution (e.g. Creating a Class-based Shopping Cart That Uses a Single Session Variable and Building a Stack Class Using VBScript).
I have learned much from these articles, and encourage others to read them and learn from them. However, my intent is to cover a kind of forgotten middle ground. Assuming that (1) you know how to code a simple VBscript class, (2) you are using Session variables in your ASP code, and (3) those Session variables describe or pertain to one or several discrete "entities" (e.g. the user, the client, the account, the process, etc.) - then I will suggest the use of VBscript classes to simplify your code and avoid many of the hassles described above.
Again, I am addressing only this middle ground! VBscript classes are sometimes deprecated, and compiled COM components offered as the obvious alternative. But my intent here is to suggest a technique of style, rather than of operation or algorithm. If you have business logic that needs to be faster, by all means, use a COM object. Starting with a VBscript class may help you during design, but you'll eventually want to port it and compile it. To make this perfectly clear, wrapping Session variables in VBscript classes will not necessarily:
However, I think you will find it keeps the darned things contained and corralled, makes your code clearer and more modular, reduces Session variable duplication and misuse, and keeps you and your team mates more in sync as you work together.
And, as I will suggest later on, this technique seems perfectly applicable to cookies, query strings, and hidden form fields as well!
Think of your user as an object. True, this would be poor etiquette in social circumstances, but it's rewarding to the savvy programmer. Consider the typical data we would normally tuck away inside Session variables to describe a user of our ASP application:
Assuming we keep all these data in a database, we would need something akin to the following in our login procedure:
Set rs = objConn.Execute("SELECT * FROM UserInfo WHERE Username='" & strUsername & _ "' AND Password='" & strPassword & "'" ) If Not rs.EOF Then Session("UserFirstName") = CStr( rs("FN") ) Session("UserLastName") = CStr( rs("LN") ) Session("UserHonorific") = CStr( rs("Honorific") ) Session("UserID") = CLng( rs("UserID") ) Session("UserEmail") = CStr( rs("Email") ) Session("UserType") = Cint( rs("UserType") ) Session("PrevLogonDate") = Cdate( rs("PreviousLogon") ) End If
So far, this is familiar stuff. If I want to greet my users with this type of message.
Welcome back, Mr. Tom Kelleher! It has been 16 days since your last login on 12/21/2000."
Then I would need to write some code like this to make it happen:
Response.Write ("Welcome back, " & Session("UserHonorific") & " " & Session("UserFirstName") & _ " " & Session("UserLastName") & "! It has been " & DateDiff("d", Session("PrevLogonDate"), Date()) & _ " since your last login on " & Session("PrevLogonDate") & "." )
Immediately, one annoyance of Session variables becomes apparent. The darn things are bulky, and make our code swell up. Suppose instead we had a magical user-object, objUser, which could expose all of these characteristics as properties? We could use this user-object to compact the code above into this:
Response.Write ("Welcome back, " & objUser.FullName & "! It has been " & _ objUser.DaysSincePrevLogon & " days since your last login on " & _ objUser.PrevLogonDate & ".")
A modest improvement perhaps, but an intriguing beginning - the code is more compact, more readable, and seems to hide some of the cluttering complexities (like the DateDiff() command). Let's go inside the user-object code, to see what's going on.
Again, there are many excellent articles on this and other websites about making a VBscript class (see Take Advantage of VBScript 5 Classes to get started). I won't cover that ground here, except to say that by creating a class, we are defining an object and assigning it properties and methods. We can then use those properties and methods directly in our code, as in the example above.
At design time, we have total control over what properties and methods we want such an object to expose. For our user-object, objUser, let's say we want these properties:
objUser.FirstName objUser.LastName objUser.Honorific objUser.FullName objUser.Email objUser.UserID objUser.UserType objUser.PrevLogonDate objUser.DaysSincePrevLogon
All of the properties above will be read-only - their values are set once and only once, at login. For clarity, though, let's add a read/write property - one from real life, in my case:
objUser.ActivePatientID
The purpose of the StatusOne web-application is to let users (mainly nurses) monitor and update online "care plans" for patients. As they work, they need to move from one patient's care plan to the next - so the programming team needs to track the ID of the current patient. We will use the .ActivePatientID property for this, updating it when the user changes patients.
Given these properties then, this is our C_User.inc file, at a minimum:
<% '---------------------------------------------------- ' C_User.inc '---------------------------------------------------- ' A simple VBscript class file to help organize and ' maintain Session variables pertaining to the ' current system user. '---------------------------------------------------- Class C_User '--- Read-only Properties -------------------- Public Property Get FirstName() FirstName = Session("UserFirstName") End Property Public Property Get LastName() LastName = Session("UserLastName") End Property Public Property Get Honorific() Honorific = Session("UserHonorific") End Property Public Property Get FullName() FullName = & Session("UserFirstName") & " " & Session("UserLastName") End Property Public Property Get NameAndTitle() NameAndTitle = Session("UserHonorific") & " " & _ Session("UserFirstName") & "" & Session("UserLastName") End Property Public Property Get Email() Email = Session("UserEmail") End Property Public Property Get UserID() UserID = Session("UserID") End Property Public Property Get UserType() UserType = Session("UserType") End Property Public Property Get PrevLogonDate() PrevLogonDate = Session("PrevLogonDate") End Property Public Property Get DaysSincePrevLogon() DaysSincePrevLogon = DateDiff("d", Session("PrevLogonDate"), Date() ) End Property '--- Read/Write Properties -------------------- Public Property Let ActivePatientID(ByVal vData) Session("ActivePatientID") = CLng( vData ) End Property Public Property Get ActivePatientID () ActivePatientID = Session("ActivePatientID") End Property End Class %>
It's fairly easy to grasp what's happening above. The Get statements let us "get" (read) the value of the Session variable, and Let statements let us set (write) that value. Because only the .ActivePatientID property has a Let statement, it is the only read/write property. All the others are read-only.
To use these properties in our code, we simply include the class file, instantiate the object, and call on properties as we need them. For instance, to generate the welcome message I demonstrated earlier, the complete code would be as follows.
<% '------------------------------------- ' here run your commands to pull the ' data from the database and set your ' Session variables (we're only hiding ' them not eliminating them!) '------------------------------------- Dim objUser Set objUser = New C_User Response.Write ("Welcome back, " objUser.FullName & "! It has been " &_ objUser.DaysSincePrevLogon & " days since your last logon on " & _ objUser.PrevLogonDate & ".") %>
Session variables are global variables, which are widely regarded as a Bad Thing. Global variables make debugging difficult, and can invite more trouble than they solve, but one nice benefit of their global-ness here is that we don't need to initialize the user-object's values when we instantiate it. In compiled programs like Visual Basic, we might have to make all these read/write, and then follow our Set command with a list of statements of this sort:
objUser.FirstName = "Tom" objUser.LastName = "Kelleher" objUser.Honorific = "Mr." ' etc.
And we would have to repeat this process each time we instantiated the object - which for us could mean once per page! But, because Session variables are global, the object is self-initializing. Assuming we have already set our Session variables elsewhere, once we instantiate our class object (Set objUser = New C_User), all of its properties are immediately ready for use.
We are not constrained to a one-for-one system of Session variables and their associated properties. The value of putting Session variables into a class starts to multiply when we invent ways to make them do double-duty for us. Look again at the .FullName and .NameAndTitle properties.
Public Property Get FullName() 'returns "Tom Kelleher" in one step... FullName = Session("UserFirstName") & " " & Session("UserLastName") End Property Public Property Get NameAndTitle() 'returns "Dr. Tom Kelleher" in one step... NameAndTitle = Session("UserHonorific") & " " & Session("UserFirstName") & _ " " & Session("UserLastName") End Property
By combining the variables behind the scenes, we are able to add properties to our class. For instance, we can easily conjure up a .ReverseName property (i.e., "Kelleher, Tom" rather than "Tom Kelleher") and an .Initials property ("T.K.").
Public Property Get ReverseName() 'returns "Smith, Pat" in one step... ReverseName = Session("UserLastName") & ", " & Session("UserFirstName") End Property Public Property Get Initials() 'returns Pat Smith's initials: "P.S." Initials = Left(Session("UserFirstName"), 1) & "." & _ Left(Session("UserLastName"), 1) & "." End Property
I mentioned earlier the problem that when we create a new Session variable, we typically expect to use it for a specific data-type. However, Session variables can't enforce this themselves, helpless little beggars that they are, but we can protect them from ill-suited data types by our class wrapper. Doing so is trivial, though your protection schemes can also be as complex as needed. For example, if the "active patient" ID must always be numeric, then we can do the following in the Property Let statement:
Public Property Let ActivePatientID(ByVal vData) If Not IsNumeric(vData) Then Err.Raise 9999, "C_User.inc", "ActivePatientID must " & _ "be numeric. Submitted value = '" & vData & "'." Exit Property End If ActivePatientID = vData End Property
So with just a little extra code, you can block all manner of troublesome data types from getting into your Session variables.
So far, these properties are merely reporting back Session variables - either singly or in combination. But we can add intelligence to our class objects, and thereby let them serve up commonly used bits of logic. For example, the .DaysSincePrevLogon property performs a DateDiff() command on the Session("DatePrevLogon") variable, to count the number of days since the last login. This is a start, but we can make them smarter.
For the StatusOne project, we built a "forum" system for threaded discussions among the users. For this, we needed a system of permissions to provide some control over who could do what on which forum discussions. Such permissions are largely determined by the user's user-type (Care Manager, Supervisor, Administrator, etc.). Most anyone can read and write to any discussion, for example, but some users can only read. Some discussions are themselves "read-only" - only Supervisors can post notes to them. Administrators obviously have super-powers, and can create/modify/delete entire discussions.
However, we wanted the top level Administrators to be able to "tweak" individual user accounts-- granting and denying specific forum permissions on a user-by-user basis. So by default, if I am not a Supervisor I cannot post notes to a "read-only" discussion, but I could be granted that one specific permission, without granting me all the other Supervisor permissions.
So each user has a set of default permissions determined by their user-type, which can then be augmented or reduced by any extra permissions granted or denied by a top level Administrator. Based on their final permission set, the first page of the Forums system presents the user with a tailored list of their available functions. As the developers, we needed a way to ask for each user, "Does this user have permission to use this function?" If so, we provide that link. Notice that such questions pertain to the user, and therefore, as programmers, they pertain to the user "object."
Now, think of a "permission" as a "property" and you can anticipate what we did.
We needed the following properties:
I don't want to drag you through the plumbing of this technique, as that would take us off-track. (Write to me if you want to know more about it.) Suffice it here that the combination of default and special permissions results in a backslash-delimited string made up of all their final "permission codes". For example, a user who can read and write to the normal forums as well as write to "read-only" forums would have a Session variable with the value "\crw\cwro\ ".
By searching this string for a specific code, we quickly determine if the user has or does not have a particular permission. We save this string to a Session variable, Session("ForumPermissions"). From this, we built our permission properties, like so...
Public Property Get CanReadWrite() If InStr( Session("ForumPermissions"), "\crw\") = 0 Then CanReadWrite = False Else CanReadWrite = True End If End Property Public Property Get CanWriteToReadOnly() If InStr( Session("ForumPermissions"), "\cwro\") = 0 Then CanWriteToReadOnly = False Else CanWriteToReadOnly = True End If End Property Public Property Get CanAdminForums() If InStr( Session("ForumPermissions"), "\cadf\") = 0 Then CanAdminForums = False Else CanAdminForums = True End If End Property Public Property Get CanAdminUsers() If InStr( Session("ForumPermissions"), "\cadu\") = 0 Then CanAdminUsers = False Else CanAdminUsers = True End If End Property
With this now in place, we can pull these property values as needed to provide or deny access to the functions in an elegant manner. To grant access to the page where a user would administrate forum discussions, we might have the following code:
If objUser.CanAdminForums Then Response.Write("Administer forums") End If
In addition, to prevent against users stumbling onto that page, we can post a "gatekeeper" command like this at the top of AdminForums.asp
If objUser.CanAdminForums = False Then Response.Redirect ("SomeOtherPage.asp") End If
This class-based technique is built on Session variables, and so it cannot rise above them. Where Session variables fail (on server farms, for instance) this technique will fail. If the user has cookies turned off in their browser, this technique will fail. When the user's session times out, this technique will fail. For problems like these, there are other solutions - such as putting such data into query strings, or into hidden form fields, or (when possible) into cookies.
After a moment's thought, it becomes apparent that the same class that helps you manage your Session variables can let you manage these as well! If we had chosen to maintain the current Patient ID number in a query string, hidden field, or cookie, we could have used the following Get statements, respectively:
Public Property Get PatientID() 'Using a query string... PatientID = Request.QueryString("PatientID") End Property Public Property Get PatientID() 'Using a cookie... PatientID = Request.Cookies("PatientID") End Property Public Property Get PatientID() 'Using a hidden field... PatientID = Request.Form("hidPatientID") End Property
So in theory, it should be possible to wrap all these state-maintaining techniques into a single class, so long as they all pertain to a common, underlying entity such as the user.
Beware! To truly reap all the benefits, your programming team should only access the Session variable data through the class. The Session variables are protected by, but not trapped within, the class wrapper. There is no reason a careless developer couldn't just issue the command Session("ActivePatientID") = 123 rather than use the now-preferred objUser.ActivePatientID = 123. We might borrow a partial solution from traditional system development: designate one person as the "owner" of the class file. Only that person can change it - or even view its internals. This makes the underlying Session variables obscure to the other developers (though, true, not unobtainable), and allows the owner to attach prefixes or other obfuscation to the actual Session variable names (e.g. Session("ActivePatientID_xyz123)), making them less vulnerable to direct usage.
The irony doesn't escape me that I discovered this technique almost simultaneously with the announcement of ASP.NET - which will largely eliminate both the underlying problems, and the specific solution I'm proposing! (See Session State in ASP.NET.) However, NT 4.0 development continues despite the advent of Windows 2000, so traditional ASP development will carry on for some time in the shadow of .NET. So take what you can from my suggestions here, and let me know what comes of it.
For our team, using a VBscript class has helped immensely to organize and contain our Session variables, and to keep our team's usage of them consistent. We've had great success with our first class object (which is, brace yourself -- a user-object), but have since created classes to manage our forum variables and some others. As our users log in with a single ID, but then can take on several different roles before using the system, I am anticipating a need for "nested" classes: So that a user object contains a child object called a "role" with its own properties, methods and restrictions. Within any specific role, a user will be working in one or another portion of the system, which might warrant a "context" class, to help manage all the Session variables pertinent to that context. Then the user, in that role, within that context is typically focusing on one patient or another - so a patient class might help as well. I'll let you know!
|
| |||||||
|
| |||||||||||||||
|
ASPToday is brought to you by
Wrox Press (http://www.wrox.com/). Please see our terms
and conditions and privacy
policy. ASPToday is optimised for Microsoft Internet Explorer 5 browsers. Please report any website problems to [email protected]. Copyright © 2001 Wrox Press. All Rights Reserved. |