Is it possible to create a generic @helper method with Razor?
Asked Answered
P

4

91

I am trying to write a helper in Razor that looks like the following:

@helper DoSomething<T, U>(Expression<Func<T, U>> expr) where T : class

Unfortunately, the parser thinks that <T is the beginning of an HTML element and I end up with a syntax error. Is it possible to create a helper with Razor that is a generic method? If so, what is the syntax?

Perfectly answered 21/1, 2011 at 15:52 Comment(3)
Still not fixed in the current MVC 4 release. :(Immovable
How is this still not fixed in VS2012?Immovable
Goodness, I can't wait for this to be added; I hope this is somewhere around "implement it yesterday" on the priority list. Partially off-topic, but alongside this, I'd like to see that the generated classes are static, unless implementation details prohibit it; reason being, is one could use generic extension helpers: @helper Foo<T>(this T o) where T : IBar { }Oakes
G
52

No, this is not currently possible. You could write a normal HTML helper instead.

public static MvcHtmlString DoSomething<T, U>(
    this HtmlHelper htmlHelper, 
    Expression<Func<T, U>> expr
) where T : class
{
    ...
}

and then:

@(Html.DoSomething<SomeModel, string>(x => x.SomeProperty))

or if you are targeting the model as first generic argument:

public static MvcHtmlString DoSomething<TModel, TProperty>(
    this HtmlHelper<TModel> htmlHelper, 
    Expression<Func<TModel, TProperty>> expr
) where TModel : class
{
    ...
}

which will allow you to invoke it like this (assuming of course that your view is strongly typed, but that's a safe assumption because all views should be strongly typed anyways :-)):

@Html.DoSomething(x => x.SomeProperty)
Gradation answered 21/1, 2011 at 16:1 Comment(2)
Hopefully this is something they add to a future version of Razor helpers. The readability of a traditional helper is much lower than the @helper syntax.Perfectly
Yeah agreed. Reverting to the older method not only sucks, but splits your helpers up arbitrarily!Sedgemoor
G
130

This is possible to achieve inside a helper file with the @functions syntax but if you want the razor-style readability you are referring to you will also need to call a regular helper to do the HTML fit and finish.

Note that functions in a Helper file are static so you would still need to pass in the HtmlHelper instance from the page if you were intending to use its methods.

e.g. Views\MyView.cshtml:

@MyHelper.DoSomething(Html, m=>m.Property1)
@MyHelper.DoSomething(Html, m=>m.Property2)
@MyHelper.DoSomething(Html, m=>m.Property3)

App_Code\MyHelper.cshtml:

@using System.Web.Mvc;
@using System.Web.Mvc.Html;
@using System.Linq.Expressions;
@functions
{
    public static HelperResult DoSomething<TModel, TItem>(HtmlHelper<TModel> html, Expression<Func<TModel, TItem>> expr)
    {
        return TheThingToDo(html.LabelFor(expr), html.EditorFor(expr), html.ValidationMessageFor(expr));
    }
}
@helper TheThingToDo(MvcHtmlString label, MvcHtmlString textbox, MvcHtmlString validationMessage)
{
    <p>
        @label
        <br />
        @textbox
        @validationMessage
    </p>
}
...
Great answered 12/3, 2011 at 22:11 Comment(8)
You do NOT have to make the method static, and thus you also do NOT need to pass your Html/Url/Model etcWayland
Hmmm why doesn't this work for me? I get a "Cannot access non-static method 'TheThingToDo' in static context"..Padegs
This worked for me once I removed the static keyword from the "DoSomething" method signature.Roberto
@Sheepy, that's only half true. You are correct you can make them non-static, but you only get System.Web.WebPages.Html.HtmlHelper rather than System.Web.Mvc.HtmlHelper. There's an excellent chance that the WebPages version will not be suitable for you, since most extension methods are written against System.Web.Mvc.HtmlHelper. Furthermore, there is no Url property, and UrlHelper requires a RequestContext which is unavailable in the WebPages version. All in all you're probably going to have to pass in the Mvc HtmlHelper.Anomalism
the helper must be part of App_Code Folder ??Barner
Yes, this file must be placed in {MyMvcProject}\App_Code`. It doesn't work as advertised when you place it elsewhere. The error *Cannot access non-static method 'TheThingToDo' in static context* disappears when you move MyHelper.cshtml` into App_Code. DoSomething should be static, so that you can call @MyHelper.DoSomething(..) in your view. If you make it non-static, you'd need to create an instance of MyHelper first.Doorway
When used together with RazorGenerator you'll need to specify the "Generator: MvcHelper" option else you'll get the same static method not found error. That solution is documented here: #12379329Disconformity
Extremely underrated answer. This should be marked as the correct answer. Not only that, this technique allows you to write much more elegant HTML helper methods by using the Razor markup.Mog
G
52

No, this is not currently possible. You could write a normal HTML helper instead.

public static MvcHtmlString DoSomething<T, U>(
    this HtmlHelper htmlHelper, 
    Expression<Func<T, U>> expr
) where T : class
{
    ...
}

and then:

@(Html.DoSomething<SomeModel, string>(x => x.SomeProperty))

or if you are targeting the model as first generic argument:

public static MvcHtmlString DoSomething<TModel, TProperty>(
    this HtmlHelper<TModel> htmlHelper, 
    Expression<Func<TModel, TProperty>> expr
) where TModel : class
{
    ...
}

which will allow you to invoke it like this (assuming of course that your view is strongly typed, but that's a safe assumption because all views should be strongly typed anyways :-)):

@Html.DoSomething(x => x.SomeProperty)
Gradation answered 21/1, 2011 at 16:1 Comment(2)
Hopefully this is something they add to a future version of Razor helpers. The readability of a traditional helper is much lower than the @helper syntax.Perfectly
Yeah agreed. Reverting to the older method not only sucks, but splits your helpers up arbitrarily!Sedgemoor
G
3

In all cases the TModel will be the same (the model declared for the view), and in my case, the TValue was going to be the same, so I was able to declare the Expression argument type:

@helper FormRow(Expression<Func<MyViewModel, MyClass>> expression) {
  <div class="form-group">
    @(Html.LabelFor(expression, new { @class = "control-label col-sm-6 text-right" }))
    <div class="col-sm-6">
      @Html.EnumDropDownListFor(expression, new { @class = "form-control" })
    </div>
    @Html.ValidationMessageFor(expression)
  </div>
}

If your model fields are all string, then you can replace MyClass with string.

It might not be bad to define two or three helpers with the TValue defined, but if you have any more that would generate some ugly code, I didn't really find a good solution. I tried wrapping the @helper from a function I put inside the @functions {} block, but I never got it to work down that path.

Grosbeak answered 26/2, 2015 at 20:7 Comment(1)
Kind of obvious when you think about it - if it's TModel you probably know it in advance.Incursion
N
1

if your main problem is to get name attribute value for binding using lambda expression seems like the @Html.TextBoxFor(x => x.MyPoperty), and if your component having very complex html tags and should be implemented on razor helper, then why don't just create an extension method of HtmlHelper<TModel> to resolve the binding name:

namespace System.Web.Mvc
{
    public static class MyHelpers
    {
        public static string GetNameForBinding<TModel, TProperty>
           (this HtmlHelper<TModel> model, 
            Expression<Func<TModel, TProperty>> property)
        {
            return ExpressionHelper.GetExpressionText(property);
        }
    }
}

your razor helper should be like usual:

@helper MyComponent(string name)
{
    <input name="@name" type="text"/>
}

then here you can use it

@TheHelper.MyComponent(Html.GetNameForBinding(x => x.MyProperty))
Nerissanerita answered 15/11, 2013 at 1:56 Comment(2)
Isn't this what @Html.IdFor(...) is for?Immovable
Yes, you can do with @Htm.IdFor but need extra process to convert it to string (.ToHtmlString()) where the helper require stringNerissanerita

© 2022 - 2024 — McMap. All rights reserved.