Introduction
If you are an experienced Microsoft ASP.NET Web Forms developer and you have been keeping up with the latest news and blogs from the Microsoft community, then you have probably heard about the Microsoft ASP.NET MVC Framework just recently released to the web.
One of the promises of the MVC Framework, among other value-added features, is that it will provide a stronger separation between the different layers of a web application. The MVC Framework does this by completely separating the web page (called “The View” in MVC terminology) from the code-behind file (now called “The Controller”). The back-end business and data layers separate into the “The Model”, hence the name MVC. This enhanced separation of concerns improves and facilitates automated Unit Testing of ASP.NET web applications.
As I have been anticipating the just released version 1.0 of the MVC Framework, and looking forward to learning and using this new framework, an idea came to mind for incorporating the current and very popular ASP.NET 2.0 Web Forms framework. My idea was to try and simulate the MVC design pattern into a current ASP.NET Web Forms application.
Sample Application
The sample application in this article is derived from my previous CodeProject article, “Developing and Testing a WPF Application Using Routed UI Commands and MbUnit”. In that article, I separated the different layers of a WPF desktop application and Unit Tested it using the MbUnit Framework automated testing tool.
This sample application is a simple ASP.NET 2.0 Web Forms application that allows the user to enter, update, and save patient information. The information entered will pass through the different layers of a typical n-tier web application, and the data will be saved into a Microsoft SQL-Server Express 2005 database.
For this application, I created several separate projects and included them into a single Visual Studio 2008 solution. The first project I created was the myMVCWebsite
ASP.NET web project, which will contain and run the PatientEntry.aspx web form.
After laying out and designing the Patient Entry web form, I immediately proceeded to create a separate myMVCController
class project. Basically, my goal was to take all the logic that would have gone into the code-behind file, PatientEntry.aspx.cs, and move it into the myMVCController
class. This way, later on, I could Unit Test the controller class logic with a testing framework.
Moving the logic out of the code-behind file required the passing of the Page
object of the ASP.NET web form to the controller class. The ASP.NET runtime engine builds the page object through the initial phases of the page life cycle, prior to executing the code-behind code for the web form. Once the code-behind code executes, the page object will already contain all the ASP.NET web controls that were defined in the PatientEntry.aspx file.
When the Page_Load
event fires in the code-behind for PatientEntry.aspx, I create the controller object and pass the page object through the controller’s constructor. Each button click event on the form will call a method in the controller. The code-behind file basically becomes a pass-through mechanism to the controller.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using myMvcController;
public partial class _PatientEntry : System.Web.UI.Page
{
myMvcController.Controller _controller;
protected void Page_Load(object sender, EventArgs e)
{
_controller = new myMvcController.Controller(this);
if (Page.IsPostBack == false)
_controller.Initial_Page_Load();
}
protected void btnSave_Click(object sender, EventArgs e)
{
_controller.btnSave_Click();
}
protected void txtPatientID_TextChanged(object sender, EventArgs e)
{
_controller.GetPatientInformation();
}
protected void btnReset_Click(object sender, EventArgs e)
{
_controller.btnReset_Click();
}
}
As you can see in the code-behind file above, there is very little code. Except for the Page.IsPostBack
statement, all the code will be handed off to the controller. I had to keep the Page.IsPostBack
statement in the code-behind file because it is a read-only property and can only be set by the ASP.NET runtime and not by the code.
Other items that should remain in the code-behind file include references to the ASP.NET intrinsic objects such as Request
, Response
, Server
, Application
, and Session
. If the web application needs to access these objects, then they should be passed to the controller as parameters when executing methods in the controller. This way, the controller can be tested without a web server, since these objects may not be available when testing the controller in a separate testing tool.
The Controller
Now, it’s on to the controller class. I created the controller class in a separate project called myMVCController
. I did this so I could test the controller in a testing tool outside of the ASP.NET runtime.
In addition, I could also perform these tests without the need for a web server. As stated before, the controller class gets instantiated from the Patient Entry code-behind, and the page object gets passed into the controller through the controller’s constructor.
The key piece of code required in the controller was accessing the objects inside the web form. Since all the web form objects are part of the page object that was passed into the controller, all that was needed was to call the FindControl
method of the page object. Doing this allowed the controller to access and update all the labels and textboxes on the web form.
private void LoadControls()
{
_txtPatientID = (TextBox)_page.FindControl("txtPatientID");
_txtFirstName = (TextBox)_page.FindControl("txtFirstName");
_txtLastName = (TextBox)_page.FindControl("txtLastName");
_txtAddressLine1 = (TextBox)_page.FindControl("txtAddressLine1");
_txtAddressLine2 = (TextBox)_page.FindControl("txtAddressLine2");
_txtCity = (TextBox)_page.FindControl("txtCity");
_txtState = (TextBox)_page.FindControl("txtState");
_txtZipCode = (TextBox)_page.FindControl("txtZipCode");
_txtDateOfBirth = (TextBox)_page.FindControl("txtDateOfBirth");
_lblMessage = (Label)_page.FindControl("lblMessage");
_txtPatientIdentity = (HiddenField)_page.FindControl("txtPatientIdentity");
}
After creating the textbox objects, the controller then loads the patient information into a patient object and simply passes the information off to the business tier (often called the “The Model” in the MVC world).
public void btnSave_Click()
{
long patientIdentity;
string validationMessage;
bool returnStatus;
long patientIdentity;
string validationMessage;
bool returnStatus;
LoadControls();
myMvcPatient.Patient patient = new myMvcPatient.Patient();
patient.PatientIdentity = Convert.ToInt64(_txtPatientIdentity.Value);
patient.PatientID = _txtPatientID.Text.ToUpper();
patient.FirstName = _txtFirstName.Text.ToUpper();
patient.LastName = _txtLastName.Text.ToUpper();
patient.AddressLine1 = _txtAddressLine1.Text.ToUpper();
patient.AddressLine2 = _txtAddressLine2.Text.ToUpper();
patient.City = _txtCity.Text.ToUpper();
patient.State = _txtState.Text.ToUpper();
patient.ZipCode = _txtZipCode.Text.ToUpper();
patient.DateOfBirth = _txtDateOfBirth.Text.ToUpper();
myMvcBusinessLayer.BusinessLayer business =
new myMvcBusinessLayer.BusinessLayer();
_lblMessage.Visible = true;
returnStatus = business.SavePatient(patient, out patientIdentity,
out validationMessage);
if (returnStatus == true)
{
_lblMessage.Text = "Patient Successfully Added";
}
else
{
_lblMessage.Text = validationMessage;
}
_txtPatientIdentity.Value = patientIdentity.ToString();
}
The Business and Data Tiers - The Model
The business and data tiers were also created in separate Visual Studio projects, myMVCBusinessLayer
and myMVCDataLayer
, respectively. Doing this allows me to test these layers separately and independently from the rest of the application.
Unit Testing with Gallio and MbUnit
In the real world, Unit Testing would be performed in parallel with development (and if you were following test driven development practices, you would be creating your Unit Tests before developing your application code). However, since this is a demonstration, I will discuss my Unit Testing approach for this sample application here.
The first step to testing my web application was to determine how it should be tested. Searching the Internet, I found many ways people were testing their web applications. Some people were testing their application by directly invoking the web page itself through several different testing tools by simulating click events etc. After spinning my wheels and consuming a lot of research time, I came to a conclusion. I decided to keep it simple, and to create some web form objects programmatically in a Unit-Testing tool.
To Unit Test this application, I downloaded the Gallio Automation Platform from http://www.gallio.org. The Gallio Automation Platform is an open, extensible, and neutral system for .NET that provides a common object model, runtime services, and tools (such as test runners) that may be leveraged by any number of test frameworks.
For my tests, I’ll be using MbUnit v3.0.5, which is the latest version of MbUnit, and comes packaged with Gallio.
To begin Unit Testing, I created a class project in Visual Studio, and called the project myMVCUnitTesting
and started coding my Unit Tests. The initial code I needed to write was to create an HtmlForm
object that contains all objects that exist on the PatientEntry
web form that needed to be tested.
private HtmlForm GenerateHtmlForm()
{
HtmlForm form = new HtmlForm();
form.ID = "myTest";
_txtAddressLine1 = new CustomTextBox();
_txtAddressLine1.ID = "txtAddressLine1";
_txtAddressLine2 = new CustomTextBox();
_txtAddressLine2.ID = "txtAddressLine2";
_txtCity = new CustomTextBox();
_txtCity.ID = "txtCity";
_txtDateOfBirth = new CustomTextBox();
_txtDateOfBirth.ID = "txtDateOfBirth";
_txtFirstName = new CustomTextBox();
_txtFirstName.ID = "txtFirstName";
_txtLastName = new CustomTextBox();
_txtLastName.ID = "txtLastName";
_txtPatientID = new CustomTextBox();
_txtPatientID.ID = "txtPatientID";
_txtPatientIdentity = new HiddenField();
_txtPatientIdentity.ID = "txtPatientIdentity";
_txtState = new CustomTextBox();
_txtState.ID = "txtState";
_txtZipCode = new CustomTextBox();
_txtZipCode.ID = "txtZipCode";
_lblMessage = new Label();
_lblMessage.ID = "lblMessage";
form.Controls.Add(_txtAddressLine1);
form.Controls.Add(_txtAddressLine2);
form.Controls.Add(_txtCity);
form.Controls.Add(_txtState);
form.Controls.Add(_txtZipCode);
form.Controls.Add(_txtDateOfBirth);
form.Controls.Add(_txtFirstName);
form.Controls.Add(_txtLastName);
form.Controls.Add(_txtPatientID);
form.Controls.Add(_txtPatientIdentity);
form.Controls.Add(_lblMessage);
return form;
}
As you may have noticed, in the above example, I created the text boxes as CustomTextBox
objects. Initially, I created these controls as regular textbox controls, but when I started testing the controller class, the Focus
methods would fail when the controller tried setting focus to a textbox. This occurred because the Focus
method is only supported when running inside the ASP.NET runtime engine.
The solution to solving this problem was to create my own custom textbox object within my test project, inherit from the standard TextBox
control, and override and suppress the Focus
method all together. I did not need to test for focus, as this is more of a visual test.
public class CustomTextBox : TextBox
{
public override void Focus()
{
}
}
To test the web form’s main functionality (which is to create a patient record), I created a test method called Create_A_Patient
. In this method, I create a page object, generated my HtmlForm
, and added the form to the page object. The page object is passed to the controller upon creation of the controller object. The Unit Test then proceeds to find the controls in the page object, and then populates the controls with patient information.
The Unit Test then calls the controller’s btnSave_Click()
method and executes MbUnit’s Assert
method to validate that the test was successful.
[Test]
public void Create_A_Patient()
{
long patientIdentity;
Page testPage = new Page();
testPage.Controls.Add(GenerateHtmlForm());
myMvcController.Controller controller =
new myMvcController.Controller(testPage);
_lblMessage = (Label)testPage.FindControl("lblMessage");
_lblMessage.Text = "";
_txtPatientIdentity =
(HiddenField)testPage.FindControl("txtPatientIdentity");
_txtPatientID = (TextBox)testPage.FindControl("txtPatientID");
_txtLastName = (TextBox)testPage.FindControl("txtLastName");
_txtFirstName = (TextBox)testPage.FindControl("txtFirstName");
_txtDateOfBirth = (TextBox)testPage.FindControl("txtDateOfBirth");
_txtAddressLine1 = (TextBox)testPage.FindControl("txtAddressLine1");
_txtAddressLine2 = (TextBox)testPage.FindControl("txtAddressLine2");
_txtCity = (TextBox)testPage.FindControl("txtCity");
_txtState = (TextBox)testPage.FindControl("txtState");
_txtZipCode = (TextBox)testPage.FindControl("txtZipCode");
_txtPatientIdentity.Value = "";
_txtPatientID.Text = "MSCEO";
_txtLastName.Text = "Gates III";
_txtFirstName.Text = "William Henry";
_txtDateOfBirth.Text = "10/28/1955";
_txtAddressLine1.Text = "Microsoft Corporation";
_txtAddressLine2.Text = "One Microsoft Way";
_txtCity.Text = "Redmond";
_txtState.Text = "WA";
_txtZipCode.Text = "98052-7329";
controller.btnSave_Click();
_lblMessage = (Label)testPage.FindControl("lblMessage");
_txtPatientIdentity = (HiddenField)testPage.FindControl("txtPatientIdentity");
patientIdentity = Convert.ToInt64(_txtPatientIdentity.Value);
TestLog.WriteLine("sql server identity value = " + patientIdentity.ToString());
Assert.AreEqual("Patient Successfully Added",_lblMessage.Text );
Assert.GreaterThan(patientIdentity, 0);
}
After coding the remainder of my tests, I proceeded to load my test assembly into Gallio’s Incarus GUI Test Runner and executed the Unit-Tests.
Houston, we have success! My Unit Tests executed successfully.
*Note: Prior to running the tests, I had to create an app.config file in my Unit Test project and add the connection string to connect to the SQL Server 2005 Express database.
This is a small example of Unit Testing. Trying to develop good Unit Testing techniques is an art all by itself. One does not really appreciate automated Unit Testing until his/her application starts to grow legs and becomes a monster or a killer application. Regression testing a monster application without automated testing is almost impossible. Having it in place will save you time and help prevent bugs from making their way into production code.
Conclusion
This article was a simple example of applying the MVC design pattern into the current ASP.NET Web Forms framework. Moving forward, I will be investigating and learning about the ASP.NET MVC framework for developing web applications. It will be interesting to see its adoption in the marketplace. At a minimum, it will have several niche markets, especially for those who are practicing test driven development, or for those who want to control what gets sent down the wire to the user’s browser from their web application. It is well known by now that the HTML generated by ASP.NET is very bloated. For the time being, the current ASP.NET Web Forms framework is still very popular, successful, and will be around for some time. It is today’s COBOL of the Microsoft web development world.
History
- 24th March, 2009: Initial version
- 24th March, 2009: Added source code and modified image