Introduction
In this article, we will see how to add Edit and Delete features to a WebGrid in ASP.NET MVC 5.
Using the code
In this article, I used EntityFramework to read data from DB. For Edit and Delete, I used in -memory collections.
This is the table I used.
Add a controller to the Controllers folder. To do this, right click Controllers folder -> Add -> Controller. In the dialog box select the option as shown in the image below and click ADD.
In the "Add Controller" dialog, select the Model class as Person. Change the name of the controller to the name you want. I named my controller as Default2. Click the Add button to add Controller and generate the corresponding views.
Before going to the view and adding code for WebGrid, I am going to add a property of type List<string> to the auto generated Person class.
Note: It is not recommeded to modify the autogenerated class. I am using this approach just for the purpose of this article.
public partial class Person
{
public int PersonId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Gender { get; set; }
public string Phone { get; set; }
public List<string> _genderList = new List<string>() {"Male", "Female"};
public List<string> GenderList
{
get { return _genderList; }
set { _genderList = value; }
}
}
I am making use of CSS and jQuery Toggle function to show and hide controls. Idea was taken from the article http://www.mikesdotnetting.com/article/202/inline-editing-with-the-webgrid
.
Open the Site.css file from Content folder and add the below CSS classes to it:
.webGrid {
margin: 4px;
border: 1px solid;
background-color: burlywood;
width: 500px;
}
.webGrid tr td {
border: 1px solid;
}
.header {
background-color:antiquewhite;
}
.altColor {
background-color: darkgray;
}
.button {
width: 50px;
}
span {
padding-left: 4px;
}
Add a JavaScript file to the Scripts folder and name it Custom.js. This file is going to contain the event handlers for the buttons. Add the following code to the file.
$(function() {
$('.edit').hide();
$('.edit-case').on('click', function() {
var tr = $(this).parents('tr:first');
tr.find('.edit, .read').toggle();
});
$('.update-case').on('click', function (e) {
e.preventDefault();
var tr = $(this).parents('tr:first');
id = $(this).prop('id');
var gender = tr.find('#Gender').val();
var phone = tr.find('#PhoneNo').val();
$.ajax({
type: "POST",
contentType: "application/json; charset=utf-8",
url: "http://localhost:55627/Default2/Edit",
data: JSON.stringify({ "id":id,"gender": gender, "phone": phone }),
dataType: "json",
success: function(data) {
tr.find('.edit, .read').toggle();
$('.edit').hide();
tr.find('#gender').text(data.person.Gender);
tr.find('#phone').text(data.person.Phone);
},
error: function (err) {
alert("error");
}
});
});
$('.cancel-case').on('click', function (e) {
e.preventDefault();
var tr = $(this).parents('tr:first');
var id = $(this).prop('id');
tr.find('.edit, .read').toggle();
$('.edit').hide();
});
$('.delete-case').on('click', function(e) {
e.preventDefault();
var tr = $(this).parents('tr:first');
id = $(this).prop('id');
$.ajax({
type: 'POST',
contentType: "application/json; charset=utf-8",
url: "http://localhost:55627/Default2/Delete/" + id,
dataType: "json",
success: function (data) {
alert('Delete success');
window.location.href = "http://localhost:55627/Default2/Index";
},
error: function() {
alert('Error occured during delete.');
}
});
});
});
Now we are ready to change the view. Open the Index.cshtml file from Views-> Default2 folder. Remove the autogenerated code from the view and change it to look like the below.
In the WebGrid, I am making only two fields as editable. One is Gender and the other is Phone number. Gender is a dropdown list and Phone number is a text box.
@model IEnumerable<MVCApp.Models.Person>
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<p>
@Html.ActionLink("Create New", "Create")
</p>
<script src="~/Scripts/Custom.js"></script>
<div id=" grid">
@{
var gender = Model.Select(app => app.GenderList);
var gender1 = from name in gender.First()
select new SelectListItem
{
Value = name,
Text = name
};
var gridview = new WebGrid(source: Model,rowsPerPage:5);
}
@gridview.GetHtml(tableStyle: "webGrid", headerStyle: "header", alternatingRowStyle: "altColor",
columns:
gridview.Columns(
gridview.Column("", style: "button",
format:
@<text>
<button class="edit-case read" id="@item.PersonId">Edit</button>
<button class="delete-case read" id="@item.PersonId">Delete</button>
<button class="update-case edit" id="@item.PersonId">Update</button>
<button class="cancel-case edit" id="@item.PersonId">Cancel</button>
</text>),
gridview.Column("Gender",canSort:false,
format:
@<text>
<span id="gender" class="read">@item.Gender</span>
@Html.DropDownList("Gender", gender1, new { @class = "edit" })
</text>),
gridview.Column("FirstName"),
gridview.Column("LastName"),
gridview.Column("Phone",
format:
@<text>
<span id="phone" class="read">@item.Phone</span>
@Html.TextBox("PhoneNo", (string)item.Phone, new { @class = "edit" })
</text>)
))
</div>
In the above view, the following piece of code, creates the items for the dropdown list.
@{
var gender = Model.Select(app => app.GenderList);
var gender1 = from name in gender.First()
select new SelectListItem
{
Value = name,
Text = name
};
}
Since we are calling the Edit and Delete controller action methods using AJAX, we have to make some changes to them. As I mentioned at the beginning of the article, I will not hit the DB for Update and Delete operations and the below methods make use of in-memory collections. You can add your logic to interact with the DB for Update and Delete operations.
Note that these methods are returning JsonResult instead of ActionResult. Also these methods are missing [ValidateAntiForgeryToken] attribute.
[HttpPost]
public JsonResult Edit(int id, string gender, string phone)
{
Person person = db.People.Find(id);
person.Gender = gender;
person.Phone = phone;
return Json(new { person }, JsonRequestBehavior.AllowGet);
}
[HttpPost, ActionName("Delete")]
public JsonResult DeleteConfirmed(int id)
{
List<Person> person = (List<Person>) Session["Persons"];
person = person.Where(p => p.PersonId != id).ToList();
Session["Persons"] = person;
bool result = true;
return Json(new {result}, JsonRequestBehavior.AllowGet );
}
The changes to Edit and Delete methods have cascading effect and need some changes to Index method as well. Change the Index method like below.
public ActionResult Index()
{
if (Session["Persons"] != null)
{
return View(List<Person> Session["Persons"]);
}
Session["Persons"] = db.People.ToList();
return View(db.People.ToList());
}
And we have to add Session_Start() method to the Global.asax.cs for this to work.
protected void Session_Start()
{
Session["Persons"] = null;
}
Now we are all set to run and test the code. Run the project and navigate to Default2 contoller's Index method to see the WebGrid.
Click Delete button to delete a record. Once the record is deleted, we can see the Paging details at the footer of the WebGrid is gone as shown in the screen shot below. Because I had a total of 6 records and set the page size to 5 to demonstate this.
Let's try editing some records now. Click Edit button to edit a record. Once the Edit button is clicked, you can see, Gender column change into dropdown list with the correct option selected and Phone change into a text box with the current phone number on it.
Also you can see, Update and Cancel buttons appear and Edit and Delete buttons disappear.
Let us change the value of Phone number.
Click Update button to save the changes and go back to display mode.
To check if Cancel is working fine, click Edit on record with FirstName as Hanna
and change gender from Female to Male and the phone number.
Click Cancel button to discard the changes made and return to display mode.
Let us click the Edit button of the same row again. To our surprise, we see, the data that was chaged in the gender and phone number fields before cancel appearing instead of the values that were seen during the display of that row.
This is because, when Cancel button is clicked, it just toggles the controlles, but does not reset the value of the edit controls. To solve this the values of the edit controlls has to be reverted back to they were before the user changed them. To do this, change the below method to
$('.edit-case').on('click', function() {
var tr = $(this).parents('tr:first');
tr.find('.edit, .read').toggle();
});
this:
$('.edit-case').on('click', function() {
var tr = $(this).parents('tr:first');
var gender = tr.find('#gender').text();
var phonenumber = tr.find('#phone').text();
tr.find('.edit, .read').toggle();
tr.find('#Gender').val(gender);
tr.find('#PhoneNo').val(phonenumber);
});
This function reads the value displayed during the display mode, and sets them to as the vaule of the controls during edit mode.
Refresh the page and click Edit on the same row. Change the value of Gender to Male and Phone and click Cancel. The changes will be cancelled and the original data will be displayed.
Click Edit the same row again, to see if it displays the correct values now.
And we see that it shows the correct values now. That's it, we are done.
Explaining the Code
This method will be executed when the Delete button is clicked. The catch with Delete functionality is, the data needs to be reloaded.
$('.delete-case').on('click', function(e) {
e.preventDefault();
var tr = $(this).parents('tr:first');
id = $(this).prop('id');
$.ajax({
type: 'POST',
contentType: "application/json; charset=utf-8",
url: "http://localhost:55627/Default2/Delete/" + id,
dataType: "json",
success: function (data) {
alert('Delete success');
window.location.href = "http://localhost:55627/Default2/Index";
},
error: function() {
alert('Error occured during delete.');
}
});
});
This line of code redirects to Index action method of Default2 controller resulting in data reload.
window.location.href = "http://localhost:55627/Default2/Index";
This method will be executed Update button is clicked.
$('.update-case').on('click', function (e) {
e.preventDefault();
var tr = $(this).parents('tr:first');
id = $(this).prop('id');
var gender = tr.find('#Gender').val();
var phone = tr.find('#PhoneNo').val();
$.ajax({
type: "POST",
contentType: "application/json; charset=utf-8",
url: "http://localhost:55627/Default2/Edit",
data: JSON.stringify({ "id":id,"gender": gender, "phone": phone }),
dataType: "json",
success: function(data) {
tr.find('.edit, .read').toggle();
$('.edit').hide();
tr.find('#gender').text(data.person.Gender);
tr.find('#phone').text(data.person.Phone);
},
error: function (err) {
alert("error");
}
});
});
In this method, following lines are used to get the input parameters for the Edit controller action method
var tr = $(this).parents('tr:first');
id = $(this).prop('id');
var gender = tr.find('#Gender').val();
var phone = tr.find('#PhoneNo').val();
If you see the Edit controller action method, it returns the updated Person object. The success
method of AJAX gets this object and sets the updated values to the correponding cells in WebGrid. This is done by these lines of code.
success: function(data) {
tr.find('.edit, .read').toggle();
$('.edit').hide();
tr.find('#gender').text(data.person.Gender);
tr.find('#phone').text(data.person.Phone);
}
Disadvantages
Inline editing using this approach has the following disadvantages.
1) Since the POST methods of Edit and Delete are called using AJAX, [ValidateAntiForgeryToken] attribute cannot be used on the controller action methods. This makes these methods vulnearable to CSRF attacks.
2) The option (ajaxUpdateContainerId
) cannot be used with this implementation. If you use that option, it will create issues in control rendering during paging.
Points of Interest
If you use [ValidateAntiForgeryToken] on the controller action methods, you will get "The required anti-forgery cookie "__RequestVerificationToken" is not present."
error message.
History
07/06/2015 - First Version