top button
Flag Notify
    Connect to us
      Site Registration

Site Registration

Using GridView And DetailsView In ASP.NET MVC - Part 2

+3 votes
544 views

ASP.NET MVC makes use of MVC design pattern and the result is far different at code level. ASP.NET MVC divides the entire processing logic into three distinct parts namely model, view and controller. In the process views (that represent UI under MVC architecture) needed to sacrifice three important features of web forms viz. Postbacks, ViewState and rich event model. Let's quickly see why this sacrifice is necessary. Remember that in the following sections when I say "web form" I mean the original web form model and when I say "MVC web page" I mean MVC based web forms, though technically they belong to the same inheritance chain.

  • In a web form controls such as Button and LinkButton always submit a form (POST request) to itself. That means under default scheme post backs originating from a web form are handled in the same web form. This contradicts the MVC pattern where a view always talks with a controller and not to itself.
  • First thing to note is that server controls were never designed keeping MVC architecture in mind. They were always intended to be used with "forms based" programming mode. The clever tricks played by web form framework and server controls such as ViewState and events though quite useful in "forms based" model they are of a very little use in MVC architecture. Since a view never submits data to itself (rather it sends it to a controller) ViewState has no role to play in MVC architecture.
  • Web form events can be either "GET" events or "POST" events. Because of the points mentioned above "POST" events of server controls (such as Click event of a Button, SelectedIndexChanged event of GridView and so on) are of little use in MVC.

Considering the above points MVC doesn't offer any direct equivalent of server controls. Under MVC scheme views make use of raw HTML, non-visual helper classes (HTML helpers) and of course third party controls.

Purely for the sake of rough analogy web forms and MVC web pages can be compared like this - The data objects such as DataSets, entities, generic lists etc. go as model under MVC, the markup that renders some UI (typically .aspx files and .ascx files) goes in views and the code that usually goes in event handlers go in controllers.

If you are thinking that the discussion so far is contradicting with the title of the article wait. Though server controls are not a recommended choice under ASP.NET MVC there are situations where you may need to use server controls. Some of these situations include:

  • You are working with server controls for over eight long years. You don't want to dump them immediately just because you wish to use ASP.NET MVC.
  • You are migrating existing ASP.NET web forms based website onto ASP.NET MVC. You simply want to re-use your efforts as much as possible.
  • Your client has asked you to develop a prototype using ASP.NET MVC. You want to do it quickly. Your team is not yet fully acclimatized with ASP.NET MVC.
  • You want to familiarize yourself with MVC concepts first rather than focusing too much on raw HTML and HTML helpers. Over a period of time you plan to master those pieces.
  • You are still evaluating third party MVC controls / helpers. Unless you are convinced that a vendor is meeting your expectations you want to continue using existing controls.

Merely using server controls on an MVC web pages doesn't break MVC architecture in any way. Improper use of the server controls, however, can break the architecture and make your views difficult to understand. When you wish to use server controls in ASP.NET MVC you should keep in mind the following points:

  • Server controls should always post data to a controller. You can use PostbackUrl property of Button and LinkButton controls to achieve this.
  • Server controls and the web forms should never use ViewState. The view will get its data from ViewData collection and send its data to a controller via POST request.
  • MVC web pages can make use of "GET" events (Page_Load for example) if required but they should never make use of "POST" events (SelectedIndexChanged event of GridView for example).

Example Scenario

To illustrate how we can use ASP.NET server controls in ASP.NET MVC web pages we will develop a sample application. In this application we will make use of GridView and DetailsView control. The application adds, edits, deletes and selects records from Employees table. Later we will also add sorting and paging capabilities to our grid.

Just to give you an idea of what we will be building see the following screen shots:

image

Figure 1

We present a list of existing employees to the user. Clicking on "Add a new Employees" link takes you to another page where a new employee record can be added.

image

Figure 2

Clicking on Edit will take the user to a data entry page where the selected employee record can be edited.

image

Figure 3

Notice that edit and add pages make use of DetailsView control and the listing page makes use of GridView control.

Create an MVC Web Application

Begin by creating a new MVC Web Application in Visual Studio. Select ASP.NET MVC 2 Empty Web Application template as shown below:

image

Creating a Model

Add a new SQL Server database to the App_Data folder and create a table named Employees. The Employees table contains three columns viz. Id, Name and Notes. Id is an identity column. Add a few sample records in the Employee table for testing purpose.

image

Then add a new LINQ to SQL Classes (.dbml) file to Models folder. Drag and drop Employees table onto its design surface from the Server Explorer. Doing so will create a LINQ to SQL class (Employee) for Employees table. This class will form the model for our MVC pages.

image

Creating a Controller

