Wednesday, 1 June 2016

Dynamic Grid with Dynamic controls for Dynamic data using Telerik KendoUI MVC Grid.

Hi friends,

It was an requirement for one of my project and I had to work hard to achieve this. Hence thought it would be worth sharing with you all.

Problem Statement:
Grid should render data with proper controls (like dropdown for list, datepicker for date, checkbox for bool value etc) to provide edit/add feature in InCell mode (User should be able to edit data within the row).
Below picture will tell you what I mean:




As I said, I used telerik kendoUI ASP.NET MVC controls for this, so here are the prerequisite thing which you need.
1. Kendo.MVC.dll (reference for your project)
2. List of scripts files from KendoUI:
  • jquery.min.js (strongly recommended to use the one which you get from Telerik)
  • kendo.all.min.js
  • kendo.aspnetmvc.min.js 
These js files should be loaded in the same order as it is mentioned here because of dependency. Change in order will throw script error.

3. Use kendo css files for look and feel as per your choice.

These all you will get if you install Telerik KendoUI ASP.NET MVC controls by downloading it from Telerik here.



Here we go for solution:

Because of dynamism of data and controls, your most important task would be, designing the Model for Grid's template and data.

First thing you would need columns metadata information for grid so that you can design grid template for dynamic data and this is what you should have.

FEGridViewModel.cs

 public class FEGridViewModel
    {
        public List<Column> Columns { get; set; }
    }

    public class Column
    {
        public string ColumnName { get; set; }

        public ColumnType ColumnType { get; set; }

        public List<KeyValueData> ListData { get; set; }

        public bool Editable { get; set; }

        public bool Unique { get; set; }
    }

    public class KeyValueData
    {
        public string Name { get; set; }

        public string Value { get; set; }

    }
    public enum ColumnType
    {
        Normal,
        List,
        TrueFalse,
        DateTime
    }


Next we should design the view for grid where we will use this model to design the grid template.

View.cshtml
@model FastEditAPI.Models.FEGridViewModel

