The previous sections briefly describe five common and simple types of design pattern. These are all applicable to ASP.NET, and reasonably easy to implement. In fact, ASP.NET and the .NET Framework automatically implement several of them. Figure 5 shows schematically how these patterns relate to ASP.NET applications; you will see descriptions of their implementation or use in the subsequent sections of this article.
The ASP.NET code-behind technology, which uses partial classes, provides a natural implementation of the MVP pattern. The code-behind file (the Presenter) contains all the logic and processing code, and populates the page (the View). Event handlers within the code-behind file handle events raised by controls, or by the page itself, to perform actions when a postback to the server occurs. To complete this pattern, the code-behind file can use a separate data access layer or component (the Model) to read from, write to, and expose the source data - usually accessed through built-in providers that are part of the .NET Framework.
The sample application uses the code-behind model throughout, thereby implementing the MVP pattern on each page. The default page (Default.aspx) demonstrates the ASP.NET application of the MVP pattern by displaying a View that contains a series of controls that allow users to execute any of a range of functions that fetch and display data (see Figure 6).
Figure 6 - Finding a customer name in the Default page of the sample application
Clicking one of the buttons causes a postback to the server and raises the event that corresponds to that button. For example, the interface component for the second button shown in Figure 6 is an ASP.NET Button control, declared within the View (the ASPX page), which initiates an event handler named btnCustModelClick in the Presenter (the code-behind file) to handle the Click event:
<asp:Button ID="btn_CustModel" runat="server" Text=" "
OnClick="btn_CustModel_Click" />
Get name for customer
<asp:Button ID="btn_CustModel" runat="server" Text=" "
OnClick="btn_CustModel_Click" />
Get name for customer
<asp:TextBox ID="txtID_CustModel" runat="server" Text="ALFKI" Columns="3" />
from CustomerModel
In the Presenter (the code-behind file) is the declaration of the btnCustModelClick method, which creates an instance of the Model (a class named CustomerModel.cs in the App_Code folder) and calls one of its methods to get the name of the customer identified by the customer ID in the text box next to that button:
protected void btn_CustModel_Click(object sender, EventArgs e)
// display the name of the specified customer
{
try
{
// use method of the CustomerModel class
CustomerModel customers = new CustomerModel();
Label1.Text += "Customer Name from CustomerModel class: ";
Label1.Text += customers.GetCustomerName(txtID_CustModel.Text);
}
catch (Exception ex)
{
Label1.Text += "PAGE ERROR: " + ex.Message;
}
Label1.Text += "<p />";
}
You can view the contents of the files Default.aspx (the View), Default.aspx.cs (the Presenter), and CustomerModel.cs (the Model) that implement the MVP pattern in this example to see all of the UI control declarations and code they contain. As you saw in Figure 6, there are buttons to execute a range of methods of the CustomerModel class, as well as methods of the Repository, Web Reference, and Service Agent implementations discussed in the following sections.
Notice that the Default.aspx page (in Figure 6) contains a drop-down list where you can select a page or view you want to see, loaded using an implementation of the Front Controller pattern. You will see details of this in the section "Implementation of the Front Controller Pattern" later in the next article. You can use this drop-down list to navigate to other pages to see the examples, for example the Use Case Controller example page or the Publish-Subscribe example page.
Implementation of the Provider Pattern
ASP.NET uses the Provider pattern in a number of places. These include providers for the Membership and Role Management system (such as the SqlMembershipProvider and SqlRoleProvider), the Site Navigation system (the XmlSiteMapProvider), and the user profile management system (such as the SqlProfileProvider). The data access components in ASP.NET, such as SqlDataSource, also rely on providers. These providers are part of the .NET Framework data-access namespaces, including SqlClient, OleDb, Odbc, and OracleClient. Each provider is configured in the Web.config file, with the default settings configured in the server's root Web.config file. Developers can create their own providers based on an interface that defines the requirements, or by inheriting from a base class containing common functionality for that type of provider. For example, developers can extend the DataSourceControl class to create their own data source controls that interface with a non-supported or custom data store.
Implementation of the Adapter Pattern
ASP.NET uses adapters to generate the output from server controls. By default, server controls use adapters such as the WebControlAdapter or DataBoundControlAdapter to generate the markup (such as
HTML) and other content output by the control. Each adapter provides methods such as Render and CreateChildControls that the server controls call to create the appropriate output. Alternative adapters are available, for example the
CSS Friendly Control Adapters that provide more flexibility for customizing the rendered HTML. Developers can also create their own adapters to provide custom output from the built-in controls, and from their own custom controls.
Implementation of the Service Agent and Proxy Patterns
The Proxy and Broker patterns provide a natural technique for connecting to and using remote services without forcing the use of O/S or application-specific protocols such as DCOM. In particular, they are ideal for use with
Web Services. In
Visual Studio, developers add a Web Reference to a project, which automatically collects the web service description (including a WSDL contract file). This lets the code generate a suitable proxy class that exposes the methods of the remote service to code in the application.
Code in the application can then use the remote service by creating an instance of the proxy class and calling the exposed methods defined in the contract. This is the code used in the sample application to call the CustomerName method of the DemoService service:
protected void btn_WSProxy_Click(object sender, EventArgs e)
// display name of specified customer using the Web Service
{
try
{
// get details from Web Service
// use 'using' construct to ensure disposal after use
using (LocalDemoService.Service svc = new LocalDemoService.Service())
{
Label1.Text += "Customer name from Web Service: "
+ svc.CustomerName(txtID_WSProxy.Text);
}
}
catch (Exception ex)
{
Label1.Text += "PAGE ERROR: " + ex.Message;
}
Label1.Text += "<p />";
}
One issue with using a Web Service is the difficulty in raising exceptions that the client can handle. The usual approach is to return a specific value that indicates the occurrence of an exception or failure. Therefore, one useful feature that a Service Agent, which wraps the Web Reference to add extra functionality, can offer is detecting exceptions by examining the returned value and raising a local exception to the calling code.
The following code, from the ServiceAgent.cs class in the sample application, contains a constructor that instantiates the local proxy for the service and stores a reference to it. Then the GetCustomerName method can call the CustomerName method of the Web Service and examine the returned value. If it starts with the text "ERROR:" the code raises an exception that the calling routine can handle.
The GetCustomerName method also performs additional processing on the value submitted to it. The Web Service only matches on a complete customer ID (five characters in the example), and does not automatically support partial matches. The GetCustomerName method in the Service Agent checks for partial customer ID values and adds the wildcard character so that the service will return a match on this partial value:
// wraps the Web Reference and calls to Web Service
// to perform auxiliary processing
public class ServiceAgent
{
LocalDemoService.Service svc = null;
public ServiceAgent()
{
// create instance of remote Web service
try
{
svc = new LocalDemoService.Service();
}
catch (Exception ex)
{
throw new Exception("Cannot create instance of remote service", ex);
}
}
public String GetCustomerName(String custID)
{
// add '*' to customer ID if required
if (custID.Length < 5)
{
custID = String.Concat(custID, "*");
}
// call Web Service and raise a local exception
// if an error occurs (i.e. when the returned
// value string starts with "ERROR:")
String custName = svc.CustomerName(custID);
if (custName.Substring(0,6) == "ERROR:")
{
throw new Exception("Cannot retrieve customer name - " + custName);
}
return custName;
}
}
To use this simple example of a Service Agent, code in the Default.aspx page of the sample application instantiates the Service Agent class and calls the GetCustomerName method. It also traps any exception that the Service Agent might generate, displaying the message from the InnerException generated by the agent.
protected void btn_WSAgent_Click(object sender, EventArgs e)
// display name of specified customer using the Service Agent
// extra processing in Agent allows for match on partial ID
{
try
{
// get details from Service Agent
ServiceAgent agent = new ServiceAgent();
Label1.Text += "Customer name from Service Agent: "
+ agent.GetCustomerName(txtID_WSAgent.Text);
}
catch (Exception ex)
{
Label1.Text += "PAGE ERROR: " + ex.Message + "<br />";
if (ex.InnerException != null)
{
Label1.Text += "INNER EXCEPTION: " + ex.InnerException.Message;
}
}
Label1.Text += "<p />";
}
Implementation of the Repository Pattern
A data repository that implements the Repository pattern can provide dependency-free access to data of any type. For example, developers might create a class that reads user information from Active Directory, and exposes it as a series of User objects - each having properties such as Alias, EmailAddress, and PhoneNumber. The repository may also provide features to iterate over the contents, find individual users, and manipulate the contents.
Third-party tools are available to help developers build repositories, such as the CodeSmith tools library (see
http://www.codesmithtools.com/). Visual Studio includes the tools to generate a typed DataSet that can expose values as properties, rather than as data rows, and this can form the basis for building a repository.
As a simple example, the sample application uses a typed DataSet (CustomerRepository.xsd in the App_Code folder), which is populated from the Customers table in the Northwind database. The DataSet Designer in Visual Studio automatically implements the Fill and GetData methods within the class, and exposes objects for each customer (CustomersRow) and for the whole set of customers (CustomersDataTable).
The class CustomerRepositoryModel.cs in the App_Code folder exposes data from the typed DataSet, such as the GetCustomerList method that returns a populated CustomersDataTable instance, and the GetCustomerName method that takes a customer ID and returns that customer's name as a String:
public class CustomerRepositoryModel
{
public CustomerRepository.CustomersDataTable GetCustomerList()
{
// get details from CustomerRepository typed DataSet
try
{
CustomersTableAdapter adapter = new CustomersTableAdapter();
return adapter.GetData();
}
catch (Exception ex)
{
throw new Exception("ERROR: Cannot access typed DataSet", ex);
}
}
public String GetCustomerName(String custID)
{
// get details from CustomerRepository typed DataSet
try
{
if (custID.Length < 5)
{
custID = String.Concat(custID, "*");
}
CustomerRepository.CustomersDataTable customers = GetCustomerList();
// select row(s) for this customer ID
CustomerRepository.CustomersRow[] rows
= (CustomerRepository.CustomersRow[])customers.Select(
"CustomerID LIKE '" + custID + "'");
// return the value of the CompanyName property
return rows[0].CompanyName;
}
catch (Exception ex)
{
throw new Exception("ERROR: Cannot access typed DataSet", ex);
}
}
}
Implementation of the Singleton Pattern
The sample application uses the Front Controller pattern to allow users to specify the required View using a short and memorable name. The Front Controller, described in detail in a later section, relies on an
XML file that contains the mappings between the memorable name and the actual URL for this view. To expose this XML document content to the Front Controller code, the application uses a Singleton instance of the class named TransferUrlList.cs (in the App_Code folder).
This approach provides good performance, because the class is loaded at all times and the Front Controller just has to call a static method to get a reference to the single instance, and call the method that translates the memorable name into a URL.
To implement the Singleton pattern, the class contains a private default constructor (a constructor that takes no parameters), which prevents the compiler from adding a default public constructor. This also prevents any classes or code from calling the constructor to create an instance.
Note that this implementation does not perform locking while it creates the instance. In reality, in this specific scenario, if a second thread should create a second instance it will not affect operation because the content is read-only and is the same for all instances. If you absolutely need to confirm that only one instance can ever exist, you should apply locks while checking for and creating the instance. For more details, see
Implementing the Singleton Pattern in C# and
Data & Object Factory Singleton Pattern
The TransferUrlList class also contains a static method that returns the single instance, creating and populating it from the XML file if there is no current instance. The class uses static local variables to store a reference to the instance, and - in this example - to hold a StringDictionary containing the list of URLs loaded from the XML file:
// Singleton class to expose a list of URLs for
// Front Controller to use to transfer to when
// special strings occur in requested URL
public class TransferUrlList
{
private static TransferUrlList instance = null;
private static StringDictionary urls;
private TransferUrlList()
// prevent code using the default constructor by making it private
{ }
public static TransferUrlList GetInstance()
// public static method to return single instance of class
{
// see if instance already created
if (instance == null)
{
// create the instance
instance = new TransferUrlList();
urls = new StringDictionary();
String xmlFilePath = HttpContext.Current.Request.MapPath(
@"~/xmldata/TransferURLs.xml");
// read list of transfer URLs from XML file
try
{
using (XmlReader reader = XmlReader.Create(xmlFilePath))
{
while (reader.Read())
{
if (reader.LocalName == "item")
{
// populate StringDictionary
urls.Add(reader.GetAttribute("name"), reader.GetAttribute("url"));
}
}
}
}
catch (Exception ex)
{
throw new Exception("Cannot load URL transfer list", ex);
}
}
return instance;
}
...
}
A StringDictionary provides good performance, as the method that searches for a matching name and URL in the list can use the ContainsKey method, and then access the matching value using an indexer. If there is no match, the code returns the original value without translating it. This listing shows the GetTransferUrl method that performs the URL translation:
public String GetTransferUrl(String urlPathName)
// public method to return URL for a specified name
// returns original URL if not found
{
if (urls.ContainsKey(urlPathName))
{
return urls[urlPathName];
}
else
{
return urlPathName;
}
}
To use the TransferUrlList Singleton class, code in the Front Controller just has to get a reference to the instance, and then call the GetTransferUrl method with the URL to translate as the value of the single parameter:
...
// get Singleton list of transfer URLs
TransferUrlList urlList = TransferUrlList.GetInstance();
// see if target value matches a transfer URL
// by querying the list of transfer URLs
// method returns the original value if no match
String transferTo = urlList.GetTransferUrl(reqTarget);
...