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 in the previous 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 filter pipeline etc, view components are invoked directly with explicit data. 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 want to use a partial, but that the rendering logic is complicated and may need to be tested.
In this post, I'll use the example of a Login widget that displays your email address when you are logged in:
and a register / login link when you are logged out:
This is a trivial example - the behaviour above is achieved without the use of view components in the templates. This post is just meant to introduce you to the concept of view components, so you can see when to use them in your own applications.
Creating a view component
View components can be defined in a multitude of ways. You can give your component a name ending in
ViewComponent
, you can decorate it with the [ViewComponent]
attribute, or you can derive from the ViewComponent
base class. The latter of these is probably the most obvious, and provides a number of helper properties you can use, but the choice is yours.
To implement a view component you must expose a public method called
InvokeAsync
which is called when the component is invoked:public Task<IViewComponentResult> InvokeAsync();
As is typical for ASP.NET Core, this method is found at runtime using reflection, so if you forget to add it, you won't get compile time errors, but you will get an exception at runtime:
Other than this restriction, you are pretty much free to design your view components as you like. They support dependency injection, so you are able to inject dependencies into the constructor and use them in your
InvokeAsync
method. For example, you could inject a DbContext
and query the database for the data to display in your component.The LoginStatusViewComponent
Now you have a basic understanding of view components, we can take a look at the
LoginStatusViewComponent
. I created this component in a project created using the default MVC web template in VisualStudio with authentication.
This simple view component only has a small bit of logic, but it demonstrates the features of view components nicely.
public class LoginStatusViewComponent : ViewComponent
{
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
public LoginStatusViewComponent(SignInManager<ApplicationUser> signInManager, UserManager<ApplicationUser> userManager)
{
_signInManager = signInManager;
_userManager = userManager;
}
public async Task<IViewComponentResult> InvokeAsync()
{
if (_signInManager.IsSignedIn(HttpContext.User))
{
var user = await _userManager.GetUserAsync(HttpContext.User);
return View("LoggedIn", user);
}
else
{
return View();
}
}
}
You can see I have chosen to derive from the base
ViewComponent
class, as that provides me access to a number of helper methods.
We are injecting two services into the constructor of our component. These will be fulfilled automatically by the dependency injection container when our component is invoked.
Our
InvokeAsync
method is pretty self explanatory. We are checking if the current user is signed in using the SignInManager<>
, and if they are we fetch the associated ApplicationUser
from the UserManager<>
. Finally we call the helper View
method, passing in a template to render and the model user
. If the user is not signed in, we call the helper View
without a template argument.
The calls at the end of the
InvokeAsync
method are reminiscent of action methods. They are doing a very similar thing, in that they are creating a result which will execute a view template, passing in the provided model.
In our example, we are rendering a different template depending on whether the user is logged in or not. That means we could test this ViewComponent in isolation, testing that the correct template is displayed depending on our business requirements, without having to inspect the HTML output, which would be our only choice if this logic was embedded in a partial view instead.
Rendering View templates
When you use
return View()
in your view component, you are returning a ViewViewComponentResult
(yes, that name is correct!) which is analogous to the ViewResult
you typically return from MVC action methods.
This object contains an optional template name and view model, which is used to invoke a Razor view template. The location of the view to execute is given by convention, very similar to MVC actions. In the case of our
LoginStatusViewComponent
, the Razor engine will search for views in two folders:- Views\Components\LoginStatus; and
- Views\Components\Shared
If you don't specify the name of the template to find, then the engine will assume the file is called default.cshtml. In the example I provided, when the user is signed in we explicitly provide a template name, so the engine will look for the template at
- Views\Components\LoginStatus\LoggedIn.cshtml; and
- Views\Components\Shared\LoggedIn.cshtml
The view templates themselves are just normal razor, so they can contain all the usual features, tag helpers, strongly typed models etc. The LoggedIn.cshtml file for our LoginviewComponent is shown below:
@model ApplicationUser
<form asp-area="" asp-controller="Account" asp-action="LogOff" method="post" id="logoutForm" class="navbar-right">
<ul class="nav navbar-nav navbar-right">
<li>
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage">Hello @Model.Email!</a>
</li>
<li>
<button type="submit" class="btn btn-link navbar-btn navbar-link">Log off</button>
</li>
</ul>
</form>
There is nothing special here - we are using the form and action link tag helpers to create links and we are writing values from our strongly typed model to the response. All bread and butter for razor templates!
When the user is not logged in, I didn't specify a template name, so the default name of default.cshtml is used:
This view is even simpler as we didn't pass a model to the view, it just contains a couple of links:
<ul class="nav navbar-nav navbar-right">
<li><a asp-area="" asp-controller="Account" asp-action="Register">Register</a></li>
<li><a asp-area="" asp-controller="Account" asp-action="Login">Log in</a></li>
</ul>
Invoking a view component
With your component configured, all that remains is to invoke it from your view. View components can be invoked from a different view by calling, in this case,
@await Component.InvokeAsync("LoginStatus")
, where "LoginStatus"
is the name of the view component. We can call it in the header of our _Layout.cshtml:<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="Contact">Contact</a></li>
</ul>
@await Component.InvokeAsync("LoginStatus")
</div>
Invoking directly from a controller
It is also possible to return a view component directly from a controller; this is the closest you can get to directly exposing a view component at an endpoint:
public IActionResult IndexVC()
{
return ViewComponent("LoginStatus");
}
Calling View Components like TagHelpers in ASP.NET Core 1.1.0
View components work well, but one of the things that seemed like a bit of a step back was the need to explicitly use the
@
symbol to render them. One of the nice things brought to Razor with ASP.NET Core was tag-helpers. These do pretty much the same job as the HTML helpers from the previous ASP.NET MVC Razor views, but in a more editor-friendly way.
For example, consider the following block, which would render a label, text box and validation summary for a property on your model called
Email
<div class="form-group">
@Html.LabelFor(x=>x.Email, new { @class= "col-md-2 control-label"})
<div class="col-md-10">
@Html.TextBoxFor(x=>x.Email, new { @class= "form-control"})
@Html.ValidationMessageFor(x=>x.Email, null, new { @class= "text-danger" })
</div>
</div>
Compare that to the new tag helpers, which allow you to declare your model bindings as
asp-
attributes:<div class="form-group">
<label asp-for="Email" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
</div>
Syntax highlighting is easier for basic editors and you don't need to use ugly
@
symbols to escape the class
properties - everything is just that little bit nicer. In ASP.NET Core 1.1.0, you can get similar benefits for calling your tag helpers, by using a vc:
prefix.
To repeat my LoginStatus example in ASP.NET Core 1.1.0, you first need to register your view components as tag helpers in _ViewImports.cshtml (where
WebApplication1
is the namespace of your view components) :@addTagHelper *, WebApplication1
and you can then invoke your view component using the tag helper syntax:
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="Contact">Contact</a></li>
</ul>
<vc:login-status></vc:login-status>
</div>
Note the name of the tag helper here,
vc:login-status
. The vc
helper, indicates that you are invoking a view component, and the name of the helper is our view component's name (LoginStatus
) converted to lower-kebab case (thanks to the ASP.NET monsters for figuring out the correct name)!
With these two pieces in place, your tag-helper is functionally equivalent to the previous invocation, but is a bit nicer to read:)
Summary
This post provided an introduction to building your first view component, including how to invoke it. You can find sample code on GitHub. In the next post, I'll show how you can pass parameters to your component when you invoke it.