Telerik MVC Grid with Dynamic Columns at Run Time from a Collection or Dictionary

15,242

Solution 1

Using dynamically defined columns with the Telerik grid control can be tricky. But in your case, it's mainly a typical pitfall of closures.

In the following loop, the compiler will bind each instance of gl => gl.Amounts[i - 1] to the variable i and evaluate it later:

for (int i = 1; i <= (Model.VendorPaymentsGLAccounts[0].Columns.Count() - 1); i++)
{
  string colTemp = Model.VendorPaymentsGLAccounts[0].Columns[i - 1];
  columns.Template(gl => gl.Amounts[i - 1]).Title(colTemp).Width(100);
}

In fact, it's evaluated after the loop has finished. So i will always have the value that lead to the completion of the loop.

The fix is to use a temporary variable:

for (int i = 1; i <= (Model.VendorPaymentsGLAccounts[0].Columns.Count() - 1); i++)
{
  string colTemp = Model.VendorPaymentsGLAccounts[0].Columns[i - 1];
  int columnIndex = i - 1;
  columns.Template(gl => gl.Amounts[columnIndex]).Title(colTemp).Width(100);
}

Solution 2

I had the same problem, and was googling plenty of hours around, and have done a lot of attempts from various assistance lines. But even so, it was not so trivial to solve!

And for that reason and to have also another working example here, I will provide also my solution!

Information: It only works at my place with an IList Model. Other collections had also caused problems!

@model IList<CBS.Web.Models.Equipment.EquipmentViewModel>
@(Html.Telerik().Grid(Model)
    .Name("Grid")

    .DataKeys(keys =>
    {
        keys.Add(m => m.ID);
    })

    .DataBinding(dataBinding =>
    {
        dataBinding.Ajax()
            // renders the grid initially
            .Select("EquipmentGrid", "Equipment");
    })                    

    .Columns(columns =>
    {
        // Equipment IDs
        columns.Bound(m => m.ID).Hidden(true);
        columns.Bound(m => m.Name).Title("Equipments").Width(200);

        // Every item (EquipmentViewModel) of the Model has the same Count of Fields
        for (int i = 0; i < (Model[0].Fields.Count()); i++)
        {
            // Name of the column is everytime same as in Model[0]
            string columnName = Model[0].Fields.ElementAt(i).FieldDefinition.Name;
            // Constructs i-counted columns, dynamically on how much
            // Fields are owned by an Equipment. But note, that all Equipment-items
            // in the Model must have the same Count and disposal of Fields! 
            columns.Template(m => m.Fields
                                    .Where(f => f.FieldDefinition.Name == columnName)
                                    .Where(f => f.EquipmentId == m.ID).First().Value)
                                    .Title(columnName)
                                    .Width(columnName.Length * 8); // * 8 was the optimal lenght per character
        }
    })

    .ClientEvents(events => events.OnRowSelect("onRowSelected"))

    .Selectable()
    .Resizable(resizing => resizing.Columns(true))
    .Pageable()
    .Scrollable()
    .Groupable()
    .Filterable()
)

Controller:

public ActionResult EquipmentGrid(Guid id)
{            
    var belongingEquipments = _equipmentRepository.GetNotDeleted()
                                .OrderBy(e => e.Name).ToList()
                                .Where(e => e.RevisionId == id);

    List<EquipmentViewModel> equVMList = new List<EquipmentViewModel>();

    for (int i = 0; i < belongingEquipments.Count(); i++)
    {
        var equVM = new EquipmentViewModel
        {
            ID = belongingEquipments.ElementAt(i).ID,
            Name = belongingEquipments.ElementAt(i).Name,
            RevisionId = belongingEquipments.ElementAt(i).RevisionId,
            EquipmentTypeId = belongingEquipments.ElementAt(i).EquipmentTypeId,

            Fields = SortFields(belongingEquipments.ElementAt(i).Fields.ToList())
        };
        equVMList.Add(equVM);
    }

    return PartialView("EquipmentGrid", equVMList);
}

Models:

