Search code examples
c#asp.netasp.net-mvcwebgrid

How can I make WebGrid columns take a real lambda to be typesafe?


When creating a WebGridColumn, it requires passing in a string for the columnName that will be used. Why doesn't it allow passing a lambda expression like most other HTML Helpers? How can I pass a simple lambda to gain intellisense hints, preserve type-safety, and prevent runtime errors due to typos? And with the column name, shouldn't WebGrid be able to use the metadata display attributes to set the header and format by default?

What we use on a regular basis:

@{ var grid = new WebGrid(model) }

@grid.GetHtml(
    // table style formats here...

    columns: grid.Columns(
        grid.Column(columnName: "StudentName",
                    header: Html.DisplayNameFor(model => model.StudentName)),
        grid.Column(columnName: "Birthdate",
                    header: Html.DisplayNameFor(model => model.Birthdate),
                    format: (item) => Html.DisplayFor(modelItem => (item as WebGridRow).Value as Person).Birthdate),
        grid.Column(columnName: "Address.Physical.StreetNumber",
                    header: Html.DisplayFor(model => model.Address.Physical.StreetNumber)),
        grid.Column(columnName: "Address.Physical.StreetName",
                    header: Html.DisplayFor(model => model.Address.Physical.StreetName))
    )
)

Solution

  • This was the question posed to me by a colleague. We use WebGrid extensively, and having to repeatedly type the same info in duplicate or triplicate for every column was... frustrating to say the least. I looked into extending WebGrid itself, but that seemed like it would take a loooot of work to make generic and type-safe. Instead, I wrote an HtmlHelper extension that returns a WebGridColumn.

    Code

    public static WebGridColumn GridColumnFor<TModel, TValue>(this HtmlHelper<IEnumerable<TModel>> html, Expression<Func<TModel, TValue>> exp, string header = null, Func<dynamic, object> format = null, string style = null, bool canSort = true)
    {
        var metadata = ModelMetadata.FromLambdaExpression(exp, new ViewDataDictionary<TModel>());
        var modelText = ExpressionHelper.GetExpressionText(exp);
    
        if (format == null && metadata.DisplayFormatString != null)
        {
            format = (item) => string.Format(metadata.DisplayFormatString, item[modelText] ?? String.Empty);
        }
    
        return new WebGridColumn()
        {
            ColumnName = modelText,
            Header = header ?? metadata.DisplayName ?? metadata.PropertyName ?? modelText.Split('.').Last(),
            Style = style,
            CanSort = canSort,
            Format = format
        };
    }
    

    Anywhere you would normally use WebGrid.Column() instead use Html.GridColumnFor().

    columns: grid.Columns(
        Html.GridColumnFor(model => model.Name),
        Html.GridColumnFor(model => model.Birthdate),
        Html.GridColumnFor(model => model.Address.Physical.StreetNumber),
        Html.GridColumnFor(model => model.Address.Physical.StreetName),
    )
    

    That is beautiful...

    What's going on?

    It uses built-in Expression functionality to get the full property name to pass to the WebGridColumn constructor, and gets the already built-in metadata from MVC to get the name to display for headers, and check for formatting.

    It also accepts overrides for the header and format values, so you can set them yourself; you may want one Person object for multiple pages, but some to call the Birthdate field "Birthday", and maybe others to call it "Date of Birth" (we try to avoid that, but sometimes it's an end user requirement), or show only the day and month:

    Html.GridColumnFor(model => model.Birthdate, format: (item) => item.Birthdate.ToString("MM/dd"))