Josh Anderson
Josh Anderson

Reputation: 6005

Telerik MVC Grid not grouping properly

I'm using the Telerik MVC Grid component to render a groupable grid populated via ajax. The grid renders fine, sorting and paging works, ajax refresh works, but when I try to do grouping, the rendering gets all screwed up. Attached are screen caps of the grid pre- and post-grouping.

The grid definition is pretty straight-forward:

<div id="tabAccounts" class="tab_content">
    @(Html.Telerik().Grid<SharedSimpleAccountListViewModel>()
            .Name("AcctGrid")
            .Columns(columns =>
            {
                columns.Bound(x => x.Number)
                    .HeaderHtmlAttributes(new { @style = "text-align: center;" })
                    .HtmlAttributes(new { @style = "text-align: center;" });
                columns.Bound(x => x.ProviderOrganizationFriendlyName)
                    .Title("Provider");
                columns.Bound(x => x.Name)
                    .Title("Account Name");
                columns.Bound(x => x.BillingLocationName)
                    .Title("Location");
            })
            .Groupable()
            .DataBinding(db => db.Ajax().Select("CustomerAccounts", "Customers", new { id = Model.Id }))
            .Pageable(pager => pager.PageSize(50))
            .Sortable()
    )
</div>

The controller action is also straight-forward (I won't paste since it's just a retrieval from the repository). I'm using the Telerik default theme, so there's no custom CSS and I've confirmed that the required scripts are included in the page.

Inspecting the HTML after the grouping, it appears that there are changes made to the table, but it's not adding the table row element for the group. Here's the HTML that exists after a grouping attempt:

<table cellspacing="0">
    <colgroup>
        <col class="t-group-col">
            <col><col><col><col>
        </colgroup>
    <thead class="t-grid-header">
        <tr>
            <th class="t-group-cell t-header"> </th>
            <th style="text-align: center;" scope="col" class="t-header">
                <a href="/Customers/Details/408?AcctGrid-orderBy=Number-asc" class="t-link">Number</a>
            </th>
            <th scope="col" class="t-header">
                <a href="/Customers/Details/408?AcctGrid-orderBy=ProviderOrganizationFriendlyName-asc" class="t-link">Provider</a>
            </th>
            <th scope="col" class="t-header">
                <a href="/Customers/Details/408?AcctGrid-orderBy=Name-asc" class="t-link">Account Name</a>
            </th>
            <th scope="col" class="t-header t-last-header">
                <a href="/Customers/Details/408?AcctGrid-orderBy=BillingLocationName-asc" class="t-link">Location</a>
            </th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td style="text-align: center;">00002</td>
            <td>Acme</td>
            <td>Test account 2 </td>
            <td class="t-last">Location 2</td>
        </tr>
        <tr class="t-alt">
            <td style="text-align: center;">00001</td>
            <td>3M</td>
            <td>Test account 1</td>
            <td class="t-last">Location 1</td>
        </tr>
    </tbody>
</table>

Any ideas what might be going on here?

Upvotes: 4

Views: 2642

Answers (1)

Josh Anderson
Josh Anderson

Reputation: 6005

The crux of the issue here is that I was doing AJAX binding but wanted to do grouping and sorting. What that required was a manual sorting process that sorted by the grouped column first, then the other sorted columns. The grid then takes care of setting up the group UI. This was a bit of a challenge for me because my project uses NHibernate as the ORM with a fairly robust service layer that handles querying. I ended up getting the grid to work with a helper class that looks like this:

public static class TelerikGridHelpers
{
    public static IEnumerable<AggregateFunctionsGroup> BuildInnerGroup<T, TObject>(IEnumerable<TObject> group, Func<TObject, T> groupSelector, Func<IEnumerable<TObject>, IEnumerable> innerSelector)
    {
        return group.GroupBy(groupSelector)
                .Select(i => new AggregateFunctionsGroup
                {
                    Key = i.Key,
                    Items = innerSelector(i)
                });
    }

    public static Func<IEnumerable<TObject>, IEnumerable<AggregateFunctionsGroup>> BuildGroup<T, TObject>(Func<TObject, T> groupSelector, Func<IEnumerable<TObject>, IEnumerable<AggregateFunctionsGroup>> selectorBuilder)
    {
        var tempSelector = selectorBuilder;
        return g => g.GroupBy(groupSelector)
                     .Select(c => new AggregateFunctionsGroup
                     {
                         Key = c.Key,
                         HasSubgroups = true,
                         Items = tempSelector.Invoke(c).ToList()
                     });
    }