namespace CBS.Web.Models.Equipment
{
    public class EquipmentViewModel
    {
        public Guid ID { get; set; }
        public string Name { get; set; }

        public Guid RevisionId { get; set; }                            
        public Guid EquipmentTypeId { get; set; }

        public virtual ICollection<FieldEntity> Fields { get; set; }
    }
}

FieldDefinition

namespace CBS.DataAccess.Entities
{
    public class FieldDefinitionEntity : EntityBase
    {
        [Required]
        public virtual Guid EquipmentTypeId { get; set; }
        public virtual EquipmentTypeEntity EquipmentType { get; set; }

        [Required(AllowEmptyStrings = false)]
        public virtual string Name { get; set; }

        public virtual int Numbering { get; set; }

        [Required]
        public virtual Guid TypeInformationId { get; set; }
        public virtual TypeInformationEntity TypeInformation { get; set; }

        public virtual ICollection<FieldEntity> Fields { get; set; }
    }
}

Field

namespace CBS.DataAccess.Entities
{
    public class FieldEntity : EntityBase
    {
        [Required]
        public virtual Guid EquipmentId { get; set; }
        public virtual EquipmentEntity Equipment { get; set; }

        [Required]
        public virtual Guid FieldDefinitionId { get; set; }
        public virtual FieldDefinitionEntity FieldDefinition { get; set; }

        public virtual string Value { get; set; }
    }
}

Solution 3

I dynamically bind the columns at runtime with reflection:

@model IEnumerable<object>
@using System.Collections
@using System.Collections.Generic
@using System.Reflection;

@(Html.Telerik().Grid(Model)
    .Name("Grid")
    .Columns(columns =>                    
        {
            Type t = Model.GetType().GetGenericArguments()[0];
            foreach (var prop in t.GetProperties())
            {
                if (IsCoreType(prop.PropertyType))
                {
                    columns.Bound(prop.PropertyType, prop.Name);
                }
            }
        })    
        .DataBinding(binding => binding.Ajax()                                        
            .Select("SelectMethod", "SomeController")                                    
        )    
    .Sortable()    
    .Pageable()
    .Filterable()
    .Groupable()
)

@functions{
    public bool IsCoreType(Type type)
    {
        if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
        {
            return IsCoreType(type.GetGenericArguments()[0]);
        }
        return !(type != typeof(object) && Type.GetTypeCode(type) == TypeCode.Object);
    }
}

Solution 4

Here is the workaround:

@(Html.Telerik().Grid(Model.Users)
    .Name("Grid")
    .Columns(columns => {
        columns.GenerateCustomColumns(columnSettings);          
       }).DataBinding(dataBinding => dataBinding.Ajax().Select("_getusers", "home"))
           .Scrollable(scrolling => scrolling.Enabled(true).Height("auto"))
           .Pageable(paging => paging.Enabled(true)
           .PageSize(10, new int[] { 5, 10, 20, 50, 100, 500 })
           .Position(GridPagerPosition.Both)
           .Total(Model.Users.Count)
           .Style(GridPagerStyles.PageSizeDropDown | GridPagerStyles.NextPreviousAndNumeric)
                           .PageTo(1))
           .Filterable(filtering => filtering.Enabled(true))
           .Reorderable(reordering => reordering.Columns(true))
               .NoRecordsTemplate(" ")
           .EnableCustomBinding(true)

)

// Extension method to genarate columns dynamically

public static class TelerikMvcGridColumnHelper
{
    public static void GenerateCustomColumns<T>(this GridColumnFactory<T>   columns,List<GridCustomColumnSettings> settings) where T:class
    {
        if (settings != null)
        {
            settings.ForEach(column =>
            {
                var boundedColumn = columns.Bound(column.Member);
                if (column.ClientFooterTemplate != null)
                    boundedColumn.ClientFooterTemplate(column.ClientFooterTemplate);

                if (!string.IsNullOrEmpty(column.Width))
                    boundedColumn.Width(column.Width);

            });
        }

    }
}

// Column settings class

