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

5

6

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

Saran answered 13/7, 2012 at 14:53 Comment(0)
G
8

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);
}
Gerrald answered 13/7, 2012 at 17:10 Comment(4)
Awesome, that worked (I had to make a small change to the "for" statement though, I removed the "- 1" from the conditional portion. Thanks for that tip, I really appreciate it. I just checked, and this also works for the dictionary approach too, using a temp variable for the dictionary key. Thanks again!Saran
How to use gl.Amounts[columnIndex] in ClientTemplate. To me it returns error columnIndex not definedWayfaring
Please open a new question and add your code. It's difficult to tell why you get an error without seeing the code.Gerrald
I was also looking for this kind of solutions it worked for me as well.Westering
V
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);
    }
}
Valvulitis answered 21/11, 2012 at 19:2 Comment(0)
L
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; }
    }
}
Lung answered 7/9, 2012 at 22:12 Comment(0)
C
1

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; }

 }
Celestial answered 18/12, 2012 at 7:25 Comment(0)
N
0

I did this simple way. NOTE : the following solution works in ajax edit mode too (not just a read-only grid) :

when the ViewModels are :

public class PriceSheetEditGridViewModel
{
    public IEnumerable<PriceSheetRowViewModel> Rows { get; set; }
    public IEnumerable<PriceSheetColumnViewModel> Columns { get; set; }
}

public class PriceSheetColumnViewModel { public int Id { get; set; } public string Title { get; set; } }

public class PriceSheetRowViewModel 
    {

        public int RowNo { get; set; }
        public string Description { get; set; }
        public double?[] Prices { get; set; }

    }

the view can be like this (part of view.cshtml file...) :

    ....
@model PriceSheetEditGridViewModel
...
                columns.Bound(o => o.Description ).Width(150);

                int i = 0;
                foreach (var col in Model.Columns)


{
                columns
                    .Bound(model => model.Prices).EditorTemplateName("PriceSheetCellPrice").EditorViewData(new { ColumnId = i })
                    .ClientTemplate("<span><#=Prices ? jsHelper.addCommas(Prices[" + i.ToString() + "]):null#></span>")
                    .Title(col.Title).Width(80);
                i++;
            }    

....

and the PriceSheetCellPrice.cshtml editor template file (in shared\editortemplates folder) :

  @model decimal?
@(Html.Telerik().NumericTextBox()
        .Name(ViewData.TemplateInfo.GetFullHtmlFieldName(string.Empty)+"["+ViewBag.ColumnId+"]")
                    .InputHtmlAttributes(new { style = "width:100%" })
})
        .EmptyMessage("")
        .DecimalDigits(0)
        .DecimalSeparator(",")
        .MinValue(0)
        .Value((double?) Model)
)
Northwards answered 13/3, 2013 at 5:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.