DEV Community

Andrew Lock "Sock"
Andrew Lock "Sock"

Posted on • Originally published at andrewlock.net on

Don't replace your View Components with Razor Components

Don't replace your View Components with Razor Components

In this post I take a brief look at Razor Components, and whether you should consider using them instead of View Components. I'm sure you can guess my conclusion from the title, but I admit that's pretty click-baity, and the conclusions are a bit more subtle.

I start by taking a brief look at View Components and what they can be used for. I then compare them to Razor Components highlighting the similarities and differences, and describing their relation to Blazor. Finally, I walk through converting a Razor partial view into a Razor Component rendered using the static mode, and decide whether or not it's worth doing!

tl;dr; Razor Components are cool, but I can't see a reason to use the static rendering mode. It doesn't give you the client interactivity of Blazor and there's a bit of an impedance mis-match with Razor Pages, so it doesn't seem worth the hassle over using Razor partials or View Components in a Razor Pages app.

What are View Components?

I wrote an introduction to View Components a few years ago, and nothing much has changed with them since then. The following section is taken from that post:

View Components are one of the potentially less well known features of ASP.NET Core Razor views. Unlike Tag Helpers which have the pretty much direct equivalent of Html Helpers from "classic" ASP.NET, View Components are a bit different.

In spirit they fit somewhere between a partial view and a full controller - approximately like a ChildAction. However whereas actions and controllers have full model binding semantics and the MVC filter pipeline etc, View Components are invoked directly with explicit parameters. They are more powerful than a partial view however, as they can contain business logic, and separate the UI generation from the underlying behaviour.

View Components seem to fit best in situations where you would ordinarily want to use a Razor partial, but where the rendering logic is complicated and may need to be tested.

In my introductory post I used the example of the classic "login status" component used in a typical ASP.NET Core app, where you want to render something different depending on whether the current user is logged in.

A typical web app showing a login widget

While this is definitely at the low-end complexity-wise for View Components, it gives the general idea - a small section of UI that has some somewhat-complex rending requirements. They can also be useful where it is rendering data unrelated to the main body of the page. For example, take a typical eStore:

A product page from Amazon.co.uk

With a traditional MVC/Razor Page app, the ViewModel used to render the product page would typically contain all the data required to render the bulk of the page body. But what about the "frequently bought together" section, or the wish list? If every View Model has to include that data too, your View Models and controllers would become cluttered. Theses are good candidates for View Components.

Razor Components

View Components seem to have their place then. But if you look at the official documentation for View Components, you'll find this interesting paragraph:

When considering if View Components meet an app's specifications, consider using Razor Components instead. Razor Components also combine markup with C# code to produce reusable UI units. Razor Components are designed for developer productivity when providing client-side UI logic and composition.

When I read that initially, I read it as View Components essentially being deprecated in favour of Razor Components. After re-reading it, and playing with Razor Components, I realised that's not really the case. Consider Razor Components, yes, but as you'll see, they're not a direct replacement.

Razor Components - do you mean Blazor?

This is where things start to get a bit fuzzy. I'm not going to dive very far into Blazor in this post, as it's a big topic, and one that has a lot of people excited, but the brief summary for those unacquainted is:

Blazor is a framework for building interactive client-side web UI with .NET

  • Create rich interactive UIs using C# instead of JavaScript.
  • Share server-side and client-side app logic written in .NET.
  • Render the UI as HTML and CSS for wide browser support, including mobile browsers.

The important point in here is that Blazor is for building client-side interactivity. Traditional MVC and Razor Pages is all about rendering HTML server-side — any client-side interactivity has to be added with JavaScript. With Blazor, you can now use C# (mostly) to add that interactivity.

I won't get into Blazor Server vs Blazor WebAssembly here, as it's not important for the purposes of this post.

The key thing is that Blazor apps are built using components - Razor Components. These are very closely related to Razor views, but with some important syntactic and stylistic differences. The most superficial changes are they use a .razor file extension instead of .cshtml, and @code{} blocks in place of @functions{}, but the changes go a lot deeper than that.

The interesting point for this post is that you can actually use Razor Components in your MVC/Razor Pages app without using Blazor!

Razor Components without the Blazor

First off, it's absolutely possible to use Blazor for adding client-side interactivity in your traditional MVC/Razor Pages app. You can add Razor Components to your Razor Pages, and they can form a little island of client-side interactivity within your otherwise server-side rendered app.

Blazor being used inside a Razor Pages app

The thing to be aware though is that you're now running a Blazor app.

If you're using Blazor Server, that means you now need to worry about all the state being stored in memory on the server (in case of restarts), you need to be aware that you're using SignalR behind the scenes, that you need to make changes to your project to support the new functionality, and that your app won't work in a disconnected scenario.