Now add a new class named EmployeeController in the Controllers folder. The EmployeeController class will have the following action methods:

  • Index : Fetches all the records from Employees table and renders Index view.
  • ShowInsertView : Shows insert view with a DetailsView control in insert mode.
  • Insert : Picks up data submitted by insert view and insert it into the Employees table.
  • ShowUpdateView : Shows update view with a DetailsView control in edit mode.
  • Update : Picks up data submitted by update view and saves it into the Employees table.
  • Delete : Deletes a record from Employees table.

These action methods are discussed next.

public ActionResult Index()
{
    DataClasses1DataContext db = new DataClasses1DataContext();
    IQueryable<Employee> emplist = from rows in db.Employees
                                   select rows;
    ViewData["emplist"] = emplist;
    return View();
}

The Index() action method creates an instance of data context class and then executes a LINQ query to fetch all the rows from Employees table. The returned rows are stored in a ViewData collection so that they can be accessed in the view. Index view is then rendered.

public ActionResult ShowInsertView()
{
    return View();
}

[HttpPost]
public ActionResult Insert(FormCollection collection)
{
    DataClasses1DataContext db = new DataClasses1DataContext();
    Employee item = new Employee();
    item.Name = collection["DetailsView1$txtName"];
    item.Notes = collection["DetailsView1$txtNotes"];
    db.Employees.InsertOnSubmit(item);
    db.SubmitChanges();
    return RedirectToAction("Index");
}

The ShowInsertView() action method simply renders ShowInsertView view. Since we are inserting a new record no data needs to be passed to the view.

Notice the Insert() action method carefully. It is marked with [HttpPost] attribute indicating that only POST requests can invoke this action method. The Insert() method receives the form data as a FormCollection parameter. FormCollection parameter is essentially a key-value collection. See how we retrieve the values entered in the DetailsView control of the view. The DetailsView control will have two textboxes with IDs txtName and txtNotes respectively. As you are probably aware, SP.NET automatically prefixes the IDs of the constituent controls with the parent control ID. So txtName becomes DetailsView1$txtName when the view data is posted. A new Employee object is constructed and inserted in the Employees table. The index view is again rendered so that the user goes back to the employee listing.

public ActionResult ShowUpdateView(int id)
{
    DataClasses1DataContext db=new DataClasses1DataContext();
    var temp = from item in db.Employees
                where item.Id == id
                select item;
    ViewData["emplist"] = temp.ToList();
    return View();
}

[HttpPost]
public ActionResult Update(int id,FormCollection collection)
{
    DataClasses1DataContext db = new DataClasses1DataContext();
    var temp = from item in db.Employees
                where item.Id == id
                select item;
    temp.First().Name=collection["DetailsView1$txtName"];
    temp.First().Notes = collection["DetailsView1$txtNotes"];
    db.SubmitChanges();
    return RedirectToAction("Index");
}

The ShowUpdateView() action method fetches an Employee record whose ID matches with the supplied ID. The ShowUpdateView() method receives an ID from employee listing page (see the first figure). The Employee collection is stored in a ViewData variable and ShowUpdateView view is rendered. Even though the LINQ query is returning a single object we still need to pass it as a generic List beause DetailsView control expects a list or array for the sake of data binding.

The Update() action method is similar to Insert() method we discussed earlier. The only difference is that it modifies an existing record instead of adding a new one. The Id of the employee to be updated is supplied as Id parameter from the view (see the first figure). After the update operation the user is again taken to the employee listing.

public ActionResult Delete(int id)
{
    DataClasses1DataContext db = new DataClasses1DataContext();
    var temp = from item in db.Employees
                where item.Id == id
                select item;
    db.Employees.DeleteOnSubmit(temp.First());
    db.SubmitChanges();
    return RedirectToAction("Index");
}

The Delete() action method simply deletes a specified employee from the Employees table and takes the user back to the employee listing. Notice that unlike Insert() and Update() action methods Delete() method is not marked with [HttpPost] attribute because we are not posting anything to it. The Id parameter will be supplied from the Index view as a part of GET request.

Creating Views

In all we need to create three views viz. Index, ShowInsertView and ShowUpdateView. These views can be seen in the figures shown above.

To create the Index view, add a new View in the Views folder. Drag and drop a GridView control on the view design surface. Add two TemplateField columns and two HyperLink columns to the GridView and configure them as follows:

image

The Id and Name template fields are bound with Id and Name columns of the Employees table. The markup after binding the Id template field is shown below.

<asp:TemplateField HeaderText="Id" InsertVisible="False" 
     SortExpression="Id">
    <ItemTemplate>
        <asp:Label ID="Label1" runat="server" 
        Text='<%# Bind("Id") %>'></asp:Label>
    </ItemTemplate>
</asp:TemplateField>

The Edit and Delete HyperLink columns essentially display a hyperlink that points to the ShowUpdateView and Delete action methods respectively.

<asp:HyperLinkField DataNavigateUrlFields="Id" 
    DataNavigateUrlFormatString="~/Employee/ShowUpdateView/{0}" 
    Text="Edit" >
