I am currently trialing the products to see if I can get them to do what I need for an app under development.
In my app I have a grid that lists all of a customers active products.
I want to be able to click on an item in the grid (link or button) to launch a modal dialog where I can display in depth info regarding that item - the popup itself would need to have tabstrip and perhaps one or two grids on itself.
So I obviously need to design a view to display all this info which is fine, and I assume the Window component is the most suitable to effect the popup?
I can't seem to find a concrete example of how to call the window from the initial grid, pass the parameter of the selected item, and then load the view within the window.
As I say I am new to all this so feeling my way a little and so any guidance is greatly appreciated.
Thanks
2 Answers, 1 is accepted
Hello Chris,
Indeed, you can achieve the desired result as follows:
- Define a custom column command within the Grid's Columns() configuration. A similar example is demonstrated in the Grid Custom Command demo.
@(Html.Kendo().Grid<Product>()
.Name("grid")
.Columns(columns => {
...
columns.Command(command => command.Custom("ViewDetails").Click("showDetails"));
})
...
)
<script>
function showDetails(e) { // The button Click event handler
}
</script>
- Define a hidden Window component in the View with the Grid:
@(Html.Kendo().Grid<Product>()
.Name("grid")
...
)
@(Html.Kendo().Window()
.Name("Details")
.Title("Product Details")
.Visible(false)
.Modal(true)
.Draggable(true)
.Width(800)
)
- Within the column command's click event handler, get a reference to the hidden Window, and call its refresh() method that will make an AJAX request to the server and load the respective View with the details.
<script>
function showDetails(e) {
e.preventDefault();
var dataItem = this.dataItem($(e.currentTarget).closest("tr")); // access the data item of the respective Grid row
var window = $("#Details").data("kendoWindow"); // get a reference to the Window
window.refresh({ // request the View and pass the "Id" property of the data item
url: '/Home/GetProductDetails?Id=' + dataItem.Id
});
window.open();
}
</script>
//HomeController.cs
public IActionResult GetProductDetails(int Id)
{
var gridRecord = gridDataCollection.Where(x => x.Id == Id).FirstOrDefault(); // get the Model instance
return PartialView("_Details", record); pass it to the Partial View
}
//_Details.cshtml
@model Product
.... // View content
Let me know if this approach meets your requirements.
Regards,
Mihaela
Progress Telerik
I have got a Window working using the custom command and passing the ID from the row of the grid – code below – this loads the view into the window whilst passing in the id :
<script type="text/javascript">
function showDetails(e) {
e.preventDefault();
var dataItem = this.dataItem($(e.currentTarget).closest("tr"));
var wnd = $("#Details").data("kendoWindow");
var id = (dataItem.AssetId);
wnd.refresh({url: "/Asset/Edit/" + id });
wnd.center().open();
}
</script>
@(Html.Kendo().Grid<Welcomm_CRM.Core.Assets.Asset>()
.Name("assetsGrid")
.Columns(columns =>
{
columns.Bound(c => c.AssetId).Width(140);
columns.Bound(c => c.AssetRef).ClientTemplate("<a href='Contact/Edit/#=data.AssetId#'>#=data.AssetRef#</a>");
columns.Bound(c => c.Username).Width(140);
columns.Command(command => command.Custom("View Details").Click("showDetails"));
})
.Sortable()
.Filterable()
.Pageable(p => p.PageSizes(new[] { 25, 50, 100 }))
.DataSource(assDataSource => assDataSource
.Ajax()
.Read(read => read.Action("CustomerAssets", "Customer", new { id = (int)Model }))
.PageSize(25)
))
@(
Html.Kendo().Window()
.Name("Details")
.Title("Asset Details")
.Content("loading asset info...")
.Visible(false)
.Modal(true)
.Draggable()
.Resizable()
)
I have the window opening the view with a tabstrip on it also.
What I want to get to ideally is the Window remaining open until the user is finished with it and manually dismisses it, but where they can add or update data in multiple tables from one place - i.e. work their way through the tabs, and interact with the forms and grids. Is that possible?
Hi Chris,
I have tested the case locally, and it appears that the Window remains open when I edit the Grid's data within a TabStrip. Would you please let me know in which cases the Window closes automatically at your end?
Best,
Mihaela
Hi Mihaela
It is where I have a form on one of the tabs - obviosuly the postback is going to the action and clearing the window - is there something built in to post back asynchronously or similar to leave the window open, and just allow the display of an alert to say the update has taken place?
Thanks
I found an example of AJAX postback here :
https://docs.telerik.com/aspnet-core/html-helpers/layout/form/razor-page
That seems to work in terms of passing the data to the action - just having an issue with getting it to update the success/error message on the open window - it may be because of code placement as the script to respond to the form events I had to put on the underlying view, not within the partial that is opened on the window to get that to work.
OK have this nearly working but the script to set the success or fail messages is failing, perhaps because the element it is looking for is within the window? The error message is "Cannot read properties of undefined (reading 'find')"
I have other forms on my underlying view each with their own validation div, so I renamed them to be specific per form but that still doesn't seem to work. I can see everything else is working bar that, as I can alert box the results OK.
Hi Chris,
Your assumption is correct - you need to select an element inside the Window to display the message. Would you share the View and the JS logic you use to display the message, so I can review them?
Hi - the code as it currently stands is below - I have commented out the .find code and doing a direct .html replacement is working for me, just would be good to know why the original example code is not working. Thanks
@using Welcomm_CRM.Core.Assets;
@model Asset
<script>
function onFormValidateField(e) {
$("#asset-validation-success").html("");
}
function onFormSubmit(e) {
e.preventDefault();
var modelData = e.model;
$.ajax({
type: 'POST',
url: "@Url.Action("AssetUpdate","Asset")",
data: modelData,
dataType: 'json',
success: function (data) {
var form = $("#assetForm").getKendoForm();
//form.validator.validationSummary.find("ul").empty();
//form.validator.validationSummary.addClass("k-hidden");
$("#asset-validation-success").html("<div class='k-messagebox k-messagebox-success'>" + data.success + "</div>");
},
error: function (data) {
var response = JSON.parse(data.responseText);
var form = $("#assetForm").getKendoForm();
var errorString = "";
$.each(response.errors, function (key, value) {
errorString += '<li>' + value + '</li>';
});
//$("#validation-success").html("");
//form.validator.validationSummary.find("ul").empty();
//form.validator.validationSummary.find("ul").append(errorString);
//form.validator.validationSummary.toggleClass("k-hidden");
$("#asset-validation-success").html("<div class='k-messagebox k-messagebox-warning'><ul>" + errorString + "</ul></div>");
}
})
}
function onFormClear(e) {
$("#asset-validation-success").html("");
}
</script>
<div id="asset-validation-success"><ul></ul></div>
@(
Html.Kendo().Form<Welcomm_CRM.Core.Assets.Asset>()
.Name("assetForm")
.HtmlAttributes(new { action = @Url.Action("Edit", "Asset"), method = "POST" })
.ButtonsTemplate("<button class=\"btn btn-primary k-form-submit\" type=\"submit\">Submit</button>")
.Validatable(v =>
{
v.ValidateOnBlur(true);
v.ValidationSummary(vs => vs
.Enable(false)
.Container("#asset-validation-success")
);
})
.Layout("grid")
.Grid(g => g.Cols(2).Gutter(10))
.Items(items =>
{
items.Add()
.Field(f => f.AssetId)
.Editor(editor => editor.Hidden());
items.Add()
.Field(f => f.CustomerId)
.Editor(editor => editor.Hidden());
items.Add()
.Field(f => f.Asset_Guid)
.Editor(editor => editor.Hidden());
items.Add()
.Field(f => f.AppId)
.Editor(editor => editor.Hidden());
items.Add()
.Field(f => f.AssetRef)
.Label(l => l.Text("Asset Ref:"));
items.Add()
.Field(f => f.AssetTypeId)
.Label(l => l.Text("Asset Type:").Optional(false))
.Editor(e =>
{
e.DropDownList()
.HtmlAttributes(new { })
.DataTextField("AssetTypeName")
.DataValueField("AssetTypeId")
.HtmlAttributes(new { style = "width:100%" })
.Size(ComponentSize.Small)
.OptionLabel("Select Type...")
.FillMode(FillMode.Outline)
.DataSource(source =>
{
source.Read(read =>
{
read.Action("AssetTypeList", "Lookup");
})
.ServerFiltering(false);
});
});
items.Add()
.Field(f => f.Username)
.Label(l => l.Text("Username").Optional(false));
items.Add()
.Field(f => f.CostCentre)
.Label(l => l.Text("Cost Centre:").Optional(false));
items.Add()
.Field(f => f.Email)
.Label(l => l.Text("Email:").Optional(false));
items.Add()
.Field(f => f.SimNumber)
.Label(l => l.Text("Sim Number:").Optional(false));
items.Add()
.Field(f => f.SupplierId)
.Label(l => l.Text("Supplier:").Optional(false))
.Editor(e =>
{
e.DropDownList()
.HtmlAttributes(new { })
.DataTextField("SupplierName")
.DataValueField("SupplierId")
.HtmlAttributes(new { style = "width:100%" })
.Size(ComponentSize.Small)
.OptionLabel("Select Supplier...")
.FillMode(FillMode.Outline)
.DataSource(source =>
{
source.Read(read =>
{
read.Action("SupplierList", "Lookup");
})
.ServerFiltering(false);
});
});
items.Add()
.Field(f => f.NetworkId)
.Label(l => l.Text("Network:").Optional(false))
.Editor(e =>
{
e.DropDownList()
.HtmlAttributes(new { })
.DataTextField("NetworkName")
.DataValueField("NetworkId")
.HtmlAttributes(new { style = "width:100%" })
.Size(ComponentSize.Small)
.OptionLabel("Select Network...")
.FillMode(FillMode.Outline)
.DataSource(source =>
{
source.Read(read =>
{
read.Action("NetworkList", "Lookup");
})
.ServerFiltering(false);
});
});
items.Add()
.Field(f => f.Disabled)
.Label(l => l.Text("Disabled:").Optional(false))
.Editor(e =>
{
e.CheckBox();
});
})
.Events(ev => ev
.ValidateField("onFormValidateField")
.Submit("onFormSubmit"))
)
Hi Chris,
My name is Alexander and I have diligently gone through the provided discussion so far. And here is my take on the matter as well.
Generally, the Telerik UI for ASP.NET Core exposes built-in PopUp Editing capabilities. Which more-or-less spares you some gymnastics. Especially when it comes to employing a Form that is associated with a given row model.
Here is a mere glimpse of what the functionality offers on the table. In terms of user experience:
The following resources go more in-depth when it comes to employing the functionality:
- Grid PopUp Editing (Demo) - open the "View Source" tab to observe the Grid's configuration.
- Grid PopUp Editing (Documentation) - illustrates the configuration niches and showcases how model state errors can be handled on the server.
Since you have mentioned that numerous tables need to be edited, I would personally advocate using the aforementioned functionality in conjunction with the hierarchical capabilities the Grid also offers. The following demo showcases such a scenario:
Notice that upon expansion the Grid rows will visualize their own TabStrips. That may have arbitrary content. In your case, the other tables can have their own PopUp editing - with their own designated forms.
Regardless, I did try sculpturing a scenario. Which more-or-less is close to yours. However, there are some missing bits and pieces. Namely, for the TabStrip. Thus, I have assembled the scenario based on my own interpretations.
Here is what each of the proactive code compartments looks like:
Index.cshtml:
<script type="text/javascript">
function showDetails(e) {
e.preventDefault();
var dataItem = this.dataItem($(e.currentTarget).closest("tr"));
var wnd = $("#Details").data("kendoWindow");
var id = (dataItem.AssetId);
wnd.refresh({url: "/Asset/Edit?id=" + id });
wnd.center().open();
}
</script>
@(Html.Kendo().Grid<Asset>()
.Name("assetsGrid")
.Columns(columns =>
{
columns.Bound(c => c.AssetId).Width(140);
columns.Bound(c => c.AssetRef).ClientTemplate("<a href='Contact/Edit?id=#=data.AssetId#'>#=data.AssetRef#</a>");
columns.Bound(c => c.Username).Width(140);
columns.Command(command => command.Custom("View Details").Click("showDetails"));
})
.Sortable()
.Filterable()
.Pageable(p => p.PageSizes(new[] { 25, 50, 100 }))
.DataSource(assDataSource => assDataSource
.Ajax()
.Read(read => read.Action("CustomerAssets", "Customer"))
.PageSize(25)
)
)
@(Html.Kendo().Window()
.Name("Details")
.Title("Asset Details")
.Content("loading asset info...")
.Visible(false)
.Modal(true)
.Draggable()
.Resizable()
)
Edit.cshtml:
@model int
@(Html.Kendo().TabStrip()
.Name("tabstrip")
.Items(items =>
items.Add().Text("Some Tab").LoadContentFrom("EditForm", "Asset", new {id = Model})
)
)
EditForm.cshtml:
@model Asset
<script>
function onFormValidateField(e) {
$("#asset-validation-success").html("");
}
function onFormSubmit(e) {
e.preventDefault();
var modelData = e.model;
$.ajax({
type: 'POST',
url: "@Url.Action("AssetUpdate", "Asset")",
data: modelData,
dataType: 'json',
success: function (data) {
var form = $("#assetForm").getKendoForm();
//form.validator.validationSummary.find("ul").empty();
//form.validator.validationSummary.addClass("k-hidden");
$("#asset-validation-success").html("<div class='k-messagebox k-messagebox-success'>" + data.success + "</div>");
},
error: function (data) {
var response = JSON.parse(data.responseText);
var form = $("#assetForm").getKendoForm();
var errorString = "";
$.each(response.errors, function (key, value) {
errorString += '<li>' + value + '</li>';
});
//$("#validation-success").html("");
//form.validator.validationSummary.find("ul").empty();
//form.validator.validationSummary.find("ul").append(errorString);
//form.validator.validationSummary.toggleClass("k-hidden");
$("#asset-validation-success").html("<div class='k-messagebox k-messagebox-warning'><ul>" + errorString + "</ul></div>");
}
})
}
function onFormClear(e) {
$("#asset-validation-success").html("");
}
</script>
<div id="asset-validation-success"><ul></ul></div>
@(
Html.Kendo().Form<Asset>()
.Name("assetForm")
// Omitted for brevity...
)
AssetController.cs:
public class AssetController : Controller
{
public IActionResult Edit(int id)
{
return PartialView("Edit", id);
}
public IActionResult EditForm(int id)
{
var data = Enumerable.Range(1, 10)
.Select(i => new Asset
{
...
});
var model = data.FirstOrDefault(model => model.AssetId == id);
return PartialView("EditForm", model);
}
[HttpPost]
public IActionResult AssetUpdate(Asset asset)
{
if (asset.AssetRef == null)
{
ModelState.AddModelError("AssetRef", "AssetRef is required");
var errorList = (from item in ModelState
where item.Value.Errors.Any()
select item).ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value.Errors.Select(e => e.ErrorMessage).FirstOrDefault()
);
Response.StatusCode = 400;
return Json(new { errors = errorList });
}
return Json(new {success = "Successfully submitted form!"});
}
}
Here is how the scenario panned out:
Attached is the sample that was previously hinted. I might be missing something here. As I did not manage to reciprocate the behavior within the sample. It would be of great help. And more than appreciated if you could consider the option of putting some muscle. By trying to replicate the behavior and send it back for further examinations.
I look forward to your reply.
Kind Regards,
Alexander
Progress Telerik
Thank you for the details, Chris.
If the issue persists at your end, feel free to modify the sample app shared by my colleague, and send it back to us for review. It would help us to diagnose the issue further and share the relevant solution.