Maximizing.ASP.dot.NET.Real.World.ObjectOriented.Development [Electronic resources] نسخه متنی

اینجــــا یک کتابخانه دیجیتالی است

با بیش از 100000 منبع الکترونیکی رایگان به زبان فارسی ، عربی و انگلیسی

Maximizing.ASP.dot.NET.Real.World.ObjectOriented.Development [Electronic resources] - نسخه متنی

Jeffrey Putz

| نمايش فراداده ، افزودن یک نقد و بررسی
افزودن به کتابخانه شخصی
ارسال به دوستان
جستجو در متن کتاب
بیشتر
تنظیمات قلم

فونت

اندازه قلم

+ - پیش فرض

حالت نمایش

روز نیمروز شب
جستجو در لغت نامه
بیشتر
لیست موضوعات
توضیحات
افزودن یادداشت جدید






Case Study: The POP Forums Architecture


The POP Forums application, which is a Web forum app for ASP.NET (http://www.popforums.com), has an architecture that divides up responsibilities into a number of different layers that interact with the ASP.NET architecture and SQL Server. Figure 4.2 shows how these parts talk to each other.

Figure 4.2. The POP Forums architecture.

The forum architecture is simple to understand, and makes altering the application easy. Writing a new data access layer for a Microsoft Access database would require only changing the one layer. Getting data in and out of the database from the UI is a matter of creating objects from the business rule classes. Creating a new post in the forum, for example, requires the creation of a Post object, populating its properties, and calling its Create() method. If you were tasked with creating a user interface, you wouldn't have to know anything about the database or the plumbing.

This application also takes advantage of the architecture provided in ASP.NET. Starting in ASP.NET v2.0, a provider model enables you to replace the data plumbing built into the framework with your own. The benefit for developers who use your code is that they can still use the well-known interfaces in the framework and not worry about the implementation underneath. In this case, the application has its own Membership provider. When the calling code manipulates ASP.NET's Membership class, the POP Forums provider handles the data plumbing. The developer doesn't care that it happens to be stored in the forum database instead of being used in the .NET Framework's built-in SQL Express database. We'll take a closer look at this provider model in Chapter 11, "Membership and Security."

Let's look at some code samples from each of the layers in the forum application. We'll start at the data end. We have a table in our SQL Server database called Posts. Not surprisingly, this is where we keep all of the thoughtful compositions made by the members of our forum. There isn't anything unusual about this table, and it has a typical primary key called PostID and a foreign key called TopicID, which tells us the topic in the Topics table with which the post is associated.

Let's concentrate on the insertion of new data into the Posts table. The first task is creating the code to do the dirty work of opening a connection, creating a command object, and executing the command, as in Listing 4.1. The listing includes the namespace and class declarations.

Listing 4.1. The simple act of inserting data into the database

C#

[View full width]


namespace PopForums.Data
{
public class SqlClient : IPopForumsData
{
public int CreateNewPost(int TopicID, DateTime PostTime,
string Name, int PeopleID, string IP, bool FirstInTopic,
string FullText, bool ShowSig)
{
SqlConnection connection = new SqlConnection(ConfigurationSettings
.AppSettings["PopForumsDbConnect"]);
connection.Open();
string sql = "INSERT INTO pf_Posts (TopicID, PostTime, Name, "
+ "PeopleID, IP, FirstInTopic, FullText, ShowSig) VALUES ("
+ "@TopicID, @PostTime, @Name, @PeopleID, @IP, @FirstInTopic, "
+ "@FullText, @ShowSig)";
SqlCommand command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@TopicID",TopicID);
command.Parameters.AddWithValue("@PostTime",PostTime);
command.Parameters.AddWithValue("@Name",Name);
command.Parameters.AddWithValue("@PeopleID",PeopleID);
command.Parameters.AddWithValue("@IP",IP);
command.Parameters.AddWithValue("@FirstInTopic",FirstInTopic);
command.Parameters.AddWithValue("@FullText",FullText);
command.Parameters.AddWithValue("@ShowSig",ShowSig);
command.ExecuteNonQuery();
command.CommandText = "SELECT @@IDENTITY";
int postID = Convert.ToInt32(command.ExecuteScalar());
connection.Close();
return postID;
}
... more methods
}
}

VB.NET

[View full width]


Namespace PopForums.Data
Public Class SqlClient
Implements IPopForumsData
Public Function CreateNewPost(TopicID As Integer, _
PostTime As DateTime, Name As String, PeopleID As Integer, _
IP As String, FirstInTopic As Boolean, FullText As String, _
ShowSig As Boolean) As Integer
Dim connection As New _ SqlConnection(ConfigurationSettings.AppSettings
("PopForumsDbConnect"))
connection.Open()
Dim sql As String = "INSERT INTO pf_Posts (TopicID, PostTime, "_
& "Name, PeopleID, IP, FirstInTopic, FullText, ShowSig) "_
& " VALUES (@TopicID,@PostTime,@Name,@PeopleID,@IP, "_
& "@FirstInTopic,@FullText,@ShowSig)"
Dim command As New SqlCommand(sql, connection)
command.Parameters.AddWithValue("@TopicID", TopicID)
command.Parameters.AddWithValue("@PostTime", PostTime)
command.Parameters.AddWithValue("@Name", Name)
command.Parameters.AddWithValue("@PeopleID", PeopleID)
command.Parameters.AddWithValue("@IP", IP)
command.Parameters.AddWithValue("@FirstInTopic", FirstInTopic)
command.Parameters.AddWithValue("@FullText", FullText)
command.Parameters.AddWithValue("@ShowSig", ShowSig)
command.ExecuteNonQuery()
command.CommandText = "SELECT @@IDENTITY"
Dim postID As Integer = _ Convert.ToInt32(command.ExecuteScalar())
connection.Close()
Return postID
End Function
... more methods
End Class
End Namespace


The AddWithValue() method of the ParameterCollection is new to v2.0 of .NET. If you're using a previous version, you can use the same syntax with the Add() method (though it has been marked as obsolete in v2.0). The v2.0 compiler will warn you if you use the obsolete Add() method.Chapter 2, "Classes: The Code Behind the Objects," an interface is little more than a signature that says what members a class must have. Listing 4.2 shows a little piece of our interface.

Listing 4.2. The IPopForumsData interface

C#


namespace PopForums.Data
{
public interface IPopForumsData
{
int CreateNewPost(int TopicID, DateTime PostTime,
string Name, int PeopleID, string IP,
bool FirstInTopic, string FullText, bool ShowSig);
...more member definitions
}
}

VB.NET


Namespace PopForums.Data
Public Interface IPopForumsData
Function CreateNewPost(TopicID As Integer, _
PostTime As DateTime, Name As String, _
PeopleID As Integer, IP As String, _
FirstInTopic As Boolean, FullText As String, _
ShowSig As Boolean) As Integer
... more member definitions
End Interface
End Namespace

Our interface doesn't have any actual code in it. Its sole purpose is to let us know how our classes that implement it should look.

That's great, but our business rules classes are going to need to hit all these methods in our SqlClient class. To do this, we'll create a class that loads our data access class and caches it, according to values in our web.config file. A single static method called Methods() in the ClientLoader class will do this, as shown in Listing 4.3.


This is a technique I blatantly used from the original forums on Microsoft's www.asp.net site. Don't worry if you don't understand what's going on in this code. In a nutshell, the method uses two keys from the config file indicating the assembly name and the class inside of it that we want to use for data access. It is then cached because loading a class in this manner on every single data call would be an expensive process and would slow the application down. The Methods() method returns an instance of the class with all of the methods for the data access. The idea is that we could just as well have it load a class meant to hit Access instead of SQL Server, provided the class also implements IPopForumsData.

Listing 4.3. Loading our data access layer

C#

[View full width]


namespace PopForums.Data
{
public class ClientLoader
{
public static IPopForumsData Methods()
{
Cache cache = HttpContext.Current.Cache;
if (cache["IPopForumsData"] == null)
{
if ((ConfigurationSettings.AppSettings["PopForumsDataClass"] == null) ||
(ConfigurationSettings.AppSettings["PopForumsDataDll"] == null))
// no data layer specified, use the internal one
cache.Insert("IPopForumsData", typeof(PopForums.Data.Provider).Module.Assembly
.GetType("PopForums.Data
SqlClient").GetConstructor(new Type[0]));
else
{
// user has specified an external data layer
string assemblyPath = "~\\bin\\" + ConfigurationSettings
.AppSettings["PopForumsDataDll"];
string className = ConfigurationSettings.AppSettings["PopForumsDataClass"];
cache.Insert("IPopForumsData", Assembly.LoadFrom(assemblyPath).GetType
(className).GetConstructor(new Type[0]));
}
}
return (IPopForumsData)( ((ConstructorInfo)cache["IPopForumsData"]).Invoke(null) );
}
}
}

VB.NET

[View full width]


Namespace PopForums.Data
Public Class ClientLoader
Public Shared Function Methods() As IPopForumsData
Dim cache As Cache = HttpContext.Current.Cache
If cache("IPopForumsData") Is Nothing Then
If ConfigurationSettings.AppSettings("PopForumsDataClass") Is Nothing Or
ConfigurationSettings.AppSettings("PopForumsDataDll") Is Nothing Then
' no data layer specified, use the internal one
cache.Insert("IPopForumsData", GetType(PopForums.Data.Provider).Module
.Assembly.GetType("PopForums.Data.SqlClient").GetConstructor(New Type(0) {}))
Else
' user has specified an external data layer
Dim assemblyPath As String = "~\bin\" + ConfigurationSettings.AppSettings
("PopForumsDataDll")
Dim className As String = ConfigurationSettings.AppSettings
("PopForumsDataClass")
cache.Insert("IPopForumsData", [Assembly].LoadFrom(assemblyPath).GetType
(className).GetConstructor(New Type(0) {}))
End If
End If
Return CType(CType(cache("IPopForumsData"), ConstructorInfo).Invoke(Nothing),
IPopForumsData)
End Function
End Class
End Namespace

The important thing to understand about the class in Listing 4.3 is that it loads an instance of the class we choose, which happens to implement IPopForumsData. If one isn't specified, it loads the default, our SqlClient class (for the purpose of this project, the SqlClient class is compiled in the same assembly as the rest of this code). The result is that we can call any method in our SqlClient class by creating an instance of it via the ClientLoader class and its Methods() method:


IPopForumsData data = PopForums.Data.ClientLoader.Methods();

As you can see, you can actually create an object whose type is an interface! Then you might see that we can call our CreateNewPost() method like this:


data.CreateNewPost( [parameters here] );

The IPopForumsData interface, the ClientLoader class, and the SqlClient class collectively make up our data access layer. This code is solely responsible for getting data in and out of the SQL Server database. To pipe the data in and out of another data store, we need only write a class that implements IPopForumsData and set values in our web.config file to load that class instead.

Now that we've found a way to isolate all our data access methods, we can create business rules. We'll create a class called Post and give it properties that match each of the fields in our database table. We'll also give it a constructor to create a new instance of a Post object, as well as methods to Create(), Update(), and Delete() data.


Our example class in Chapter 5, "Object-Oriented Programming Applied: A Custom Data Class," does something similar to this, only it makes database calls directly instead of calling the methods from a class dedicated to data access. That example combines the data access layer with the business rules.

To add a new post entry to the database, we want our class to call the method from our data access layer and pass in the values as parameters. The Create() method of the Post class can perform any calculations, caching, or validation that we require and then execute the method from the data access layer, as in Listing 4.4.

Listing 4.4. The Create() method of the Post class

C#

[View full width]


public int Create()
{
Cache cache = HttpContext.Current.Cache;
if (cache["pftopic" + _TopicID.ToString()] != null) cache.Remove("pftopic" + _TopicID
.ToString());
if (cache["pfpeopleposts" + _PeopleID.ToString()] != null) cache.Remove("pfpeopleposts"
+ _PeopleID.ToString());
IPopForumsData data = PopForums.Data.ClientLoader.Methods();
int postID = data.CreateNewPost(_TopicID,_PostTime,_Name,_PeopleID,_IP,_FirstInTopic
,_FullText,_ShowSig);
_PostID = postID;
Topic objTopic = new Topic(_TopicID);
objTopic.Update(); // this also calls the forum's update, which will refresh lastpostime
return postID;
}

VB.NET

[View full width]


Dim cache As Cache = HttpContext.Current.Cache
If Not (cache(("pftopic" + _TopicID.ToString())) Is Nothing) Then
cache.Remove(("pftopic" + _TopicID.ToString()))
End If
If Not (cache(("pfpeopleposts" + _PeopleID.ToString())) Is Nothing)
Then
cache.Remove(("pfpeopleposts" + _PeopleID.ToString()))
End If
Dim data As IPopForumsData = PopForums.Data.ClientLoader.Methods()
Dim postID As Integer = data.CreateNewPost(_TopicID, _PostTime, _Name, _PeopleID, _IP,
_FirstInTopic, _FullText, _ShowSig)
_PostID = postID
Dim objTopic As New Topic(_TopicID)
objTopic.Update() ' this also calls the forum's update, which will refresh lastpostime
Return postID
End Function

This method does several things that fit squarely in the business rules layer of our application that we wouldn't want to leave to the user interface or the data access layer. You can see that it removes two cache entriesone that stores data on the parent topic and one that stores data on the user making the post. Those cached values are invalidated because the topic has changed, as well as information about the user with regards to the posts they've made. After that has been taken care of, it creates an instance of our data access class and calls the method to create a new post. The parameters that begin with an underscore are the private members of the class that correspond to properties, as we saw in Chapter 2 in Listing 2.7. Finally, the method creates a Topic object (another business rule class) and causes it to update its data, which forces the topic to calculate a new "last post" time.

It's not that critical that you understand all of those action items in the method, but it is important that you understand why they happen in this discrete business rules layer of the application. It isn't important to know how the data is stored or how the UI gets data into the class. Only the rules that deal with the data and how it's handled or calculated are important.

This gets us to what I call the user interface "glue." Despite having a great many declarative controls, ASP.NET still needs a bit of code to make the user interface work with your business rules layer. This glue code can appear in any of three places: in the page itself in a <script> block, in a partial class (unofficially known as "code-beside"), or in a class that the page inherits (known as "code-behind," the default place for code in versions of Visual Studio prior to 2005). Where you put this code is a matter of preference, but if you put it in the .aspx page file, some people may consider this an application design fault because it doesn't create that separation many programmers require. I put this code in with the rest of the page for convenience and to make it accessible for people not using Visual Studio. Considering you can't change the .aspx page without impacting the code-behind, or vice-versa, I don't feel that this separation is entirely necessary.


Partial classes are new as of v2.0 of .NET. In ASP.NET, they're merged on first-run with pages to form one complete class. We'll go into more detail in Chapter 6, "The Nuts and Bolts of IIS and Web Application."

Continuing up the chain, the code in this layer is ridiculously straightforward, and you could probably understand it even if you didn't know anything about the rest of the application. Listing 4.5 shows how we simply create an instance of the Post class, assign some values to its properties, and then call its Create() method.

Listing 4.5. Using the Post class in our user interface glue

C#


Post post = new Post();
post.TopicID = topic.TopicID;
post.PostTime = DateTime.UtcNow;
post.Name = user.Username;
post.PeopleID = (int)user.UserId;
post.IP = Request.UserHostAddress;
post.FirstInTopic = false;
post.FullText = ReplyBox.Text;
post.ShowSig = ShowSigCheckBox.Checked;
post.Create();

VB.NET


Dim post As New Post()
post.TopicID = topic.TopicID
post.PostTime = DateTime.UtcNow
post.Name = user.Username
post.PeopleID = CInt(user.UserId)
post.IP = Request.UserHostAddress
post.FirstInTopic = False
post.FullText = ReplyBox.Text
post.ShowSig = ShowSigCheckBox.Checked
post.Create()

Again, the important point to consider with this code is that it's very single-minded. Remember all of the things that the Create() method does with caching and updating the parent topic? This code doesn't need to worry about any of that. Its sole purpose is acquiring what's in the controls and the request and passing that to the business rules layer. Even further removed, it doesn't need to worry about the data store or how data is written to it.

That leaves only the actual user interface layer. This final layer consists only of the HTML that defines the page and its controls. It doesn't need to know anything about how to feed data into the business rules.

We've traversed five layers in the application, starting at the database itself and working our way back to the user interface. Each layer does its own thing, and it only has to know how to interact with the layers above and below it. The implementation in the adjacent layers is not important.

This architecture is friendly to developers who have certain areas of expertise. I'm not much of an Oracle programmer, but a friend or co- worker that has guru status could jump in there and make a great Oracle data access layer to work with the rest of the application. I'm also not much of a designer, but another person could create a killer user interface and have no problem using the business rules classes to process the data.


/ 146