Creating Wizard in ASP.NET MVC (Part 1)
At times you want to accept user input in your web applications by presenting
them with a wizard driven user interface. A wizard driven user interface allows
you to logically divide and group pieces of information so that user can fill
them up easily in step-by-step manner. While creating a wizard is easy in
ASP.NET Web Forms applications, you need to implement it yourself in ASP.NET MVC
applications. There are more than one approaches to creating a wizard in ASP.NET
MVC and this article shows one of them. In Part 1 of this article you will
develop a wizard that stores its data in ASP.NET Session and the wizard works on
traditional form submission.
To develop a wizard in ASP.NET MVC you will use the following approach:
- Each wizard step will have an action method in the controller and a
view.
- The data accepted in each wizard step is stored in a view model class
designed for that step.
- All the action methods for wizard steps will accept three parameters -
step's view model object and two string parameters indicating the Next /
Previous status.
- The action methods mentioned above grab the data from view model object
and store it in Session till the final step.
- The action methods return a view for the next step if Next button is
clicked. If Previous button is clicked they return a view for the previous
step and they return the same view if there are any model validation errors.
- Model validations are checked only when Next button is clicked.
Now that you have some idea about the approach we will be taking for
developing a wizard, let's create a sample application that illustrates how this
approach can be implemented. Begin by creating a new ASP.NET MVC application
based on Empty template. Then right click on the Models folder and add an
ADO.NET Entity Data Model for the Customers table of Northwind database. The
following figure shows the Customer model class in the designer.
As you can see the Customer class has several properties. For the sake of
creating the wizard let's group them in three steps as follows:
- Basic Details : Customer ID, CompanyName
- Address Details : Address, City, Country, PostalCode
- Contact Details : ContactName, Phone, Fax
Note that a few properties have been omitted from the wizard just to keep
things tidy.
You need to create a view model class for each of the wizard steps outlined
above. So, you need to add BasicDetails, AddressDetails and ContactDetails
classes to the Models folder. These are simple POCOs as shown below:
public class BasicDetails
{
[Required]
public string CustomerID { get; set; }
[Required]
[StringLength(30)]
public string CompanyName { get; set; }
}
public class AddressDetails
{
[Required]
public string Address { get; set; }
[Required]
public string City { get; set; }
[Required]
public string Country { get; set; }
[Required]
public string PostalCode { get; set; }
}
public class ContactDetails
{
[Required]
public string ContactName { get; set; }
[Required]
public string Phone { get; set; }
[Required]
public string Fax { get; set; }
}
As you can see the three classes namely BasicDetails, AddressDetails and
ContactDetails contain only those properties that are relevant to the
corresponding wizard step. Additionally, they use data annotations for basic
validations. You can add more data annotations as per your requirement. For this
example, the above attributes are sufficient.
Now, add HomeController to the Controllers folder. The HomeController
contains five methods in all - Index(), GetCustomer(), RemoveCustomer(),
BasicDetails(), AddressDetails() and ContactDetails(). The Index() action method
and GetCustomers() / RemoveCustomer() helper methods are shown below:
public ActionResult Index()
{
return View("BasicDetails");
}
private Customer GetCustomer()
{
if (Session["customer"] == null)
{
Session["customer"] = new Customer();
}
return (Customer)Session["customer"];
}
private void RemoveCustomer()
{
Session.Remove("customer");
}
The Index() action method simply returns a view that represents the first
step of the wizard - BasicDetails. The GetCustomer() helper method does the job
of retrieving a Customer object from Session and return it to the caller. The
GetCustomer() method first checks whether a Customer object is stored in the
Session or not. If Customer object exists that object is returned, otherwise a
new Customer object is created and stored in the Session with a key customer.
The RemoveCustomer() method simply removes the customer key and associated
Customer object from the Session.
Each wizard step has an action method. Since this example has three wizard
steps you need to add three action methods. The BasicDetails() action method is
shown below:
[HttpPost]
public ActionResult BasicDetails(BasicDetails data,
string prevBtn, string nextBtn)
{
if (nextBtn != null)
{
if (ModelState.IsValid)
{
Customer obj = GetCustomer();
obj.CustomerID = data.CustomerID;
obj.CompanyName = data.CompanyName;
return View("AddressDetails");
}
}
return View();
}
The BasicDetails() action method accepts three parameters - BasicDetails
object, prevBtn and nextBtn. The BasicDetails view posts the form to
BasicDetails action method and hence it is marked with [HttpPost] attribute. The
three parameters of BasicDetails() action method are passed in by the default
model binding process of ASP.NET MVC. The BasicDetails object contains the
values of CustomerID and CompanyName as entered on the BasicDetails view. Inside
the BasicDetails() action method you need to know which of the two buttons (Next
/ Previous) was clicked by the user. That's why the two string parameters
prevBtn and nextBtn are used. If prevBtn or nextBtn is not null it indicates it
indicates that the button was clicked. The BasicDetails view doesn't have
Previous button since it is the first step of the wizard. The BasicDetails()
still accepts prevBtn parameter for the sake of consistency with other wizard
step methods.
Inside, the code checks the ModelState.IsValid property to determine whether
the the model contains valid data. If IsValid returns true GetCustomer() is
called to retrieve the Customer object from the Session. The CustomerID and
CompanyName properties of the Customer object are set with the corresponding
properties of BasicDetails object and AddressDetails view is returned. If there
are any model validation errors the BasicDetails view will be returned.
The AddressDetails() method works on the similar lines as that of
BasicDetails() and is shown below:
[HttpPost]
public ActionResult AddressDetails(AddressDetails data,
string prevBtn, string nextBtn)
{
Customer obj = GetCustomer();
if (prevBtn!=null)
{
BasicDetails bd = new BasicDetails();
bd.CustomerID = obj.CustomerID;
bd.CompanyName = obj.CompanyName;
return View("BasicDetails",bd);
}
if (nextBtn != null)
{
if (ModelState.IsValid)
{
obj.Address = data.Address;
obj.City = data.City;
obj.Country = data.Country;
obj.PostalCode = data.PostalCode;
return View("ContactDetails");
}
}
return View();
}
The AddressDetails view has Previous as well as Next button and posts to
AddressDetails() action method. The AddressDetails() method accepts
AddressDetails object and prevBtn and nextBtn parameters. If the Previous button
was clicked, the code prepares an instance of BasicDetails object and populates
its CustomerID and CompanyName properties from the Customer object from Session.
The code then returns BasicDetails view with BasicDetails object as its model.
This way user is taken to the previous step of the wizard.
Then the code checks whether Next button was clicked. If so, IsValid property
of ModelState is checked as before. If there are no model validation errors data
from AddressDetails object is stored in the Customer object from the Session.
The code then return ContactDetails view.
The ContactDetails() action method does the job of saving the newly added
Customer to the database and is shown below:
[HttpPost]
public ActionResult ContactDetails(ContactDetails data,
string prevBtn, string nextBtn)
{
Customer obj = GetCustomer();
if (prevBtn != null)
{
AddressDetails ad = new AddressDetails();
ad.Address = obj.Address;
ad.City = obj.City;
ad.Country = obj.Country;
ad.PostalCode = obj.PostalCode;
return View("AddressDetails", ad);
}
if (nextBtn != null)
{
if (ModelState.IsValid)
{
obj.ContactName = data.ContactName;
obj.Phone = data.Phone;
obj.Fax = data.Fax;
NorthwindEntities db = new NorthwindEntities();
db.Customers.Add(obj);
db.SaveChanges();
RemoveCustomer();
return View("Success");
}
}
return View();
}
The ContactDetails view posts to the ContactDetails() action method. The
ContactDetails() action method accepts ContactDetails object and prevBtn and
nextBtn parameters. As before, it checks whether the Previous button was
clicked. If so, a new instance of AddressDetails class is created and is filled
with the data from Customer object from the Session. The code then returns
AddressView by passing AddressDetails object as its model.
If user clicks on Next button, model is checked for any validation errors
using IsValid property. If there are no validation errors properties of Customer
object stored in the Session are assigned values of the corresponding
ContactDetails object properties. Then a Entity Framework context is
instantiated and the Customer object is added to the Customers DbSet. Calling
SaveChanges() saves the data to the database. RemoveCustomer() is then called so
as to remove the Session object. Finally, Success view is returned from the
method.
Next, add four views - BasicDetails, AddressDetails, ContactDetails and
Success. The markup of BasicDetails view is shown below:
@model WizardInMVC.Models.BasicDetails
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>BasicDetails</title>
</head>
<body>
@using (Html.BeginForm("BasicDetails", "Home", FormMethod.Post))
{
<h1>Step 1 : Basic Details</h1>
@Html.LabelFor(m=>m.CustomerID)<br />
@Html.TextBoxFor(m=>m.CustomerID)
@Html.ValidationMessageFor(m=>m.CustomerID)<br />
@Html.LabelFor(m=>m.CompanyName)<br />
@Html.TextBoxFor(m=>m.CompanyName)
@Html.ValidationMessageFor(m=>m.CompanyName)
<br />
<input type="submit" name="nextBtn" value='Next' />
}
</body>
</html>
As you can see BasicDetails view has its model set to BasicDetails class. The
view renders a form using BeginForm() Html helper that posts to BasicDetails()
action method of HomeController. Form fields for CustomerID and CompanyName are
rendered using LabelFor() and TextBoxFor() helpers. The validation errors are
emitted using ValidationMessageFor() helper. Note that the name of the Next
button must match the corresponding parameter name of the BasicDetails() action
method (nextBtn in this case). The following figure shows the BasicDetails view
in action:
The AddressDetails view is similar to BasicDetails but has Previous button
also. The markup of AddressDetails view is shown below:
@model WizardInMVC.Models.AddressDetails
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>AddressDetails</title>
</head>
<body>
@using (Html.BeginForm("AddressDetails", "Home", FormMethod.Post))
{
<h1>Step 2 : Address Details</h1>
@Html.LabelFor(m=>m.Address)<br />
@Html.TextBoxFor(m=>m.Address)
@Html.ValidationMessageFor(m=>m.Address)
<br />
@Html.LabelFor(m=>m.City)<br />
@Html.TextBoxFor(m=>m.City)
@Html.ValidationMessageFor(m=>m.City)
<br />
@Html.LabelFor(m=>m.Country)<br />
@Html.TextBoxFor(m=>m.Country)
@Html.ValidationMessageFor(m=>m.Country)
<br />
@Html.LabelFor(m=>m.PostalCode)<br />
@Html.TextBoxFor(m=>m.PostalCode)
@Html.ValidationMessageFor(m=>m.PostalCode)
<br />
<input type="submit" name="prevBtn" value='Previous' />
<input type="submit" name="nextBtn" value='Next' />
}
</body>
</html>
AddressDetails view renders fields for Address, City, Country and PostalCode
model properties. It also has prevBtn and nextBtn buttons that represent the
Previous and Next button respectively. The AddressDetails view posts the form to
AddressDetails() action method of HomeController. The following figure shows how
AddressDetails view looks like along with validation errors.
The final wizard step - ContactDetails - consists of form fields for
ContactName, Phone and Fax. It has two buttons Previous and Finish. The markup
of ContactDetails view is shown below:
@model WizardInMVC.Models.ContactDetails
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>ContactDetails</title>
</head>
<body>
@using (Html.BeginForm("ContactDetails", "Home", FormMethod.Post))
{
<h1>Step 3 : Contact Details</h1>
@Html.LabelFor(m=>m.ContactName)<br />
@Html.TextBoxFor(m=>m.ContactName)
@Html.ValidationMessageFor(m=>m.ContactName)
<br />
@Html.LabelFor(m=>m.Phone)<br />
@Html.TextBoxFor(m=>m.Phone)
@Html.ValidationMessageFor(m=>m.Phone)
<br />
@Html.LabelFor(m=>m.Fax)<br />
@Html.TextBoxFor(m=>m.Fax)
@Html.ValidationMessageFor(m=>m.Fax)
<br />
<input type="submit" name="prevBtn" value='Previous' />
<input type="submit" name="nextBtn" value='Finish' />
}
</body>
</html>
As you can see the ContactDetails view posts to ContactDetails() action
method of HomeController. Notice that this time nextBtn has a value of Finish
since it is the last step of the wizard. The following figure shows
ContactDetails in action:
Finally, you need to add the Success view that displays a success method and
has a link to run the wizard again. The markup of success view is shown below:
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Success</title>
</head>
<body>
<h3>Customer Data Saved Successfully!</h3>
@Html.ActionLink("Add Another Customer","Index","Home")
</body>
</html>
As you can see the ActionLink() helper renders an action link that points to
the Index action method of HomeController. The following figure shows how the
Success view looks like:
That's it! You can now run the wizard and test whether it works as expected.
In the second part of this article you will learn to create a wizard using Ajax
techniques.