    public static IEnumerable<AggregateFunctionsGroup> ApplyGrouping<T>(IQueryable<T> data, IList<GroupDescriptor> groupDescriptors)
    {
        Func<IEnumerable<T>, IEnumerable<AggregateFunctionsGroup>> selector = null;
        foreach (var descriptor in groupDescriptors.Reverse())
        {
            var tempDescriptor = descriptor;
            if (selector == null)
                selector = g => BuildInnerGroup(g.Select(p => p), p => p.GetType().GetProperty(tempDescriptor.Member).GetValue(p, null), i => i.ToList());
            else
                selector = BuildGroup(p => p.GetType().GetProperty(tempDescriptor.Member).GetValue(p, null), selector);
        }

        return selector != null
                   ? selector.Invoke(data).ToList()
                   : null;
    }

    public static List<Order> GenerateOrderList<T>(this T translator, GridCommand command) where T : IPropertyNameTranslator
    {
        var orders = new List<Order>();
        // Step 1 is to add the grouping orders
        if (command.GroupDescriptors.Any())
            orders.AddRange(from descriptor in command.GroupDescriptors
                            let sortField = translator.TranslatePropertyToDomainProperty(descriptor.Member)
                            select descriptor.SortDirection == ListSortDirection.Ascending ? Order.Asc(sortField) : Order.Desc(sortField));

        // Then the sorting
        if (command.SortDescriptors.Any())
            orders.AddRange(from descriptor in command.SortDescriptors.Where(c => !command.GroupDescriptors.Where(g => g.Member == c.Member).Any())
                            let sortField = translator.TranslatePropertyToDomainProperty(descriptor.Member)
                            select descriptor.SortDirection == ListSortDirection.Ascending ? Order.Asc(sortField) : Order.Desc(sortField));

        return orders;
    }

    public static List<ViewOrder> GenerateViewOrderList<T>(this T translator, GridCommand command) where T : IPropertyNameTranslator
    {
        var orders = new List<ViewOrder>();
        // Step 1 is to add the grouping orders
        if (command.GroupDescriptors.Any())
            orders.AddRange(from descriptor in command.GroupDescriptors
                            let sortField = translator.TranslatePropertyToDomainProperty(descriptor.Member)
                            select new ViewOrder { PropertyName = sortField, Ascending = descriptor.SortDirection == ListSortDirection.Ascending});

        // Then the sorting
        if (command.SortDescriptors.Any())
            orders.AddRange(from descriptor in command.SortDescriptors.Where(c => !command.GroupDescriptors.Where(g => g.Member == c.Member).Any())
                            let sortField = translator.TranslatePropertyToDomainProperty(descriptor.Member)
                            select new ViewOrder { PropertyName = sortField, Ascending = descriptor.SortDirection == ListSortDirection.Ascending });

        return orders;
    }

}

Note that I'm using ViewModels that have flattened property names, so if my domain object has a property of type Address, the ViewModel might have a property name of AddressStreet and AddressCity. My IPropertyTranslator interface specifies a translation process where I can go from the string sort member names in the GridCommand object to what my domain expects.

The Order class in the second-to-last method is an NHibernate Order. This method is used to generate the list of Order objects that I pass to my service layer when I retrieve the results. The ViewOrder is a utility class I use in the UI. I still need to refactor those last two methods since they are repetitive.

Here's an example of how I use that helper class to pull the GridModel for the grid:

    public GridModel GetAllOrdersGrid(GridCommand command)
    {
        var svc = DependencyResolver.Current.GetService<IOrderService>();

        var propertyTranslator = new OrdersViewModelTranslator();
        var orders =
            propertyTranslator.GenerateOrderList(command).ToList();

        IFutureValue<long> total;
        var orders = svc.FindAll(((command.Page - 1) * command.PageSize), command.PageSize, orders, out total);

        var mapper = new Mapper<DomainOrder, OrdersViewModel>();
        var viewModels = orders.Select(mapper.MapToViewModel);


        return command.GroupDescriptors.Any()
                   ? new GridModel
                   {
                       Data = TelerikGridHelpers.ApplyGrouping(viewModels.AsQueryable(), command.GroupDescriptors),
                       Total = Convert.ToInt32(total.Value)
                   }
                   : new GridModel { Data = viewModels, Total = Convert.ToInt32(total.Value) };
    }

There's a bit in there that's irrelevant to the whole question of grouping, but it's a real world example, so maybe it will help.

Upvotes: 4

Related Questions