Search code examples
c#asp.net-mvcasp.net-mvc-4razorrazor-2

Output of helper within extension method being swallowed in Razor view


Following on from this question foreach with index I have been trying to do something along the following lines:

Using an extension method:

    public static void Each<T>(this IEnumerable<T> ie, Action<T, int> action)
    {
        var i = 0;
        foreach (var e in ie) action(e, i++);
    }

Do an iteration in my view using the index, and outputting the result of a helper method.

            <div class="col-md-12">
                @{ 
                    Model.Take(5).Each((item, n) =>
                    {
                           @RenderItem(item, n == 3);
                    });
                }
           </div

With the following helper

@helper RenderItem(Item item, bool special = false)
{
      <p>Special rendeing for special items in here</p>
}

However the output is swallowed and not output. Is there a trick to get this to work?


Solution

  • Using this extension method you are not sending anything to the view. The return value of @RenderItem is never sent to the view. Razor helpers are functions that simply return a HelperResult but you need to send this HelperResult to the view. When you invoke a helper using @MyHelper this renders the HelperResult because that is what the @ does: render something.

    But, when you are doing:

    Model.Take(5).Each((item, n) =>
          {
               @RenderItem(item, n == 3);
          });
    

    You are just calling your helper but not rendering anything to the screen (note the ending ;). In this case the @ is not the render operator, is just the "go to Razor" switch. What probably you would like to render is the output of .Each but you can't because Each is a void method.

    I played a little with your code, just to show you these concepts. First I changed your Each method to return an IEnumerable<HelperResult>:

        public static IEnumerable<HelperResult> Each<T>(this IEnumerable<T> ie, Func<T, HelperResult> action)
        {
            var i = 0;
            foreach (var e in ie) yield return action(e);
        }
    

    When a method expects a HelperResult in code (as the Func<T,HelperResult>) you can pass a Razor expression on it, so in my view i can do:

     @Enumerable.Range(1, 10).Each(i => @RenderItem(i, i == 3))
    

    This will invoke my RenderItem helper but the output is just the following (note that if you put a breakpoint in RenderItem the breakpoint won't hit due the lazy invocation of Linq, just add a .ToList() after the Each call to force evaluation):

    WebApplication1.FOo+<Each>d__0`1[System.Int32]
    

    (If you used .ToList() this will change to something like System.Collections.Generic.List1[System.Web.WebPages.HelperResult]`)

    This is because Razor knows how to render a HelperResult but not a IEnumerable<HelperResult> which is what we really have.

    So... what we need to do? Yes, just iterate over the result (using a standard foreach) and displaying every result:

    @{
        var x = Enumerable.Range(1, 10).Each(i => @RenderItem(i, i == 3));
        foreach (var ix in x)
        {
            @ix
        }
    }
    

    That will work as expected, you now are rendering your HelperResult one by one.

    Of course, this code is just to show how Razor templates work :)

    Hope it helps.