public class GridCustomColumnSettings : GridColumnSettings
 {
    public string ClientFooterTemplate { get; set; }

 }
Share:
15,242
Eric Garrison
Author by

Eric Garrison

Senior Applications Developer.

Updated on June 22, 2022

Comments

  • Eric Garrison
    Eric Garrison about 2 years

    After spending the last couple days searching, I'm officially stuck. I'm working on a binding an object to the Telerik MVC 3 Grid, but the catch is that it needs to have dynamically created columns (not auto generated). Three of the columns are known, the others are unknown, and this is the tricky part. Basically, it can be like these examples:

    KnownColumn1 | KnownColumn2 | UnknownColumn1 | KnownColumn3 KnownColumn1 | KnownColumn2 | UnknownColumn1 | UnknownColumn2 | UnknownColumn3 | KnownColumn3 etc.

    Because I'm putting the unknown columns in a list (I've tried a dictionary too so I can get the column names), this has complicated things for me when binding. My code is below:

    Model (There can be zero to hundreds of rows, but this model is in a view model of type List, there can also be 0 to 20 plus columns that are dynamically added)

    public class VendorPaymentsGLAccount
    {
        public string GeneralLedgerAccountNumber { get; set; }
        public string GeneralLedgerAccountName { get; set; }
        public string DisplayName { get { return string.Format("{0} - {1}", GeneralLedgerAccountNumber, GeneralLedgerAccountName); } }
        public Dictionary<string, double> MonthAmount { get; set; }
        public double Total { get { return MonthAmount.Sum(x => x.Value); } }
        public List<string> Columns { get; set; }
        public List<double> Amounts { get; set; }
    
        public VendorPaymentsGLAccount() { }
    }
    

    View (The section that's commented out was trying to use the dictionary)

    <fieldset>
        <legend>General Ledger Account Spend History</legend>
        @if (Model.VendorPaymentsGLAccounts != null)
        {
    
                <br />
                @(Html.Telerik().Grid(Model.VendorPaymentsGLAccounts)
                        .Name("Grid")
                        .Columns(columns =>
                        {
                            columns.Bound(gl => gl.DisplayName).Title("General Ledger Account").Width(200).Filterable(false).Sortable(false);
    
                            //foreach (var month in Model.VendorPaymentsGLAccounts[0].MonthAmount)
                            //{
                            //    //columns.Bound(gl => gl.MonthAmount[month.Key.ToString()].ToString()).Title(month.Key.ToString()).Width(100).Filterable(false).Sortable(false);
                            //    //columns.Template(v => Html.ActionLink(v.VoucherID, "VoucherSummary", new { id = v.VoucherID, bu = v.BusinessUnitID, dtt = v.InvoiceDate.Ticks })).Title("Voucher").Width(100);
                            //    columns.Template(gl => Html.ActionLink(gl.MonthAmount[month.Key.ToString()].ToString(), "VoucherSummary")).Title(month.Key.ToString()).Width(100);
                            //}
    
                            for (int i = 1; i <= (Model.VendorPaymentsGLAccounts[0].Columns.Count() - 1); i++)
                            {
                                string colTemp = Model.VendorPaymentsGLAccounts[0].Columns[i - 1];
                                columns.Template(gl => gl.Amounts[i - 1]).Title(colTemp).Width(100);
                            }
    
                            columns.Template(gl => String.Format("{0:C}", gl.Total)).Title("Total");
                        })
                        .Sortable()
                        .Pageable()
                        .Filterable()
                        .Footer(true))
        }
        else
        {
            <br />
            @:There are no records that match your selected criteria.
        }
    </fieldset>
    

    Using the dictionary approach, I was able to get the columns generated correctly with the right header text, but the values for the columns (in my testing there were only 2 columns) were the same. Can anyone help with this? This seems to be an oddball issue. Just trying to figure out how to do this correctly.

    Update: Here is a screen shot using the dictionary approach that shows the issue. The column headings are correct, but the values are the same for both of the dynamic columns.

    Problem Screenshot