</asp:HyperLinkField>
<asp:HyperLinkField DataNavigateUrlFields="Id" 
    DataNavigateUrlFormatString="~/Employee/Delete/{0}" 
    Text="Delete" >
</asp:HyperLinkField>

Notice how the DataNavigateUrlField and DataNavigateUrlFormatString properties are used. At run time in place of {0} the employee Id for that row will be substituted. Recollect that the ShowUpdateView() and Delete() actions methods accept employee Id as a parameter.

Now place a HyperLink control below the GridView we just configured. Set its Text and NavigateUrl properties to "Add a new Employee" and ~/Employee/ShowInsertView respectively.

Final task is to bind the GridView with the data we pass through ViewData variable. We do this in the Page_Load event as shown below:

protected void Page_Load(object sender, EventArgs e)
{
    GridView1.DataSource = ViewData["emplist"];
    GridView1.DataBind();
}

This complete the Index view. We will revisit Index view when we implement sorting and paging features to the GridView in Part 2 of this article.

Now add another view in the Views folder and name it as ShowInsertView.aspx. Drag and drop a DetailsView control on it and set its DefaultMode property to Insert. This way when the view is rendered the DetailsView will be ready to accept a new entry. The DetailsView will have two template fields for Name and Notes columns respectively (though we won't bind them with anything as such). Since employee ID is identity column we need not include it in the DetailsView. Design the InsertItemTemplate of both the template fields to include textboxes. Then add a Button control to the footer template of the DetailsView and set its Text and PostbackUrl properties to Save and ~/Employee/Insert respectively. This way clicking the Save button will post the form to Insert() action method we coded earlier. Your DetailsView should resemble Figure 2. The markup of DetailsView is given below:

<asp:DetailsView ID="DetailsView1" runat="server" DefaultMode="Insert">
 <Fields>
     <asp:TemplateField HeaderText="Name :">
         <InsertItemTemplate>
           <asp:TextBox ID="txtName" runat="server" 
             Text='<%# Bind("Name") %>' />
         </InsertItemTemplate>
     </asp:TemplateField>
     <asp:TemplateField HeaderText="Notes :">
        <InsertItemTemplate>
           <asp:TextBox ID="txtNotes" runat="server" 
             Text='<%# Bind("Notes") %>' Rows="3" 
             TextMode="MultiLine"></asp:TextBox>
        </InsertItemTemplate>
    </asp:TemplateField>
    </Fields>
    <FooterTemplate>
     <asp:Button ID="Button1" runat="server" 
       PostBackUrl="~/Employee/Insert" 
       Text="Save" Width="75px" />
    </FooterTemplate>
</asp:DetailsView>

Similarly, add a view named ShowUpdateView.aspx and design its DetailsView as shown in Figure 3. This time the DefaultMode property of the DetailsView should be set to Edit. The DetailsView EditItemTemplate has three template fields viz. Id, Name and Notes. The Id column is not editable. The markup of the DetailsView is given below:

<asp:DetailsView ID="DetailsView1" runat="server" DefaultMode="Edit">
    <Fields>
        <asp:TemplateField HeaderText="Id :">
            <EditItemTemplate>
                <asp:Label ID="Label1" runat="server" 
                 Text='<%# Bind("Id") %>'></asp:Label>
            </EditItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Name :">
            <EditItemTemplate>
                <asp:TextBox ID="txtName" runat="server" 
                  Text='<%# Bind("Name") %>'></asp:TextBox>
            </EditItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Notes :">
            <EditItemTemplate>
                <asp:TextBox ID="txtNotes" runat="server" 
                  Text='<%# Bind("Notes") %>' Rows="3" 
                  TextMode="MultiLine"></asp:TextBox>
            </EditItemTemplate>
        </asp:TemplateField>
    </Fields>
    <FooterTemplate>
        <asp:Button ID="Button1" runat="server" 
            PostBackUrl='<%# Eval("Id","~/Employee/Update/{0}") %>' 
            Text="Save" Width="75px" />
    </FooterTemplate>
</asp:DetailsView>

Notice the PostbackUrl property of  the Save button carefully. It is set to ~/Employee/Update/{0}. The Update() action needs employee Id as a parameter. The employee Id is passed via Id property of the model. The DetailsView is finally bound with the model in the Page_Load event as shown below:

protected void Page_Load(object sender, EventArgs e)
{
    object emplist = ViewData["emplist"];
    DetailsView1.DataSource = emplist;
    DetailsView1.DataBind();
}

Recollect that we are saving employee object to be edited in a ViewData variable in the ShowUpdateView() action method. The same object is bound with the DetailsView.

That's it! Run the web application and navigate to Index view (your URL should be something like http://localhost:XXXX/Employee/index where XXXX is the port number assigned by development web server). Try adding new employee entries as well as edit existing records.

Disabling ViewState completely

Though our application is functioning as expected, it has one flaw. The individual views still maintain ViewState of server controls (GridView, DetailsView etc.). In MVC applications the ViewState is of little use and if not disabled unnecessarily makes the view heavier. One quick way to rectify the problem is to set EnableViewState property of the page to false. This way the ViewState will be reduced to a small value.

<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" 
value="Ly91biiCAQJThIvtDDyFLvl0HWiJ1O/Egm9fLkQEf72LVCkYT/EI
C88uk3xc+Ku3pXDLM6jhDLA7sY6nOh5Hj1Fg93VSiGZHl5/T5O4U69A=" />

Though we have disabled the ViewState using EnableViewState property the control state is still maintained and cannot be disabled as such. If you wish to get rid of this small chunk of ViewState also then add the following overridden methods in the view page server side code.

protected override void SavePageStateToPersistenceMedium(object state)
{
}

protected override object LoadPageStateFromPersistenceMedium()
{
 return null;
}

The SavePageStateToPersistenceMedium() is intended t save ViewState and ControlState information of the page. We override it with an empty implementation so that no ViewState or ControlState is saved. The LoadPageStateFromPersistenceMedium() simply returns null. This way the ViewState hidden field becomes:

<input type="hidden" name="__VIEWSTATE" 
id="__VIEWSTATE" 
value="" />
posted Oct 5, 2016 by Shivaranjini

  Promote This Article
Facebook Share Button Twitter Share Button LinkedIn Share Button


Related Articles

In Part 1 of this article series you developed a wizard in an ASP.NET MVC application. Although the wizard developed in Part 1 works as expected it has one shortcoming. It causes full page postback whenever you click on Previous or Next button. This behavior may not pose much problem if a wizard has only a few steps. However, if a wizard has many steps and each step accepts many entries then full page postback can deteriorate the user experience. To overcome this shortcoming you can add Ajax to the wizard so that only the form is posted to the server. In this part of the series you will convert the application developed in Part 1 to use Ajax. In the next part you will further enhance the wizard using jQuery so that data is posted to the server only on the final step.

To convert the wizard application developed previously to use Ajax you will use Ajax helper of ASP.NET MVC. To use Ajax helper you need to make certain changes to the application. These changes are listed below:

  • All the four views namely BasicDetails, AddressDetails, ContactDetails and Success will now be partial views.
  • All the action methods will now return the corresponding partial views.
  • The wizard will be launched by Index view and initially BasicDetails partial view will be displayed.

You might wonder as to why we are making these changes. These changes are required since we wish to use Ajax helper. The Ajax helper allows you to submit a form using an Ajax request and the returned response is displayed in a DOM element. For example, you may create a form that makes a post request to an action method and displays a success message returned by the action method in a <div> element. In our specific case when one wizard step is submitted to the server using an Ajax request the server responds by sending the next or previous wizard step. Thus a form submits to the server and renders another form in the browser. This requires that the wizard steps return only the HTML needed to display that step and not the page level items such as <script> and <link>. The page level items just mentioned will go inside Index view that launches the wizard.

Ok. Let's begin our development. Add an Index view to the project and key-in the following markup into it:

@{
Layout = null;
}

<!DOCTYPE html>

<html>
<head>
<meta name="viewport" content="width=device-width" />
<script src="~/Scripts/jquery-1.10.2.js"></script>
<script src="~/Scripts/jquery.unobtrusive-ajax.js">
</script>
<title>Index</title>
</head>
<body>
<div id="divContainer">
  @Html.Partial("BasicDetails")
</div>
</body>
</html>

Notice the above markup carefully. The <head> section of the page contains <script> references to jquery-1.10.1.js and jquery.unobtrusive-ajax.js. These files are necessary for the proper working of Ajax helper. Also notice that the body section contains a <div> with ID of divContainer. This <div> plays an important role in the functioning of the wizard because it houses various wizard steps. As you can see from the code initially it hosts BasicDetails partial view.

Now add a Partial View and name it as BasicDetails. The BasicDetails partial view contains the following markup:

@model WizardInMVC.Models.BasicDetails


@{
AjaxOptions options = new AjaxOptions();
options.HttpMethod = "POST";
options.InsertionMode = InsertionMode.Replace;
options.UpdateTargetId = "divContainer";
}

@using (Ajax.BeginForm("BasicDetails","Home",options))
{
<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' />
}

The BasicDetails partial view has BasicDetails class as its data model. It creates an instance of AjaxOptions class. The AjaxOptions class is used to supply various configuration settings while using the Ajax helper. In this case we set HttpMethod, InsertionMode and UpdateTargetId properties. The HttpMethod property indicates the form submission method and it is set to POST in this case. The InsertionMode and UpdateTargetId properties are very important for us. The UpdateTargetId property indicates an ID of a DOM element that will be updated with the response returned from the server. In our example UpdateTargetId is divContainer, the <div> element you added inside the Index view. The InsertionMode property governs how the server response should be added to the UpdateTargetId. The InsertionMode has three possible values - InsertAfter, InsertBefore and Replace. In our example we wish to replace the whole content of divContainer with the response (i.e. a form making a wizard step) and we set it to Replece.

Then a <form> is rendered using Ajax.BeginForm() helper method. The BeginForm() method accepts three parameters viz. action method name, controller name and AjaxOptions object. Inside it contains the same markup as the earlier example (see Part 1 for more details).

On the same lines you need to add AddressDetails, ContactDetails and Success partial views. The complete markup of these partial views is given below:

AddressDetails

@model WizardInMVC.Models.AddressDetails

@{
AjaxOptions options = new AjaxOptions();
options.HttpMethod = "POST";
options.InsertionMode = InsertionMode.Replace;
options.UpdateTargetId = "divContainer";
}

@using (Ajax.BeginForm("AddressDetails","Home",options))
{
<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' />
}

ContactDetails

@model WizardInMVC.Models.ContactDetails

@{
AjaxOptions options = new AjaxOptions();
options.HttpMethod = "POST";
options.InsertionMode = InsertionMode.Replace;
options.UpdateTargetId = "divContainer";
}

@using (Ajax.BeginForm("ContactDetails","Home",options))
{
<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' />
}

Success

<h3>Customer Data Saved Successfully!</h3>
@Html.ActionLink("Add Another Customer","Index","Home")

Notice that all the partial views that use Ajax helper declare their own AjaxOptions object and pass it to BeginForm() method.

Once you create all the partial views modify the action methods to return partial view instead of view. The following code shows the modified BasicDetails() action method:

[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 PartialView("AddressDetails");
    }
  }
  return PartialView();
}

