After writing this post, I realized that it is obnoxiously long, and maybe includes a little bit too much hand-holding. Nevertheless, I decided to post this very detailed step-by-step walkthrough of implementing a simple AJAX form in MVC3 with Razor. I'm probably going also post a shortened version of this topic for those that don't need a complete walkthrogh.
So I've been attempting to dive into ASP.NET MVC 3 lately, and have hit plenty of bumps along the way. This is my first encounter with any ASP.NET, or web development in general, so it is not surprising that I've had so much trouble. I spent about 3 evenings on the problem I'm describing below, so I decided to write up a quick post about it. Please remember that this is my first encounter with ASP.NET, so it is definitely possible that there are better ways to do this. If I do eventually find one, I will update this post. Feel free to educate me if you find any errors.
Problem: I want to have a page that lists a bunch of items (household products in my example). From this page I also want to be able to add items. When I add an item, I don't want to have to reload the entire page, but just the list of items.
Soultion:
For my example I will be using the following model (Product) and data context (BoringStoreContext):
namespace BoringStore.Models
{
public class Product
{
public int ID { get; set; }
public string Name { get; set; }
[DataType(DataType.Currency)]
public decimal Price { get; set; }
}
public class BoringStoreContext : DbContext
{
public DbSet<Product> Products { get; set; }
}
}
Normally I generate a controller with read, write and views already implemented, then just bend it to my will. However, for this post I will do an empty controller so that I can focus on the things we NEED.
// Controllers\ProductController.cs
namespace BoringStore.Controllers
{
public class ProductController : Controller
{
public ActionResult Index()
{
BoringStoreContext db = new BoringStoreContext();
return View(db.Products);
}
}
}
This only created the controller for me (in <project>\Controllers), so I now need to create a strongly typed view (Model class = IEnumerable<Product>) in Views\Product. I'm going to create this new view to map to the Index command, so the view will be named Index.cshtml, and I end up with:
@* Views\Product\Index.cshtml *@
@model IEnumerable<BoringStore.Models.Product>
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
So this is the page that I'm going to use to both list Products and allow the user to add Products.We'll start with the list of products. This will be done in its own Partial View, ProductListControl.cshtml.
@* Views\Product\ProductListControl.cshtml *@
@model IEnumerable<BoringStore.Models.Product>
<table>
<!-- Render the table headers. -->
<tr>
<th>Name</th>
<th>Price</th>
</tr>
<!-- Render the name and price of each product. -->
@foreach (var item in Model)
{
<tr>
<td>Html.DisplayFor(model => item.Name)</td>
<td>Html.DisplayFor(model => item.Price)</td>
</tr>
}
</table>
Now that we have created our partial view, we need to go back to the Index.cshtml to tell it to render the partial view with the model data passed to it. This is done through the @Html.RenderPartial command. the div id is important as you will see later.
Take a moment to launch your site, visit the ~/Product page, and make sure you see an empty table. If you don't, you did something wrong, so start over.
Now we want to be able to add Products on the same page, but we want to be able to do so without reloading the entire page. We only want to reload the partial view where the new Product will be displayed. To do this we will start in the controller. We now need to be able to pass in a new Product as well as a list of available products to the Index view. This means we need an ProductIndexViewModel. See Rachel Appel's post for more information on ViewModels in MVC here: http://rachelappel.com/use-viewmodels-to-manage-data-amp-organize-code-in-asp.net-mvc-applications
// ViewModels\ProductIndexViewModel.cs
namespace BoringStore.ViewModels
{
public class ProductIndexViewModel
{
public Product NewProduct { get; set; }
public IEnumerable<Product> Products { get; set; }
}
}
We now need to update the controller to build this ViewModel and pass it to the view.
// In Controllers\ProductController.cs
public ActionResult Index()
{
BoringStoreContext db = new BoringStoreContext();
ProductIndexViewModel viewModel = new ProductIndexViewModel
{
NewProduct = new Product(),
Products = db.Products
};
return View(viewModel);
}
And now, update the view to use a ProductIndexViewModel. Note that we have to update the model passed to the @Html.RenderPartial command to Model.Products.
@* Views\Product\Index.cshtml *@
@model BoringStore.ViewModels.ProductIndexViewModel
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<div id='productList'>
@{ Html.RenderPartial("ProductListControl", Model.Products); }
</div>
Now that we have the ViewModel, we can start on the form for adding a Product. Let's jump back over to the Controller again, and add the following function:
// In Controllers\ProductController.cs
public ActionResult Index_AddItem(ProductIndexViewModel viewModel)
{
BoringStoreContext db = new BoringStoreContext();
db.Products.Add(viewModel.NewProduct);
db.SaveChanges();
return PartialView("ProductListControl", db.Products);
}
This is the action that the AJAX form is going to call to add an item. Here we simply add the item to our database context and then return a PartialView of the ProductListControl view that we created earlier. Now lets insert the AJAX form into the Index view:
<!-- Added to Views\Product\Index.cshtml -->
<script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>
@using (Ajax.BeginForm("Index_AddItem", new AjaxOptions { UpdateTargetId = "productList" }))
{
<div>
@Html.LabelFor(model => model.NewProduct.Name)
@Html.EditorFor(model => model.NewProduct.Name)
</div>
<div>
@Html.LabelFor(model => model.NewProduct.Price)
@Html.EditorFor(model => model.NewProduct.Price)
</div>
<div>
<input type="submit" value="Add Product" />
</div>
}
Here we have created an AJAX form with lables and editors for the Product. Take note of the UpdateTargetID in the AjaxOptions. This is telling the renderer that the div that we created earlier with the partial view in it is what should be updated with the return value from the action called when this form is submitted. Launch your site and try it out. You should be able to enter data for a new Product, hit the Add Product button, and the table below should update with the product you just added.
Let me know if you have any issues or if you find an alternative that you believe is better.
[TODO Add final source code here]
<!-- Appended to Views\Product\Index.cshtml -->
<div id='productList'>
@{ Html.RenderPartial("ProductListControl", Model); }
</div>