Reputation: 1900
I am trying to write a customer HtmlHelper that allows easy creation of an Html Table from a view being passed an IEnumerable <TModel>
where TModel is a Model Object Type. I would like the calling syntax from the view to be of the style:
@Html.TableFor(model => model.AddedUserID, model => model.ClientID......., model model => AnothreFildIWantDisplayed));
I am starting off by trying to get this working with one expression being passed first e.g.
@Html.TableFor(model => model.AddedUserID)
Once that works I will get the comma separated list working using params and taking more than one expression.
My View code looks like this
@using MyNameSpaceToMyHelper
@model IEnumerable<User>
@{
ViewBag.Title = "Index";
}
<p>
</p>
@Html.TableFor(model => model.AddedUserID));
My attempt at writing an HtmlHelper is:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using System.Web.UI;
namespace MyNameSpaceToMyHelper
{
public static class GridExtensions
{
public static MvcHtmlString TableFor<TModel, TValue>(this HtmlHelper<IEnumerable<TModel>> html, Expression<Func<TModel, TValue>> expression)
{
var writer = new HtmlTextWriter(new StringWriter());
writer.RenderBeginTag(HtmlTextWriterTag.Table);
writer.RenderBeginTag(HtmlTextWriterTag.Thead);
writer.RenderBeginTag(HtmlTextWriterTag.Th);
writer.RenderBeginTag(HtmlTextWriterTag.Td);
//Colum Headers
writer.Write(html.DisplayNameFor(expression));
writer.RenderEndTag();
writer.RenderEndTag();
writer.RenderEndTag();//Close THead
//Column Data
//html.DisplayFor((Expression<Func<TModel, TValue>>)expression);
foreach (ViewDataDictionary<TModel> vdm in html.ViewData.Values)
{
writer.RenderBeginTag(HtmlTextWriterTag.Tr);
writer.RenderBeginTag(HtmlTextWriterTag.Td);
//DOESN'T COMPILE
html.DisplayFor(expression);
writer.RenderEndTag();
writer.RenderEndTag();
}
writer.RenderEndTag(); //Close Header
return new MvcHtmlString(writer.InnerWriter.ToString());
}
}
}
This does not compile because of:
html.DisplayFor(expression);
As far as my understanding goes this is because the HtmlHelper passed to my method is for HtmlHelper<IEnumerable<TModel>>
whereas when using DisplayFor
it needs to be HtmlHelper<TModel>
.
Because of this error it seems feasible that I could get this working by splitting the table header and data into two separate method calls and from my view I could call the header method then have a for loop inside the actual view which calls a method for each row or something. Something like this
@using MyNameSpaceToMyHelper
@model IEnumerable<User>
@{
ViewBag.Title = "Index";
}
<p>
</p>
@Html.TableHeadersFor(model => model.AddedUserID));
@foreach (var item in Model) {
@Html.TableRowsFor(model => model.AddedUserID));
}
This should work because TableHeadersFor
can call DisplayNameExtensions.DisplayNameFor
which takes an HtmlHelper<IEnumerable<TModel>>
and TableRowsFor
could take an HtmlHelper<TModel>
and call DisplayExtensions.DisplayFor
with an HtmlHelper<TModel>
so the incompatible HtmlHelper types problem would go away.
However I do not ideally want to do this if at all possible as I want the calling syntax to be as simple as possible. A basic premise of my question is to try and maintain a simple calling syntax without writing a for loop in the view and duplicating the expressions for choosing the columns. The entire purpose here is to design something re-usable and as easy to use for others as possible
I had a look in ILSpy at how DisplayNameExtensions.DisplayNameFor
works as this handles HtmlHelper<IEnumerable<TModel>>
as it looks like somehow an exstension method taking an HtmlHelper<IEnuermable<TModel>>
is calling internally an extension method on HtmlHelper<TModel>
but the code ILSpy is producing doesn't even recompile so I getting no where understanding how .Net is able to do this.
Is this possible in one extension method or will I be forced to split it compromising the calling syntax?
UPDATE 1 I have managed to get the following working with quite a lot of work:
@Html.BeginTableFor(model => model.AddedUserID
, model => model.Active
, new { @class = "grid" })
@foreach (var item in Model) {
@Html.DisplayRowFor(model => item.AddedUserID
, model => item.Active)
}
@Html.EndTableFor()
It's not as concise as I would like and involves a heap of work especially because each expression to BeginTableFor and DisplayRowFor can return a different type meaning I have basically had to implement a method overload for any number of expressions (I've done up to 100 so will work upto 100 columns!). This is basically the same problem they had in .Net with the Action and Func delegates where they had to write different versions for different numbers of parameters e.g. Action T1, T2,..,T16. It's obviously extremely messy but I would rather have nice calling syntax than avoid many method overloads.
Upvotes: 4
Views: 3826
Reputation: 12468
Have a look at this article for some direction and modify according to your needs.
First you need a HTML extensions like this:
public static class RazorExtensions {
public static HelperResult List<T>(this IEnumerable<T> items,
Func<T, HelperResult> template) {
return new HelperResult(writer => {
foreach (var item in items) {
template(item).WriteTo(writer);
}
});
}
}
Then in your HTML you can do the following:
{
var comics = new[] {
new ComicBook {Title = "Groo", Publisher = "Dark Horse Comics"},
new ComicBook {Title = "Spiderman", Publisher = "Marvel"}
};
}
<table>
@comics.List(
@<tr>
<td>@item.Title</td>
<td>@item.Publisher</td>
</tr>)
</table>
Upvotes: 1