@(Html.Kendo().Grid<dynamic>()
.Name("dynamicGrid")
.Columns(columns =>
{
    foreach (var cols in Model.Columns)
    {
        if (cols.ColumnType == FastEditAPI.Models.ColumnType.List)
        {
            columns.ForeignKey(cols.ColumnName, new SelectList(cols.ListData, "Value", "Name", cols.ListData.First().Value)).EditorTemplateName("GridForeignKey")
            .Title(cols.ColumnName);
        }
        else if (cols.ColumnType == FastEditAPI.Models.ColumnType.TrueFalse)
        {
            columns.Template(@<text></text>).ClientTemplate("<input type='checkbox' #= " + cols.ColumnName + " ? checked='checked':'' # class='chkbx' />").Title(cols.ColumnName);
        }
        else if (cols.ColumnType == FastEditAPI.Models.ColumnType.DateTime)
        {
            columns.Bound(cols.ColumnName).Format("{0:dd/MM/yyyy}").EditorTemplateName("Date").Title(cols.ColumnName);
        }
        else
        {
            columns.Bound(cols.ColumnName).EditorTemplateName("String");
        }
    }
})
        .ToolBar(toolbar =>
        {
            toolbar.Save();
        })
        .Editable(editable => editable.Mode(GridEditMode.InCell))
        .Pageable()
        .Navigatable()
        .Sortable()
        .Scrollable()
        .DataSource(dataSource => dataSource
        .Ajax()
        .Batch(true)
        .PageSize(20)
        .ServerOperation(false)
        .Events(events => events.Error("error_handler"))
            .Model(model =>
            {
                foreach (var cols in Model.Columns)
                {
                    if (cols.Unique)
                        model.Id(cols.ColumnName);
                    if (!cols.Editable)
                        model.Field(cols.ColumnName, cols.Type).Editable(false);
                }
            })
            .Read(r => r.Action("feGrid_Read", "FE"))
            .Update(r => r.Action("feGrid_Update", "FE"))
            )



Here we go, Based on the column metadata code here will create controls. i.e. if column metadata says it's a List then code will create a drop down. Except this rest other templates are quite straight forward and the dropdown template can be only achieved here through ForeignKey concept.
 columns.ForeignKey(cols.ColumnName, new SelectList(cols.ListData, "Value", "Name", cols.ListData.First().Value)).EditorTemplateName("GridForeignKey")
            .Title(cols.ColumnName);
This ForeignKey concept requires an editor template which you should have under View=>Shared=>EditorTemplates folder with the name of "GridForeignKey" (this name you can provide with your choice but it must be same as provided in above code (as per your view). This is how GridForeignKey.cshtml should be:


GridForeignKey.cshtml
@model object        
@(
 Html.Kendo().DropDownListFor(m => m)      
        .BindTo((SelectList)ViewData[ViewData.TemplateInfo.GetFullHtmlFieldName("") + "_Data"])
)


Next important and interesting thing is model type for grid data which must be of type 'System.Dynamic.dynamic' so we can create dynamically a class of having same properties as the name of columns there in column metadata.

Now let's see how we are going to write/create dataset for grid with dynamic columns name.
Oops I forgot to tell you that, your main action controller's method which will render your view should return the model (I mean FEGridViewModel).

FEController.cs
public ActionResult Index()
        {
            FEGridViewModel model1 = new FEGridViewModel();
            model1.Columns = new List<Column>()
            {
                new Column() { ColumnName = "id", Type = typeof(String), Editable = false, Unique = true, ColumnType = ColumnType.Normal},
                new Column() { ColumnName = "name", Type = typeof(String), Editable = true, Unique = false, ColumnType = ColumnType.Normal},
                new Column() { ColumnName = "corp_name", Type = typeof(String), Editable = true, Unique = false, ColumnType = ColumnType.Normal},
                new Column() { ColumnName = "cc_currency_id", Type = typeof(String), Editable = true, Unique = false, ColumnType = ColumnType.List,
                    ListData = new List<KeyValueData> {
                        new KeyValueData { Name = "Dollars - USD", Value = "Dollars - USD" },
                        new KeyValueData { Name ="Other", Value ="Other" },
                        new KeyValueData { Name ="¥ yen", Value ="¥ yen" },
                        new KeyValueData { Name ="Zimbabwee Dollar", Value ="Zimbabwee Dollar" },
                        new KeyValueData { Name ="French Franc", Value ="French Franc" },
                    }
                },
                new Column() { ColumnName = "delete_flag", Type = typeof(String), Editable = true, Unique = false, ColumnType = ColumnType.List,
                ListData = new List<KeyValueData> {
                        new KeyValueData { Name = "Delete", Value ="Delete" },
                        new KeyValueData { Name = "Not Delete", Value ="Not Delete" },
                    }
                },
                new Column() { ColumnName = "date", Type = typeof(DateTime), Editable = true, Unique = false, ColumnType = ColumnType.DateTime },
                new Column() { ColumnName = "IsActive", Type = typeof(DateTime), Editable = true, Unique = false, ColumnType = ColumnType.TrueFalse },
            };

            return View(model1);
        }



And finally the grid dynamic data will be returned from the controller method "feGrid_Read" of "FE" controller (or whatever details provide in grid UI for event Read based on your code). This controller method will be called through Ajax call by Telerik. Thanks to Telerik to take care such things and making developer life easy.

Now let's create the data set for grid:

public ActionResult feGrid_Read([DataSourceRequest] DataSourceRequest request)
        {
            List<string> strList = new List<string>() { "Dollars - USD", "Other", "¥ yen", "Zimbabwee Dollar", "French Franc" };
            List<dynamic> flexiList = new List<dynamic>();

                for (var i = 1; i <= 30; i++)
                {
                    string curr = strList[new Random().Next(1, 5)];
                    var flexible = new ExpandoObject() as IDictionary<string, Object>; ;
                    flexible.Add("id", (100 + i).ToString());
                    flexible.Add("name", "Data0" + i.ToString());
                    flexible.Add("corp_name", "T");
                    flexible.Add("cc_currency_id", curr);
                    //flexible.Add("cc_currency", new KeyValueData() { Name = curr , Value = curr });
                    flexible.Add("delete_flag", "Not Delete");
                    flexible.Add("date", DateTime.Now.ToShortDateString());
                    flexible.Add("IsActive", i%2==0? true : false);
                    //flexible.Add("flags", new KeyValueData() { Name = "Not Delete", Value = "Not Delete" });
                    flexiList.Add(flexible);
                }
 DataSourceResult dsresult = (flexiList as IEnumerable<dynamic>).ToDataSourceResult(request);
            var serializer = new JavaScriptSerializer();
            serializer.RegisterConverters(new JavaScriptConverter[] { new ExpandoJSONConverter() });
            var json = serializer.Serialize(dsresult);
            return new MyJsonResult(json);
}


because of dynamic data we need to serialize data to json manually. Hence you will be required below two classes which I have used in above code, "ExpandoJSONConverter" and "MyJsonResult". Thanks to Telerik again for creating these two classes.

ExpandoJSONConverter.cs
 public class ExpandoJSONConverter : JavaScriptConverter
    {
        public override object Deserialize(IDictionary<string, object> dictionary, Type type, JavaScriptSerializer serializer)
        {
            throw new NotImplementedException();
        }
        public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer)
        {
            var result = new Dictionary<string, object>();
            var dictionary = obj as IDictionary<string, object>;
            foreach (var item in dictionary)
                result.Add(item.Key, item.Value);
            return result;
        }
        public override IEnumerable<Type> SupportedTypes
        {
            get
            {
                return new ReadOnlyCollection<Type>(new Type[] { typeof(System.Dynamic.ExpandoObject) });
            }
        }
    }


MyJsonResult.cs
public class MyJsonResult : ActionResult
    {

        private string stringAsJson;

        public MyJsonResult(string stringAsJson)
        {
            this.stringAsJson = stringAsJson;
        }

        public override void ExecuteResult(ControllerContext context)
        {
            var httpCtx = context.HttpContext;
            httpCtx.Response.ContentType = "application/json";
            httpCtx.Response.Write(stringAsJson);
        }
    }


And we are done.

Thank You for reading. Feel free to provide your valuable feedback.