As you can see there is no change in the logic of the action method. The only difference is that instead of View() method it uses PartialView() method. Also change AddressDetails() and ContactDetails() action method to return partial views instead of views. If you run the wizard you will find that even after navigating to different wizard steps the URL in the browser's address bar remains unchanged indicating that Ajax requests are being made to the server rather than full page postback.

image

As you can see even if you are on Step 2 the address bar still points to /home/index.

That's it! In the next part you will learn to develop a wizard that replies more on client side technologies and posts data to the server only at the last step.

READ MORE

In Part 1 and Part 2 of this article series you developed a wizard in an ASP.NET MVC application using full page postback and Ajax helper respectively. In this final part of this series you will develop a client side wizard using jQuery. The navigation between various wizard steps (Next, Previous) happens without any postback (neither full nor partial). The only step that causes form submission to the server is clicking on the Finish wizard button.

The jQuery version of the wizard uses only one view - Index - that contains all the three steps as three <div> elements. Using jQuery code you show only one <div> at a time giving a wizard appearance to the view.

The HomeController class now contains two overloads of Index() action method as shown below:

public class HomeController : Controller
{
  public ActionResult Index()
  {
    return View();
  }

  [HttpPost]
  public ActionResult Index(Customer obj)
  {
    if (ModelState.IsValid)
    {
      NorthwindEntities db = new NorthwindEntities();
      db.Customers.Add(obj);
      db.SaveChanges();
      return View("Success");
    }
    return View();
  }
}

