Kendo.Grid does not send some data to the controller on "Create" operation - kendo-grid

My Kendo.Grid need to perform CRUD operations on the data on SAVE CHANGES click
This is the Grid's definition:
#(Html.Kendo().Grid(Model.TicketReportPropertyList)
.Name("TicketReportPropertyGrid")
.Columns(columns =>
{
columns.Bound(c => c.ID).Hidden();
columns.Bound(c => c.PropertyName).Title("Property Name").EditorTemplateName("_PropertyNameEditor").Width(900);
columns.Bound(c => c.Amount).Title("Amount").Format("{0:C}").Width(90);
columns.Command(c => c.Custom("Delete").Click("DeleteRecord"));
})
.Events(events => events.DataBound("Databound").SaveChanges("SaveGrid").Edit("Edit"))
.ToolBar(toolbar =>
{
toolbar.Create();
toolbar.Save();
})
.Editable(editable => editable.Mode(GridEditMode.InCell).DisplayDeleteConfirmation(false))
.DataSource(dataSource => dataSource
.Ajax()
.Batch(true)
.ServerOperation(false)
.Model(model =>
{
model.Id(c => c.ID);
model.Field(c => c.PropertyName);
model.Field(c => c.Amount);
})
.Events(events =>
{
events.RequestEnd("onRequestEnd");
})
.Read(read => read.Action("GetData", "TicketReportProperty", Model))
.Create(create => create.Action("AddTicketReportProperty", "TicketReportProperty"))
.Update(update => update.Action("UpdateTicketReportProperty", "TicketReportProperty"))
.Destroy(delete => delete.Action("DeleteTicketReportProperty", "TicketReportProperty"))
)
)
When doing the update, the following method of the controller is invoked:
[HttpPost]
public ActionResult UpdateTicketReportProperty([DataSourceRequest] DataSourceRequest request, [Bind(Prefix = "models")]IEnumerable<TicketReportPropertyEntity> ticketReportPropertyList)
{
TicketReportPropertyModel model = new TicketReportPropertyModel();
model = new TicketReportPropertyModel().UpdateTicketReportProperties(ticketReportPropertyList);
if (!model.Success)
{
ModelState.AddModelError("TicketReportProperty", model.ErrorDescription);
}
return Json(new[] { model.TicketReportPropertyList }.ToDataSourceResult(request, ModelState));
}
When Update is performed, all the data I need is populated inside of ticketReportPropertyList parameter and I can update the database
However, when performing a Create operation, the controller's method got hit, but ticketReportPropertyList is not populated with some of the parameters I need as happen during Update.
When adding new record the following method is invoked:
[HttpPost]
public ActionResult AddTicketReportProperty([DataSourceRequest] DataSourceRequest request, [Bind(Prefix = "models")]IEnumerable<TicketReportPropertyEntity> ticketReportPropertyList)
{
TicketReportPropertyModel model = new TicketReportPropertyModel();
model = new TicketReportPropertyModel().AddTicketReportProperty(ticketReportPropertyList);
if (!model.Success)
{
ModelState.AddModelError("TicketReportProperty", model.ErrorDescription);
}
return Json(new[] { model.TicketReportPropertyList }.ToDataSourceResult(request, ModelState));
}
This is TicketReportPropertyEntity:
public class TicketReportPropertyEntity
{
public int ID { get; set; }
public int TicketID { get; set; }
public decimal Amount { get; set; }
public string PropertyName { get; set; }
public int ReportPropertyID { get; set; }
}
What am I missing here?

I believe your issue can be fixed by simply replacing:
[Bind(Prefix = "models")]IEnumerable<TicketReportPropertyEntity> ticketReportPropertyList
with
TicketReportPropertyEntity ticketReportPropertyList
You are not sending a list when creating each row, you are simply sending one object.

