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?

mkedobbs
  • 4,327
  • 2
  • 23
  • 28
  • Still not fixed in the current MVC 4 release. :( – Alex Dresko Mar 03 '12 at 21:30
  • 1
    How is this still not fixed in VS2012? – Alex Dresko Oct 09 '12 at 19:54
  • 7
    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(this T o) where T : IBar { }` – Dan Lugg Mar 14 '13 at 07:14

4 Answers4

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>
}
...
Edward Brey
  • 40,302
  • 20
  • 199
  • 253
chrismilleruk
  • 2,516
  • 2
  • 16
  • 8
  • You do NOT have to make the method static, and thus you also do NOT need to pass your Html/Url/Model etc – Sheepy Sep 08 '11 at 00:15
  • Hmmm why doesn't this work for me? I get a "Cannot access non-static method 'TheThingToDo' in static context".. – TweeZz Oct 18 '11 at 09:03
  • This worked for me once I removed the static keyword from the "DoSomething" method signature. – Giscard Biamby Jan 11 '12 at 16:26
  • 12
    @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`. – Kirk Woll Feb 26 '12 at 16:19
  • 1
    the helper must be part of App_Code Folder ?? – Vishal Sharma Aug 28 '13 at 11:49
  • 1
    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. – Grilse Mar 11 '15 at 13:42
  • 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: http://stackoverflow.com/questions/12378829/is-there-an-example-on-using-razor-to-generate-a-static-html-page/31609879#31609879 – Tony Wall Jul 24 '15 at 11:53
  • 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. – Aaron Hudon Jul 18 '17 at 16:15
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)
Ian Kemp
  • 28,293
  • 19
  • 112
  • 138
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • 10
    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. – mkedobbs Jan 22 '11 at 23:23
  • 2
    Yeah agreed. Reverting to the older method not only sucks, but splits your helpers up arbitrarily! – George R Jul 27 '11 at 05:33
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.

bradlis7
  • 3,375
  • 3
  • 26
  • 34
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))
ktutnik
  • 6,882
  • 1
  • 29
  • 34