The story:
The idea came to my mind when summarizing multiple views using partials. I thought it would be great if we could define partial views with open generic models.
I thought at first to post the idea in ASP.NET uservoice website. When I searched there, I noticed the same idea was suggested a few years ago https://aspnet.uservoice.com/forums/41201-asp-net-mvc/suggestions/2706166-support-for-generics-in-razor-view.
There, @danroth27 had directed users to a new track ( #727 ) in ASP.NET github and asked users to comment practical examples/use cases of the suggestion.
After visiting the issue, I noticed that it was closed, because nobody had given any comment. I didn't know if someone was still allowed to comment on a closed issue or not, but I dared to try it.
I was happy to see @Eilon's quick reply some hours later still welcoming use cases for this idea.
As the scenario I wanted to mention was a little long, he recommended me to post it as new issue.
So, here am I.
The scenario involves multiple pieces of code fragments (C#, Razor views), but I do my best to make it short and only mention the meat.
Intro
Suppose we need to implement an administration panel for a website consisting of various modules, such as Blogs, Photos, News, Products, etc.
Data Layer
We use EF code first for our models and data layer.
Model objects
We use a base class for all of our model classes as below.
public class BaseEntity
{
public int Id { get; set; }
public bool IsDeleted { get; set; }
public DateTime? CreatedDate { get; set; }
public virtual User CreatedBy { get; set; }
public DateTime? ModifiedDate { get; set; }
public virtual User ModifiedBy { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
}
This structure provides us the following features:
This is our model classes:
public class Blog: EntityBase
{
public string Title { get; set; }
public string Content { get; set; }
public string ImageUrl { get; set; }
}
public class Photo: EntityBase
{
public string Title { get; set; }
public string SmallImageUrl { get; set; }
public string LargeImageUrl { get; set; }
}
public class News: EntityBase
{
public string Title { get; set; }
public string Content { get; set; }
public string Source { get; set; }
public string ThumbnailUrl { get; set; }
}
public class Product: EntityBase
{
public string Title { get; set; }
public decimal Price { get; set; }
public string SmallImageUrl { get; set; }
public string LargeImageUrl { get; set; }
}
We don't add a BlogDate, NewsDate or Author property to Blog and News models. We use CreatedDate and CreatedBy properties to satisfy this need.
For the administration panel we create an "Admin" area in our MVC application.
There, for each module, we create a controller. So, we have a BlogController, NewsController, PhotoController and ProductController.
I don't go through the code of the data layer, repositories, the controllers and their actions. They are not our concern.
I go right to the Views which is the topic of this post.
Views
We use HTML tables to provide a simple data grid with edit/delete links for each module.
This is the View of Index action in our BlogController:
@model IEnumerable<Blog>
@{
ViewBag.Title = "Blogs";
}
<table class="table table-striped">
<thead>
<tr>
<th>Id</th>
<th>Title</th>
<th>Image</th>
<th>Created Date</th>
<th>Created By</th>
<th>Modified Date</th>
<th>Modified By</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>@item.Id</td>
<td>@item.Title</td>
<td><img src="@item.ImageUrl"/></td>
<td>@item.CreatedDate</td>
<td>@(item.CreatedBy?.UserName)</td>
<td>@item.ModifiedDate</td>
<td>@(item.ModifiedBy?.UserName)</td>
<td>@Html.Action("Edit", new { Id = item.Id })</td>
<td>@Html.Action("Delete", new { Id = item.Id })</td>
</tr>
}
</tbody>
</table>
@Html.Pager(ViewBag,CurrentPage, ViewBag.PageSize, ViewBag.PageCount, ViewBag.RecordCount)
The View receives and shows one page of data. Thus, a pager is displayed at the bottom of the page using an extension method named Html.Pager() and the arguments are passed to it from ViewBag.
This suffices the whole structure of the page.
Not surprisingly, the Views for the Index action in all our other controllers, (NewsController, PhotoController and ProductController) follow a similar code.
For example, this is the view for the Index action of ProductController.
@model IEnumerable<Product>
@{
ViewBag.Title = "Products";
}
<table class="table table-striped">
<thead>
<tr>
<th>Id</th>
<th>Title</th>
<th>Price</th>
<th>Image</th>
<th>Created Date</th>
<th>Created By</th>
<th>Modified Date</th>
<th>Modified By</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>@item.Id</td>
<td>@item.Title</td>
<td>@item.Price</td>
<td><img src="@item.SmallImageUrl"/></td>
<td>@item.CreatedDate</td>
<td>@(item.CreatedBy?.UserName)</td>
<td>@item.ModifiedDate</td>
<td>@(item.ModifiedBy?.UserName)</td>
<td>@Html.Action("Edit", new { Id = item.Id })</td>
<td>@Html.Action("Delete", new { Id = item.Id })</td>
</tr>
}
</tbody>
</table>
@Html.Pager(ViewBag,CurrentPage, ViewBag.PageSize, ViewBag.PageCount, ViewBag.RecordCount)
Now let's go for the presented idea.
It would be a great help if we could define a generic View like AdminGrid.cstml like below:
@model IEnumerable<T> where T: EntityBase
<table class="table table-striped">
<thead>
<tr>
<th>Id</th>
@Html.Partial<T>("AdminGridHead")
<th>Created Date</th>
<th>Created By</th>
<th>Modified Date</th>
<th>Modified By</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>@item.Id</td>
@Html.Partial<T>("AdminGridBody", item)
<td>@item.CreatedDate</td>
<td>@(item.CreatedBy?.UserName)</td>
<td>@item.ModifiedDate</td>
<td>@(item.ModifiedBy?.UserName)</td>
<td>@Html.Action("Edit", new { Id = item.Id })</td>
<td>@Html.Action("Delete", new { Id = item.Id })</td>
</tr>
}
</tbody>
</table>
@Html.Pager(ViewBag,CurrentPage, ViewBag.PageSize, ViewBag.PageCount, ViewBag.RecordCount)
We are in fact extracting the repetitive or common HTML code out of all of our views into a single View. This is somehow following the DRY principle.
Notice that I used two generic Html.Partial
It's important to know that, despite we have a single AdminGrid.cshtml View, we should have separate AdminGridHead and AdminGridBody partial views, each one for a type that will be used for the generic T parameter in AdminGrid's model part, i.e. Blog, News, Photo, Product, etc.
added at 2017-12-23
The following could be AdminGridHead and AdminGridBody partial views for Blog type.
// AdminGridHead
<th>Title</th>
<th>Image</th>
// AdminGridBody
@model Blog
<td>@Html.DisplayFor(m => m.Title)</td>
<td>@Html.DisplayFor(m => m.Image)</td>
md5-34239864ce8288b18dc5e847463c0d75
public class BlogController: Controller
{
// ...
public ActionResult Index(int page = 1, int pagesize = 5)
{
var model = BlogRepository.GetPage(page, pagesize);
return View<Blog>(model);
}
}
It is important to call a generic View<T>() in place of the non-generic View() method which is inherited to controllers.
I explain the reason a little further.
This was the whole scenario.
I will explain potential problems we have in front of us when implementing this feature in future comments.
Because, when I looked at MVC's source code, I noticed that it's not that easy to implement this feature as it seems in the first glance (what I personally thought too at first!).
The key point is, we need to carry the generic T, from the top api like Html.Partial
Now, why we need a generic View<T>() method in our controllers? Why we can't call the old View() method in our actions to return a View whose model is generic?
The reason is that, the View cannot be instantiated without its generic parameters known.
Suppose we have the following View named AdminGrid.cshtml which has a generic IEnumerable
@model IEnumeranle<T> where T: EntityBase
<table>
@foreach (var item in Model)
{
<tr>
<td>@item.Id</td>
<td>@item.CreatedDate</td>
</tr>
}
</table>
Let's suppose such a View is supported.
The C# class that is required to be generated for this View which should inherit WebViewPage
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Mvc;
using System.Web.WebPages;
public partial class _Areas_Admin_Views_Shared_AdminGrid_cshtml<T> : System.Web.Mvc.WebViewPage<IEnumerable<T>> where T: EntityBase
{
public _Areas_Admin_Views_Shared_AdminGrid_cshtml()
{
}
public override void Execute()
{
WriteLiteral("<table>");
foreach (var item in Model)
{
WriteLiteral("<tr>");
WriteLiteral("<td>");
Write(item.Id);
WriteLiteral("</td>");
WriteLiteral("<td>");
Write(item.CreatedDate);
WriteLiteral("</td>");
WriteLiteral("</tr>");
}
WriteLiteral("</table");
}
}
The important thing in this class is that despite classes that are generated for other Views it is still a generic class with a generic T parameter.
This class cannot be instantiated unless we provide its generic parameter when instantiating it.
This is why we need a generic View<T>() method in our controllers and specify its generic parameter, so that the ViewEngine (and ViewPageActivator at the end) are able to instantiate the view from its generated class.
I was thinking that what happens if our view has a generic model with multiple generic types.
@model MyModel<T, U>
// ...
Clearly, we can no longer use the generic View<()> method in Controller class.
I think the correct way to implement this feature is to add overload methods with a Type[] array argument to which we can pass an array of types that are required when instiating from the class of the generic Views. This way we can support a model with any number of generic type parameters.
This also applies to IViewPageActivator that is responsible for instantiating views. In fact, we need to add another Create() method to the interface with a Type[] parameter.
public interface IViewPageActivator
{
object Create(ControllerContext controllerContext, Type type);
object Create(ControllerContext controllerContext, Type type, Type[] genericTypes);
}
The following could be the implementation of this new method in the DefaultViewPageActivator:
public object Create(ControllerContext controllerContext, Type type, Type[] genericTypes)
{
try
{
var genericType = type.MakeGenericType(genericTypes);
return Activator.CreateInstance(genericType);
}
catch (MissingMethodException exception)
{
// ...
}
}
+1 to this, I just spent a little while puzzling out that you can't do this (trying to setup a partial view to render pagination controls for various pages with different models, taking a PaginatedList
From a Milestone to the Backlog on May 14? Where is this? I can't believe I haven't seen what I consider to be an overwhelming Use Case for this, albeit one at which I am fully aware of the countless eyes that will roll. Send in a generic model collection, reflect on its items' property names for column headers and then iterate through the list to display those items' values. Reflection, slow, I get it. But how much dev time would be saved for simple key/value lookup lists, like Country? Let the eye rolling begin.
+1
Generics is not a cutting edge feature anymore, this is a must have in C#, and I cannot imagine an enterprise grade C# project without generics now. I'm really surprised that we don't hear much more noises about it.
Maybe for some reasons implementing this missing feature on Razor views is really hard for the MS team?
Actually I made my hands dirty a little to implement this feature.
As far as the codes were in MVC source code I handled the changes and applied appropriate changes to provide open generics feature in views.
But when I went deeper I reached to Razor Engine source code and there I stuck. The Razor Engine source code was very sophisticated. I couldn't invest more time on that. So, I dropped the case.
Clearly the folks in Razor Engine project can apply the needed changes faster.
@mansoor-omrani Can you upload your progress to GitHub? And point out where in the Razor Engine it's complicated?
Yes. Sure. I'll upload the changes I applied.
I tried to push the changes. But it failed. This is the messages I received.
Authentication failed. You may not have permission to access the repository or the repository may have been archived. Open options and verify that you're signed in with an account that has permission to access this repository.
By the way, I was working on "https://github.com/aspnet/AspNetWebStack" repository.
I opened a new issue in the aforementioned branch.
@mansoor-omrani Can you upload your progress to GitHub? And point out where in the Razor Engine it's complicated?
I created a pull request for code changes in my fork. I was working on legacy MVC.
I strongly support the need for passing a generic item into the model of a view. This is very much needed in all future versions of Asp.Net Core.
This is not something we plan to do.
Most helpful comment
+1
Generics is not a cutting edge feature anymore, this is a must have in C#, and I cannot imagine an enterprise grade C# project without generics now. I'm really surprised that we don't hear much more noises about it.
Maybe for some reasons implementing this missing feature on Razor views is really hard for the MS team?