If you're using Blazor WebAssembly, you need to be aware that it's currently in preview only, that you have a large payload to download (the .NET interpreter running in Web Assembly), and that you can't support legacy clients (e.g. Internet Explorer).

What if you don't want all those extra trade-offs? Maybe you just want traditional server-side rendered HTML, but want to use the Razor component model?

You're in luck, that's possible too. Razor Components have three render modes:

  • ServerPrerendered: The component is rendered to HTML in the response, and then connects back to the server when run in the browser to provide the interactivity.
  • Server: The component isn't rendered in the HTML, instead a marker is rendered that connects back to your app when run in the browser.
  • Static: The component is rendered to static HTML in the response, and that's it, done.

So by rendering components using the Static mode, you don't need the extra Blazor overhead, but you can still use Razor Components. So, what does that actually look like in practice?

Converting the login partial view to Razor Components

As an experiment, I decided to convert the _LoginPartial.Identity.chtml file included in the default Razor Pages template to a Razor Component.

The partial looks like this:

@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager

<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello @User.Identity.Name!</a>
    </li>
    <li class="nav-item">
        <form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Page("/", new { area = "" })" method="post" >
            <button type="submit" class="nav-link btn btn-link text-dark">Logout</button>
        </form>
    </li>
}
else
{
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register">Register</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login">Login</a>
    </li>
}
</ul>

This partial only has a small amount of logic in it, but it uses some injected services, has links to other pages, and has a form (for logout). Lots of bits to play with!

Creating the stub component

I'm actually going to cheat a bit to start with. Instead of injecting the services used in the partial to determine the current user, I'm going to use an existing pre-built Razor component, the AuthorizeView component. This component is designed exactly for this purpose - it lets you display different content depending on whether you're logged in or not.

I'll start by creating a component called LoginDisplay.razor in the Pages/Shared folder of the project, and add the following content:

<CascadingAuthenticationState>
    <AuthorizeView>
        <Authorized>
            <h1>Hello, @context.User.Identity.Name!</h1>
        </Authorized>
        <NotAuthorized>
            <h1>Not Logged in</h1>
        </NotAuthorized>
    </AuthorizeView>
</CascadingAuthenticationState>

There's a few more things we need to add before we can test our component.

Update your Startup.ConfigureServices method by adding the following method:

public void ConfigureServices(IServiceCollection services)
{
    // ... existing services

    services.AddServerSideBlazor();
}

Now, just to be clear, we're adding Blazor because we need some of the services it uses, but we're not actually going to run it as a Blazor app. There'll be no connection from the component to the server, or any client-side interaction.

Next, we'll add an _Imports.razor file in Pages/Shared and add the following:

@using System.Net.Http
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
@using RazorComponentsIntro // The project's namespace

That will put all the namespaces we need in scope.

Finally, we update _Layout.cshtml and replace the reference to the login partial:

<partial name="_LoginPartial" />

with a reference to our new Razor component. Note that this is where we specify the render mode of the component to be Static:

<component type="typeof(LoginDisplay)" render-mode="Static" />

If we run the app now, we'll see our component rendered! It's ugly currently, but it works 🙂

Razor page rendering Razor component

Now we have something that works, we can see about fleshing it out!

Some teething problems

My first attempt at restoring the functionality involved copying the Razor from the _LoginPartial.Identity.chtml file to the appropriate section of the new component. After all, Razor Components use Razor right?

The end result looked fine initially:

Razor page rendering Razor component

but on closer inspection there were some problems. The "Register" and "Log In" links were missing their typical "hover" behaviour (mouse cursor change) and clicking them did nothing!

Inspecting the HTML rendered in the browser for the Register button revealed the problem:

<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register">Register</a>

The problem is that the Razor markup I added hadn't been transformed to generate the href attribute, as you'd expect in Razor views or Razor Pages. Specifically, the Tag Helpers asp-area and asp-page are being ignored, and just rendered as normal attributes. That explains the behaviour in the browser, but why is it happening?

Well it turns out that you can't use tag helpers in Razor Components:

Tag Helpers aren't supported in Razor Components (.razor files). To provide Tag Helper-like functionality in Blazor, create a component with the same functionality as the Tag Helper and use the component instead.

That's a bit of a pain, but as we're just generating href attributes with the tag helpers, we can just hard code them in for now. We could also pass the URLs to use in as parameters to the component, and I guess that's the sort of thing you'd likely do in a real app, but it was a lot easier to just hard code everything.

Unfortunately, while I don't go into it here, passing parameters from Razor Pages to a Razor component is a bit of a pain as you have to use the param-* syntax. I initially started to do this in _Layout.cshtml, using the Url helper to generate the values, but that got old very quickly…

