Thursday 2 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.

12 comments:

  1. This is a very nice example of a dynamic grid! You can also check out MVC grid from ShieldUI, it is extremely lightweight and has many options like e.g. export to PDF or Excel. Take a look at https://demos.shieldui.com/mvc/grid-general/filtering

    ReplyDelete
  2. Hi Binos,

    Could you please upload "feGrid_Update" function? I tried many way (include using ExpandoObject) but I couldn't get post data.

    ReplyDelete
    Replies
    1. sorry for late reply!
      If you are following my example as described here then here is the code for "feGrid_Update" action method which I had:
      [AcceptVerbs(HttpVerbs.Post)]
      public ActionResult feGrid_Update([DataSourceRequest] DataSourceRequest request, [Bind(Prefix = "models")]IEnumerable gridData, string tabName)
      {
      var data = Session["QueryData"] as List;
      if (gridData?.Count() > 0)
      {
      foreach (dynamic vm in gridData)
      {
      //var item = data.Find(d => d.Columns.Name == vm.Columns.Name);
      //if (item != null)
      // data.Remove(item);
      //data.Add(item);
      }
      }
      Session["QueryData"] = data;
      return Json(gridData.ToDataSourceResult(request, ModelState));
      }

      Note: You can ignore the session here

      Delete
    2. The above Update method is not working, Do I need to deserialize the IEnumerable gridData? gridData is coming as Object. I am not sure how to TypeCast it. Could you please help me on this?

      Delete
  3. HLO this post is still active?

    ReplyDelete
  4. Hi Binos,
    This post is very helpful for creating dynamic grid.

    Could you please help me to create add a record in grid using popup editor custom template. editor template I have one dropdown. So how to create Editor Template for that What should I pass as model in that?

    ReplyDelete
  5. Hey Binod, The Update action feGrid_Update is really working piece that you posted above. If you have really working method can you update that. How to pass DataTable to Update actionresult.

    ReplyDelete
  6. How can this be done using Kendo for jQuery? Heeelp.

    ReplyDelete
  7. Sorry friends, Not in touch with this now but i'm sure you will get some clue to achieve the same using jQuery as well..

    ReplyDelete
  8. Hi, very usefull post. Revealed a way for solution via Newtonsoft:

    public class MyJsonResult : ActionResult
    {
    private string stringAsJson;

    public MyJsonResult(object obj)
    {
    this.stringAsJson = Newtonsoft.Json.JsonConvert.SerializeObject(obj);
    }

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

    public ActionResult Grid_Read([DataSourceRequest] DataSourceRequest request)
    {
    return new MyJsonResult(manager.Get(model).ToDataSourceResult(request));
    }

    ReplyDelete
  9. How do I make an initial filter?
    If the table is dynamic, then the following code does not work
    ..Filter(filter => filter.Add(field => field.Price).Is Greater Than(0))
    The compiler does not see field.Price, since it is not in dynamic

    ReplyDelete