views:

492

answers:

4

I'm trying to build a generic grid view in an ASP.NET MVC application.

Let me explain with some code:

public interface ITrustGrid<T>
{
    IPagedList<T> Elements { get; set; }
    IList<IColumn<T>> Columns { get; set; }
    IList<string> Headers { get; }
}

This is an interface of a class that allows me to set columns and expressions in my controller.

I pass implementations to a partial view like this:

<% Html.RenderPartial("SimpleTrustGridViewer", ViewData["employeeGrid"] as TrustGrid<EmployeeInfoDTO>); %>

The problem is that I can't figure out how to make the partial view that renders the grid generic.

In other words, I want to turn this:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<ITrustGrid<EmployeeInfoDTO>>" %>

into something like this:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<ITrustGrid<T>>" %>

=> How can I make my partial view generic in the most simple way?

EDIT:

I solved this by using a TrustGridBuilder that has a public TrustGrid GetTrustGrid() method which returns a non-generic TrustGrid. The TrustGrid contains strings instead of linq stuff. So I execute the linq in the GetTrustGrid() method and put the strings in a TrustGrid object.

Thanks for everybody to help me on the right track.

+3  A: 

It's not possible to do it like that. The reason is the .aspx will generate a class that you don't have much control on it and you can't add a generic parameter to it. I guess the most straightforward way is to pass it as object.

Mehrdad Afshari
hmm I tried that, but wasn't able to figure out how to cast it back to something useful.
Thomas Stock
+2  A: 

You could make all your model types that you would pass into this partial inherit from a base class/interface that establishes the basic behavior that would be used by this partial view and accept any object of that class/interface type, or just have the view accept any type of object and then use reflection to base your partial view behavior off of.

EXAMPLE:

public interface IDisplayModel
{
    string DisplayText{get;set;}
    string ImageUrl{get;set;}
    string AltText{get;set;}
}

public interface ITrustGrid<T> where T : IDisplayModel
{    
    IPagedList<T> Elements { get; set; }    
    IList<IColumn<T>> Columns { get; set; }    
    IList<string> Headers { get; }
}

<%@ Control Language="C#" 
    Inherits="System.Web.Mvc.ViewUserControl<ITrustGrid<IDisplayModel>>" %>

Naturally, your IDisplayModel would vary based on your desired behavior. This would then allow you to pass in anything to this partial that implements this base interface to establish general behavior.

Adam Carr
Could you give a short example on how to achieve the reflection solution?
Thomas Stock
Added example and changed the contol inherits attribute to accept "ITrustGrid<IDisplayModel>" Hopefully this is what you were looking for.
Adam Carr
Thanks but I don't like about this solution that I have to make every class implement the IDisplayModel interface.I would prefer a solution where I can add something in the partial view to make it work. Perhaps I could add a Type member to my ITrustGrid interface? I'm trying something like that at the moment, but I'm quite new at this generic stuff :)
Thomas Stock
You can use reflection in the view and have your view model ITrustGrid<Object> and then use reflection to determine type and display logic accordingly inside your partial but this would make the view more bloated than I would prefer. Good luck.
Adam Carr
Argh, I can't figure it out.. Been trying all kinds of things but can't get the Type of Model. I tried adding a 'Type' member to my interface but that didn't work either..
Thomas Stock
+1  A: 

I agree with Mehrdad, as far as I know it isn't possible to make generic views. In one of my projects, I used an interface much like your one, and then passed delegate functions to the view that handle the specific rendering of each item.

For instance, I would have used a non-generic view data class with an additional field:

public interface ITrustGrid {
    IPagedList Elements { get; set; }
    IList<IColumn> Columns { get; set; }
    IList<string> Headers { get; }

    Func<object, string> ElementRenderer { get; }
}

In your main view you'll prepare the view data:

<%
ITrustGrid data = (ITrustGrid)ViewData["employeeGrid"];
data.ElementRenderer = new Func<object, string>(delegate(o) {
    var employee = (Employee)o;
    //render employee
    return html;
});

Html.RenderPartial("SimpleTrustGridViewer", data);
%>

While in your grid partial you'll process the grid as usual, and then call the delegate to render each single cell:

<%
foreach(var element in ViewData.Elements){
    %>
    <tr>
        <td><%=ViewData.ElementRenderer(element) %></td>
    </tr>
    <%
}
%>

Of course, the code above only renders a single cell for each element, you'll have to create a slightly more complex delegate to render multiple columns (or pass in an array of delegates, one for each column).

I think this would be one of the cleanest ways to do it, albeit a bit cumbersome.

Lck
You brought me on the right track, thanks.
Thomas Stock
A: 

@ Lck:

I'm doing something like that in my controller:

var columns = new List<IColumn<EmployeeInfoDTO>>
                  {
                      new Column<EmployeeInfoDTO>("Full name", e => string.Format("{0} {1}", e.FirstName, e.Name)),
                      new Column<EmployeeInfoDTO>("Examination date", e => e.ExaminationDate.HasValue? string.Format("{0} days ago", currentDate.Subtract(e.ExaminationDate.Value).Days) : "Unknown")
                  };

var employeeGrid = new TrustGrid<EmployeeInfoDTO> { Columns = columns, Elements = GetEmployees(currentPageIndex)};

ViewData["employeeGrid"] = employeeGrid;

So in my partial view I can do this:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<ITrustGrid<EmployeeInfoDTO>>" %>
<table>
    <thead>
        <tr>
            <%
                foreach (string header in Model.Headers)
                    Response.Write(Html.Th(header));
            %>
        </tr>
    </thead>
    <tbody>
        <%
            foreach (var element in Model.Elements)
            {
                Response.Write("<tr>");
                foreach (var column in Model.Columns)
                    Response.Write(Html.Td(column.ValueExpression(element)));
                Response.Write("</tr>");
            }
        %>
    </tbody>
</table>
<div class="pager">
    <%= Html.Pager(Model.Elements.PageSize, Model.Elements.PageNumber, Model.Elements.TotalItemCount)%>
</div>

As you see, none of the code in my partial depends on the Type used. So I still think there's a simple solution.

Thomas Stock
Since your partial doesn't depend on the type used, can you just use Object? ITrustGrid<Object>?
Adam Carr
I get "The model item passed into the dictionary is of type 'Helpers.TrustGrid`1[LinqToSqExample.Model.EmployeeInfoDTO]' but this dictionary requires a model item of type 'Helpers.ITrustGrid`1[System.Object]'." when calling the partial view with TrustGrid<EmployeeInfoDTO>
Thomas Stock
Model is null when I call the partial with TrustGrid<object>
Thomas Stock
I get the same result as you describe above if I inherit from ITrustGrid<customtype> on TrustGrid (concrete class). If I change my TrustGrid inheritance to ITrustGrid<Object>, then it works for me.
Adam Carr
hmm will try that tomorrow. Thanks for checking it out.
Thomas Stock
I have altered my code. TrustGrid<T> now is TrustGridBuilder<T> and it has a .GetTrustGrid method that returns a TrustGrid instance. The TrustGrid instance has a IEnumerable<string> Header and a IEnumerable<IEnumerable<string>> Elements member.
Thomas Stock