Ajax
1
MVC – Ajax and Modal Views
AJAX stands for Asynchronous JavaScript and XML. It is a client-side capability that allows
silent updates of parts of a web page. Every browser provides a component called
XmlHttpRequest that is invoked via Javascript and has the capability to make asynchronous calls
back to the web server from where the page was obtained. One can set up Javascript timers to
automate the periodic retrieval of data using the XmlHttpRequest object.
Even though we can program the XmlHttpRequest object directly using Javascript, JQuery
makes it a lot easier to set it up and issue an asynchronous request to the web server. ASP.Net
MVC also provides a server-side solution to AJAX via the Ajax.BeginForm extension method.
We will demonstrate the two approaches to AJAX i.e., via JQuery and serializable partial views,
and via the Ajax.BeginForm.
Using SQL Server Management Studo, create a database called ProductsDB. Add a table called
Categories with the folling data.
The CategoryId column is of type int, and the CategoryName column is of type carchar(50).
Add another table called Products with the following design.
Then put the following data in the Products table.
2
Using Visual Studio, create a new project. Choose the web type of project, and select the MVC
template as shown below.
Add the following connection string information to the web.config file. Replace ALPHA with
the name of your database server.
<configuration> <connectionStrings> <add name="PRODUCTSDB" connectionString="server=ALPHA;integrated security=true;database=ProductsDB" /> </connectionStrings> <appSettings>
Add a class to the Models folder called Category with the following code in it.
public class Category { public int CategoryId { get; set; } public string CategoryName { get; set; } }
3
Add a folder called DataLayer to the project. Then add an interface called IEntity to it with the
following code in it.
public interface IEntity { // any class that needs conversion to a List from a DataTable will implement this void PopulateFields(DataRow dr); }
Add a class called MyEntityBase to the Models folder with the following code in it. This class
uses reflection to populate the fields of a class from a given DataRow of a DataTable. All
repository methods that need to convert a DataTable to a list will indirectly use this method via
the generic DBList class..
public class MyEntityBase : IEntity { public void PopulateFields(DataRow dr) { // use reflection to populate the fields of this class from DataRow Type tp = this.GetType(); foreach (PropertyInfo pi in tp.GetProperties()) pi.SetValue(this, dr[pi.Name]); } }
Add a class called DBList to the DataLayer folder with the following code in it.
class DBList { public static List<T> GetList<T>(DataTable dt) where T : IEntity, new() { // will do conversion from dt to List<> for reference types List<T> TList = new List<T>(); foreach (DataRow dr in dt.Rows) { T t1 = new T(); // populate the columns from dr into the fields of the t1 // delegate the work of populating t1 to the class T t1.PopulateFields(dr); TList.Add(t1); } return TList; } public static List<T> GetListValueType<T>(DataTable dt, string colname) where T : IConvertible // will do conversion from dt to List<>for value types including string { List<T> TList = new List<T>(); foreach (DataRow dr in dt.Rows) TList.Add((T)dr[colname]); return TList; } }
4
Add a class called DBHelper to the DataLayer folder with the following code in it. This class
helps in populating the parameters to a parameterized sql.
public class DBHelper { public static void AddSqlParam(List<SqlParameter> PList, string paramName, SqlDbType paramType, object paramValue, int size=0) { SqlParameter p = null; if (size == 0) p = new SqlParameter(paramName, paramType); else p = new SqlParameter(paramName, paramType, size); p.Value = paramValue; PList.Add(p); } }
Add a class called Products to the Models folder with the following code in it.
public class Product : MyEntityBase { //public int ProductId { get; set; } int productId; public int ProductId { get { return productId; } set { productId = value; } } public string ProductName { get; set; } public string Description { get; set; } public decimal Price { get; set; } public int StockLevel { get; set; } public int CategoryId { get; set; } public bool OnSale { get; set; } public bool Discontinued { get; set; } }
Since we will be display a dropdown with categories and a list of products belonging to the
product category, we need to create a view model encapsulating this information. Add a class
called ProductCatVM to the models folder with the following code in it.
public class ProductCatVM { public List<SelectListItem> CatList { get; set; } public List<Product> PList { get; set; } public int categoryIdSelected { get; set; } }
Add a class called RepositoryProducts to the Models folder with the following code in it. It has
the methods to obtain products from the database, insert, update and delete products.
public class RepositoryProducts { // purpose of Repository is to compose SQL, issue it to DataAccess // and return a strongly typed data to the caller DataAccess _dataAccess = new DataAccess(); public List<Category> GetCategories()
5
{ List<Category> CList = new List<Category>(); try { string sql = "select * from Categories"; DataTable dt = _dataAccess.GetManyRowsCols(sql, null); // convert DataTable to List<Category> foreach (DataRow dr in dt.Rows) { Category cat = new Category(); cat.CategoryId = (int) dr["CategoryId"]; cat.CategoryName = (string)dr["CategoryName"]; CList.Add(cat); } } catch(Exception) { throw; } return CList; } public List<Product> GetProductsByCategory(int catid) { List<Product> PList = new List<Product>(); try { string sql = "select * from Products where CategoryId=@CategoryId"; List<SqlParameter> ParamList = new List<SqlParameter>(); SqlParameter p1 = new SqlParameter("@categoryId", SqlDbType.Int); p1.Value = catid; ParamList.Add(p1); DataTable dt = _dataAccess.GetManyRowsCols(sql, ParamList); // convert DataTable to List<Product> PList = DBList.GetList<Product>(dt); } catch (Exception) { throw; } return PList; } public bool AddProduct(Product pr) { bool ret = false; try { string sql = "insert into Products (ProductId,ProductName, Description," + "Price,StockLevel,CategoryId,OnSale,Discontinued) values (@ProductId," + "@ProductName,@Description,@Price,@StockLevel,@CategoryId,@OnSale," + "@Discontinued)";
6
List<SqlParameter> PList = new List<SqlParameter>(); DBHelper.AddSqlParam(PList, "@ProductId", SqlDbType.Int, pr.ProductId); DBHelper.AddSqlParam(PList,"@ProductName", SqlDbType.VarChar, pr.ProductName,50); DBHelper.AddSqlParam(PList, "@Description", SqlDbType.Text, pr.Description); DBHelper.AddSqlParam(PList, "@Price", SqlDbType.Money, pr.Price); DBHelper.AddSqlParam(PList, "@StockLevel", SqlDbType.Int, pr.StockLevel); DBHelper.AddSqlParam(PList, "@CategoryId", SqlDbType.Int, pr.CategoryId); DBHelper.AddSqlParam(PList, "@OnSale", SqlDbType.Bit, pr.OnSale); DBHelper.AddSqlParam(PList, "@Discontinued", SqlDbType.Bit, pr.Discontinued); int rowsModified = _dataAccess.InsertUpdateDelete(sql, PList); if (rowsModified > 0) ret = true; } catch (Exception) { throw; } return ret; } public bool UpdateProduct(Product pr) { bool ret = false; try { string sql = "Update Products set ProductName=@ProductName," + "Description=@Description,Price=@Price,StockLevel=@StockLevel," + "CategoryId=@CategoryId,OnSale=@OnSale," + "Discontinued=@Discontinued where ProductId=@ProductId"; List<SqlParameter> PList = new List<SqlParameter>(); DBHelper.AddSqlParam(PList, "@ProductId", SqlDbType.Int, pr.ProductId); DBHelper.AddSqlParam(PList, "@ProductName", SqlDbType.VarChar, pr.ProductName, 50); DBHelper.AddSqlParam(PList, "@Description", SqlDbType.Text, pr.Description); DBHelper.AddSqlParam(PList, "@Price", SqlDbType.Money, pr.Price); DBHelper.AddSqlParam(PList, "@StockLevel", SqlDbType.Int, pr.StockLevel); DBHelper.AddSqlParam(PList, "@CategoryId", SqlDbType.Int, pr.CategoryId); DBHelper.AddSqlParam(PList, "@OnSale", SqlDbType.Bit, pr.OnSale); DBHelper.AddSqlParam(PList, "@Discontinued", SqlDbType.Bit, pr.Discontinued); int rowsModified = _dataAccess.InsertUpdateDelete(sql, PList); if (rowsModified > 0) ret = true;
7
} catch (Exception) { throw; } return ret; } public Product GetProduct(int pid) { Product pr = null; try { string sql = "select * from Products where ProductId=@ProductId"; List<SqlParameter> PList = new List<SqlParameter>(); DBHelper.AddSqlParam(PList, "@ProductId", SqlDbType.Int, pid); DataTable dt = _dataAccess.GetManyRowsCols(sql, PList); if (dt.Rows.Count > 0) { DataRow dr = dt.Rows[0]; pr = new Product(); pr.ProductId = (int)dr["ProductId"]; pr.ProductName = (string)dr["ProductName"]; pr.Description = (string)dr["Description"]; pr.Price = (decimal)dr["Price"]; pr.StockLevel = (int)dr["StockLevel"]; pr.CategoryId = (int)dr["CategoryId"]; pr.OnSale = (bool)dr["OnSale"]; pr.Discontinued = (bool)dr["Discontinued"]; } } catch (Exception) { throw; } return pr; } public bool DeleteProduct(int pid) { bool ret = false; try { string sql = "Delete from Products where ProductId=@ProductId"; List<SqlParameter> ParamList = new List<SqlParameter>(); List<SqlParameter> PList = new List<SqlParameter>(); DBHelper.AddSqlParam(PList, "@ProductId", SqlDbType.Int, pid); int rows = _dataAccess.InsertUpdateDelete(sql,ParamList); if (rows > 0) ret = true; } catch (Exception) { throw;
8
} return ret; } }
By right clicking on the Controllers folder, add a controller called ProductsController as shown
below.
Add the following actions to it.
public class ProductsController : Controller { Repository _rep = new Repository(); public ActionResult ShowProducts() { ProductCatVM pcvm = new ProductCatVM(); List<Category> CList = _rep.GetCategories(); // convert List<category> to List<SelectListItem> so that it can // be bound to a drop down pcvm.CatList = new List<SelectListItem>(); foreach (Category cat in CList) { SelectListItem si = new SelectListItem {
9
Value = cat.CategoryId.ToString(), Text = cat.CategoryName }; pcvm.CatList.Add(si); } pcvm.categoryIdSelected = CList[0].CategoryId; pcvm.PList = _rep.GetProductsByCategory(pcvm.categoryIdSelected); return View(pcvm); } [HttpPost] public ActionResult ShowProducts(ProductCatVM pcvm) { List<Category> CList = _rep.GetCategories(); // convert List<category> to List<SelectListItem> so that it can // be bound to a drop down pcvm.CatList = new List<SelectListItem>(); foreach (Category cat in CList) { SelectListItem si = new SelectListItem { Value = cat.CategoryId.ToString(), Text = cat.CategoryName }; pcvm.CatList.Add(si); } pcvm.categoryIdSelected = pcvm.categoryIdSelected; pcvm.PList = _rep.GetProductsByCategory(pcvm.categoryIdSelected); return View(pcvm); } }
Right click on the ShowProducts action, and choose “Add View” with the following
specification.
10
Modify the code in the ShowProducts.cshtml to appear as:
@model ProductsApp.Models.ProductCatVM @{ ViewBag.Title = "ShowProducts"; } h2>Show Products</h2> @using (Html.BeginForm()) { @Html.AntiForgeryToken() <div class="form-horizontal"> <hr /> @Html.ValidationSummary(true, "", new { @class = "text-danger" }) <div class="row"> <div class="col-md-2"> @Html.Label("Category", htmlAttributes: new { @class = "control- label" }) @Html.DropDownListFor(model => model.categoryIdSelected, Model.CatList, new { htmlAttributes = new { @class = "form-control" } }) </div> <div class="col-md-10"> @{ Html.RenderPartial("_ShowProducts", Model.PList); } </div> </div> </div> } @section Scripts { <script> $(function () { // change event for the dropdown and post the form back to server $("#catgeoryIdSelected").css({ 'background-color': '#4499ee', 'color': 'yellow' }) $("#categoryIdSelected").change(function () { $(this).closest('form').submit(); }) }) </script> }
By right clicking on the Products folder (under the Views folder, add a partial view called
_ShowProducts as shown beow.
11
The generated code for the partial view _ShowProducts.cshtml appears as:
@model IEnumerable<ProductsApp.Models.Product> <table class="table"> <tr> <th> @Html.DisplayNameFor(model => model.ProductId) </th> <th> @Html.DisplayNameFor(model => model.ProductName) </th> <th> @Html.DisplayNameFor(model => model.Description) </th> <th> @Html.DisplayNameFor(model => model.Price) </th> <th> @Html.DisplayNameFor(model => model.StockLevel) </th> <th> @Html.DisplayNameFor(model => model.OnSale) </th> <th> @Html.DisplayNameFor(model => model.Discontinued) </th> <th></th> </tr> @foreach (var item in Model) { <tr> <td> @Html.DisplayFor(modelItem => item.ProductId) </td> <td> @Html.DisplayFor(modelItem => item.ProductName) </td>
12
<td> @Html.DisplayFor(modelItem => item.Description) </td> <td> @Html.DisplayFor(modelItem => item.Price) </td> <td> @Html.DisplayFor(modelItem => item.StockLevel) </td> <td> @Html.DisplayFor(modelItem => item.OnSale) </td> <td> @Html.DisplayFor(modelItem => item.Discontinued) </td> <td> @Html.ActionLink("Edit", "Edit", new { id=item.ProductId }) | @Html.ActionLink("Details", "Details", new { id=item.ProductId }) | @Html.ActionLink("Delete", "Delete", new { id=item.ProductId }) </td> </tr> } </table>
Add a link to the “ShowProducts” action in the _Layout.cshtml.
<ul class="nav navbar-nav"> <li>@Html.ActionLink("Home", "Index", "Home")</li> <li>@Html.ActionLink("About", "About", "Home")</li> <li>@Html.ActionLink("Contact", "Contact", "Home")</li> <li>@Html.ActionLink("Show Products", "ShowProducts", "Products")</li> </ul>
Build and test the application.
13
AJAX using plain Javascript and JQuery: As explained earlier, every browser provides the XmlHttpRequest component that has the
capability to make asynchronous calls back to the server where the page was obtained from,
obtain the data and update part of the page in a silent manner.
Add the following actions to the home controller.
public ActionResult GetStockPrice() { ViewBag.Symbol = "IBM"; return View("GetStockPrice"); } public ActionResult GetStockPriceAjax(string symbol) { string url = "http://www.nasdaq.com/aspx/infoquotes.aspx?symbol=" + symbol; WebClient wc = new WebClient(); string response = wc.DownloadString(url); // parse the response for int pos1 = response.IndexOf("LastSale1'>"); int pos2 = response.IndexOf("$ ", pos1 + 1); int pos3 = response.IndexOf("</label>", pos2 + 1); string price = response.Substring(pos2 + 7, pos3 - pos2 - 7).Trim(); return Content(price); }
The first action will send a simple form to the user so that the user can type in a stock symbol
and using the Javascript invoke the second action i.e., “GetStockPriceAjax” via
XmlHttpRequest. The action “GetStockPriceAjax” further makes a socket connection to the
Nasdaq web site using the WebClient class, obtains the response, and then filters it for just the
stock price, and then returns it as a simple string back to the caller.
Add a view by right clicking on the GetStockPrice action and choosing “Add View”.
Modify the code in the GetStockPrice.cshtml to appear as:
14
@{ ViewBag.Title = "GetStockPrice"; } <h2>Stock Price</h2> @using (Html.BeginForm()) { string symbol = ViewBag.Symbol; @Html.Label("Enter Stock Symbol") @Html.EditorFor(x=>symbol) <input type="button" onclick="getStock()" name="btnGetPrice" value="Get Stock Price Via Ajax" /> } Stock Price : <span id="stkPrice"></span> <hr/> <div id="status"></div> @section Scripts { <script> var xmlhttp; // global variable var count = 0; // global variable function getStock() { count++; // count = count + 1 document.getElementById("status").innerHTML = "count = " +count; getStockPrice(); setTimeout("getStock()", 8000); // 8 sec timer } function getStockPrice() { xmlhttp = new XMLHttpRequest(); // ajax component if (xmlhttp) // see if it got created properly { xmlhttp.onreadystatechange = myCallback; xmlhttp.open("GET", "GetStockPriceAjax?symbol=" + document.getElementById("symbol").value, true); xmlhttp.send(); // makes the socket connection } function myCallback() { if (xmlhttp.readyState == "4") // response is loaded { if (xmlhttp.status == "200") // OK { price = xmlhttp.responseText; document.getElementById("stkPrice").innerHTML = price } } } } </script> }
The above code uses a Javascript timer to periodically call the getStock function every 8
seconds. This function further calls the getStockPrice function which uses Ajax to trigger the
15
server side action GetStockPriceAjax and passes it the stock symbol using the get request. Once
the callback function receives the stock price, it displays it in a span.
Add a link to the GetStockPrice action in the _Layout.cshtml.
<li>@Html.ActionLink("Get Stock Price", "GetStockPrice", "Home")</li>
Build and test the application. You will notice that the stock price will quietly get updated every
eight seconds (if the stock market is active i.e., between 9 a.m. and 4 p.m.)
You will have to click on the “Get Stock Price Via Ajax” button one time. If you change the
symbol to any other e.g., AAPL, the price will change for the symbol, when the 8 second timer
expires and the Javascript makes another call to the server.
Instead of using XmlHttpRequest object directly, we can use JQuery to make ajax calls back to
the server. JQuery provides $.getJSON and $.ajax methods for this purpose.
Add an action called GetStockPriceJQ to the Home controller as shown below.
public ActionResult GetStockPriceJQ() { ViewBag.Symbol = "IBM"; return View(); }
Add the corresponding view by right clicking on the above action and choosing “Add View”.
Choose empty template. Type the following code in it.
@{ ViewBag.Title = "GetStockPriceJQ"; } <h2>Get Stock Price JQ</h2> @using (Html.BeginForm()) { string symbol = ViewBag.Symbol;
16
@Html.Label("Enter Stock Symbol") @Html.EditorFor(x => symbol) <input type="button" onclick="getStock()" name="btnGetPrice" value="Get Stock Price Via Ajax" /> } Stock Price : <span id="stkPrice"></span> <hr /> <div id="status"></div> @section Scripts { <script> var xmlhttp; // global variable var count = 0; // global variable function getStock() { count++; // count = count + 1 $("#status").html("count = " +count); getStockPrice(); setTimeout("getStock()", 8000); // 8 sec timer } function getStockPrice() { var url = "GetStockPriceAjax?symbol=" + $("#symbol").val(); $.getJSON(url, function (result) { $("#stkPrice").html(result); }); } </script> }
The above code uses getJSON method of JQuery to make an ajax call to the web server. Note
that it internally uses the XmlHttpRequest component to make the asynchronous call.
Add a link to the GetStockPriceJQ in the _Layout.cshtml file.
<li>@Html.ActionLink("Stock Price JQ", "GetStockPriceJQ", "Home")</li>
Build and test the above view. It should behave same as the GetStockPrice.
17
AJAX in MVC: For more involved views, where a portion of the view needs to be silently updated by going back
to the server, there are two approaches that we can use in MVC. The first to use JQuery on the
client side which will make an ajax call to the server. The server should return a partial view in
this case. The partial view has to be serialized to a JSON string in this technique.
The second technique requires minimal Javascript and the ajax behavior is accomplished via the
Ajax.BeginForm() extension method. It emits the proper Javacsript to the browser.
Since in the JQuery approach, the view has to be serialized as a JSON string, add a class called
ControllerBase to the Controllers folder with the following code in it.
public class ControllerBase : Controller { public string SerializeControl(string controlPath, object model) { ViewResult v = View(); if (String.IsNullOrEmpty(v.ViewName)) v.ViewName = RouteData.GetRequiredString("action"); ViewEngineResult result = null; StringBuilder sb = new StringBuilder(); StringWriter textWriter = new StringWriter(sb); HtmlTextWriter htmlWriter = new HtmlTextWriter(textWriter); if (v.View == null) { result = new ViewEngineResult(new RazorView(this.ControllerContext, controlPath, "", false, null), new RazorViewEngine()); v.View = result.View; } ViewContext viewContext = new ViewContext(ControllerContext, v.View, ViewData, TempData, htmlWriter); viewContext.ViewData.Model = model; v.View.Render(viewContext, htmlWriter); string rval = sb.ToString(); htmlWriter.Close(); textWriter.Close(); return rval; } public JsonResult ReturnJsonGet(string code, string result, string errors) { return Json(new { code = code, data = result, errors = errors }, JsonRequestBehavior.AllowGet); } }
The above code provides capability to be able to serialize any view (i.e., partial view in practical
cases) as a JSON string.
Modify the ProductsController to inherit from the ControllerBase as: public class ProductsController : ControllerBase
18
Add the following actions to the ProductsController.
public ActionResult ShowProductsAjax() { ProductCatVM pcvm = new ProductCatVM(); List<Category> CList = _rep.GetCategories(); // convert List<category> to List<SelectListItem> so that it can // be bound to a drop down pcvm.CatList = new List<SelectListItem>(); foreach (Category cat in CList) { SelectListItem si = new SelectListItem { Value = cat.CategoryId.ToString(), Text = cat.CategoryName }; pcvm.CatList.Add(si); } pcvm.categoryIdSelected = CList[0].CategoryId; pcvm.PList = _rep.GetProductsByCategory(pcvm.categoryIdSelected); return View(pcvm); }
public ActionResult GetProductsPartial(int id) // id is the categoryid { List<Product> PList = _rep.GetProductsByCategory(id); if (Request.IsAjaxRequest()) { return ReturnJsonGet("200", SerializeControl("~/Views/Products/_ShowProducts.cshtml", PList), ""); } return PartialView("_ShowProducts", PList); }
The GetProductsPartial method shown above will be invoked by JQuery from the client, and as
you can see, it returns the serialized partial view which the JQuery will render in the browser.
Add the view for ShowProductsAjax.
19
Type the following code in ShowProductsAjax.cshtml.
@model ProductsApp.Models.ProductCatVM @{ ViewBag.Title = "ShowProducts"; } <h2>ShowProducts</h2> @using (Html.BeginForm()) { @Html.AntiForgeryToken() <div class="form-horizontal"> <h4>ProductCatVM</h4> <hr /> @Html.ValidationSummary(true, "", new { @class = "text-danger" }) <div class="row"> <div class="col-md-2"> @Html.Label("Category", htmlAttributes: new { @class = "control- label" }) @Html.DropDownListFor(model => model.categoryIdSelected, Model.CatList, new { htmlAttributes = new { @class = "form-control" } }) </div> <div class="col-md-10" id="divProducts"> <!-- product for a selected category will be displayed here via JQuery--> </div> </div> </div> } <div> @Html.ActionLink("Back to List", "Index") </div> @section Scripts { <script> $(function () { $("#catgeoryIdSelected").css({ 'background-color': '#4499ee', 'color': 'yellow' }) $.getJSON("/Products/GetProductsPartial/" + $("#categoryIdSelected").val(), function (data) { if (data) $("#divProducts").html(data.data); else $("#divProducts").html("problem in loading data"); });
20
$("#categoryIdSelected").change(function () { //$(this).closest('form').submit(); // make an ajax call to receive the partialview for products, then // render it in divProducts $("#divProducts").html("Please wait, loading data.."); $.getJSON("/Products/GetProductsPartial/" + $("#categoryIdSelected").val(), function (data) { if (data) $("#divProducts").html(data.data); else $("#divProducts").html("problem in loading data"); }); }); }); </script> }
The above code makes an ajax call using getJSON to the GetProductsPartial action that returns
the JSON serialized partial view.
Add a link for GetProductsAjax to the _Layout.cshtml.
<li>@Html.ActionLink("Show Products Ajax", "ShowProductsAjax", "Products")</li>
Build and test the above capability.
If you make a selection change in the category dropdown, you will notice that the corresponding
products are obtained from the server and displayed quietly without the entire browser doing a
refresh.
Since the trip back to the web server may be slow depending upon the internet availability, it is a
good practice to let the user know that the data is being loaded whenever ajax is involved. To
simulate this behavior, add a Thread.Sleep statement to the GetProductsPartial action as shown
below.
21
public ActionResult GetProductsPartial(int id) // id is the categoryid {
Thread.Sleep(4000); List<Product> PList = _rep.GetProductsByCategory(id); if (Request.IsAjaxRequest()) { return ReturnJsonGet("200", SerializeControl("~/Views/Products/_ShowProducts.cshtml", PList), ""); } return PartialView("_ShowProducts", PList); }
Now test the GetProductsAjax page. You will see the “Please wait, loading data” message for a
few seconds before displaying the products information.
Ajax Using Ajax.BeginForm(): An alterative to the above ajax approach is to use the Ajax.BeginForm. This requires
minimal javascript and also does not require serialization of a partial view. The same
GetProductsAjax behavior can be accomplished by the following actions and the corresponding
view.
public ActionResult ProductsViaAF() { ProductCatVM pcvm = new ProductCatVM(); List<Category> CList = _rep.GetCategories(); // convert List<category> to List<SelectListItem> so that it can // be bound to a drop down pcvm.CatList = new List<SelectListItem>(); foreach (Category cat in CList) { SelectListItem si = new SelectListItem { Value = cat.CategoryId.ToString(), Text = cat.CategoryName }; pcvm.CatList.Add(si); }
22
pcvm.categoryIdSelected = CList[0].CategoryId; pcvm.PList = _rep.GetProductsByCategory(pcvm.categoryIdSelected); return View(pcvm); } public ActionResult GetProductsAF(string CategoryIdSelected) { List<Product> PList = null; if (CategoryIdSelected != null) PList = _rep.GetProductsByCategory(int.Parse(CategoryIdSelected)); else PList = _rep.GetProductsByCategory(int.Parse("100")); return PartialView("_ShowProducts", PList); }
Add a view called ProductsViaAF.
Type the following code in it.
@model ProductsApp.Models.ProductCatVM @{ ViewBag.Title = "ShowProductsAF"; } <h2>ShowProducts</h2> @Html.AntiForgeryToken() <div class="form-horizontal"> <hr /> @Html.ValidationSummary(true, "", new { @class = "text-danger" }) <div class="row"> <div class="col-md-2">
23
@using (Ajax.BeginForm("GetProductsAF", "Products", Model.categoryIdSelected, new AjaxOptions { InsertionMode = InsertionMode.Replace, HttpMethod = "Get", OnFailure = "ProductsLoadFailed", // javascript function OnSuccess = "ProductsLoadOK", AllowCache = false, LoadingElementId = "divLoading", UpdateTargetId = "divProducts", // PartialView will be displayed here })) // no need to serialize the partial view { @Html.Label("Category", htmlAttributes: new { @class = "control- label" }) @Html.DropDownListFor(model => model.categoryIdSelected, Model.CatList, new { htmlAttributes = new { @class = "form-control" } }) } </div> <div class="col-md-10" style="display:none" id="divLoading">
Please wait, loading data..</div> <div class="col-md-10" id="divProducts"> @{ Html.RenderPartial("_ShowProducts", Model.PList); } </div> </div> </div> <div id="#divStatus"></div> @section Scripts { $(function () { $("#catgeoryIdSelected").css({ 'background-color': '#4499ee', 'color': 'yellow' }) $("#categoryIdSelected").change(function () { $("#divLoading").html("Please wait, loading data.."); $("#divProducts").html(""); $(this).closest('form').submit(); }); }) function ProductsLoadFailed(response) { $("#divLoading").html("problem in receiving data..") } function ProductsLoadOK(response) { $("#divStatus").html("data loaded..") } </script> }
The above code uses the Ajax.BeginForm to set up an ajax request back to the web server.
Through the UpdateTargetId, the display for the partial view brought back is specified. The first
parameter to the Ajax.BeginForm method indicates the action that will be triggered via the ajax
call.
24
Add a link to the ProductsViaAF action in the _Layout.cshtml page.
<li>@Html.ActionLink("Show Products AF", "ProductsViaAF", "Products")</li>
If you build and trigger the “Show Products AF” link and then change the category, you will
notice that the category drop down disappears and the browser also does a full reload instead of a
silent update for the products corresponding to the category selected. This is because to be able
to use the Ajax.BeginForm, you need the Microsoft Unobtrusive Ajax library.
Using the Project->Manage Nuget Packages, browse for Unobtrusive library as shown below,
and then click install.
25
After the unobtrusive library is installed, add a reference to it in the ProductsViaAF.cshtml file
as shown below (in bold).
@section Scripts { <script src="~/Scripts/jquery.unobtrusive-ajax.min.js"></script> <script> $(function () { $("#catgeoryIdSelected").css({ 'background-color': '#4499ee', 'color': 'yellow' })
Now if you try the ProductsViaAF, it will have the proper Ajax behavior.
Modal Views in MVC: Modal views can be added to an application by using the JQuery showDialog() method. The
modal view itself is a partial view i.e., the javascript triggers an action that returns a partial view.
We will provide capability to be able to create a new product, be able to edit an existing product
and be able to delete a product. For these purposes, add a class called ProductCreateEditVM to
the models folder with the following code in it. This will be used in both creating a new product
and editing an existing product.
public class ProductCreateEditVM { public List<SelectListItem> CatList { get; set; } public Product Prod { get; set; } public int CategoryIdSelected { get; set; } }
For modal dialogs using JQuery, we need to download the jquery ui library using the Project-
>Manage Nuget packages, as shown below.
Search for “jquery ui” and install it in your project.
26
In the App_Start folder, add the following statements (shown in bold) to the Bundle.Config.cs
file.
public static void RegisterBundles(BundleCollection bundles) { bundles.Add(new ScriptBundle("~/bundles/jquery").Include( "~/Scripts/jquery-{version}.js")); bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include( "~/Scripts/jquery.validate*"));
bundles.Add(new ScriptBundle("~/bundles/jqueryui").Include( "~/Scripts/jquery-ui*")); bundles.Add(new ScriptBundle("~/bundles/modernizr").Include( "~/Scripts/modernizr-*")); bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include( "~/Scripts/bootstrap.js", "~/Scripts/respond.js")); bundles.Add(new StyleBundle("~/Content/css").Include( "~/Content/bootstrap.css",
"~/Content/themes/base/jquery-ui.min.css", // important for modal popup "~/Content/site.css")); }
Add the following actions to the Products controller.
public ActionResult ProductsModal() // displays cat dropdown and products {
27
ProductCatVM pcvm = new ProductCatVM(); List<Category> CList = _rep.GetCategories(); pcvm.CatList = new List<SelectListItem>(); foreach (Category cat in CList) { SelectListItem si = new SelectListItem { Value = cat.CategoryId.ToString(), Text = cat.CategoryName }; pcvm.CatList.Add(si); } pcvm.categoryIdSelected = CList[0].CategoryId; pcvm.PList = _rep.GetProductsByCategory(pcvm.categoryIdSelected); return View(pcvm); } [HttpPost] public ActionResult ProductsModal(ProductCatVM pcvm) { // once the ProductCatVM is posted to us, we need to repopulate the dropdown List<Category> CList = _rep.GetCategories(); // convert List<category> to List<SelectListItem> so that it can // be bound to a drop down pcvm.CatList = new List<SelectListItem>(); foreach (Category cat in CList) { SelectListItem si = new SelectListItem { Value = cat.CategoryId.ToString(), Text = cat.CategoryName }; pcvm.CatList.Add(si); } pcvm.PList = _rep.GetProductsByCategory(pcvm.categoryIdSelected); return View(pcvm); }
public ActionResult CreateProduct() { ProductCreateEditVM pcevm = new ProductCreateEditVM(); List<Category> CList = _rep.GetCategories(); // convert List<category> to List<SelectListItem> so that it can // be bound to a drop down pcevm.CatList = new List<SelectListItem>(); foreach (Category cat in CList) { SelectListItem si = new SelectListItem { Value = cat.CategoryId.ToString(), Text = cat.CategoryName }; pcevm.CatList.Add(si); } pcevm.CategoryIdSelected = CList[0].CategoryId;
28
pcevm.Prod = new Product(); return View(pcevm); } [HttpPost] public ActionResult CreateProduct(ProductCreateEditVM pcevm) { // always check if model state is valid before inserting/updating database if (!ModelState.IsValid) { return View(pcevm); } List<Category> CList = _rep.GetCategories(); // convert List<category> to List<SelectListItem> so that it can // be bound to a drop down pcevm.Prod.CategoryId = pcevm.CategoryIdSelected; pcevm.CatList = new List<SelectListItem>(); bool ret = false; try { ret = _rep.AddProduct(pcevm.Prod); } catch(Exception ex) { ViewBag.Status = ex.Message; } foreach (Category cat in CList) { SelectListItem si = new SelectListItem { Value = cat.CategoryId.ToString(), Text = cat.CategoryName }; pcevm.CatList.Add(si); } if (ret) ViewBag.Status = "Product created successfully.."; return View(pcevm); } public ActionResult EditProduct(int id) // has to be called id { ProductCreateEditVM pcevm = new ProductCreateEditVM(); pcevm.Prod = _rep.GetProduct(id); List<Category> CList = _rep.GetCategories(); // convert List<category> to List<SelectListItem> so that it can // be bound to a drop down pcevm.CatList = new List<SelectListItem>(); foreach (Category cat in CList) { SelectListItem si = new SelectListItem { Value = cat.CategoryId.ToString(),
29
Text = cat.CategoryName }; pcevm.CatList.Add(si); } pcevm.CategoryIdSelected = pcevm.Prod.CategoryId; // return View(pcevm); //for non modal return PartialView("_EditProduct", pcevm); } [HttpPost] public ActionResult EditProduct(ProductCreateEditVM pcevm) { // always check if model state is valid before inserting/updating database if (!ModelState.IsValid) { return View(pcevm); } List<Category> CList = _rep.GetCategories(); // convert List<category> to List<SelectListItem> so that it can // be bound to a drop down pcevm.Prod.CategoryId = pcevm.CategoryIdSelected; pcevm.CatList = new List<SelectListItem>(); bool ret = false; try { ret = _rep.UpdateProduct(pcevm.Prod); } catch (Exception ex) { ViewBag.Status = ex.Message; } foreach (Category cat in CList) { SelectListItem si = new SelectListItem { Value = cat.CategoryId.ToString(), Text = cat.CategoryName }; pcevm.CatList.Add(si); } if (ret) ViewBag.Status = "Product updated successfully.."; // return View(pcevm); return Json(new { result = true, responseText = "update successfull.." }); } public ActionResult DeleteProduct(string id, string did) { // id is catid, did is the product id try { bool ret = _rep.DeleteProduct(int.Parse(did)); if (ret) return new HttpStatusCodeResult(200);
30
else return new HttpStatusCodeResult(500); } catch (Exception ex) { TempData["Message"] = ex.Message; return new HttpStatusCodeResult(500); } }
Add a view called ProductsModal to the Products folder (under the views folder) with an empty
template.
Modify the code in the ProductsModal.cshtml to appear as:
@model ProductsApp.Models.ProductCatVM @{ ViewBag.Title = "ProductsModal"; } <h2>Index</h2> <p> @Html.ActionLink("Create New", "CreateProduct") </p> @using (Html.BeginForm()) { <table class="table"> <tr> <td> @Html.Label("Category") <br /> @Html.DropDownListFor(x => x.categoryIdSelected, Model.CatList) </td> <td> <table class="table"> <tr> <th> @Html.DisplayNameFor(model => model.PList[0].ProductId) </th> <th> @Html.DisplayNameFor(model => model.PList[0].ProductName) </th> <th> @Html.DisplayNameFor(model => model.PList[0].Description) </th> <th> @Html.DisplayNameFor(model => model.PList[0].Price) </th> <th> @Html.DisplayNameFor(model => model.PList[0].StockLevel) </th> <th> @Html.DisplayNameFor(model => model.PList[0].OnSale) </th> <th> @Html.DisplayNameFor(model => model.PList[0].Discontinued) </th>
31
<th></th> </tr> @foreach (var item in Model.PList) { <tr> <td> @Html.DisplayFor(modelItem => item.ProductId) </td> <td> @Html.DisplayFor(modelItem => item.ProductName) </td> <td> @Html.DisplayFor(modelItem => item.Description) </td> <td> @Html.DisplayFor(modelItem => item.Price) </td> <td> @Html.DisplayFor(modelItem => item.StockLevel) </td> <td> @Html.DisplayFor(modelItem => item.OnSale) </td> <td> @Html.DisplayFor(modelItem => item.Discontinued) </td> <td> @Html.ActionLink("Edit", "EditProduct", new { id = item.ProductId }, new { @class = "myedit" }) | @Html.ActionLink("Details", "Details", new { id = item.ProductId }) | @Html.ActionLink("Delete", "Delete", new { id = item.ProductId }, new { @class = "mydelete" }) </td> </tr> } </table> </td> </tr> </table> } <div>@ViewBag.Message</div> <div id="EditDialogDiv" style="display:none"></div> <div id="dlgConfirm" style="display:none" title="Delete Selected Row?"> <p> <span class="ui-icon ui-icon-alert" style="float:left;margin:0 7px 20px 0"></span> Selected Row Delete, Confirm? </p> </div> @section Scripts { <script src="~/Scripts/jquery-ui-1.12.1.min.js"></script> <script> $(function () {
32
// write the change event for the dropdown and post the form back to server $("#catgeoryIdSelected").css({ 'background-color': '#4499ee', 'color': 'yellow' }) $("#categoryIdSelected").change(function () { //alert('ok') $(this).closest('form').submit(); }); //-------------delete handling-------------------------- $(".mydelete").on("click", function (e) { // determine the row where delete is clicked var rowDel = $(this).closest('tr'); $("#dlgConfirm").show(); // make the div dlgConfirm visible $("#dlgConfirm").dialog({ buttons: { "Confirm": function () { $(this).dialog("close"); // determine the productid for the row pid = rowDel.children('td:first').text(); $.ajax({ url: "DeleteProduct/" + @Model.categoryIdSelected + "?did=" + pid, success: function () { rowDel.remove().draw(); $("#dlgConfirm").hide(); // make the div invisible }, error: function (xmlhttp, textStatus, errorThrown) { alert(errorThrown); } }); }, "Cancel": function () { $(this).dialog("close"); } }, "modal":true }); e.preventDefault(); return false; }); //------------------------------------------------------ //-------------------Edit Modal Processing--------------------- var DialogEdit = function () { this.showDialog = function (url) { $("#EditDialogDiv").dialog({ autoOpen: true, width: "600", resizable: false, title: "Edit Product", modal: true, closeText: "X", dialogClass: 'alert', position: { my: "center", at: "top+150" }, closeOnEscape: true,
33
open: function () { $(this).load(url); }, buttons: { "Edit Product": function () { editProductProcessing(url); // javascript function }, Cancel: function () { $(this).dialog("close"); } } }); } this.closeDialog = function () { $(this).dialog("close"); } } $(".myedit").on("click", function (e) { // click event for the edit hyperlink dialogEdit = new DialogEdit(); url = $(this).attr('href'); dialogEdit.showDialog(url); e.preventDefault(); // cancel the click event return false; }); }); function editProductProcessing(posturl) { // use ajax to post data back to server $.ajax({ url: posturl, type: "POST", data: $('form').serialize(), success: function (data) { if (data) { if (data.result == true) { $("#divStatus").html(data.responseText).removeClass("text- danger"); $("#divStatus").html(data.responseText).addClass("text- success"); } } }, error: function (XMLHttpRequest, textStatus, errorThrown) { $("#divStatus").removeClass("text-success"); $("#divStatus").html(textStatus + " Error:" + errorThrown).addClass("text-danger"); } }); } </script> }
Add a link to the ProductsModal in the _Layout.cshtml. <li>@Html.ActionLink("Products Modal", "ProductsModal", "Products")</li>
34
Study the code in the controller actions for creating, editing and deleting a product. Note that the
editing and deleting of products is initiated from the ProductsModal view via links in each
product row being displayed by the following code.
@Html.ActionLink("Edit", "EditProduct", new { id = item.ProductId }, new { @class = "myedit" }) | @Html.ActionLink("Details", "Details", new { id = item.ProductId }) | @Html.ActionLink("Delete", "Delete", new { id = item.ProductId }, new { @class = "mydelete" })
A css class of “myedit” is assigned to the edit link, and a css class of “mydelete” is assigned to
the delete link. The javascript (JQuery) code in the ProductsModal.cshtml uses these css classes
to issue an ajax call back to the server to obtain a partial view for editing an existing product, or
showing a delete confirmation when the user clicks on the delete link.
For example, when the user clicks on the delete link for a row, the following javascript and the
html in the ProductsModal.cshtml displays the modal delete confirmation dialog.
<div id="dlgConfirm" style="display:none" title="Delete Selected Row?"> <p> <span class="ui-icon ui-icon-alert" style="float:left;margin:0 7px 20px 0"></span> Selected Row Delete, Confirm? </p> </div> //-------------delete handling-------------------------- $(".mydelete").on("click", function (e) { // determine the row where delete is clicked var rowDel = $(this).closest('tr'); $("#dlgConfirm").show(); // make the div dlgConfirm visible $("#dlgConfirm").dialog({ buttons: { "Confirm": function () { $(this).dialog("close"); // determine the productid for the row pid = rowDel.children('td:first').text(); $.ajax({ url: "DeleteProduct/" + @Model.categoryIdSelected + "?did=" + pid,
35
success: function () { rowDel.remove().draw(); $("#dlgConfirm").hide(); // make the div invisible }, error: function (xmlhttp, textStatus, errorThrown) { alert(errorThrown); } }); }, "Cancel": function () { $(this).dialog("close"); } }, "modal":true }); e.preventDefault(); return false; });
If the user clicks on the confirm button, the above javascript makes an ajax call to the
DeleteProduct action and passes it the category id and the product id.
Similarly, study the code in the ProductsModal.cshtml for editing a product.
There is a link to the create product in the beginning of the ProductsModal.cshtml. Add a view
for the CreateProduct by choosing a template of create and model class of
ProductCreateEditVM. Modify the code in CreateProduct.cshtml to appear as:
@model ProductsApp.Models.ProductCreateEditVM @{ ViewBag.Title = "CreateProduct"; } <h2>Create New Product</h2> @using (Html.BeginForm()) { @Html.AntiForgeryToken()
36
<div class="form-horizontal"> <h4>Product</h4> <hr /> @Html.ValidationSummary(true, "", new { @class = "text-danger" }) <div class="form-group"> @Html.LabelFor(model => model.Prod.ProductId, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.Prod.ProductId, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Prod.ProductId, "", new { @class = "text-danger" }) </div> </div> <div class="form-group"> @Html.LabelFor(model => model.Prod.ProductName, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.Prod.ProductName, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Prod.ProductName, "", new { @class = "text-danger" }) </div> </div> <div class="form-group"> @Html.LabelFor(model => model.Prod.Description, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.Prod.Description, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Prod.Description, "", new { @class = "text-danger" }) </div> </div> <div class="form-group"> @Html.LabelFor(model => model.Prod.Price, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.Prod.Price, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Prod.Price, "", new { @class = "text-danger" }) </div> </div> <div class="form-group"> @Html.LabelFor(model => model.Prod.StockLevel, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.Prod.StockLevel, new { htmlAttributes = new { @class = "form-control" } })
37
@Html.ValidationMessageFor(model => model.Prod.StockLevel, "", new { @class = "text-danger" }) </div> </div> <div class="form-group"> @Html.Label("Category", htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.DropDownListFor(model => model.CategoryIdSelected, Model.CatList, new { htmlAttributes = new { @class = "form-control" } }) </div> </div> <div class="form-group"> @Html.LabelFor(model => model.Prod.OnSale, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> <div class="checkbox"> @Html.CheckBoxFor(model => model.Prod.OnSale) @Html.ValidationMessageFor(model => model.Prod.OnSale, "", new { @class = "text-danger" }) </div> </div> </div> <div class="form-group"> @Html.LabelFor(model => model.Prod.Discontinued, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> <div class="checkbox"> @Html.CheckBoxFor(model => model.Prod.Discontinued) @Html.ValidationMessageFor(model => model.Prod.Discontinued, "", new { @class = "text-danger" }) </div> </div> </div> <div class="form-group"> <div class="col-md-offset-2 col-md-10"> <input type="submit" value="Create Product" class="btn btn-default" /> </div> </div> </div> <div>@ViewBag.Status</div> } <div> @Html.ActionLink("Back to List", "ProductsModal") </div> @section Scripts { @Scripts.Render("~/bundles/jqueryval") }
38
For editing a product, we need a partial view, as the editing view will be displayed in a modal
dialog. Add a partial view to the Products folder (under the Views) with the following
specification.
Modify the code in the _EdtProduct.cshtml to appear as:
@model ProductsApp.Models.ProductCreateEditVM @{ ViewBag.Title = "Edit Product"; Layout = null; } <h2>Edit Product</h2> @using (Html.BeginForm()) { @Html.AntiForgeryToken() <div class="form-horizontal"> <h4>Product</h4> <hr /> @Html.ValidationSummary(true, "", new { @class = "text-danger" }) <div class="form-group"> @Html.LabelFor(model => model.Prod.ProductId, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.Prod.ProductId, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Prod.ProductId, "", new { @class = "text-danger" }) </div> </div>
39
<div class="form-group"> @Html.LabelFor(model => model.Prod.ProductName, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.Prod.ProductName, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Prod.ProductName, "", new { @class = "text-danger" }) </div> </div> <div class="form-group"> @Html.LabelFor(model => model.Prod.Description, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.Prod.Description, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Prod.Description, "", new { @class = "text-danger" }) </div> </div> <div class="form-group"> @Html.LabelFor(model => model.Prod.Price, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.Prod.Price, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Prod.Price, "", new { @class = "text-danger" }) </div> </div> <div class="form-group"> @Html.LabelFor(model => model.Prod.StockLevel, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.Prod.StockLevel, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Prod.StockLevel, "", new { @class = "text-danger" }) </div> </div> <div class="form-group"> @Html.Label("Category", htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.DropDownListFor(model => model.CategoryIdSelected, Model.CatList, new { htmlAttributes = new { @class = "form-control" } }) </div> </div> <div class="form-group">
40
@Html.LabelFor(model => model.Prod.OnSale, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> <div class="checkbox"> @Html.CheckBoxFor(model => model.Prod.OnSale) @Html.ValidationMessageFor(model => model.Prod.OnSale, "", new { @class = "text-danger" }) </div> </div> </div> <div class="form-group"> @Html.LabelFor(model => model.Prod.Discontinued, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> <div class="checkbox"> @Html.CheckBoxFor(model => model.Prod.Discontinued) @Html.ValidationMessageFor(model => model.Prod.Discontinued, "", new { @class = "text-danger" }) </div> </div> </div> </div> <div>@ViewBag.Status</div> <div id="divStatus"></div> } @section Scripts { @Scripts.Render("~/bundles/jqueryval") }
Build and test the creating, editing of a product via the Products Modal link.
41