OK, so for creates kendo initializes a new TicketReportPropertyList and then binds values from your columns. So your missing columns will be null. One mechanism to resolve that is to specify DefaultValue for those columns in your model definition. The value can come from a field in your model, a set value, value of a hidden, etc. So I typically have a model for the page the grid is displayed on with values for added items. Then I can do:
.Model(model =>
{
model.Id(c => c.ID);
model.Id(c => c.TicketID).DefaultValue(Model.TicketID);
model.Id(c => c.ReportPropertyID).DefaultValue(Model.ReportPropertyID);
model.Field(c => c.PropertyName);
model.Field(c => c.Amount);
})
Another way would be to handle the Grid's edit event, check for inserts and set the values:
if (e.model.isNew()) {
model.set("TicketID", $("#TicketID).val()); // hidden
model.set("ReportPropertyID", $("#ReportPropertyID).val()); // hidden
}

Related

Processing function in Yii2 Kartik GridView property

In my Kartik GridView viewfile, I am attempting to process a function for the detailRowCssClass property of an ExpandRowColumn. Regardless of setup, (such as applying an empty function or returning direct strings), the result is always the same and an object is returned.
'detailRowCssClass' => function($data){
if($data->status == 0)
{
return GridView::TYPE_INFO;
}
elseif($data->status == 1)
{
return GridView::TYPE_WARNING;
}
elseif($data->status == 2)
{
return GridView::TYPE_SUCCESS;
}
},
returns a class of [object Object]
Does anyone know a workaround, or what I am fundamentally missing in that this does not return a string? Thanks!
The problem is, that detailRowCssClass of the class kartik\grid\ExpandRowColumn is a simple string and not a closure. The appropriate parts from the source file vendor/kartik-v/yii2-grid/src/ExpandRowColumn.php:
class ExpandRowColumn extends DataColumn
{
...
/**
* #var string the CSS class for the detail content table row.
*/
public $detailRowCssClass;
...
/**
* #inheritdoc
* #throws InvalidConfigException
*/
public function init()
{
if (!isset($this->detailRowCssClass)) {
$this->detailRowCssClass = $this->grid->getCssClass(GridView::BS_TABLE_INFO);
}
...
$clientOptions = Json::encode(
[
'gridId' => $this->grid->options['id'],
'hiddenFromExport' => $this->hiddenFromExport,
'detailUrl' => empty($this->detailUrl) ? '' : $this->detailUrl,
'onDetailLoaded' => $onDetailLoaded,
'expandIcon' => $this->expandIcon,
'collapseIcon' => $this->collapseIcon,
'expandTitle' => $this->expandTitle,
'collapseTitle' => $this->collapseTitle,
'expandAllTitle' => $this->expandAllTitle,
'collapseAllTitle' => $this->collapseAllTitle,
'rowCssClass' => $this->detailRowCssClass,
'animationDuration' => $this->detailAnimationDuration,
'expandOneOnly' => $this->expandOneOnly,
'enableRowClick' => $this->enableRowClick,
'enableCache' => $this->enableCache,
'rowClickExcludedTags' => array_map('strtoupper', $this->rowClickExcludedTags),
'collapseAll' => false,
'expandAll' => false,
'extraData' => $this->extraData,
]
);
...
}

Monthpicker in Kendo UI for MVC works but sets current month when changing focus from grid cell

I have set up a "monthpicker" in a cell of a Kendo grid. The picker works fine and the column shows MMMM yyyy (e.g. April 2019)
However, when I move focus from the cell it doesn't set the cell as dirty and reverts back to current month and year.
Editor template (called Month.cshtml)
#model DateTime?
#{string[] formats = { "MMMM yyyy" }; }
#(Html.Kendo().DatePickerFor(m => m)
.Name("monthpicker")
.Start(CalendarView.Year)
.Depth(CalendarView.Year)
.Format("MMMM yyyy")
.DateInput()
.Culture("en-US")
.ParseFormats(formats)
.HtmlAttributes(new { style = "width: 100%", title = "monthpicker" })
)
Model:
[Display(Name = "Month", ResourceType = typeof(Resources.Resources))]
[UIHint("Month")]
public DateTime Month { get; set; }
View
#(Html.Kendo().Grid<GrindrodDataCapture.Models.MonthlyOceanPlan>()
.Name("grid")
.Columns(columns =>
{
columns.Bound(c => c.Month).Format("{0:MMMM yyyy}");
//etc
})
.ToolBar(toolbar =>
{
toolbar.Create();
toolbar.Save();
})
.Editable(editable => editable.Mode(GridEditMode.InCell))
.Pageable()
.Sortable(sortable =>
{
sortable.SortMode(GridSortMode.SingleColumn);
})
.Filterable()
.DataSource(dataSource => dataSource
.Ajax()
.Events(events => events.Error("error_handler"))
.Sort(p => { p.Add("Month").Descending(); })
.Model(model => model.Id(p => p.ID))
.Read(read => read.Action("MonthlyOceanPlans_Read", "MonthlyOceanPlanGrid"))
.Create(create => create.Action("MonthlyOceanPlans_Create", "MonthlyOceanPlanGrid"))
.Update(update => update.Action("MonthlyOceanPlans_Update", "MonthlyOceanPlanGrid"))
.Destroy(destroy => destroy.Action("MonthlyOceanPlans_Destroy", "MonthlyOceanPlanGrid"))
)
I got a support response from Telerik which fixed it :)
"Hi Evan,
I noticed that the name of the date picker editor does not match the name of the field it edits. The editor is bound to its corresponding field of the model using the name setting. Currently the binder will try to bind the editor to the monthpicker field of the model. However, the actual field in the model is called Month.
Furthermore, when using a WidgetFor helper, you can omit the name configuration as the name is automatically set to the name of the field.
Could you pleas remove the Name configuration of the editor and let me know if the editor binds as expected?"

Post additional data as list kendo multiselect read

I have a two kendo multiselect and i want my roles multiselect to sort of cascade from the other in a way that on the roles multiselect read i want to post a list of the values selected in my systems multiselect. This is what my roles multiselect looks like:
#(Html.Kendo().MultiSelect()
.Name("Roles")
.DataTextField("Name")
.DataValueField("Id")
.Placeholder("Select roles")
.DataSource(source =>
{
source.Read(read =>
{
read.Action("GetRoles", "UserAdmin").Data("additionalItemsGetRoles");
})
.ServerFiltering(true);
})
)
<script>
function additionalItemsGetRoles() {
var multiselect = $("#Systems").data("kendoMultiSelect").dataItems();
var length = multiselect.length;
var systems = [];
for (var i = 0; i < length; i++) {
systems.push({
Name: multiselect[i].Name,
SystemId: multiselect[i].SystemId,
Description: multiselect[i].Description
});
}
var json = JSON.stringify(systems);
console.log(json);
return json;
}
</script>
and here is what my action method looks like:
public ActionResult GetRoles([DataSourceRequest] DataSourceRequest request, IList<SystemViewModel> systems)
{
And here is what my console.log(json) shows in the console.
And here is my viewmodel:
public string SystemId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
I tried to set the action method as [httpPost] but then it can't find the method at all.
Everytime it posts to the controller it get null. What am i doing wrong here?
OK, if I understand correctly, what you want to do is to filter one multiselect list, based on the selected items in another multiselect list. Yes?
I am actually doing something similar, and here is what I did:
First, setup the two MultiSelect "widgets"
Set the Change event on the first MultiSelect ("regionChange")
Set the .Data parameter of DataSource to a js function ("regionfilter")
#(Html.Kendo().MultiSelect()
.Name("region")
.DataTextField("Name")
.DataValueField("Id")
.AutoBind(false)
.Events(e => e.Change("regionChange"))
.DataSource(ds => ds
.Read(read => read.Action("GetRegionsForFilter", "Authorization")
)
)
#(Html.Kendo().MultiSelect()
.Name("branch")
.DataTextField("Name")
.DataValueField("Id")
.AutoBind(false)
.DataSource(ds => ds
.Read(read => read.Action("GetBranchesForFilter", "Authorization")
.Data("regionfilter"))
.ServerFiltering(true)
)
)
Define js functions (I have an additional function to get the MultiSelect values, because I am using this on a couple of other MultiSelect "widgets" no the page, AND I am actually doing reverse filtering for some (such as Branch/Region though I snipped out the filtering being done on region)
function regionChange() {
var value = this.value(),
grid = $("#grid").data("kendoGrid");
<-- snipped logic related to filtering grid -->
$('#branch').data('kendoMultiSelect').dataSource.read();
}
function regionfilter() {
var values = getMultiSelectValues("region");
return { regions: values.toString() };
}
function getMultiSelectValues(multiSelectControl) {
var multiSelect = $("#" + multiSelectControl).data("kendoMultiSelect");
var values = multiSelect.value($("#value").val());
return values;
}
Finally, in my controller I am just returning a JsonResult (get request) that accepts a string argument (comma separated list of strings)
public JsonResult GetBranchesForFilter(string regions)
{
var list = _repository.Branches().GetAll().Select(x => new { x.Id, x.Name, x.RegionId });
if (!String.IsNullOrWhiteSpace(regions))
list = list.Where(x => regions.Contains(x.RegionId.ToString()));
return Json(list.OrderBy(o => o.Name), JsonRequestBehavior.AllowGet);
}

Kendo UI chart converting dates in different browsers based on Time Zone

I am using Kendo UI chart in one of my projects today noticed a weird behaviour in different browser, I have date on x-axis, and it is auto transforming dates in different browsers with different time zone.
Like in UTC+5 it is showing date range from 3/1/2014 to 3/31/2014 while in UTC-6 it is showing date range from 2/28/2014 to 3/30/2014.
Basically this is happening due to the difference between the timezones of the client and the server and the form these dates are trasnfered and re-created on both sides into Date (JS) /DateTime (.NET) objects.
Basically the whole situtation is explained in details here. The dataSource that the Chart is using is the same as the one that the Grid uses so there is not difference.
Here is some example code from a project I have which you can use. Check the requestEnd handler
#(Html.Kendo().Grid<KendoMVCWrappers.Models.Person>().Name("persons")
.DataSource(dataSource => dataSource
.Ajax()
.Events(ev => ev.RequestEnd("convert"))
.Model(model => model.Id(m => m.PersonID))
.Read(read => read.Action("GetPersons", "Home"))
.Update(up => up.Action("UpdatePerson", "Home"))
)
.Filterable()
.Columns(columns =>
{
columns.Bound(c => c.PersonID);
columns.Bound(c => c.Name);
columns.Bound(c => c.BirthDate);
columns.Command(cmd => cmd.Edit());
})
.Pageable()
.Sortable()
)
<script type="text/javascript">
function convert(e) {
if (e.response.Data && e.response.Data.length) {
var offsetMiliseconds = new Date().getTimezoneOffset() * 60000;
var persons = e.response.Data;
for (var i = 0; i < persons.length; i++) {
persons[i].BirthDate = persons[i].BirthDate.replace(/\d+/,
function (n) { return parseInt(n) + offsetMiliseconds }
);
}
}
}
</script>
And the setter code of the ViewModel. Using a setter eases the whole situation since you have to do it in multiple places (before creating an object when it is fetched from the database and when it is created from the ModelBinder).
public class Person
{
public int PersonID { get; set; }
public string Name { get; set; }
private DateTime birthDate;
public DateTime BirthDate
{
get { return this.birthDate; }
set
{
this.birthDate = new DateTime(value.Ticks, DateTimeKind.Utc);
}
}
}
Good luck!
Got null response error when tried to parse date as mentioned in this post in onRequestEnd.
http://www.telerik.com/support/code-library/using-utc-time-on-both-client-and-server-sides
I resolve this by parsing in datasource parse method instead of requestEnd.
parse :function(data)
{
return ConvertToUTC(data);
}
function ConvertToUTC(data)
{
// iterate over all the data elements replacing the Date with a version
// that Kendo can work with.
$.each(data, function(index, item){
if(index == "data")
{
for(i =0 ;i< item.length; i++)
{
// Days is my date property in item collection
item[i].Days = item[i].Days.replace(/\d+/,
function (n) {
var time = parseInt(n);
return parseInt(time) + new Date(time).getTimezoneOffset() * 60000;
}
);
}
}
});
return data;
}

Editable Detail Template Kendo UI Grid

Update
I have managed to get the form rendering (see image) with values and editor templates.
Sorry for blacked out labels. NDA requirements...
This now works across multiple rows because I am using the uid of the row to give the detail template a unique name doing this:
#(Html.Kendo().TabStrip()
.Name("Details_#=uid#")
I've updated the source below to the latest version and have included a list of issues that I'd still love help with.
Outstanding issues:
Serializing the detail template along with the grid when I save changes
Assign labels and validation rules using Data Annotations on the View Model (This doesn't seem to work since I can't seem to get Html helpers working. Advice would be great!
Orignal Post text (Source code has been updated)
I am trying to create a Batch Grid where each item contains a detail template.
Each detail template contains a tabstrip that I want to hold extra form data within.
Right now, I have the standard batch grid working, but I can't seem to get the information on screen for each item AND have it be editable. Also, the detail template breaks when more than one row is present. The template still renders, but buttons are non-functional because both templates have the same id understandably breaking the user's ability to interact, but I'm unsure of how to ensure unique identifiers for each row template (maybe use the parent uid somehow?)
I am also unsure of how to serialize this form data once I have completed this first step, but I can make a separate question for that should it prove necessary.
Grid
#(Html.Kendo().Grid(Model.ItemModelList)
.Name("ItemGrid")
.Columns(columns =>
{
//Other columns omitted for brevity
columns.Bound(i => i.Description).Width(100);
columns.Command(command =>
{
command.Destroy();
}).Width(60);
})
.ClientDetailTemplateId("ItemDetails")
.ToolBar(toolbar =>
{
toolbar.Create();
toolbar.Save();
})
.Editable(editable => editable.Mode(GridEditMode.InCell).CreateAt(GridInsertRowPosition.Bottom))
.Pageable()
.Sortable()
.Scrollable()
.Resizable(resize => resize.Columns(true))
.DataSource(dataSource => dataSource
.Ajax()
.Batch(true)
.ServerOperation(false)
.Events(events =>
{
events.Error("ItemGrid_ErrorHandler");
})
.Model(model =>
{
model.Id(i => i.ItemModelID);
model.Field(i => i.DimensionUOM).DefaultValue("in");
model.Field(i => i.WeightUOM).DefaultValue("lbs");
})
.Create(create => create.Action("CreateProducts", "ItemGrid"))
.Read(read => read.Action("GetProducts", "ItemGrid"))
.Update(update => update.Action("UpdateProducts", "ItemGrid"))
.Destroy(destroy => destroy.Action("DeleteProducts", "ItemGrid"))
)
)
Detail Template
<script id="ItemDetails" type="text/kendo-tmpl">
#(Html.Kendo().TabStrip()
.Name("Details_#=uid#")
.SelectedIndex(0)
.Items(items =>
{
items.Add().Text("test").Content(#<div>
<table id="testForm">
<tr>
<td>TEST</td>
</tr>
<tr>
</tr>
</table>
</div>);
items.Add().Text("test2").Content(#<div>
<table id="test2Form">
<tr>
<td><label>A</label></td>
<td><label>B</label></td>
<td><label>C</label></td>
<td><label>D</label></td>
<td><label>E</label></td>
</tr>
<tr>
<td>
<input class="k-textbox" value="#=objectA#">
</td>
<td>
#(Html.Kendo().DropDownList()
.Name("objectB")
.Value("#=objectB#")
.DataTextField("Text")
.DataValueField("Value")
.BindTo(new SelectList((System.Collections.IEnumerable)ViewBag.objectBListing, "Value", "Value"))
.ToClientTemplate()
)
</td>
<td>
#(Html.Kendo().DropDownList()
.Name("objectC")
.Value("#=objectC#")
.DataTextField("Text")
.DataValueField("Value")
.BindTo(new SelectList((System.Collections.IEnumerable)ViewBag.objectCListing, "Value", "Value"))
.ToClientTemplate()
)
</td>
<td><input class="k-textbox" value="#=objectD#"></td>
<td><input class="k-textbox" value="#=objectE#"></td>
</tr>
</table>
</div>);
})
.ToClientTemplate()
)
You should create a model for your extra information and add it as a property of your ItemModelList like this:
public class BaseMode
{
public string UID { get; set; } // Create your own UID, distinguished from Telerik UID by casing.
}
public class ExtraInfoModel : BaseModel
{
[DisplayName("Object A")]
[Required] // For example
public string ObjectA { get; set; }
[DisplayName("Object B")]
[UIHint("DropDownList")]
public int? ObjectB { get; set; }
[DisplayName("Object C")]
public int? ObjectC { get; set; }
public ExtraInfoModel(string uid)
{
this.UID = uid;
}
}
public class ItemModelList : BaseModel
{
public ExtraInfoModel ExtraInfo { get; set; }
public ItemModelList()
{
this.UID = Guid.NewGuid().ToString(); // Not sure about syntax, please review.
this.ExtraInfo = new ExtraInfoModel(this.UID); // Guarantee ExtraInfo.UID is the same as parent UID when you get model from DB.
}
}
Create a partial view for your detail extra info, using a kendo grid in your second tab:
#model ExtraInfoModel
#(Html.Kendo().TabStrip()
.Name("Details_#=UID#")
.SelectedIndex(0)
.Items(items =>
{
items.Add().Text("test").Content(#<text>
<div>
<table id="testForm">
<tr>
<td>TEST</td>
</tr>
<tr></tr>
</table>
</div>
</text>);
items.Add().Text("test2").Content(#<text>
#(Html.Kendo().Grid<ExtraInfoModel>()
.Name("gvDetail_#=UID#")
.Columns(c =>
{
c.Bound(m => m.ObjectA).ClientTemplate(Html.Kendo().TextBox()
.Name("ObjectA")
.HtmlAttributes(new { id = "ObjectA_#=UID#" })
.Value(Model.AgencyCode)
.ToClientTemplate()
.ToHtmlString());
c.Bound(m => m.ObjectB).ClientTemplate(Html.Kendo().DropDownList()
.Name("ObjectB")
.HtmlAttributes(new { id = "ObjectB_#=UID#" })
.Value((Model != null && Model.ObjectB.HasValue) ? Model.ObjectB.ToString() : string.Empty)
.OptionLabel("Select B...")
.BindTo(ViewBag.Data)
.ToClientTemplate()
.ToHtmlString());
// Config ObjectC same as ObjectB.
})
.BindTo(new ExtraInfoModel[] { Model }) // Your detail grid has only one row.
)
</text>);
})
)
In your main view page, using server detail template instead of client detail template. I suggest to use server template because I'm using server binding in step 2. Of course, you can change it to ajax binding or local binding (by defining event OnDetailExpand)
#Html.Kendo().Grid(Model.ItemModelList)
...
.DetailTemplate(#<text>
#Html.Partial("YourPartialName", item.ExtraInfo) // item stands for ItemModelList, which is the binding data item.
</text>)
And the last one, for your first issue, serializing extra info data on save, we should handle change event of each extra info property to set value and dirty flag to master data item. Remember that kendo grid in batch editing only submits dirty data items. Back to step 2:
c.Bound(m => m.ObjectB).ClientTemplate(Html.Kendo().DropDownList()
.Name("ObjectB")
.HtmlAttributes(new { id = "ObjectB_#=UID#" })
.Value((Model != null && Model.ObjectB.HasValue) ? Model.ObjectB.ToString() : string.Empty)
.OptionLabel("Select B...")
.BindTo(ViewBag.Data)
.Events(e => e.Change("onChangeObjectB")) // Added this line
.ToClientTemplate()
.ToHtmlString());
<script type="text/javascript">
function onChangeObjectB(e) {
var selectedValue = this.value();
var sender = e.sender;
if (sender != undefined && sender.length > 0) {
var detailRow = sender.closest(".k-detail-row");
var masterRow = detailRow.prev();
var mainGrid = $("#ItemGrid").data("kendoGrid");
if (mainGrid != null) {
var masterRowIndex = masterRow.parent().find(".k-master-row").index(masterRow);
var dataItem = mainGrid.dataSource._data[masterRowIndex];
dataItem.ObjectB = selectedValue;
dataItem._dirty = true;
}
}
}
</script>
And the save action will work as normal.