Creating a Message Board with ADO and XML
In the case study presented in this chapter, we take both of our previous case studies and use what we’ve learned together to form a big project. We’ll be calling this our dotBoard! This case study will detail the process necessary to design and implement your own message board using ADO.NET and a bit of XML. First, we will go through the process necessary to create the data structures in MS Access and SQL Server. We will analyze our application and break down the data into small pieces in order to represent them in a database. Next, we will determine the best way to design our application and go through the design of all the classes we will use to power the message board, and determine what methods and properties each class should contain.
Once the data analysis is done, we are going to develop our classes that will represent the core “business objects” in our application. These objects will be the guts of dotBoard and are what we will use in our User Interface to allow our users to interact indirectly with the data in our database. The last step we will perform is creating the User Interface itself and allow users to interact with our message board.
One major point we should realize, however, is that no matter how large a project this message board seems, it is in fact incredibly simple once broken down into its smaller pieces. In fact, as you delve deeper and deeper into .NET, you will notice how much simpler it is to build most applications. With the right programming practices and .NET as your technology of choice, you can build complex applications in a much more efficient manner than some of the older technologies in existence.
Setting up the database is one of the most important parts of any application. How do you represent your ideas in a structured, well-formed way? The first and most important step is to break down what you know you want your application to do, analyze those tasks, and then extract the important parts.
A message board has several distinguishable elements once you start to analyze it. The first and most obvious is you need to store information on subjects and threads. If you’ve ever looked at a message board before, you’ll notice that it’s broken down into three levels. The first level is a general heading, describing what it is going to contain. We’ll call this level Board. Board can contain any number of Threads. Finally, a Thread contains any number of Messages. The last area of data involved is data representative of a message board user. Users do not fit into this three level hierarchy, but instead are a distinct part of each level. This very distinct hierarchy is a great place to start when defining your data.
The first thing to do is break down the type of information that describes a Board. This can be done many different ways: brainstorm, use your vast knowledge of all things data, or actually go to some bulletin boards on the Web and take a look at the kind of information they capture and display. This last option is probably the easiest, as there are numerous examples on the Web to look at.
That said, let’s go through a Board and determine what type of information a Board uses. Our board will have the following information displayed on the user interface: name, description, list of threads, thread count, post count, the moderator, and some sort of unique identifier. Not all of those fields need their values to be saved in the database. For instance, the thread count and post count can be easily retrieved at runtime by just calculating them. That leaves Name, Description, Moderator, and a Unique Identifier.
Threads are less complex than Boards. A Thread contains the following fields: board ID, subject, post count, creator, and some sort of unique identifier. The board ID should be created by a relationship between the two tables, and post count can be retrieved at runtime. That leaves subject and creator.
Finally, the last item we must capture is the user data. Most bulletin boards you visit do not allow anonymous posting. That is, in order to post, you must have your own user data. Our message board will function the same way. This makes it much easier to write your SQL statements and preserve database integrity. User information will contain the following fields: a unique identifier, username, password, first name, last name, e-mail address, whether or not this user is an administrator, and whether or not this user has been banned from posting.
Setting up your Access database is a pretty quick process. You can use the dotBoard.mdb file located on your CD, or follow the steps provided next. If you want to create your own database, open up Microsoft Access (either 97 or 2000), and create a new database called dotBoard.mdb.
The Microsoft Access database is rather straightforward. As was described previously, the Board table (see Figure 13.1) will contains four fields: BoardID, BoardName, BoardDescription, and ModeratorID. BoardID should be an AutoNumber, with Indexed set to Yes (No Duplicates), and should also be the primary key. BoardName is a Text field, with Required set to Yes, and with a Field Size of 100. BoardDescription is a Text field, with Field Size set to the maximum access allows, which is 255.
The Threads table will also contain four fields:ThreadID, ThreadSubject, CreatorID, and BoardID. ThreadID should be an AutoNumber, with Indexed set to Yes (No Duplicates), and should also be the primary key. ThreadSubject is a Text field, with Field Size set to the maximum access allows, which is 255. CreatorID is a Number, with Field Size set to Long Integer, and Required set to Yes. BoardID is a Number, with Field Size set to Long Integer, and Required set to Yes (see Figure 13.2).
The Posts table will contain six fields: PostID, PostSubject, PostBody, CreatorID, ThreadID, and PostDate. PostID should be an AutoNumber, with Indexed set to Yes (No Duplicates), and should also be the primary key. PostSubject is a Text field, with Field Size set to the maximum access allows, which is 255. PostBody is a Memo field with Required set to Yes. CreatorID is a Number, with Field Size set to Long Integer, and Required set to Yes. ThreadID is a Number, with Field Size set to Long Integer, and Required set to Yes. PostDate will be a Date/Time field with a Default Value of Now() and Required set to Yes. See Figure 13.3 for the Posts table.
The Users table will contain eight fields: UserID, Username, Password, FirstName, LastName, Email, IsAdmin, IsBanned. UserID should be an AutoNumber, with Indexed set to Yes (No Duplicates), and should also be the primary key. Username is a Text field, with its Field Size set to 50 and Required set to Yes. Password is a Text field, with its Field Size set to 50 and Required set to Yes. FirstName is a Text field, with its Field Size set to 100 and Required set to Yes. LastName is a Text field, with its Field Size set to 200 and Required set to Yes. Email is a Text field, with its Field Size set to 255 and Required set to Yes. IsAdmin and IsBanned are Yes/No fields, with Format set to True/False and Required set to Yes. See Figure 13.4 for the Users table.
The last step is to define the relationships between the tables. Posts relates to Threads on ThreadID. Threads relates to Board on BoardID. Users relates to Posts on CreatorID, to Threads on CreatorID, and Board on ModeratorID (see Figure 13.5).
Setting up a SQL Server database is rather effortless, especially since you can let the database do everything for you by executing a SQL script. The only thing you need to do is open up your SQL Enterprise Manager, navigate to the server you want to create your database on, and open up the Databases node. Right-click the Databases node and select New Database. Name your database dotBoard and select OK.
The only other action you need to take is open up SQL Query Analyzer, and execute the SQL Script shown in Figure 13.6 (which can also be found on your CD, called dotBoard Setup.sql).
Lastly, go back to your SQL Enterprise Manager, navigate to your database, and select the Diagrams node. Right-click and select New Database Diagram. Click Next, then select our four database tables and hit Next. The diagram should be created automatically for us and should look a lot nicer than the MS Access version. (see Figure 13.7).
When designing an application, there are two possible main routes. The first is the procedural approach (anyone familiar with ASP 3.0 and earlier who did not use COM objects to handle logic knows exactly what I mean): the simple “Page1” does this, “Page2” does this, and so on. You have a set of “top-down” ASP scripts (that is, your code starts at the top and executes until it hits the bottom), with functions including files, which make up your application. There is technically nothing wrong with this approach, as there are many large-scale applications that are written exactly this way.
Your other choice is to take a more Object-Oriented (OO) approach. In an OO world, you create a set of classes and interfaces that make up the core of your application. Using these classes, you create a user interface that will define what an average user would consider your “application.” This approach allows the designer of the classes to encapsulate a good majority of all the code and logic behind the application, without exposing it to whomever might be building the user interface (and likely, the same person will be building both). An OO approach also allows your application to be used in a variety of ways, and would allow someone to build multiple user interfaces on top of the exact same set of classes.
Both approaches have their merits and flaws. With the procedural approach, you will be stuck in ASP.NET for your user interface, and if you want to “copy” logic from one place to another, you either have to create globally scoped functions, or copy and paste code. The procedural approach does tend to be a bit easier to create, though, because you do not have the additional overhead of having to create classes to handle your logic and data. The Object-Oriented approach effectively encapsulates your entire application into a small set of classes, grouped into an assembly .dll file, which can be created from another application and used. This allows you to hand another developer your assembly file, and let him or her go about building the actual user interface without you ever needing to know what the user interface was. The drawback to building an application in an OO-manner is whomever is developing the classes needs to take a lot of care to get it done properly, and be able to build it in such a manner as to not tie it exclusively to one type of user interface.
When deciding whether or not dotBoard should be procedural or Object-Oriented, take into account these things. First and foremost, you need to be able to maintain your code. If your code is modularized into multiple functions and organized very well, then the procedural approach doesn’t seem too bad. However, if your code is placed haphazardly throughout your application, finding your bugs and improving code at a later date might be harder. If your code is organized into a set of classes and public interfaces (the methods and procedures that an application can “see”), it is typically easier to maintain your code, as each piece of each class is a very small piece of the application as a whole, and making changes won’t likely take a large amount of code.
The other thing you should think about is that dotBoard is being written in VB.NET. For anyone who has built an application in straight ASP, you would probably be more comfortable with the procedural approach. For anyone who has built an application in ASP and created VB COM objects, you would most likely be more comfortable with the Object-Oriented approach, but feel some trepidation about speed and performance issues. For the master gurus out there who are building C++ ATL COM objects and using them in ASP, you might scoff at VB.NET and think you would rather stick with C++ ATL COM. Well, all of you have very valid points. A straight procedural approach is generally looked down upon in a professional environment, VB COM objects in ASP are typically regarded as slow and frequently memory- and processor-intensive, and well, nobody can read the C++ ATL code anyway, so it doesn’t count!
Seriously, though, every point made is very valid about every technology discussed. That’s where VB.NET comes in. The .NET runtime is remarkably fast. The Just-In-Time (JIT) compilation of your code only happens the first time it is executed; so, after that initial execution, your code runs incredibly fast until you change it (at which point the JIT compilation happens again).VB.NET is also a fully Object-Oriented language. It provides developers with every good OO technique available, and it is actually quite easy to write OO applications with it.
All that said, it’s pretty obvious dotBoard should be an Object-Oriented application. Don’t worry if you’ve never written any OO code before. Object-Oriented techniques are relatively easy to implement, and even if you don’t think you’ve ever used any objects before, you probably have (especially if you’ve done any development in ASP).
Now that we’ve decided on object orientation, we need to analyze our application and determine what our objects will “look like.” At this point, you might say, “we’ve already done that while analyzing and building our database,” and you’d be right. We have already done that. Half of our design work is now already done! The only other part we have to do is map the data we’ve already analyzed to VB.NET types and group them accordingly. We’re going to do that with the wonder of UML (Unified Modeling Language). If you don’t know what UML is, don’t worry; all we’re using it for here is to show you some pretty pictures of what our classes are going to look like. Please note that all of these objects and all files will be found on the CD that accompanies this book.
To make it easier for each of your objects to have access to the database, we’re going to create a singular data access object that does everything for you. We’re going to call this class DataControl, and it is going to be comprised of solely shared methods. A shared method means you do not need to create an instance of a User object to call it. DataControl will contain two Shared methods, GetDataSet and ExecuteNonQuery. GetDataSet returns a DataSet, and ExecuteNonQuery executes a SQL statement and returns nothing. This class is pretty straightforward, and is shown in Figure 13.8 (likewise, it can also be found on your CD as DataControl.vb).
You see that the two methods in DataControl are in fact, rather simple. As was discussed earlier in this book, these functions connect to the database and do a specific function (execute SQL scripts and one returns a DataSet). The one thing to note is that the connection string is being retrieved from the ConfigurationSettings.AppSettings. These are dynamic settings that the .NET runtime gives you access to. When you’re running an ASP.NET application, they are located in the web.config file. In another type of application, they are located in ProjectName.exe.config. That’s it for our Data object. The next step is to take a look at our User object.
When we looked at the user information when thinking about the database, we discovered a number of fields that needed to reside in the User table. Luckily all our classes will be structured in a way to nearly match the database; the User class is no exception. The only difference is that this User is a VB.NET class and not a database table.
There are four basic types of users: Guests, Registered users, Administrators, and Moderators. All of these should be represented when we build our User class. Again, you might say something like “but this is an object-oriented application, and if we have multiple types of one object, shouldn’t they be separate?” Again, you would be right. There are three types of users. All have similar properties; the only difference is that some do certain things that others can’t. For instance, a registered user in a bulletin board would have the ability to post threads and messages, whereas a guest user would not. A registered user would also have the ability to edit his or her profile and edit his or her messages, whereas a guest user would not be able to. An administrator would have the ability to do everything a registered user could, except globally. A moderator can modify posts and threads in boards he or she has moderator privileges to.
Now that we’ve identified the multiple types of users, we need to determine if we should have multiple types of users in our application. A Guest can only browse a bulletin board, as no security is necessary for browsing. A Registered User can create and edit posts, and modify his or her profile. An Administrator can do anything he or she wants to the bulletin board. A Moderator can do what a Registered User can, and can act like an Administrator on the board he or she is given moderation rights to.
You may want to build some neat OO objects here, but all these things can be accomplished through a single User class. Take a look at Figure 13.9.
You see that our User object will have the exact same fields as our database table, which is named exactly the same. This makes it a bit easier to remember which field in the object matches up to which field in the database. The other thing you should notice is the three items down at the bottom of the diagram: Create, Validate, and Update. All are methods the User object will have. Update() will update the user’s details and save them to the database. Validate is a shared method of the User class, and can be used to perform all user validation. Create is also a shared method, and can be used to create a brand new user in the database.
That’s it. That’s the whole User object. Not much to it is there? It has a Boolean field to signify whether or not it is an administrator, and each Board object will store the ID of the administrator of that Board, so the User object doesn’t have to. The only other thing to mention is guest users—a guest user will just be a User that is Nothing. That is, if you are currently a guest in the application, you won’t have a User object created for you. Let’s take a look at the code involved to create this User object in Figure 13.10 (which can also be found on your CD called User.vb).
That part is clear enough. We declare the User class, and the private variables necessary to represent each user. Next, declare the public properties for each of these private variables as shown in Figure 13.11.
With that out of the way, let’s look at the methods the User object will have. As we saw earlier, there will be three methods: Validate, CreateUser, and Update. Validate is a shared method which will give a developer the ability to validate and return a valid User object, or throw an exception. CreateUser is also a shared method that gives the developer the ability to create a new User object. Finally, Update will allow a developer to update the private fields in the User object and commit them to the database. This will be for tasks like saving passwords and updating e-mail addresses. Let’s take a look at the first method, Validate, in Figure 13.12.
The Validate method accepts a username and a password as parameters, and attempts to verify that those parameters are a valid combination for a registered user. If the password is empty, it throws an ArgumentException. If, while looking up the username, it finds that the username is not present in the database, it again throws an ArgumentException. If the username exists, but the user is banned, then it throws an Exception. If the username exists, the user is not banned, and the password passed in was incorrect, once again it throws an ArgumentException. Finally, if the username is valid and the password is correct, it returns a new User object, passing in the first DataRow to the User constructor.
At this point, you’re probably wondering why we haven’t discussed the constructor of the User object. Well, wait no longer! Here’s the code for the User object constructor in Figure 13.13.
There are two constructors here. The second constructor is what the Validate method called. That constructor forwards the DataRow on to another method called inflate, which will be discussed in a moment. The first constructor accepts a user ID as a parameter. This user ID is synonymous with the UserID field in the User table. The constructor looks up the user based on the user ID. If that user ID is not found, it throws an ArgumentException. If the user ID is found, it forwards the first DataRow in the DataSet to the fillData method in Figure 13.14.
As you can see, the inflate method accepts a DataRow as a parameter, and populates all the private fields with values from the database. This is frequently called “inflating” your objects, hence the appropriately named subroutine. The other thing to notice is that inflate is a private subroutine. This is because you don’t want any objects outside of the current User object to have access to this method. It does “utility” work on the object, and is unnecessary for any other object to call this method.
Now that we’ve discussed how to Validate and return a valid User object, let’s move on to creating users. Any user can have any username. The only restriction is that no two users can have the same username. This is because if you had two users with the same username, the only way to identify which one you wanted is to have some other sort of unique identifier. Unfortunately, people can typically remember names and usernames much better than they could some (relatively) random number. So, in order to keep this username unique, you have to manually check. If you were a database administrator, you would probably insist on creating a unique index on the username field in the database, which is completely reasonable. If you feel you need the extra “security” in place to make sure the same username isn’t taken twice, go ahead and put it in there, but it’s in the CreateUser method as well, which we will now take a look at in Figure 13.15.
First, the CreateUser function scans the database to see if the request username already exists. If it does, it throws an ArgumentException. If the username doesn’t exist, it builds a SQL statement to insert a new row into the user table and executes it. Finally it calls the Validate method and returns the result.
The last method to discuss is the Update method. This method updates the database with the current state of the object. See Figure 13.16 for the Update method.
Again, this method is rather simple. It generates a SQL statement to update the database. The If statements are there to insert the correct Boolean value into the database instead of “True” or “False.” Finally, after building the SQL statement, it executes it and exits the method.
Now that we’ve designed the User class, let’s take a look at the Board class. A lot of the concepts in the User class will be taken from the Board class. That is, the Board class will mimic the Board table in the database, and will have a couple of similarly named methods as in the User class. Let’s take a look at a UML diagram of the Board class in Figure 13.17.
Just by looking at this diagram, you can see that the Board class has a lot more functionality than the User class. Notice the four fields from the Board table: BoardID, Name, and Description. Just like the User class, these are directly representative of what exists in the database. The other two fields you shouldn’t recognize. ChildThreads returns a list of the Threads that exist in this Board. ChildThread is a property that accepts a ThreadID to return a specific Thread that is directly located in a specific Board.
The methods available to a Board object should be somewhat self-explanatory. The Update method does exactly what the User class Update method did: updates the database with the private fields in the database. The Delete method deletes the Board from the database. DeleteThread deletes a specific Thread from the database. DeletePost deletes a specific Post that is located somewhere in this Board. CreateThread creates a new Thread and adds it to the private list of Threads in this Board. Like the User class, the Board class has a way to create new Boards, called CreateBoard. Let’s start off by showing the basics of the Board class in Figure 13.18 (which can also be found on your CD under the name Board.vb).
The public properties in this class are a little more complex than the properties in the User class. The public properties for the private fields are easy to understand, but ChildThread and ChildThreads are a bit more complex, as is the private myThread variable. Let’s start with myThread, which is defined as being of type ThreadList. If you’re familiar with the System.Collections namespace, you’ll definitely notice that this is not one of the built-in .NET collections. ThreadList is actually a custom list that wraps an ArrayList, which will be discussed a bit later. For now, just accept the fact that this list collects all the Threads in a given Board.
The ChildThreads property returns the private myThreads variable. The ChildThread property accepts a ThreadID as a parameter, and looks up that ThreadID in the myThreads list. It loops through the list, and compares the ID of the Thread in the list with the ThreadID passed in. If it finds a match, it returns that Thread, otherwise it throws an ArgumentException. Again, ThreadList will be discussed later, but for now, let’s move on to the shared CreateBoard method, as shown in Figure 13.19.
The first step in this method is to check to see if the user that is requesting a new Board be created is an admin. If the user is not an admin, it throws an exception. If the user is an admin, it then checks to see if a Board with that name has already been created. Like the username field in the User class, the name field in the Board class should be unique. This makes it easier to manage your Boards and to make sure they’re named appropriately. If the Board name already exists, it throws an exception, otherwise it generates the SQL statement necessary to create a Board. It then executes the SQL statement and returns a new Board object based on the Board name. Let’s take a look at the Board constructor, shown in Figure 13.20, to see what it does.
The Board constructor takes the Board name as a parameter, and looks in the database for that Board name. If it cannot find the Board, it throws an Exception, otherwise it passes the first DataRow in the DataSet to the inflate method. The inflate method functions exactly as it did in the User class: it fills up the private fields with values. The only difference here is that the myThreads variable is initialized and the BoardID is passed to it. Again, the ThreadList will be discussed a bit later, but trust that the ThreadList takes the BoardID passed in and creates a collection of the Threads in this Board. Next, let’s take a look at the Update method in Figure 13.21.
The Update method in the Board class does exactly what the User class’s Update method did. The only real difference here is that it checks to make sure the user requesting the update is really an admin. If the user is not an admin, then it throws an exception. Next, take a look at Figure 13.22 for the CreateThread method.
The CreateThread method builds the SQL statement necessary to insert a new Thread into the database, and then reinitializes the private ThreadList variable by calling its InitializeThreads method. You may be wondering why the Board class has the Create method for its child objects, whereas both the User class and Board class have their Create method located in their class definitions. This is because both the User class and Board class do not have any parent-child relationships with any other classes. When you have a parent object and multiple child objects, the typical place to put the creation of the child objects is in the parent object. This is a matter of semantics—if you prefer to have your child objects create themselves, feel free to do it that way.
Let’s explore how to delete objects. The Board class contains the Delete, DeleteThread, and DeletePost methods. The Board class can obviously delete itself, but why would it also contain the ability to delete both threads and posts? It has these two methods because the Board class is where the ModeratorID lives, and Moderators can delete both threads and posts, so it just seems natural to put these two delete methods in the Board class. Look at Figure 13.23 for the code.
The first step in the Delete method is to check to make sure the requesting user has the appropriate access rights to delete this board. If the user is not an admin, then an ArgumentException is thrown. If the user does have access rights to delete a Board, then the SQL statement is built to delete the Board from the database. The SQL statement is executed, and the Board is officially deleted. You can see the DeleteThread method in Figure 13.24.
The first step in the DeleteThread method is to make sure the requesting user has the appropriate access to delete this thread. If the user is neither an admin nor a moderator of this Board, then an ArgumentException is thrown. If the user does have access to delete a thread, then the SQL statement is built to delete the thread from the database. The SQL statement is executed, and the ThreadList is reinitialized by calling its InitializeThreads method.
The next method we need is the DeletePost method. Take a look at Figure 13.25 for its implementation.
Just as in the DeleteThread method, the first step in the DeletePost method is to make sure the requesting user has the appropriate access rights to delete this post. If the user is neither an admin nor a moderator of this Board, then an ArgumentException is thrown. If the user does have access to delete a post, then the SQL statement is built to delete the post from the database. The SQL statement is executed, and the Threads ChildPosts property is reinitialized by calling its InitializePosts method.
We promised you that we would discuss the ThreadList, and here it is. As was mentioned earlier, the ThreadList class is a class that wraps an ArrayList. By wraps, we mean it contains a private ArrayList thereby holding its list of Threads, and exposes certain custom functionalities not necessarily pre-built into the ArrayList class. Let’s take a look at a UML diagram for the ThreadList class in Figure 13.26.
As you can see from this diagram, there isn’t much to the ThreadList. It contains a count of the number of Threads in the list, contains an Item property to allow you to access the Threads in the list, and gives you the ability to manually force the reinitialization of the list through the InitializeThreads method. Again, let’s start at the basics and build up from there in Figure 13.27 (which can also be found on your CD under the name ThreadList.vb).
The ThreadList class contains only two private fields: list and mBoardID. The list variable is used to hold all your Threads, and mBoardID is used to look up the Threads in a given Board. The constructor accepts a BoardID, and calls the InitializeThreads method, as shown in Figure 13.28.
The InitializeThreads method is rather straightforward, but there is one major concept that needs to be mentioned. First, a SQL statement is built to select the Threads located in the appropriate Board (this is where the mBoardID variable comes into play). The SQL statement also joins on the Users table, to allow for the Thread object to know about the User who created the Thread. Next, the list is initialized, and each DataRow in the resultant DataSet is added to the list. This is where the important concept is. The private list currently contains a set of DataRow objects. Obviously, you do not want to expose a bunch of DataRow objects as your list of Threads, so this is where the Item property comes into effect. See Figure 13.29 regarding the Item property.
The Item property is a little more complex than the average property. Let’s review it, line by line. Line 1 creates a variable called myObject of type Object and sets it equal to the object that is at the specified index of the ArrayList. Line 2 compares the type of the object to the type of the Thread class. If they are the same, it does nothing; if not, it enters the Else part of the If statement (lines 5–9). Next, a Thread variable called myThread is declared and set to a new Thread on line 6, passing in the object that is in the specified index in the ArrayList. That object is cast to a DataRow using CType. Line 9 sets the object at the specified index in the ArrayList to the myThread variable. Finally, on line 12 it returns the Thread that is in the specified index of the ArrayList (and again, is cast to be a Thread object).
You may be wondering to yourself exactly what all of this accomplishes. Well, if you remember from the InitializeThreads method, the ArrayList is filled with DataRow objects. We do not want to directly expose anyone using our objects to DataRow objects, so we need to instead give them Thread objects. So, behind the scenes, every time a new index is requested from the ArrayList, we quietly “switch” the variable in that index from a DataRow to the appropriate Thread object. You may also ask why this class doesn’t just put the Threads into the ArrayList from the start instead of doing it this way. The answer is simple: there is no need for the overhead of having multiple Thread objects (each with other objects inside them) in the list when you can save memory and time instantiating objects by just keeping the data for each Thread object until it is actually requested. When developing large-scale applications with many parent-child hierarchical relationships, a technique like this will save you and your application a lot of time.
The Thread class is the “middle child” in our hierarchy of objects. Luckily for us, a lot of its functionality and concepts are borrowed directly from the Board class, so this should be pretty quick. Let’s take a look at another UML diagram in Figure 13.30.
Like every class we’ve examined so far, the Thread class shares the same private fields as the Thread table in the database. Like the Board class, the Thread class contains two properties to access its children: ChildPost and ChildPosts. ChildPost retrieves an individual Post object from its list, and ChildPosts returns the entire PostList. PostList will be discussed a bit later. Thread also contains the method to create child Posts. Let’s start with the basics in Figure 13.31.
First, you’ll notice the private fields that are the same as the fields in the database. You’ll also notice that a Thread has a Creator field and property that are User objects representing the user that created this Thread. Like the Board class, this class has a constructor that accepts a DataRow as a parameter and then calls inflate to fill up the private fields using that DataRow. Also like Board, you have two child object properties, ChildPost and ChildPosts. ChildPost is used to return a single Post, and ChildPosts is used to return the entire PostList. Let’s take a look at the next method in the Thread class, CreatePost, in Figure 13.32.
Taking a look at the CreatePost method, you’ll notice that it does almost exactly what CreateThread did in the Board class. It builds a SQL statement to create a new Post, then executes that statement and reinitializes the private PostList object.
Being that we’re almost finished creating our classes, it’s time to look at the PostList class. You may be thinking to yourself “I wonder if the PostList class is similar to the ThreadList class”. Such thinking should be rewarded. PostList and ThreadList are nearly identical, except for in regards to what type of object they collect. Again, let’s take a look at the UML diagram for the class first in Figure 13.33, then in Figure 13.34 we’ll review the basics of this class (something which can also be found on your CD as PostList.vb).
Just like ThreadList, PostList contains a Count property, a method to initialize posts in a thread, and a constructor that accepts the ID of the parent object. The only real difference here is that this class gets values from the User table instead of the Thread table. Next, let’s examine the Item function in Figure 13.35.
In reviewing this Item function, note that it looks remarkably similar to the Item function in the ThreadList class. In fact, it is exactly the same except that it uses Post instead of Thread. Other than that difference, PostList is exactly the same as ThreadList.
So far, you should have noticed most of the classes in our code share a lot of the same ideas: add, update, lists, mimicking the database tables. Well, the Post class is no different. In fact, it is rather similar to both the Board and Thread classes. Let’s take a look at the UML diagram for this class in Figure 13.36.
Just like the other classes, this one is remarkably similar to its brothers—especially the Thread class. The only real difference between this class and the Thread class is that Post has a Body field, pulls its values from the Post table, and doesn’t have any child objects. Let’s take a look at the whole class in Figure 13.37 (which can be found on your CD as Post.vb), as there really isn’t much to it.
As you can see, this class has five private fields with the corresponding five public properties. In addition, it has a constructor that accepts a DataRow parameter which passes the DataRow to the inflate method. Finally, it has an update method, with the rule that only the creator of the Post can actually edit the Post. Doesn’t seem too hard, does it?, Especially after all the other classes we’ve dealt with. It almost seems passé.
We’ve finally gotten every class in our message board object library finished; now all we need is a way to get a list of every Board object from our database. This is accomplished using the MessageBoard class. We won’t bother to show you a UML diagram of the MessageBoard class, as there is only one method in it: GetBoards. Let’s take a look at the code in Figure 13.38 (which can be found on your CD under the name MessageBoard.vb).
This class is fairly easy to understand. What it does is look up each BoardName from the database, and create a new Board object based on that name. It then adds each Board to its list, and finally returns the list.
That’s it. Every single one of our objects to be used in dotBoard is completely finished. You may wonder why we did all this work ahead of time instead of just jumping into the application itself. That is a very good question, and as such, has a very good answer. We did all this work designing and setting things up so that when we actually build our application, it will go smoothly, quickly, and won’t require a lot of coding in the User Interface. Any good application splits the User Interface from the actual implementation of the application, which is exactly what we did. We are about to move on to the user interface of our message board application. You will see that using the work we’ve already done, the rest of this application is going to be very straightforward and easy.
Finally, we’ve gotten to our User Interface. Our database is constructed. All of our message board classes are created; the final thing to do is to put a UI on top of it all. Just like when we created the classes our applications are going to use, we need to sit and think for a few minutes to determine exactly what it is our message board will do. The obvious requirements are that a user must be able to register, log in, and modify his or her profile. Anyone must be able to browse the Boards, Threads, and Posts. Registered users must be able to create threads and posts, and administrators must have the ability to administer users and create and delete boards.
Sound like a lot? Well, since we have a good majority of this work already built into our numerous classes, most of our work now is to create the UI and tie events to methods our objects will handle. The only other thing our message board should be able to do is be “changed” at will. That is, colors, fonts, and any other sort of styling element should be able to be changed without needing to actually modify every single control we place on our form. This will be discussed in a moment, but for now, rest assured, it will be very exciting, and most of the work will be done for us! Let’s start by figuring out how to register and log in.
The first step in designing our application is to create the ASP.NET application. You can either get the solution from the CD, or create your own. If you get the files from the CD, they are in a folder called “dotBoardUI” in the Chapter 13 folder. The dotBoard.sln file is the main solution file, and everything else in that folder is a part of the project. Either way, your application should be named dotBoardUI, to go with your dotBoardObjects class library. After you have created your application, add your dotBoardObjects project to your solution, and add a reference to the newly added project to your ASP.NET application. Next, rename Web Form1.aspx to default.aspx. This will make it easier when it comes time to deploy your application, as default.aspx is typically one of the default documents IIS serves when the browser doesn’t request a specific file in your application.
Now that you have your project created and the appropriate references made, let’s get started on the groundwork for our application. If you think about it, every page you make will likely need access to the currently logged in user. There are many reasons for this as you’ll see later, so for now just assume that every page will need that information. There are many ways to do this. For instance, you can copy and paste the code necessary to get this information on every page. Anyone familiar with programming techniques should sense a red flag go up at that statement. Copying and pasting the code is a terrible idea, for so many reasons that we don’t have space to state them here. Another solution available is to create a public module with the common functions your pages would need. This is a good solution, but let’s do it a little differently. We are going to have one Web Form that all our Web Forms will inherit from. Why would we do this? So every Web Form you create will have direct access to the common methods, and every user control you put on these Web Forms will be able to get the information easily.
Add a new class to your project and name it FormBase.vb. We’re not adding a Web Form in this case because we don’t need any sort of UI for our FormBase; we just need access to a common set of methods. Take a look at the basic code in Figure 13.39 (which can also be found on your CD called FormBase.vb).
Pretty easy, right? What we have here is a class that inherits from System. Web .UI.Page. This allows all our Web Forms to inherit directly from this class, instead of inheriting from System. Web.UI.Page. The next thing we need is for our FormBase to be able to have a reference to the currently logged in user (if there is one). Here is the code to do just that in Figure 13.40.
All we have here is a private dotBoardObjects.User object, and a public property to retrieve it. The Set property sets the private field with the value passed in, and adds the user ID of the passed in User object to the session. We do this so a user does not have to log in multiple times while perusing your message board. You’ll see where this comes into play later. The other property we have is one that returns a Boolean value of whether or not there is a currently logged in user. This property makes it easier for someone to determine if there is a logged in user. Basically, instead of having to test for Nothing over and over, you use this Boolean property.
That is all the state maintaining we’ll need in our base class. The only other thing our FormBase class needs to do is fulfill that last requirement we talked about. That is, the ability to modify every control on every form without needing to actually rename the class names on elements. This is probably one of the most interesting techniques dotBoard will use. Basically, what we will do is create the code necessary to automate the process of restyling every control in every Web Form. This might sound like a daunting task, but actually once you take a look at it, it is rather simple. The first step we need to take is to open up our web.config file and add the following lines of XML directly beneath the <configuration> tag as shown in Figure 13.41 (which can also be found on your CD called web.config).
Okay, now what exactly does that mean? Your <appSettings> are custom settings you create and have access to in your application. We are creating two custom settings, which are added using the <add> tag. The key attribute is the name of the settings, and the value attribute is obviously the value. Here we are adding two keys, ConnectionString and XmlConfigFile. ConnectionString is what you use to connect to your database with. Remember the DataControl class and how it accessed System.Configuration.ConfigurationSettings? The ConnectionString key is exactly what that class will use. The other key is XmlConfigFile, which is used to hold the location to your XML file that will hold the style information we discussed earlier. Please change the values of each to represent where you actually have the files on your computer located.
We now have the ConnectionString and XmlConfigFile keys added to our appsettings. Let’s start discussing how we will accomplish the “sweeping” change of styles, without needing to manually apply any styles on your controls. First take a look at the following Cascading Style Sheet (CSS) file you should add to your project, as shown in Figure 13.42.
You can see here that we have a number of styles we will want to apply to many different elements throughout our application. Manually setting these styles is hardly desirable, and maintaining these settings if any of your class names change would be a nightmare. So, what can be done to prevent us from having to maintain this? Enter the styles.xml file in Figure 13.43 (which can also be found on your CD under the name Styles.xml).
You should now notice that the values of these XML tags correspond to an appropriate class name in the preceding stylesheet declaration. Now all we need to do is find a way to associate these XML tags with the appropriate controls on every single page. We can accomplish this through two methods, as shown in Figure 13.44.
That’s a lot to digest all at once, so let’s break it down. The first thing you’ll see is that ApplyStyles accepts a ControlCollection as a parameter. This collection can be obtained from Page.Controls or Control.Controls. Next, the subroutine checks to see if the XML document has been loaded yet. If it hasn’t, it retrieves the location of the styles.xml file from the AppSettings and loads it. If there was an error in the loading of the document, it throws an exception. If there are no problems with the XML document, it loops through every Control in the ControlCollection that was passed in. For every control, it sets a variable “style” to the value of what the GetStyleName function returns. GetStyleName takes your control’s fully qualified type name (represented in the code by objControl.GetType().ToString()), and looks for that in the XML document. It does this by calling the SelectSingleNode function of the XMLDocument object. It builds an XPath query string and looks for the appropriate node with the type attribute that is the same as the type string passed into the GetStyleName function. If it finds that node, it returns the InnerText of the appropriate node; otherwise it returns an empty string.
Control is returned to the ApplyStyles method, and the style that was returned is tested to make sure it is not an empty string; there is no point in setting the value if it is empty. Next, the Control is cast to be a variable of type WebControl. Since the only Control that can have its style attribute programmatically manipulated is the WebControl, and since every control in System.Web.UI.WebControls inherits directly from WebControl, it is safe to perform this cast. Just make sure you do not add anything other than WebControls to your styles.xml file and this will work without error. Next, the CssClass property of your WebControl is tested to make sure it is currently an empty string. It does this because if you specifically set a style on one of your controls, you most likely do not want that style overridden by this method. If it is empty, it sets the CssClass property to the style String that was returned by the GetStyleName function. Finally, if the Control has child controls, it recursively calls ApplyStyles, but instead with the Control.ChildControls ControlCollection as the parameter.
With these two functions, every type of Control you add to your styles.xml file will automatically get CSS styles applied to them, without any maintenance on your part other than a small XML file. Wondering how this will actually get used? All you need to do is in your classes that inherit from FormBase, call the ApplyStyles method passing the ChildControls of the page you are currently on. Feel free to try this. Modify the stylesheet and styles.xml file all you want. Just rest assured that every control type you add to your XML file will automatically have the CSS classes applied to them that you want.
Since we don’t have any users created in the database yet, let’s take a look at how to register with dotBoard. How to create the User Controls and Forms won’t be discussed, but the source code is available on your CD, as well as multiple screen shots for each Web Form and User Control. Take a look at the register.aspx page on Figure 13.45.
First, we have the Page_Load subroutine, which handles the Page.Load event. All this event does is call the ApplyStyles method of the FormBase class. Next, we have the btnRegister_Click subroutine that handles the Register button’s click event. The first thing that subroutine does is make sure the page is currently in a valid state. This validity is determined whether or not all of the validation controls you added to your form return a valid result. Only once every validation control becomes valid does Page.IsValid ever return true. Next, a User object is declared and the CreateUser method is called. If the CreateUser method throws an exception, then the custom validator on our form is set to invalid and its ErrorMessage property is set to the Message property of the Exception thrown. If the CreateUser succeeded, then a reference to the parent Page, casted to the FormBase type, is created and the CurrentUser property is set to the User that was just created. Once all this is done, the user is redirected to default.aspx.
As we discussed when we went over FormBase, every page will need to know about the currently logged in user. Likely, every page will also need a login form so the user can log in from anywhere. The best way to do this is to create a Web User Control. Take a look at our userArea.ascx control in Figure 13.47.
Boy that’s ugly, isn’t it? Don’t worry, that’s why we created the style code in FormBase. Anyway, what we have here are two panels. The top panel contains the controls necessary to log a user in, while the bottom panel contains the welcome message and any specific actions the user can take. Let’s take a look at the code-behind for this page in Figure 13.48.
Okay, there’s a lot here, so let’s break it down. The Page_Init subroutine handles the Page.Init event. When this subroutine gets called, it attempts to log in the user based on the Session userId value. If that value exists, it uses it and initializes the CurrentUser object; otherwise, it exits. Finally, the subroutine hides or shows the correct panel and admin link depending on whether the user was successfully logged in or not and if the use is an admin or not, and then changes the text of the welcome label to the logged in users first and last name.
BtnLogin_Click handles the event when the user clicks the Login button. The first thing it does is check to make sure values have been entered into the username and password fields. If so, it attempts to validate the user with the username and password the user entered. If an exception is thrown, the error label text is set to the message of the exception thrown. If not, it sets the CurrentUser property of the FormBase to the currently logged in user, and then redirects the user back to the page they are currently on. It does this to make sure all controls on the page have gotten a chance to know that the user has logged in.
Finally, we have four link buttons, the first one redirects the user to the register page we’ve already seen, while the other clears the user ID out of Session and redirects them back to default.aspx. The third redirects the user to profile.aspx, the user profile page. The fourth one redirects the user to admin.aspx, the admin page.
Finally, open up your default.aspx page, and drag your userArea.ascx user control onto the page. You now have a fully functioning login/register area to your message board, where anyone can register and log in and receive customized links depending on what type of user they are. See Figure 13.49 to see what the page looks like.
The next step in building dotBoard is to determine how to browse through the Boards, Threads, and Posts. When a user first enters the site and views the default page, they should be shown a list of Boards and descriptions they can choose to view. This code is located in default.aspx and default.aspx.vb.
Browsing through our boards isn’t very difficult. All we need to do is use a Repeater control, and create a custom DataSet out of our list of Board objects. Unfortunately, the only control we can drag and drop onto a Web Form is a Repeater control, and you can’t drag controls into the Repeater, so we are going to have to look at the actual quasi-HTML that ASP.NET uses and write the repeated content by hand, as shown in Figure 13.50.
The repeater code creates a header template, separator template, and the actual item template. The only thing we haven’t discussed thus far is what data source the Repeater should use. Since the Repeater control requires a real data source (i.e., DataSet or something similar), what needs to be done is our list of Boards needs to be “translated” into a DataSet. Take a look at the updated code-behind for the default page in Figure 13.51.
Notice the addition to the Page_Load method in this file. This subroutine now calls the DisplayBoards subroutine. DisplayBoards restructures the list of Boards into an appropriate form for a Repeater control to use. First, it creates a DataSet and gets the list of Boards from the MessageBoard class. Next, it creates a new table in the DataSet and adds three columns to it. Next, it loops through the list of Boards and builds an object array of the fields to add to the DataSet. It then adds a new row by passing in the object array to the Add method of the Rows collection. Finally, it accepts the changes, and forces the Repeater control to DataBind to the DataSet. Look at Figure 13.52 to see what this page looks like.
Once the user has clicked one of the board links from default.aspx, they are taken to board.aspx. This page will be responsible for determining which board was selected and for displaying the appropriate threads. Displaying the Threads in a Board will function nearly identically to how displaying Boards functioned. Let’s take a look at the important quasi-HTML that this page uses in Figure 13.53.
The repeater code creates a separator template and the actual item template. It DataBinds the appropriate fields in the data source to items in the template. Let’s take a look at how we get the data into the data source in Figure 13.54.
Just like default.aspx, the data binding is relatively straightforward. First, we need to get a reference to the current Board. We do this by requesting the board name from the query string and initializing the board using it. Next, we set a label’s text property to the name of the board, so the user knows what board he’s in. Then we create a DataSet, add a table to it, and add all the required columns. Afterward, we iterate through the Board’s child threads and create an object array to hold the necessary fields to add to the DataSet. Finally, we add all the rows to the DataSet and force the Repeater control to DataBind. Take a look at Figure 13.55 to see what the completed page looks like.
The last piece to browsing the message board is to see individual Posts themselves. Just like Boards and Threads, displaying this data is accomplished by using a Repeater control and a DataSet. Let’s take a look at the important quasi-HTML and the code-behind in Figures 13.56 and 13.57.
Again, this code is nearly identical to that of the last two pages we’ve dealt with. The only real difference is that one of the fields is actually building a short HTML string. This is because the repeater can’t handle if statements. So, in order to hide or show users’ e-mail addresses depending on whether the viewer is logged in or not, we need to build a string instead of directly inserting the value. If the user is logged in, then the anchor tag for the poster’s e-mail address is built; otherwise, an empty string is used.
Registered users (Members) get a special set of functions they can access, like creating threads and posts, editing their profile, and editing the messages they’ve posted. A guest (that is, an unregistered user) is limited to a very small set of functionalities—specifically, viewing the threads and messages (see Figure 13.58).
The next step in building our application’s User Interface is to allow a registered user to modify his or her member profile. This includes first name, last name, password, and e-mail address. Let’s take a look at the profile.aspx page in Figure 13.59.
The profile page contains text boxes for every field in the User object, except for the user ID and username. These two fields are read only, and should never be changed. Like the register page, this page contains a number of Validation controls with their display value set to none, and a ValidationSummary control added to the page to aggregate all the errors a user might receive while inputting information. When this page first loads, it should default all fields (except for passwords) with their existing values, so a user does not have to type everything over, just change the fields he or she wants to change. Upon clicking the Update Profile button, the user’s details should be updated and the user given a message explaining that their profile was updated. Let’s take a look at the implementation of these features in Figure 13.60.
Updating the user profile is rather easy. First, the Page_Load method checks to make sure there is a valid, logged in user. If not, it redirects the user back to default.aspx. If the user is logged in and the page has not posted back to itself yet, then it sets the values of the text boxes to the existing values of the current user object. Afterward, it applies the styles to the page and exits.
When the Update button is clicked, the btnUpdate_Click method is called. The subroutine first checks to make sure all the validation controls have returned valid results. If not, it exits the subroutine. If they have returned valid results, it first checks to see if the user entered a new password, and if so, sets the current user object’s password to what the user entered. Next, each of the User objects’ fields are set to what the user entered, then the User object is updated to the database. Finally, the message label indicating that the profile was updated successfully is displayed.
The last thing to do for registered users is generate a page for them to create new threads and posts. In order to get to this page, let’s take a look at board.aspx and thread.aspx again. We need to add a LinkButton to each one. When clicked, that link button needs to redirect the user to createpost.aspx. See Figures 13.61 and 13.62.
The function of these buttons is almost the same. The first one redirects the user to createpost.aspx?boardName=[The selected Board], and the second redirects the user to createpost.aspx?boardName=[The selected Board]&threadId=[The selected Thread]. The same page handles the creation of new Threads and Posts, so if you are creating a new Post, you just pass in the ThreadID along with the board name. If you are creating a brand new Thread, you just pass in the board name. Let’s take a look at createpost.aspx to see what controls are on that page. See Figure 13.63.
The create post page contains the necessary controls to accept user input and create a new thread and/or post. The other controls on the page are a ValidationSummary, two RequiredFieldValidators, and a Panel that contains the current Thread information. Obviously, if the user is creating a new Thread and Post, the Thread panel will not be visible, whereas, if the user is creating a new Post inside a Thread, the Thread panel will be visible and display the appropriate Thread subject. Let’s take a look at the code necessary to initialize this form in Figure 13.64.
First, what we do is verify that there is a logged in user. If there isn’t, we redirect the user back to the default page. If the user is valid, we get a reference to the current board and if the ThreadID was passed in, we get a reference to the appropriate Thread as well. Finally, if the page hasn’t posted back to itself and we have a current Thread, we default the text box and label values with the Thread’s subject. All that’s left is to take a look at the code that actually creates Posts and Threads, as shown in Figure 13.65.
What happens in this bit of code is that we first check to make sure the page is valid. If not, we do nothing; otherwise, we attempt to create the Thread and/or Post. If the ThreadID is currently “0” (that is, no ThreadID was given to the page), then we create a new Thread and set the private mThread variable to the new Thread (remember that when adding a new Thread, since Threads are ordered by their ThreadID field, new Threads appear at the top of the ThreadList). Lastly, we create a new Post from the current Thread object and redirect the user to the thread.aspx page to view the new and/or updated Thread.
Administrators need to do a few things that other people can’t. First, they need the ability to delete anything—boards, threads, and posts. They also need the ability to edit any post, and modify any user’s admin or banned status. Let’s take a look at the useradmin.aspx screen in Figure 13.66.
This page allows administrators to promote other users to administrator status, and ban problematic users from logging into the site. First, we have a DropDownList control that we will DataBind to a DataSet. There is also a LinkButton that will show the admin panel at the bottom once we’ve selected a user to administer. The two radio button lists will be used to display and set the current admin/banned status of the selected user. Finally, when the user clicks the Modify User button, the current user will be updated with the new banned and admin values the administrator entered. Let’s first take a look at the code necessary to set up the form in Figure 13.67.
The first thing this method does is guarantee that there is a logged in user, and that the currently logged in user is an administrator. If either of these is not true, it sends the user back to default.aspx. Next, it makes sure the page has not posted back to itself; since there’s no need to DataBind a drop-down list every time the page is executed, as ASP.NET will handle that for us. If the page has not posted back to itself, it builds a SQL statement to retrieve the UserIDs and Usernames from the Users table in the database. It then gets a DataSet from the dotBoardObjects.DataControl class, and dynamically binds the DropDownList to the DataSet. Finally, it applies the styles to this page and exits.
The next thing we need to do is get the ability to select a user from the drop-down list, and have the page load that user’s information. The click event handler for the Choose User link handles this. Let’s take a look at the code for it in Figure 13.68.
This gets the user ID from the DropDownList’s SelectedItem. Value property, and creates a new user object from it. Next, the appropriate radio buttons are selected depending on whether or not the user is banned or is an admin. Finally, the admin panel and the two radio button lists are set to visible so they will appear when the page refreshes. Next, we need to handle when the administrator clicks the Modify User button and update the selected user based on what the administrator entered in. See Figure 13.69 for the code involved.
Just like before, the first thing we do is get a reference to the selected User object. The next step is to determine which radio buttons were selected, and set the IsAdmin and IsBanned properties accordingly. The last step is to update the selected user by calling its Update method. Now you can promote other users to be administrators or ban them from entering your site again. If a banned user attempts to log on, they will receive an error explaining to that their account was banned. You may be wondering why we don’t just delete the banned user. We don’t do this because the Thread and Post tables are dependent on the User table, and deleting a User from the User table would not be allowed due to the relationships involved.
The other thing that administrators can do is create and delete boards, delete threads, and delete posts. Let’s start with creating a board. The first step involved in this is adding a new LinkButton to the user area user control. This button will be named “lnkaCreateBoard” and will have its text property set to Create New Board. Once clicked, it should redirect the user to createboard.aspx. Let’s take a look at that code in Figure 13.70.
Now that we have the administrator going to the create board page, let’s take a look at that page. See Figure 13.71.
Like all our other pages that accept user input, this page has controls on it for every piece of information we need to perform the task at hand. Also, like the other pages, there is a validation control for every text box to make sure the user enters the required information. Let’s take a look at the code-behind for this form in Figure 13.72.
Like every other admin page so far, this page guarantees that the current user is a logged-in administrator, and if not, redirects to the default page. After the user has entered the required information to create a board and clicks the Create Board button, the btnCreate_Click method is called. First, the method checks to make sure the page is valid, then it creates the board based on the values the administrator entered. Finally, it redirects the administrator back to the default page so he can see his newly created board.
The last things an administrator should be able to do are delete Boards, Threads, and Posts. This functionality can be placed on the appropriate pages where this information is actually displayed. What we will do is next to every Board, Thread, and Post we will place an HtmlAnchor control next to each item that will point to an .aspx page named delete[type of object to delete].aspx. For instance, deleting Boards will link to deleteBoard.aspx. Let’s go over the three places in our code that need to change because of this new feature in Figures 13.73, 13.74, and 13.75.
You can see that all of these changes is very similar. Each gets slightly more complicated as you get further down the object hierarchy; you need to pass more information to get a reference to the correct objects. Now all we need to do is create the three pages that will handle deleting our objects. All three are very similar, and are shown in the following figures, Figures 13.76, 13.77, and 13.78.
A lot of code, for sure, but it should all be relatively easy to follow. Each page retrieves the objects necessary to delete whatever it is trying to delete, then calls the appropriate delete method on the board object. When it finishes, each one redirects the user back to the default page. If the person accessing this page is neither logged in nor an admin, it does nothing but the final redirect. You don’t want anyone who is not an admin deleting your boards, so even on pages in which the user never sees the UI, it’s still a good idea to perform every security check necessary.
The final administrative interface we need to create is to give the Administrators the ability to edit posts, in the case of offensive or undesired language that doesn’t necessarily need to be deleted. First, we’ll need to add another button to the view thread page right next to the Delete button. See Figure 13.79 for the changes.
All that has changed is a new HTML anchor tag is added that points to a new page called editPost.aspx. Let’s take a look at this page and examine what controls are on it (see Figure 13.80).
You should notice that this page looks very similar to the create post page. In fact, it is nearly identical—so identical that we could have reused the same page instead of creating the new one. The only reason we aren’t using the create post page is for the sake of simplicity; there’s no need to complicate pages we have already finished for new functionality. All we need to do now is take a look at the code-behind page in Figure 13.81.
You should immediately notice how similar the code-behind of the edit post page is to create post page. Again, we could have used the same page, but to keep things simple we’re using two separate pages. The Page_Load method first checks to make sure there is a logged in user, and that the user is an administrator. Next, it gets a reference to the appropriate Board, Thread, and Post objects, and fills the label and text box controls on the page with values. The btnEditPost_Click method makes sure the page is valid, then sets the values on the Post object, commits it to the database, and redirects to the thread view page so the user can see the changes.
Our message board is 100 percent complete and ready for use. We have analyzed our message board and created a solution to fit with all our requirements. Our message board is an Object-Oriented application that is scalable, maintainable, and well-defined. We have created all the necessary classes to maintain our data and the relationships between our data through the use of custom list objects and classes. We also have a built-in security model where every action that requires administrative access is checked before the requestor is allowed to perform the operation.
Our User Interface is somewhat extensible in that it dynamically applies styles to multiple types of WebControls that we defined using CSS and an XML document. Each Web Form we created inherits the FormBase class, which allows all our Web Forms to have access to a few common methods and properties, in addition to the System. Web.UI.Page methods and properties. Our User Interface contains all the necessary interfaces to browse through Boards, Threads, and Messages, as well as interfaces to administer users, and those that contain interfaces to create and delete Boards, Threads, and Messages.
All in all we have a functioning message board that could be placed anywhere and run on top of SQL Server or MS Access. It was accomplished in an Object-Oriented manner and hopefully by now you understand the use for designing OO applications. We have also separated the UI and UI logic from the actual “business rules” applied to our objects. If we wanted, we could take our dotBoardObject class library and put a Windows Form front end on it, a Web Service front end on it, or even attach a Console Application front end. All because we kept our UI completely separate from our implementation.
Analyze your data and create the tables necessary to represent the solution to our problem. Make sure you have broken down each piece of data into the smallest possible representation of that data. For instance, you wouldn’t want to have a field in your database for the user’s full name; instead, you would want first and last name fields.
Analyze our solution and determine the types of methods each of our objects will contain. You need to provide interfaces to modify, add, and delete every relationship and field in each of your objects.
Frequently Asked Questions
The following Frequently Asked Questions, answered by the authors of this book, are designed to both measure your understanding of the concepts presented in this chapter and to assist you with real-life implementation of these concepts. To have your questions about this chapter answered by the author, browse to www.syngress.com/solutions and click on the “Ask the Author” form.
A: Absolutely not, although when applications are designed in an OO manner, they are typically more scalable and maintainable, and allow for the use of multiple User Interfaces. You are not forced to create applications in an OO manner, but good programming practices typically stress Object Orientation.
A: Yes, typically the OO approach adds a bit of overhead to everything you do. For instance, the creation of the custom DataSet in order to view Boards, Threads, and Posts spends extra time that wouldn’t have been lost if you had gone directly to the database instead of accessing the data through objects. The price of scalability and maintainability is a possible performance loss. Luckily, with .NET, execution is very fast after the initial compile, so it’s also very likely that you would never notice the speed loss.
A: Very important. In ASP 3.0 and 2.0 (heck, even ASP 1.0), all validation had to be done by hand. Empty fields needed to be validated as well as e-mail addresses and URIs. With Validation controls, ASP.NET does all of this for us, allowing us to focus more on the logic and business rules in our application.
A: First, you would need to create a table in your database to store the list of IP addresses, and provide a way for an administrator to enter an IP address into it. Then, at every page you want to disallow this list of IP addresses from viewing, compare the IP address of the requesting user and compare it to the list of IP addresses you have banned. If it exists in your list of banned addresses, redirect them to another page or do whatever else you feel is appropriate.