Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Javascript

Kendo Dropdown within Kendo Sub-grid using EditorTemplate+MVC

0.00/5 (No votes)
26 May 2016CPOL2 min read 23.1K  
Kendo dropdown within Kendo sub-grid using Editor Template and Client Template +ASP.NET- MVC

Introduction

This article will guide you in rendering a Kendo dropdown withtin Kendo sub-grid using Editor Templates, ASP.NET MVC.

Background

Kendo sub-grid originally had a HTML dropdown instead of a Kendo dropdown.It also had a long running javascript code instead. I did browse around many articles to achieve it but everyone had something or the other missing from it / unclear, hence kind of learned it along the way!!. 

The best part of this implementation being, when rendering for the first time, if the dropdown does not have any value saved already, it renders it blank, but once clicked on the gridcell, it turns into a dropdown for selection. 
Furthermore, as soon as you navigate out from the grid cell, the editing mode ends and it returns back to the original last saved value from Database.

Using the code

In the following code, OrderGrid (grid) is the Main Kendo grid that houses another Kendo grid (grid2_#=OpportunityId) within. 

In OrderGrid.cshtml

C++
@(Html.Kendo().Grid<Models.OrderGridViewModel>()

    .Name("grid")
    .HtmlAttributes(new { style = "line-height:10px;" })
    .Columns(columns =>
    {
         columns.Bound(m => m.VersionName).Title("Ver");
         columns.Bound(m => m.OrderStatusText).Title("Status");
         columns.Bound(m => m.StartDateTime).Title("Start Date");
         columns.Bound(m => m.EndDateTime).Title("End Date");
    })        
    .Selectable(s => s.Mode(GridSelectionMode.Single).Type(GridSelectionType.Cell))
    .Events(events => events.DataBound("grid_dataBound"))
    .Pageable()
    .Sortable() // Enable sorting
    .ClientDetailTemplateId("OrderDetailsAll")
    .DataSource(dataSource => dataSource
    .Ajax()
    .PageSize(5)
    .Read(read => read.Action("Get", "Order"))
    )
)

<script id="OrderDetailsAll" type="text/kendo-tmpl">
  
@(Html.Kendo().Grid<Models.OrderDetailAllViewModel>()
    .Name("grid2_#=OpportunityId#")
            .Events(e =>
            {
                e.DataBound("grid2_onDataBound");
                e.Edit("onEdit");
            })
    .Editable(editable => editable.Mode(GridEditMode.InCell))
    .Columns(columns =>
    {        
        columns.Bound(m => m.IncludeInForecast).HtmlAttributes(new { Align = "center" }).Title("Include In Forecast?").ClientTemplate("\\# if (IncludeInForecast  == true ) { \\#" + "Yes" +
                      "\\# }else { \\#" + "No" +
                      "\\# } \\#"); //this is a checkbox within Kendo Sub-grid               
    );                
        columns.Bound(m => m.SalesStrategyId).EditorTemplateName("SalesStrategies").Title("Sales Strategy").ClientTemplate("\\#=SalesStrategyName\\#"); //this is a dropdown within Kendo Sub-grid
    })
        .DataSource(dataSource => dataSource
            .Ajax()
            .Read(read => read.Action("GetDetailsAll", "Order", new { opportunityId = "#=OpportunityId#" }))
           .Model(model =>
               {                                 
                   model.Field(x => x.IncludeInForecast).Editable(true);
                   model.Field(x => x.EditIncludeInForecast).Editable(false);                   
                   model.Field(x => x.SalesStrategyList).DefaultValue(ViewData["ssDefault"] as OMSWeb.Models.OrderDetailAllViewModel);
               })
        )
        .ToClientTemplate())         
</script>

Notice that for ClientTemplate we have ("\\#=SalesStrategyName\\#") - this is necessary to indicate that the property is for the sub-grid at one level down.


Note the EditorTemplateName (SalesStrategies) - Now add a new Folder - EditorTemplates at the same level as your page resides, within your Views folder and add a new cshtml file called "SalesStrategies.cshtml" within it.( e.g. my folder hierarchy was - \~OMSWeb\Views\Order\EditorTemplates\SalesStrategies.cshtml, OMSWeb is the Web Project)

