mpiskunov
mpiskunov

Reputation: 21

Call Component.InvokeAsync() from within an IViewComponentHelper extension method

I have a solution which has both an ASP.NET Core 3.1 web application project as well as a Razor Client Library (RCL) project. I am trying write a view component which will be distributed with the Razor Client Library, but can be referenced from the ASP.NET Core web application.

I can successfully render this ViewComponent when I call the InvokeAsync() Tag Helper on the web application's _Layout.cshtml:

@await Component.InvokeAsync("NavBar", new {})

But I want to make it so that the RCL is not dependent on a string name provided on the web application. Instead, I would like to call the ViewComponent via an extension method like this:

@{ await Component.RenderMyViewComponentAsync(); }

To me, this way is more beneficial to whoever will use this shared RCL Library, as they don't need to specify the exact name of the ViewComponent, and can rely on IntelliSense to complete the extension method.

I have created a helper class that takes the ViewComponent object and simply calls its InvokeAsync() method within an extension method:

public static class PartialHelper
{
    public static async Task<IHtmlContent> RenderMyViewComponentAsync(this IViewComponentHelper vcHelper)
    {
        return await vcHelper.InvokeAsync("NavBar", new { });
    }
}

And, of course, inside of NavBarViewComponent.cs, I have implemented the InvokeAsync() method:

public class NavBarViewComponent : ViewComponent
{
    public async Task<IViewComponentResult> InvokeAsync()
    {
        return View();
    }
}

Here's the problem, though: I'm not seeing my view render on the screen from the latter method, even though both ways of doing it will still hit my NavBarViewComponent.InvokeAsync().

From what I see, both ways of returning the ViewComponents are functionally equivalent and use the same methods, except @{ await Component.RenderMyViewComponentAsync(); } goes through a helper function first.

I've tried debugging both ways, but to no avail. I am stuck!

Is there any way to achieve what I'm asking for here? If any clarification is needed, please ask.

Upvotes: 2

Views: 13342

Answers (3)

Jeremy Caney
Jeremy Caney

Reputation: 7624

In addition to answering your specific question, I also want to answer the question behind your question. Your general objective seems to be the simplification of the syntax, without relying on strings which don't provide any design- or compile-time validation.

I really like your approach of using extension methods as a solution for this, and will likely borrow that myself. But another approach that solves a similar objective is to invoke your ViewComponent as a Tag Helper:

<vc:NavBar />

This gives you basic IntelliSense support in Visual Studio, and is much cleaner than the InvokeAsync() approach.

That said, it's worth noting that the Tag Helper approach does have a couple of critical limitations, as I've discussed in a previous answer:

  1. It doesn't support excluding any optional parameters you may have defined on your InvokeAsync() method*.
  2. Neither Visual Studio nor the compiler will provide (obvious) warnings if you're missing any parameters.

Given that, your approach of using extension methods may still be preferable if your view components have any parameters—and, especially, if any of those parameters have logical defaults.

*Note: As of ASP.NET Core 6 Preview 6, View Components invoked as Tag Helpers will now honor optional parameters (source).

Upvotes: 0

user1844646
user1844646

Reputation: 51

Had the same issue with the extension method, so thank you for the answer.

One addition - instead of using a string, use the generic to catch errors at compile time:

await Component.InvokeAsync<NavBar>();

or directly from the controller:

public IActionResult Index()
{
    return ViewComponent(typeof(NavBar));
}

Upvotes: 3

Jeremy Caney
Jeremy Caney

Reputation: 7624

Your extension method is actually working exactly like it should. The issue is in how you're calling it from your _Layout.cshtml. You're using the following syntax:

@{ await Component.RenderMyViewComponentAsync(); }

That treats it as though it's within a script block, instead of rendering the IHtmlContent to the view. The IHtmlContent is correctly returned, but it isn't assigned to anything or written to the output.

Instead, you simply need to use the same style syntax you used when calling InvokeAsync():

@await Component.RenderMyViewComponentAsync()

Then this will render exactly like you're expecting.

Upvotes: 2

Related Questions