As you can see the second Index() method is marked with [HttpPost] attribute and accepts Customer parameter. Inside it checks whether there are any model state errors or not. In case of any errors Index view is returned otherwise data is added to the database and the Success view is returned.

Currently, there are no model validations on Customer class.  To add then using data annotations you can create a metadata class as shown below:

public class CustomerMetadata
{
  [Required]
  public string CustomerID { get; set; }
  [Required]
  public string CompanyName { get; set; }
  [Required]
  public string Address { get; set; }
  [Required]
  public string City { get; set; }
  [Required]
  public string Country { get; set; }
  [Required]
  public string PostalCode { get; set; }
  [Required]
  public string ContactName { get; set; }
  [Required]
  public string Phone { get; set; }
  [Required]
  public string Fax { get; set; }
}

[MetadataType(typeof(CustomerMetadata))]
public partial class Customer
{
}

As you can see the CustomerMetadata class defines all the properties that are accepted through the wizard. All the properties are marked with [Required] attribute. The CustomerMetadata class is linked with the Customer model class by creating a partial class and then decorating it with [MetadataType] attribute.

Now, add the Index view to the project. The Index view that contains all these <div> elements looks like this:

@model WizardInMVC.Models.Customer

@{
Layout = null;
}

<!DOCTYPE html>

<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index</title>
</head>
<body>
@using (Html.BeginForm("Index", "Home", FormMethod.Post))
{
<div id="divBasic">
<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="button" name="nextBtn" value='Next' />
</div>
<div id="divAddress">
<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="button" name="prevBtn" value='Previous' />
<input type="button" name="nextBtn" value='Next' />
</div>
<div id="divContact">
<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="button" name="prevBtn" value='Previous' />
<input type="submit" name="nextBtn" value='Finish' />
</div>
}
</body>
</html>