C++
@using Kendo.Mvc.UI
@using Utility;
@using System.Collections;
@model OMSWeb.Models.OrderDetailAllViewModel

@(Html.Kendo().DropDownListFor(m => m.SalesStrategyList)
    .OptionLabel("-- Select Sales Strategy --")
    .DataValueField("Value")
    .DataTextField("Text")        
    .BindTo(ViewData["ssList"] as IEnumerable<List<SelectListItem>>)
    .AutoBind(true)
    .Name("SalesStrategyddList")
    .Events(e =>
    {
      e.DataBound("onDataBound"); //Logic/code for this event is written in OrderGrid.cshtml page below
    })
)

ViewModel (OrderDetailAllViewModel.cs)
(Please note that the model contains other relevant properties as well but have omitted it to keep it simpler and cleaner)

C++
public class OrderDetailAllViewModel : BaseModel
{
    public OrderDetailAllViewModel()
    {
        this.SalesStrategyList = new List<SelectListItem>();
    }
    
    public string SalesStrategyName { get; set; }
    public int? SalesStrategyId { get; set; }
    [UIHint("SalesStrategies")] //This has to match the EditorTemplateName given above for Bound column - SalesStrategy in the sub-grid
    public List<SelectListItem> SalesStrategyList { get; set; }
}

Controller Code (in ~OMSWeb\Controllers\OrderController.cs), OrderBusinessService.cs

C++
//to get OrderDetails for sub-grid (in OrderController.cs file)
public ActionResult GetDetailsAll([DataSourceRequest] DataSourceRequest request,Guid opportunityId)
{
         _iOrderBusinessService = new OrderBusinessService();
         List<OrderDetailAllViewModel> mList = new List<OrderDetailAllViewModel>();

         if(opportunityId != null)
         {
              mList = _iOrderBusinessService.GetOrderDetails(opportunityId).OrderBy(d => d.SubSystemId).ThenBy(d => d.StartDate).ToList(); //function call as described immediately below
         }

          DataSourceResult result = mList.ToDataSourceResult(request);
          if (mList!= null && mList[0].SalesStrategyList.Any())
          {                
              ViewData["ssList"] = mList[0].SalesStrategyList;
          }
          return Json(result);
}

//Gets Sales Strategy from Database, handles null/empty values (in OrderBusinessService.cs file)
public List<OrderDetailAllViewModel> GetOrderDetails(Guid opportunityId)
{
    OrderDetailAllViewModel m;
    List<OrderDetailAllViewModel> mList = new List<OrderDetailAllViewModel>();
    ISalesStrategy ssMgr = new SalesStrategyManager();
    List<OrderDetail> dList = _iOrderManager.GetOrderDetails(opportunityId, false);//Gets OrderDetails from OrderManager which in turn calls the Database

    foreach (OrderDetail d in dList)
    {        
        m.SalesStrategyId = d.SalesStrategyId.HasValue ? d.SalesStrategyId.Value : 0; //Have value a Zero if null
        var ssItem = ssMgr.GetSalesStrategyById(m.SalesStrategyId.Value);
        if(ssItem != null)
        {
            m.SalesStrategyName = string.IsNullOrEmpty(ssItem.SalesStrategyName) ? string.Empty : ssItem.SalesStrategyName;
        }
        else
        {
            m.SalesStrategyName = string.Empty; //Name as empty string
        }
        mList.Add(m);
    }
    return mList;
}


// To get SalesStrategies from Database (in OrderController.cs file)
public JsonResult GetSalesStrategies([DataSourceRequest] DataSourceRequest request)
{
    _iOrderBusinessService = new OrderBusinessService();
    OrderDetailAllViewModel vm = new OrderDetailAllViewModel();
    var salesStrategyList = _iOrderBusinessService.GetSalesStrategiesList();
    ViewData["ssList"] = vm.SalesStrategyList;
    return Json(salesStrategyList, JsonRequestBehavior.AllowGet);
}

