For the code, go here: https://github.com/NewHope1/KendoGridBatchPageSubmit.git
In my previous blog I demonstrated how to put the Kendo grid in batch mode and submit all CRUD operations with the grid’s built-in save button. The grid performed as needed; we were able to save all operations in the CRUD handlers. Typically, however, the gird won’t be the only component in a view; there would be other fields in the view whose values need to be committed to the database.
Consider the view below. It has customer Name, Title, and Address that need to be saved along with the customer orders in the grid. The grid’s built-in “save” button (in the previous blog, not show in view here) only saves the grid’s rows, it has nothing to do with any other fields in the view; it certainly won’t postback the view to the controller. The typical thing to do here is to simply slap a submit button which postback the entire view fields to the controller and from the controller we read the fields valus and store them in the database. Well, the Kend Grid, for whaterver reason I don’t want to get into, does not postback its rows. So if you go to the postback method in the controller, you won’t be able to see the grid rows. Let me demonstrate this limitation.
Simply putting a Submit button that posts the view fields won’t completly work. The grid won’t send its rows to the controller.
Let’s makes some changes to the customer info and its orders (changed customer Name, Title, Address, changed Freight in the first order, added a new order)
Click Submit —
You can see the Orders in the ViewModel are empty. I even tried to return the bound ViewModel orders and that’s null.
I found sporatic mention of this limitation between in the Telerik echosystem and Stack Overflow on this issue, nothing that offered a complete solution to this limitation. I set out to provide a solution to this limitation.
So do do this now we’re going to remove the grid’s Save records button and the
- Add three hidden fields to the view to hold added, updated, and deleted rows
- On the client, when pressing the submit button, capture affected rows and store them in the hidden fields
- In the Controller, in the Edit postback method definition, add the FormsCollection parameter
- Read out the values of the hidden fields from the FormsCollection parameter into variables
- Parse out the row values into entities
- Commit the entities into the database
Notice that we now only need to have the Read handler in the grid; the Create, Update, and Delete handlers are not needed and should be removed so that they don’t get called asynchronously when we submit the view. All CRUD processing and commiting to the datbase is going to be handled all in tle postback Edit method.
1. Add three hidden fields to the view to hold added, updated, and deleted rows
@model KendoGridBatch.Customer
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
@using Kendo.Mvc.UI
@using (Html.BeginForm())
{
@*
Kendo Grid doesn't' return its rows when submitted with the view submit button, so
we use these hidden fields to send the rows to the controller
*@
@Html.Hidden("CreatedRowsData");
@Html.Hidden("UpdatedRowsData");
@Html.Hidden("DeletedRowsData");
2. On the client, when pressing the submit button, capture affected rows and store them in the hidden fields
<script>
// Handler called when clicking Save button on form
$("#saveObsBtn").click(function ()
{
var grid = $("#grid").data("kendoGrid");
//get the new and the updated rows
var currentData = grid.dataSource.data();
var updatedRecords = [];
var newRecords = [];
for (var i = 0; i < currentData.length; i++)
{
if (currentData[i].isNew())
{
//this row is new
newRecords.push(currentData[i]);
}
else if (currentData[i].dirty)
{
//this row is updated
updatedRecords.push(currentData[i].toJSON());
}
}
//get the deleted rows
var deletedRecords = [];
for (var i = 0; i < grid.dataSource._destroyed.length; i++)
{
deletedRecords.push(grid.dataSource._destroyed[i].toJSON());
}
//added, updated and deleted rows will be picked up from these fields on the server
$('#CreatedRowsData').val(kendo.stringify(newRecords));
$('#UpdatedRowsData').val(kendo.stringify(updatedRecords));
$('#DeletedRowsData').val(kendo.stringify(deletedRecords));
})
</script>
3. In the controller, in the Edit postback method definition, add the FormsCollection parameter
[AcceptVerbs("Post")]
public ActionResult Edit(string lookupBtn, string submitBtn, FormCollection fc, Customer viewModel, [Bind(Prefix = "models")] IEnumerable<Order> orders)
{
FormsCollection fc parameter contains the hidden fields
4. Read out the row values from the FormsCollection into variables
var addedRows = fc["CreatedRowsData"];
var updatedRows = fc["UpdatedRowsData"];
var deletedRows = fc["DeletedRowsData"];
5. Parse out the row values into entities
List<Order> addedOrders = getOrders(addedRows);
List<Order> updatedOrders = getOrders(updatedRows);
List<Order> deletedOrders = getOrders(deletedRows);
getOrders is a custom method to parse out the entities from the string rows:
/// <summary>
/// Return a list of Order objects from a string record
/// </summary>
/// <param name="record">input string with columns delimeted by comma. For example:
/// "[{\"OrderID\":10254,\"CustomerID\":null,\"EmployeeID\":null,\"OrderDate\":\"1996-07-11T04:00:00.000Z\",\"RequiredDate\":\"1996-08-08T04:00:00.000Z\",\"ShippedDate\":\"1996-07-23T04:00:00.000Z\",\"ShipVia\":2,\"Freight\":45,\"ShipName\":\"Chop-suey Chinese\",\"ShipAddress\":\"Hauptstr. 31\",\"ShipCity\":\"Bern\",\"ShipRegion\":null,\"ShipPostalCode\":\"3012\",\"ShipCountry\":\"Switzerland\",\"Customer\":null,\"Order_Details\":[]}]"
/// This parameter comes from the client side
/// </param>
/// <returns>List of Order objects</returns>
private List<Order> getOrders(string record)
{
if (record == "[]") // no new rows added to grid
{
return null;
}
List<Order> rVal = new List<Order>();
Dictionary<string, string> columns = new Dictionary<string, string>();
string tempRecord = record;
ArrayList recs = new ArrayList();
var matcher = new Regex(@"{(.*?)}"); // match all records in between { and }
var matches = matcher.Matches(record).Cast<Match>().Select(m => m.Value).Distinct(); // cast to IEnumerable so we can operate on
foreach (string match in matches)
{
tempRecord = match.Replace("{", String.Empty); // remove beginning {
tempRecord = tempRecord.Replace("}", String.Empty); // remove ending }
tempRecord = tempRecord.Replace("\"", String.Empty); // remove all double quotes
recs.Add(tempRecord);
};
// loop through all records (rows) and convert each one into an entity
foreach (String rec in recs)
{
var fields = rec.Split(',');
// loop through the individual record fields
foreach (var field in fields)
{
var fieldName = field.Split(':')[0];
// Deal with OrderDate and RequiredDate fields case. E.g., OrderDate:1996-07-11T04:00:00.000Z
string fieldValue = ( (fieldName == "OrderDate" || fieldName == "RequiredDate" || fieldName == "ShippedDate") && field.Split(':')[1].Length != 0 && field.Split(':')[1] != "null" ? (field.Split(':')[1] + ":" + field.Split(':')[2] + ":" + field.Split(':')[3]) : field.Split(':')[1]);
columns.Add(fieldName, fieldValue);
}
Order newObj = new Order();
newObj.OrderID = columns["OrderID"] != "null" ? Convert.ToInt16(columns["OrderID"]) : 0;
newObj.CustomerID = columns["CustomerID"] != "null" ? columns["CustomerID"] : String.Empty;
newObj.Freight = Convert.ToDecimal(columns["Freight"]);
newObj.ShipCity = columns["ShipCity"];
newObj.ShipCountry = columns["ShipCountry"];
DateTime orderDate;
newObj.OrderDate = DateTime.TryParseExact(columns["OrderDate"], "yyyy-MM-ddThh:mm:ss.000Z", null, System.Globalization.DateTimeStyles.None, out orderDate) ? orderDate : DateTime.MinValue;
rVal.Add(newObj);
columns.Clear();
}
return rVal;
}
I’d say the above code is the most involved code in this solution. Yes it may look rather cluegy, but it works and gets the job done (as much as I strive for good design, at times there is a necessity to compromise for the sake of getting things done).
6. Commit the entities into the database
// process addedRows
if (addedOrders != null)
{
addedOrders.ForEach(o =>
{
if (!customer.Orders.ToList().Exists(c => c.OrderID == o.OrderID))
{
var newOrder = _database.Orders.Create();
newOrder.OrderDate = o.OrderDate;
newOrder.Freight = o.Freight;
newOrder.ShipCity = o.ShipCity;
newOrder.ShipCountry = o.ShipCountry;
newOrder.OrderID = newOrder.OrderID + 1; // to allow adding more than one row so that the next time around in the loop, it doesn't find that this is an existing row
customer.Orders.Add(newOrder);
}
});
}
// process updatedRows
if (updatedOrders != null)
{
updatedOrders.ForEach(u =>
{
if (customer.Orders.ToList().Exists(c => c.OrderID == u.OrderID))
{
var existingOrder = customer.Orders.AsQueryable().FirstOrDefault(f => f.OrderID == u.OrderID);
existingOrder.OrderDate = u.OrderDate;
existingOrder.Freight = u.Freight;
existingOrder.ShipCity = u.ShipCity;
existingOrder.ShipCountry = u.ShipCountry;
}
});
}
// process deletedRows
if (deletedOrders != null)
{
deletedOrders.ForEach(d =>
{
if (customer.Orders.ToList().Exists(c => c.OrderID == d.OrderID))
{
var existingOrder = customer.Orders.AsQueryable().FirstOrDefault(f => f.OrderID == d.OrderID);
_database.Entry(existingOrder).State = System.Data.Entity.EntityState.Deleted;
}
});
}
_database.SaveChanges(); // Commit to database
Test Code
So let’s run through a test now. I made the following changes to the records I show at the beginning of this blog:
- Changed cutometer info (Name, Title, and Address)
- Delete last two orders 11029 and 11041 (not showing in picture after been deleted)
- Add two new orders (their Order ID showing as 0)
- Updated order 10254 Freight and order 10370 City
Notice how the Kendo Grid conveniently marks added and updated cells with a small triangle in the cell’s top left corner. We click Submit…
Inspecting the FormsCollection parameter coming into the Edit method postback, we see it has the changed rows:
After the save we see the records all saved accrodingly:
In this blog I have demonstrated a resolution to a Kendo Grid Limitation – the grid’s inability to post its rows to the controller using a view submit button. I overcame this limitation by following a traditional programming “hack” of utilizing hidden fields and accessing those fields from the controller on the server, then I parsed out the fields and created EF entities that I eventually saved to the database.
I hope this solutions helps you in your specific application need.