- Terminology
- Why Was the Online Community Built the Way It Was?
- The Overall Logical Structure
- The Database
- The Modules
- How the Code Files Are Organized
- Search
- Security
- Moving On
How the Code Files Are Organized
We're going to start looking at the code very soon now! Just before we do, it is worth taking a look at the folder structure the application uses. This will help you find the files you want to read in more detail or the code you need to change to get a particular result.
The root Community folder should be in your inetpub/wwwroot folder (or wherever you installed it, if you chose to put it in a different location). In it, you will find the folder structure shown in Figure 3.4.
Figure 3.4 The online community application folder structure.
The Bin folder is where the compiled code that ASP.NET actually uses to run the site is stored. We don't need to mess with it directly, apart from when we install third-party add-ons to the community that come in compiled form. This folder already contains files for the community itself, the persistence service, and the additional Microsoft WebControls, one of which is used in the application.
The Data folder is where we store any data that might be transient. For example, the ImageGallery module uses it to store the image files for uploaded images. The Data folder has a subfolder for modules, which in turn has a subfolder for each module that needs to store data.
We could have stored the image data directly in the database as binary columns, but storing them in the Data folder makes linking to them much easier. (We can simply point the user's browser at them rather than extract each image from the database every time someone views it.)
The Global folder is where we keep the code that is not specific to a particular module. The Global folder itself contains the core business service class (CoreModule.vb), which is probably the most-used class in the entire application, along with some other classes we will look at later. The Controls folder contains ASP.NET controls that are used throughout the application, along with base classes that module-specific controls inherit from. The PersistentObjects folder contains the core persistent objects we discussed earlier. Finally, the Search subfolder contains two classes that are used for managing search results.
The Images folder contains image files that are used by the system itself (for example, the site logo). It is important to distinguish between the images stored in this folder and the transient images that are stored in the Data folder. This folder is for images that are used by the online community system itself, whereas the Data/Modules/ImageGallery folder contains images uploaded by members.
The Modules folder contains all of the code that implements the modules. Each module has its own subfolder, within which are some classes and controls. Each module also has a Views subfolder to store the controls that implement its views.
The OJB.NET folder contains the XML files that define how the object relational mapper should convert between objects and relational data and vice versa.
Now that we have taken an overview of the application from top to bottom, let's start digging into the code to see how it works.
Code Tour:Default.aspx
It makes sense to start our exploration of the code with the file that is loaded when we first visit the application. This is default.aspx. As we will discover, this file actually handles most of the requests for the application, so understanding what it does is very important.
Start by opening the Community project in VS.NET. Double-click default.aspx in the Solution Explorer to see the design view for the file, as shown in Figure 3.5.
You can immediately see that the page consists of a simple table structure with user controls for standard page elements, such as the header, navigation, and so on. The other thing you have probably noticed is that there is a big gap in the center where the content should be. Clearly, the content is being added by the code.
If you click the HTML view for default.aspx, you will see what you would expecta simple table structure with a sprinkling of user controls. Find the table cell that forms the content area in the center. It looks like this:
<td id="sectionItems" vAlign="top" width="400" runat="server"> </td>
Figure 3.5 The design view of default.aspx.
The cell is, as expected, empty, but some things to note are that it has an ID ("sectionItems") and it is set as a server-side control (runat="server"). This is a sure sign that it is being manipulated from the code-behind file (default.aspx.vb).
Let's open default.aspx.vb and see what it does.
There are nearly 200 lines of code in default.aspx.vb, so an overview of what it contains is shown in Figure 3.6.
default.aspx.vb has a pretty simple taskit analyzes the URL query string for the request and loads appropriate controls into the content area of the page. The key to this is the Page_Load event handler:
Private Sub Page_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles MyBase.Load
Page_Load does not actually do anything itselfafter it has determined what needs to be done, it calls the appropriate method. The code from the methods could have simply been put within Page_Load, but it would be very difficult to make out what is going on.
So what is Page_Load doing?
Figure 3.6 Code map: default. aspx.vb.
First, it creates an instance of the core business service module and stores it in a member of the class (_coreModule) so that it can be accessed by any of the other methods.
_coreModule = New CoreModule()
TIP
Throughout the code for the online community application, private members of classes are prefixed with an underscore to identify them clearly.
Next, it checks whether there is a Member parameter in the query string. If there is, a member ID has been specified, so the code to display a member's details is called.
'check whether a member has been specified If Not Request.QueryString("Member") Is Nothing Then 'we need to display a members page DisplayMemberPage()
If there was no member ID, we next check whether a module has been specified:
Else 'check whether a module has been specified If Not Request.QueryString("Module") Is Nothing Then
If there is a module ID, we make a call to the core module service we created earlier to get a CommunityModule persistent object that represents the specified module:
'get the module from the ID Dim moduleToDisplay As CommunityModule = _CoreModule.GetModuleByID(Request.QueryString("Module"))
We now want to know whether we are displaying the global module, a particular module instance, or a single module item (such as a particular news report). We first check whether an item has been specified:
If Not Request.QueryString("Item") Is Nothing Then 'an item has been specified so we should display it DisplayItem(moduleToDisplay)
If there is no item ID, we check for an instance ID:
Else If Not Request.QueryString("Instance") Is Nothing Then DisplayModuleInstance()
If there was no item ID or instance ID, we assume that the user wants us to display the global instance:
Else 'no instance specified so we should display the global module DisplayGlobalModule(moduleToDisplay) End If End If
If there was no module ID, we check whether there are any search terms in the URL:
Else If Not Request.QueryString("Search") Is Nothing Then 'display search results DisplaySearchResults()
Finally, if we have found nothing else, our default case is to display a section:
Else DisplaySection() End If End If End If End Sub
In the case of the first request to the application, with no URL parameters, it is the DisplaySection() method that will be called. Let's follow the execution into that method and see what happens there:
Private Sub DisplaySection()
The first thing we want to do is determine whether a sectionID has been specified at all. If no ID was specified, we need to get the default home page section ID that is stored in the Web.config. (If you open Web.config, you will find the configuration setting in the AppSettings section.)
'check whether a sectionID has been specified 'otherwise, set sectionID to the configured homepage section Dim sectionID As Integer If Request.QueryString("Section") Is Nothing Then sectionID = ConfigurationSettings.AppSettings("HomePageSectionID") Else sectionID = Request.QueryString("Section") End If
Now that we have a section ID, we want to get a Section persistent object, which will contain all of the details we need to display the section. This is done with a call to the core module service class:
'get the Section object for the selected section Dim theSection As Section = _CoreModule.GetSectionByID(sectionID)
It is possible that this call will not return a section (if the ID does not match any sections), so we need to check whether we got a Section object back:
'check that we have a section object If Not theSection Is Nothing Then
Next, we check whether the section belongs to a member by checking its IsGlobal property. If the section does belong to a member (that is, it is not global), we tell the navigation control the ID of the member in question (so that the navigation control can show the other sections owned by the member):
If theSection.IsGlobal = False Then Navigation1.MemberID = theSection.Member.PrimaryKey1 End If
This is the first real use of a persistent object we have seen. In fact, we are using two persistent objects here. First, we access the IsGlobal property of a Section object (theSection). Then, if it does belong to a member, we access the Member property, which is a Member object. We then take the value of the PrimaryKey1 property from the Member object to pass to the navigation control. PrimaryKey1 is the integer identifier for the member.
Each persistent object has a PrimaryKey1 property, which is unique within that type of persistent object. This property is not provided by default by the persistence service, which uses an array of integers for the primary key to support multifield primary keys. However, because the online community application uses only a single primary key value, all of the persistence objects in it derive from CommunityPO, which was created to provide the PrimaryKey1 property. The property simply accesses the primary key array and returns the first element.
We didn't have to specifically tell the system to retrieve the member from the databasewe simply accessed the Member property and the persistence service did the rest.
Now, we want to get down to the business of displaying the section on the page. This basically involves displaying each of the section items that are in the section. Therefore, we loop through all the SectionItem objects in the SectionItems collection of the Section object:
'loop through the SectionItems of the Section object Dim si As SectionItem Dim currentControl As SectionItemControl For Each si In theSection.SectionItems
Again, notice how we do not explicitly request the SectionItem objectswe simply access the property, and the persistence service does the rest.
Now comes the complicated part. Each section item will relate to a specific module view that needs to be displayed on the page. The module views are user controls that are held within the Views folder of their modules. We can, therefore, access the correct module view control for each section item and load it with a call to Page.LoadControl:
'Create a control of the correct type for each SectionItem and add to 'the page currentControl = CType(Page.LoadControl("Modules/" +
si.ModuleView.CommunityModule.Name _ + "/views/" + si.ModuleView.Name + ".ascx"), SectionItemControl)
The module view control will not be able to do very much if it does not know which section item it is displaying, so we set its SectionItem property to the current section item:
currentControl.SectionItem = si
Finally, we add the control to the content area of the page. (Remember the table cell that has the ID "sectionItems" and was set to "runat=server"?)
sectionItems.Controls.Add(currentControl) Next
By looping through all the section items in the section, the code will add them all to the page, in the order in which they are stored in the SectionItems collection.
End If End Sub
The other Display... methods do very similar jobs to DisplaySection, so there seems little point in going through the code for all of them in detail. However, it would be worth taking some time at this point to have a read through the code. It is all commented with explanations of what is going on and, if you get stuck on anything, you can always ask me questions at this book's forum.
In the code we have looked at so far from default.aspx.vb, we saw several calls to the core business service class (CoreModule). Let's make that class our next stop. It's the foundation on which most of the application rests, so the earlier you understand what it does, the better.
Code Tour:CoreModule.vb
You will find CoreModule.vb in the Community/Global folder. If you open it in Visual Studio .NET, you will find that it is an even longer code file than the one we have already looked at. A summary of its contents is shown in Figure 3.7.
As you can see from the code map, the class consists mainly of methods for dealing with the various core persistent objects. There are different numbers of methods for different objects, depending on what operations are required for each. CoreModule also includes a method for retrieving search results and a property for returning its name ("Core").
Why does CoreModule need to provide the name property? Actually, it is not really required for the core module, but we have to include it because it is a requirement of the ModuleBase class, which CoreModule inherits from. The Name property is important for all of the other module classes because it provides the link between the code and the data about the module that is represented by a persistent object and ultimately stored in the database.
In our tour of default.aspx.vb, we saw several calls to an instance of CoreModule. One of these was to retrieve a particular Section object:
Dim theSection As Section = _CoreModule.GetSectionByID(sectionID)
Figure 3.7 Code map: CoreModule.vb.
This is an example of one of the most common uses of CoreModuleretrieving a persistent object based on an ID. Let's scroll down to the GetSectionByID method and see how it achieves this:
Public Function GetSectionByID(ByVal pID As Integer) As Section Return QueryFacade.FindObject(Type.GetType("Community.Section"),
New Integer() {pID}) End Function
QueryFacade is the class we use to access the persistence service. It only provides static methods, so we do not create an instance of it.
In this case, we are using one of several overloads of the FindObject method. This one takes a Type object for the type of object we want to find and an array of integers that specifies the primary key of the object we want.
There are several other overloads of FindObject. All of them take a Type object as their first parameter, but they differ in their second parameter:
FindObject(Type,String())Used for string-based primary keys.
FindObject(Type,ArrayList)Enables us to use an array list, rather than an array, for the primary keys.
FindObject(Type,Criteria)Accepts an OJB.NET Criteria object that specifies how to select an object. We will be looking more at Criteria objects later in this chapter.
FindObject(Type,String)Accepts a SQL query, which should be used to select the object. We will use this later in the book.
Therefore, we specify the type we want (using the Type.GetType method to get a Type object for the class we want) and the primary key. The persistence service then retrieves the object so that we can return it. If the object was in the persistence service cache, no database access will be required to do this. If it is not in the cache, the persistence service will query the database to get the data for the object.
Let's now look at what we do when we want to get several objects from the persistence service. Scroll down to the GetGlobalSections method, which we use for getting all of the sections that are not owned by a member and are not the home page. (We use this in the navigation control.)
Public Function GetGlobalSections() As IList Dim crit As Criteria = New Criteria crit.AddEqualTo("_MemberID", 0) crit.AddNotEqualTo("_primaryKey", _ ConfigurationSettings.AppSettings("HomePageSectionID")) Return QueryFacade.Find(SectionType, crit) End Function
This is somewhat more complicated than the GetSectionByID method. Overall, what is happening is that we create a Criteria object to specify how we want to select objects and then we call QueryFacade.Find to run the query.
Let's step through line by line and look at what is being done.
First, we create a new Criteria object.
Next, we add two subcriteria to the Criteria. Criteria objects are like a tree composed of other Criteria objects. In this case, our main Criteria has two branchesone that specifies that the _MemberID of the selected objects must be 0 (sections that have no member have their _MemberID field set to 0) and one that specifies that selected objects must not have a primary key equal to the home page ID (which we retrieve from the configuration file).
Note that we specify the field names by the names of the private fields from the persistent object classes. (Open Global/CoreObjects/Section.vb if you want to see the private fields the Section persistent object has.) Remember that in this application, all private fields are prefixed by an underscore.
Our code does not have any information about the structure of the database (table names, column names, and so forth) in itthe persistence service repository, which we will be looking at later in this chapter, holds the information required to link persistent object classes with database tables.
After we have the Criteria object set up how we want it, we make a call to QueryFacade.Find(Type, Criteria) .
The Find method is very similar to the FindObject method, except that it returns a list of objects rather than a single object.
There are only two overloads of FindObjectthe one we are using here that takes a Criteria object as its second parameter and an overload that takes a SQL query string as its second parameter. It does not make sense to try to find groups of objects by a primary key.
We have seen how we find persistent objects that are stored by the persistence service, but how do they get there in the first place?
Scroll down to the CreateSection method:
Public Function CreateSection(ByRef pMember As Member, _ ByVal pName As String, _ ByVal pDescription As String) _ As Section Return New Section(pMember, pName, pDescription) End Function
This might look ridiculously simple considering that this is how new Section objects are persisted to the database. All we do is create a new Section object and return it! How does it get to the database?
This is another case of the persistence service doing a lot of work behind the scenes for us. As mentioned earlier, all calls to CoreModule methods take place in a transaction. When the end of the transaction is reached, the persistence service writes changes made to persistent objects to the database.
Therefore, when the CreateSection method completes and the Section object is returned, the transaction completes and the persistence service writes the new Section object to the database, using the same mapping as is used to retrieve data to link class fields to database columns.
Next, let's look at the method we use for making changes to an existing section:
Public Function UpdateSection(ByVal pID As Integer, _ ByVal pName As String, _ ByVal pDescription As String) Dim section As Section = GetSectionByID(pID) section.Name = pName section.Description = pDescription End Function
We first use the GetSectionByID method to get the section we want to update.
Then, we set the properties we want to change.
That's it. Again, the persistence service does the work for us. When the transaction completes, the changes to the Section object we have altered are persisted to the database.
The final kind of operation is the deletion of objects. Again, this is made very simple by the persistence service:
Public Function DeleteSection(ByVal pSection As Section) pSection.Delete() End Function
All we have to do is call the Delete method of the persistent object to mark the object for deletion.
At this stage, it would be a good idea to browse through CoreModule.vb and take a look at the various methods it provides. None of them are very complicated, and they all have comments that explain what they do. They are used throughout the application, so it is very useful to have an idea of their functions.
Let's now take a look at the Section class itself to see what features it has that enable the persistence service to do its job.
Open Global/CoreObjects/Section.vb and take a look at the code.
The first thing to notice is that the class derives from the CommunityPO class. We use the CommunityPO class as a base for all of our persistent objects so that we can add extra facilities to what the persistence service provides in its standard base classes.
Imports Ojb.Net.facade.Persist Public Class Section Inherits CommunityPO
Then, we have declarations of the private fields of the class:
Private _MemberID As Integer Private _Name As String Private _Description As String Private _Member As Member = Nothing Private _SectionItems As IList = Nothing
Notice that there are two fields related to the member that owns the sectionMemberID, which is an integer, and _Member, which is a Member object. The persistence service will automatically populate the _Member field with the correct Member object.
Next, there is a special constructor, which is called the reconstructor:
Public Sub New(ByVal ID As Integer, _ ByVal pMemberID As Integer, _ ByVal pName As String, _ ByVal pDescription As String) MyBase.New(ID) _MemberID = pMemberID _Name = pName _Description = pDescription End Sub
We should not use this constructor to create new instances of Sectionit exists for the use of the persistence service only. The key feature here is the call to MyBase.New(Integer). It is this call that ensures that the persistent object is retrieved from the persistence service rather than created as a new Section.
Notice that we do not populate the _Member field in this reconstructorthe persistence service will do that for us.
We also do not need to populate the _SectionItems propertythe persistence service will retrieve the correct SectionItem objects when they are needed.
After the reconstructor comes the standard constructor:
Public Sub New(ByVal pMember As Member, _ ByVal pName As String, _ ByVal pDescription As String) _Member = pMember _Name = pName _Description = pDescription _SectionItems = New ArrayList End Sub
This time, we do not make a call to the constructor of the base class and we populate the _Member field rather than the _MemberID fieldthe persistence service will populate the _MemberID field for us.
In this instance, we have to initialize the _SectionItems field to an empty ArrayList.
Next up is a ReadOnly property to provide code that uses the Section object with access to the member that owns the section:
Public ReadOnly Property Member() As Member Get Return _Member End Get End Property
The SectionItems property, which is also read-only, is very similar.
The code for read/write properties is slightly different, though. Here is the property that enables us to get or set the name of the section:
<Mutator()> _ Public Property Name() As String Get Return _Name End Get Set(ByVal Value As String) _Name = Value End Set End Property
This property (and all other properties and methods that make changes to the persistent object's data) are decorated with the Mutator attribute. It is this attribute that lets the persistence service know when it needs to update the object to the database. If we don't include it, changes will not be persisted at the end of the transaction.
Therefore, each persistent object is a class that represents a particular aspect of the application data that includes a reconstructor, a constructor, and mutator attributes on properties and methods that cause changes to the data of the object.
Before we took a diversion to look at the persistent object code, we were following the code that loads the section items for a particular section and draws them onto the page.
The process we have seen is as follows:
default.aspx is loaded.
The Page_Load event determines that there are not URL parameters, pulls the home page section ID from the Web.config, and calls DisplaySection().
DisplaySection calls CoreModule.GetSectionByID to get the Section object that represents the home page section.
DisplaySection loops through all the SectionItem objects in the section, creates an appropriate view control for each, and adds them to the page.
But what happens next? What do those view controls do?
It makes sense to take a look at an example of one of these view controls at this stage. We will use the DisplayLatestHeadlines view provided by the News module.
Code Tour:DisplayLatestHeadlines.ascx
You will find the DisplayLatestHeadlines.ascx in the Community/Modules/News/Views folder. If you double-click it in the Solution Explorer, you will see that it has a very simple user interface.
It consists of three elementsa SectionItemHeader user control, a Repeater (shown as a series of "data-bound" text items), and a SectionItemFooter user control.
The two user controls provide standard headers and footers for module views, helping us maintain consistency across the community and also saving us the time of designing headers and footers for each view.
We could have implemented the header and footer by rendering the HTML for them at a higher level (most likely in default.aspx.vb when we added the view controls to the page), but that would force us to use the header and footer for every single view. It is possible that we might want to build a view that does not use the standard header and footer, so it is more flexible to allow each view to use the standard controls if we choose to.
The Repeater is what does the work of displaying the headlines. Let's switch to the HTML view and see what it does:
<asp:Repeater id="Repeater1" runat="server"> <ItemTemplate> <div> <a class="SummaryItemText" href="default.aspx?Module= <%#Container.DataItem.ModuleInstance.CommunityModule.PrimaryKey1%> &Item=<%#Container.DataItem.PrimaryKey1%>"> <%#Container.DataItem.Title%> </a> </div> </ItemTemplate> </asp:Repeater>
You can see that the Repeater will create a set of <div> elements that each contain a link. Both the link itself and the text inside the link are created from the DataSource of the Repeater.
To fully understand what is happening, we need to know what that DataSource is, so let's take a look in the code-behind file (DisplayLatestHeadlines.aspx.vb).
Here, we find a single event handler, Page_Load:
Private Sub Page_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles MyBase.Load
The first thing it does is create an IList object and also an instance of NewsModule, the business service class for the News module.
Dim newsItems As IList Dim newsMod As NewsModule
Next, it checks whether it is displaying a global module instance. It does this with the IsGlobal method it inherits from SectionItemControl.
If it is global, it calls NewsModule.GetLatestGlobalNewsItems. If it is not, it calls newsMod.GetLatestNewsItems. In either case, the results are stored in the newsItems object that was declared earlier.
If Me.IsGlobal = True Then newsItems = NewsModule.GetLatestGlobalNewsItems(10) Else newsMod = New NewsModule(Me.SectionItem.ModuleInstance) newsItems = newsMod.GetLatestNewsItems(10) End If
Finally, it makes the newsItems object the DataSource for the Repeater and DataBinds it.
Repeater1.DataSource = newsItems Repeater1.DataBind() End Sub
It seems likely, then, that the DataSource for the Repeater is a collection of objects that represents news items. To be sure, however, we need to follow the code into the News module business service class (NewsModule.vb).
Code Tour:NewsModule.vb
You will find NewsModule.vb in Community/Modules/News. If you open it, you will see a set of methods that looks similar to those we saw in CoreModule.vb, as shown in Figure 3.8.
NewsModule performs a very similar role to CoreModule, but it only deals with the requirements of the News module.
The methods we are interested in are GetLatestGlobalNewsItems and GetLatestNewsItems. They are similar, so let's look at GetLatestGlobalNewsItems because it is nearer the top of the file.
Public Shared Function GetLatestGlobalNewsItems(ByVal number As Integer)
As IList
The first part of the method should be familiarit follows the standard sequence for requesting objects from the persistence service:
Dim crit As Criteria = New Criteria() crit.addEqualTo("_ModuleInstance._ShowInGlobal", True) crit.addOrderBy("_DatePosted", False)
We tell the criteria to accept only objects whose module instances are set to display globally. Also note that an OrderBy requirement was added to the criteria to instruct the persistence service to order the results by the _DatePosted field.
As expected, the next step is to request a set of objects from the persistence service:
Dim newsItems As IList =
QueryFacade.Find(Type.GetType("Community.NewsItem"), crit)
Figure 3.8 Code map: NewsModule.vb.
Here, we have the answer to our question about what type of object is returnedit is Community.NewsItem. This is a persistent object we have not seen yet. It represents a single news item and is used only by the News module.
The next part of the method creates a new collection (an ArrayList was chosen for its efficiency) and pulls off as many news items from the list returned by the broker as requested in the number parameter:
Dim trimmedNewsItems As ArrayList = New ArrayList() Dim i As Integer = 0 While i < number And i < newsItems.Count trimmedNewsItems.Add(newsItems(i)) i = i + 1 End While
The trimmed list of NewsItem objects is then returned:
Return trimmedNewsItems End Function
(If you are thinking that this is an inefficient way to go about getting a certain number of items from the database, well done! We will be looking at how we can improve this later in the book.)
Now we know that the News module returns a list of NewsItem objects to DisplayLatestHeadlines.ascx.vb and this is used as the DataSource for the Repeater that displays the headlines. Let's look again at the code for the Repeater:
<asp:Repeater id="Repeater1" runat="server"> <ItemTemplate> <div>
First, it creates a link of the form
default.asx?Module=[moduleID]&Item=[ItemID] <a class="SummaryItemText" href="default.aspx?Module= <%#Container.DataItem.ModuleInstance.CommunityModule.PrimaryKey1%> &Item=<%#Container.DataItem.PrimaryKey1%>">
The moduleID comes from
Container.DataItem.ModuleInstance.CommunityModule.PrimaryKey1
and the ItemId comes from
Container.DataItem.PrimaryKey1
We know that Container.DataItem is going to refer to a NewsItem object (as the DataSource is a list of NewsItem objects), so what we are doing here is accessing properties of a NewsItem object.
We get the title of the news item in a similar way:
<%#Container.DataItem.Title%> </a> </div> </ItemTemplate> </asp:Repeater>
This shows that we can access the properties of our persistent objects directly from our data-binding definitions, provided we use a list of persistent objects of the same type as the DataSource for the data-bound control.
This is something that you will see throughout the online community applicationwe use a list of persistent objects as a DataSource and then access their properties in the data-binding definitions.