As you can see there are three <div> elements namely divBasic, divAddress and divContact. They contain various form fields for the respective wizard step. The markup shown above is essentially the summation of the markups in individual views from the Part 1 and Part 2 examples. This markup is quite straightforward. There is a minor change though - the Previous and Next buttons are of type button rather than submit. This is because these buttons no longer cause form submission. To turn the above markup into a wizard you need to write some jQuery code as shown below:

$(document).ready(function () {
  $("div").hide();
  $("div:first").show();
  $(":button").click(function () {
    var parentDiv = $(this).parent();
    $("div").hide();
    if($(this).val()=="Previous")
    {
      var prevDiv = parentDiv.prev();
      prevDiv.show();
    }
    if ($(this).val() == "Next") {
      var nextDiv = parentDiv.next();
      nextDiv.show();
    }
  });
});

As you can see the above jQuery code first hides all the <div> elements from the view. This is done using the element selector and hide() method. Then the first <div> element is made visible using :first selector and show() method. This way initially when the page loads only the first wizard step will be visible.

The code then wires event handlers for the click event of the Previous and Next buttons. This is done by selecting these buttons using :button selector and then using click() method. Notice that inside the click event handler nowhere we hardcode the IDs of various <div> elements. This way adding or removing a wizard step doesn't need any change in the jQuery code. Inside the click event handler, the code retrieves a reference to the parent <div> element of the button being clicked. This is done using parent() method called on this. The code then hides all the <div> elements. It then checks the button that was clicked - Previous or Next. If Previous button is clicked a reference to the previous <div> element is retrieved using prev() method called on parentDiv variable. Similarly if Next button is clicked, a reference to the next <div> element is retrieved using next() method called on parentDiv. Accordingly, either previous <div> or next <div> is displayed using show() method.

That's it! Test the wizard by running the application and then navigating between wizard steps.

READ MORE

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.

image

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:

image

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.

image

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:

image

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:

image

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.

READ MORE

In Part 1 of this series you created and configured a basic project using ASP.NET MVC 6. Although the project runs as expected and outputs a message in the browser, it isn't database driven yet. In this part we will add database support to the project in the following ways:

  • Display a list of customers from the Northwind database
  • Allow for modification of existing customers
  • Provide basic validation capabilities

In this part we will modify the Index view to look like this:

image

As you can see, the page displays a list of customers from the Customers table of the Northwind database. Each table row has an Edit link. Clicking on the Edit link takes you to the Edit view so that the customer details can be modified. (see below).

image

Once you modify the details and click on the Save button the modified details are saved in the database. In case there are any validation errors they are displayed on the page like this:

image

Ok. Let's begin our development!

Open the same project that we created in Part 1 and add Customer class to the Models folder using the Add New Items dialog. The complete code of this class is shown below:

[Table("Customers")]
public class Customer
{
    [Required]
    [StringLength(5)]
    public string CustomerID { get; set; }
    [Required]
    public string CompanyName { get; set; }
    [Required]
    public string ContactName { get; set; }
    [Required]
    public string Country { get; set; }
}

The Customer class is mapped to the Customers table using the [Table] attribute and contains four public properties namely CustomerID, CompanyName, ContactName and Country. Basic validations such as [Required] and [StringLength] are also added to these properties.

Then add NorthwindDbContext class to the Classes folder and write the following code to it.

public class NorthwindDbContext:DbContext
{
    public DbSet<Customer> Customers { get; set; }

    protected override void OnConfiguring
      (DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(AppSettings.ConnectionString);
    }
}

The NorthwindDbContext class represents our data context and hence inherits from the DbContext base class. It consists of a single DbSet - Customers. Notice how the OnConfiguring() method has been overridden to specify the database connection string.

The overridden OnConfiguring() method supplies optionsBuilder parameter. The UseSqlServer() method accepts a database connection string. Recollect that we have stored the database connection string in the ConnectionString static property of the AppSettings class during startup of the application.

Now modify the Index() action of the HomeController as shown below:

public IActionResult Index()
{
    using (NorthwindDbContext db = new NorthwindDbContext())
    {
        List<Customer> data = db.Customers.ToList();
        return View(data);
    }
}

The Index() action simply instantiates the NorthwindDbContext and fetches all the customers in the form of a List. This List is supplied to the Index view as its model.

Now, open the Index view and modify it as shown below:

@model List<MVC6Demo.Models.Customer>

<html>
<head>
    <title>My First MVC 6 Application</title>
</head>
<body>
    <h1>List of Customers</h1>
    <table border="1" cellpadding="10">
        @foreach (var item in Model)
        {
            <tr>
                <td>@item.CustomerID</td>
                <td>@item.CompanyName</td>
                <td><a asp-action="Edit" 
                     asp-controller="Home" 
                     asp-route-id="@item.CustomerID">
                    Edit</a></td>
            </tr>
        }
    </table>
</body>
</html>