After restoring all the href attributes (and the action attribute of the <form> tag) I was left with something that looked like this:

<CascadingAuthenticationState>
    <ul class="navbar-nav">
        <AuthorizeView>
            <Authorized>
                <li class="nav-item">
                    <a class="nav-link text-dark" href="Identity/Account/Manage">Hello, @context.User.Identity.Name!</a>
                </li>
                <li class="nav-item">
                    <form class="form-inline" method="post" action="Identity/Account/LogOut?returnUrl=/">
                        <button type="submit" class="nav-link btn btn-link text-dark">Log out</button>
                    </form>
                </li>
            </Authorized>
            <NotAuthorized>
                <li class="nav-item">
                    <a class="nav-link text-dark" href="Identity/Account/Register">Register</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link text-dark" href="Identity/Account/Login">Log in</a>
                </li>
            </NotAuthorized>
        </AuthorizeView>
    </ul>
</CascadingAuthenticationState>

What's more, I could register and login, everything worked! As a bonus, the Razor component code is arguably more readable than the Razor view version that relies on large C# if-else blocks.

Razor page rendering Razor component

Almost everything worked… As long as you don't try and log out… 🙁

Error trying to log out

Blazor and CSRF AntiForgeryTokens

A look into the logs for the application revealed that the problem is due to a missing AntiForgeryToken in the form request. Razor Pages automatically adds a hidden field containing an antiforgery token to forms by default. When a form is POSTed, the presence of the token is validated by the Razor Page handler / MVC action. If the field has an invalid value, or is missing, the request is rejected with a 400 Bad Request response.

That's what we're seeing here. As Razor Components don't use Tag Helpers, the antiforgery token isn't added, and the Logout Razor Page rejects the request!

So what's the solution? I found references to the problem in a (now rather old) Gist from Steve Sanderson, that describes exactly this problem. I can't find any other references to the problem (or solution) anywhere else.

There is a possible solution though, and it's the one used by the Blazor project templates that ship with dotnet new/Visual Studio when you add authentication. They turn off the antiforgery verification for the logout endpoint. That's not an especially big problem - the worst an attacker could do with an exploit is force you to logout, so it's not a big problem security-wise.

To achieve this we have to override the LogOut.cshtml Razor Page by placing a similarly named file in the magic path Areas/Identity/Pages/Account/LogOut.cshtml, with the contents:

@page
@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager

@attribute [IgnoreAntiforgeryToken]

@functions {
    public async Task<IActionResult> OnPost()
    {
        if (SignInManager.IsSignedIn(User))
        {
            await SignInManager.SignOutAsync();
        }

        return Redirect("~/");
    }
}

This removes the antiforgery token check, and means we can log out again! And with that, our mission is complete.

Final thoughts

In my previous post on View Components, I replaced the existing Razor login partial with an equivalent view component. The process was pretty simple and easy, and generally seemed like a good way to extract moderately complex logic out of the view.

The same can't be said for Razor Components. If all you're trying to do is encapsulate some complex logic inside a Razor Pages app, then Razor Components are not the way to go. The fact they can't use Tag Helplers and issues such as the antiforgery example means I can't see any reason you would choose to use the Static render mode in this way. In other words, Razor Components without Blazor don't make a lot of sense to me.

Don't get me wrong, Razor Components (or Blazor) absolutely can have their place within Razor Pages by providing isolated pockets of client interactivity. There's two important phrases there:

  • Isolated pockets: Passing in lots of parameters from Razor Pages into a component is a bit of a pain, and you've already seen the issue going the other way, where Blazor can't easily add the antiforgery token to forms.
  • Client interactivity: The example in this post had nothing to do with client interactivity; we were just rendering static HTML. If we were rendering something that needed a rich client experience then that's an entirely different scenario.

And to be fair, this is exactly the use case for Razor Components. The thing that fooled me was suggesting you should evaluate Razor Components if you're using View Components. The key word there is evaluate; Razor Components and View Components satisfy two different use cases. Make sure you use the right one!

Summary

In this post I gave a brief introduction to View Components. I then described razor components, their relation to Blazor, and their various render-modes. For the second half of the post I worked through converting the Login Razor partial view to an equivalent Razor Component. I ran into a few issues in the conversion, such as the lack of Tag Helpers and no antiforgery support.

Those issues and the lack of obvious advantages were enough to convince me that Razor Components aren't generally worth using in a Razor Pages app using their Static render mode. If you have isolated pockets requiring client-interactivity I can absolutely see the case for using Razor Components in combination with Blazor. But for simply encapsulating view rendering logic in a server-side rendered app, stick to View Components and partials!

Discussion (0)