Here the logic being, on the Databound event ofthe SalesStrategy dropdown, i'm making ajax call to get values from the database. Inorder to bind back to the saved values, existingValue is used (the following code is written in OrderGrid.schtml)

C++
<script type="text/javascript">

var dataItem,selectedOrderDetailId,existingValue,newSelectedValue,existingForecast,selectedProposalId,newIIFValue;

function onDataBound(e) 
{
    $.ajax({
        url: "/Order/GetSalesStrategies",
        type: "GET",
        success: function (result)
        {
            if(result != null && result.length > 0)
            {
                $("#SalesStrategyddList").kendoDropDownList({
                    dataTextField: "Text",
                    dataValueField: "Value",
                    selectedIndex: <code>existingValue</code>, //this is assigned in the onEdit event that is fired before onDataBound
                    dataSource: result,
                    optionLabel: "-- Select Sales Strategy --"
                });
                $("#SalesStrategyddList").val(existingValue);
            }
        },
    });
}

function onEdit(e)
{
    selectedOrderDetailId = e.model.OrderDetailId;
    <code>existingValue</code> = e.model.SalesStrategyId;
    existingForecast = e.model.ForecastYesNo;
    selectedGuid = e.model.OpportunityId;
    selectedProposalId = e.model.ProposalId;
    var isEditIIF = e.model.EditIncludeInForecast;

    var orderDetailGrid = $('#grid2_' + selectedGuid).data("kendoGrid");
   
    if (e.container[0].innerHTML.indexOf('Sales Strategy') >= 0) //check if SalesStrategyddList edited
    {
        orderDetailGrid.table.on("change", "#SalesStrategyddList", onChange); //trigger onChange event for SalesStrategyddList only
    }
    else if (e.container[0].innerHTML.indexOf('Forecast') >= 0) //check if IncludeInForecast edited
    {
        // custom logic to handle IncludeInForecast checkbox
        if (isEditIIF)
        {
            $(this)[0].element[0].disabled = false;
            orderDetailGrid.table.on("change", "#IncludeInForecast", onChange); //trigger onChange event for IncludeInForecast only when EditIncludeInForecast true
        }
        else 
        {
            $(this)[0].element[0].disabled = true;                
        }
    }
}

function onChange(e)
{
    var orderDetailGrid = $('#grid2_' + selectedGuid).data("kendoGrid");

    if (e.target.name == "IncludeInForecast")
    {
        //code for custom logic to handle change for IncludeInForecast
    }
    else if (e.target.name == "SalesStrategyddList")
    {            
        newSelectedValue = e.target.value;
    
        //call for SalesStrategy Update
        var dataToSend =
        {
            orderDetailId: selectedOrderDetailId,
            selectedSalesStrategy: newSelectedValue
        };

        if (selectedOrderDetailId != 0 && newSelectedValue != 0)
        {
            $.ajax({
                url: "/Order/UpdateSalesStrategy",
                type: "POST",
                data: dataToSend,
                success: function (result)
                {                    
                    if (result != null && result == "True")
                    {
                       orderDetailGrid.dataSource.page(1); //refresh datagrid if update was successfull to reflect new changes
                    }
                },
            });
        }       
    }
}

</script>

Ajax call to Controller code to update SalesStrategy in Database (in OrderController.cs file)

C++
[HttpPost]
public bool UpdateSalesStrategy(int orderDetailId, int selectedSalesStrategy)
{
    _iOrderBusinessService = new OrderBusinessService(); //this is turn calls OrderManager to update in Database
    return _iOrderBusinessService.UpdateSalesStrategy(orderDetailId, selectedSalesStrategy);
}

Points of Interest

In case where there is a Kendo Dropdown within a Kendo grid, onEdit event captures the saved value of the SalesStrategy, while onDatabound event of the Kendo dropdown (for SalesStrategy), make an ajax call to get the values from Database. I did browse through a lot of posts but did not find a better way to acccomplish this.

Please note - In one of the images (SalesStrategy_OnCellClick.jpg) the shows tooltip as well (on mouse hover in the SalesStrategy Grid Cell)- Please note i've NOT included that code in this post, will do so if anyone asks for / requires it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)