The Index view displays a list of customers in a table. This code is quite similar to MVC 5.x except the Edit link. In MVC 5.x you use ActionLink() HTML helper to render hyperlinks. The above code uses an anchor Tag Helper to achieve the same. The asp-action and asp-controller attributes points to the action method and the controller. The asp-route-id attribute specifies the ID route parameter to a CustomerID. This way the Edit links will take this form:

/home/edit/ALFKI

To get the Tag Helper intellisense you need to add _ViewImports.cshtml file to the Views folder (you can do that using Add New Items dialog). Once added place the following code in it:

@addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers"

The @addTagHelper directive tells the framework to use Tag Helpers from the specified assembly. You will now get various tag helper related attributes in the Visual Studio intellisense.

Ok. Next, add Edit() action to the HomeController as shown below:

public IActionResult Edit(string id)
{
    using (NorthwindDbContext db = new NorthwindDbContext())
    {
        Customer data = db.Customers.Where(i => 
                 i.CustomerID == id).SingleOrDefault();
        var query = (from c in db.Customers
                        orderby c.Country ascending
                        select new SelectListItem() 
                    { Text = c.Country, Value = c.Country })
                    .Distinct();
        List<SelectListItem> countries = query.ToList();
        ViewBag.Countries = countries;
        return View(data);
    }
}

The Edit action receives a CustomerID as its parameter. Inside, the code fetches a Customer matching the supplied ID and passes it to the Edit view. The Edit view also needs a list of countries for the Country column. So, a List of SelectListItem (Microsoft.AspNet.Mvc.Rendering namespace) is created and filled with unique countries from the Customers table. This List is passed to the view through the Countries ViewBag property.

Then add Edit view under Views/Home folder and key-in the following markup in it:

@model MVC6Demo.Models.Customer

<html>
<head>
    <title>My First MVC 6 Application</title>
    <style>
        .field-validation-error
        {
            color:red;
        }
        .validation-summary-errors
        {
            color:red;
        }
    </style>
</head>
<body>
<h1>Edit Customer</h1>
<form asp-controller="Home" asp-action="Save" method="post">
<table border="1" cellpadding="10">
<tr>
<td>
<label asp-for="CustomerID">Customer ID :</label>
</td>
<td>
<input asp-for="CustomerID" readonly="readonly" />
<span asp-validation-for="CustomerID"></span>
</td>
</tr>
<tr>
<td>
<label asp-for="CompanyName">Company Name :</label>
</td>
<td>
<input asp-for="CompanyName" />
<span asp-validation-for="CompanyName"></span>
</td>
</tr>
<tr>
<td>
<label asp-for="ContactName">Contact Name :</label>
</td>
<td>
<input asp-for="ContactName" />
<span asp-validation-for="ContactName"></span>
</td>
</tr>
<tr>
<td>
<label asp-for="Country">Country :</label>
</td>
<td>
<select asp-for="Country" 
asp-items="@ViewBag.Countries"></select>
<span asp-validation-for="Country"></span>
</td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Save" />
</td>
</tr>
</table>
</form>
<div asp-validation-summary="ValidationSummary.All"></div>
<br />
<a asp-action="Index" asp-controller="Home">Go Back</a>
</body>
</html>

The above markup uses the following Tag Helpers:

  • form
  • label
  • input
  • select
  • field validation and validation summary

The asp-action and asp-controller attributes of the form tag helper are set to Save and Home respectively. This way the form will be POSTed to the Save() action of the HomeController. The asp-for attribute of the label and the input tag helpers specify a model property that is bound with the label and the input field respectively. The asp-items attribute of the select tag helper specify that the <option> elements be generated from the Countries ViewBag property.

The field level validations are displayed by adding <span> elements and setting their asp-validation-for attribute to the appropriate model property. This way the model validation errors will be displayed in the <span> elements. The validation summary is displayed in a <div> element by setting its asp-validation-summary attribute to ValidationSummary.All. The look and feel of the validation tag helpers is controlled through CSS classes - .field-validation-error and .validation-summary-errors (see top of the markup).

Now add Save() action to the HomeController and write the following code to it:

public IActionResult Save(Customer obj)
{
    using (NorthwindDbContext db = 
                  new NorthwindDbContext())
    {
        var query = (from c in db.Customers
                        orderby c.Country ascending
                        select new SelectListItem() 
         { Text = c.Country, Value = c.Country }).Distinct();
        List<SelectListItem> countries = query.ToList();
        ViewBag.Countries = countries;

        if (ModelState.IsValid)
        {
            db.Entry(obj).State = EntityState.Modified;
            db.SaveChanges();
        }
        return View("Edit", obj);
    }
}

The code that fills the Countries ViewBag property is same as before. Then the code checks whether the ModelState is valid using the IsValid property. If the model state is valid the modified Customer details are saved to the database. This is done by setting the State property to Modified and then calling SaveChanges() method.

That's it! Run the application and check if the customer details can be modified successfully.

In this part you instantiated NorthwindDbContext locally. In the next part we will use MVC 6 dependency injection to inject it into the HomeController. Till then keep coding!

READ MORE

In my previous article I illustrated how jQuery can be used to select and delete records in an ASP.NET MVC application. A few readers asked how the same can be accomplished using AngularJS instead of jQuery. This article shows just that.

Recollect how our Index view looks like and how it allows you to select all rows through the header checkbox or individual rows through the respective checkboxes.

image

As you can see there are two ways to select records for deletion:

  • You select checkboxes for rows to be deleted individually.
  • You can check the checkbox placed in the header row to select all the rows. This checkbox toggles the checked state of the other checkboxes.

Once selected you can click on the Delete Selected Customers button to actually delete the records.

In order to implement such a functionality using ASP.NET MVC and AngularJS you will need to modify the Index view as shown below:

@model List<SelectAllDeleteDemo.Models.Customer>
...
...
<html ng-app>
<body ng-controller="MyController">
    <h1>List of Customers</h1>
    <input type="button" id="delete" 
           value="Delete Selected Customers" 
           ng-click="DeleteSelected()" />
    <br /><br />
    <table border="1" cellpadding="11">
        <tr>
            <th><input type="checkbox" id="checkAll" 
                 ng-click="ToggleSelectAll()" /></th>
            <th>CustomerID</th>
            <th>CompanyName</th>
            <th>Country</th>
        </tr>
        @{ int i = 0;}
        @foreach (var item in Model)
        {
            <tr>
                <td><input type="checkbox" 
                     class="checkBox" 
                     value="@item.CustomerID" 
                     ng-checked="selectAll" 
                     ng-model="customerIDs[@i].selected" /></td>
                <td>@item.CustomerID</td>
                <td>@item.CompanyName</td>
                <td>@item.Country</td>
            </tr>
            i++;
        }
    </table>
</body>
</html>

Notice a few things about this markup:

  • The <html> tag has ng-app attribute indicating that this page is an AngularJS application.
  • The <body> tag specifies ng-controller attribute to be MyController. You will write this controller shortly.
  • The Delete Selected Customers button wires a click event handler using ng-click attribute and it is set to DeleteSelected().
  • The header checkbox wires click event handler using ng-click to ToggleSelectAll(). This event handler is responsible for toggling the state of all the checkboxes.
  • The checkboxes in individual rows are bound with selected property of objects stored in customerIDs array. This part will be clear once you see the code shortly.

Now add a script reference to AngularJS and also add a <script> block. Then write the following jQuery code:

<script src="~/Scripts/angular-1.2.js"></script>
<script>
    function MyController($scope, $http) {
        $scope.selectAll = false;
        $scope.customerIDs = new Array();

        @foreach (var item in Model)
        {
            @:$scope.customerIDs.push({ 'customerID': 
             '@item.CustomerID', 'selected': false });
        }

        $scope.DeleteSelected = function () {
            var selectedIDs = new Array();
            angular.forEach
            ($scope.customerIDs, function (item) {
                if(item.selected)
                {
                    selectedIDs.push(item.customerID);
                }
            });
            var promise = $http.post
                    ("/home/delete", selectedIDs);
            promise.success(function (msg) {
                alert(msg);
            }).error(function () {
                alert("Error");
            });
        }

        $scope.ToggleSelectAll = function () {
            $scope.selectAll = !$scope.selectAll;
            angular.forEach(
                $scope.customerIDs, function (item) {
                item.selected = $scope.selectAll;
            });
        }
    }
</script>

The code consists of MyController function - the AngularJS controller - with two model properties and two event handlers.

The selectAll model property is initially set to false indicating that all the checkboxes are unchecked. Recollect that selectAll property is bound with the ng-checked attribute of individual checkboxes. The customerIDs array stores JavaScript objects, each holding a CustomerID and its checked state. Notice how this array is filled by dynamically emitting push() script calls through Razor code block. Recollect that checkboxes from the individual rows are model bound with customerIDs[i].selected property. This way checking or unchecking a checkbox individually (rather than header checkbox) toggles the corresponding entry from the customerIDs array.

The DeleteSelected() function handles the click event of the Delete Selected Customers button. Inside, the code iterates through the customerIDs array using forEach() and determines whether selected property is true or false. If the selected property is true, that customerID is pushed into a local array - selectedIDs. This way you get an array of CustomerIDs that are checked in the table. Then the code makes an Ajax call to the /home/delete action method. This is done using the post() method of $http. The post() method takes two parameters - url of the resource to be invoked and data to be sent along with the request. In this case you pass the selectedIDs array to the Delete() action method. The success() and error() method simply wire the respective callbacks to the promise object returned by the post() method.

The ToggleSelectAll() function acts as the click event handler for the header checkbox. Inside, it toggles the selectAll model property. The code then iterates through all the elements of the customerIDs array and set selected property to the value of selectAll. This way all the checkboxes from the table rows toggle their state.

That's it! Run the application and test the functionality.

READ MORE
...