View Components in ASP.NET Core MVC
As part of ASP.NET MVC 6, a new feature called view components has been introduced. View components are similar to child actions and partials views, allowing you to create reusable components with (or without) logic. Here's the summary from the ASP.NET documentation:
View components include the same separation-of-concerns and testability benefits found between a controller and view. You can think of a view component as a mini-controller—it’s responsible for rendering a chunk rather than a whole response. You can use view components to solve any problem that you feel is too complex with a partial.
Before ASP.NET Core, you would've probably used a child action to create a reusable component that requires some code for its logic. ASP.NET MVC 6, however, doesn't have child actions anymore. You can now choose between a partial view or a view component, depending on the requirements for the feature you're implementing.
#Writing A Simple View Component
Let's implement a simple dynamic navigation menu as a view component. We want to be able to display different navigation items based on some conditional logic (e.g. the user's claims or the hosting environment). Like controllers, view components must be public, non-nested, and non-abstract classes that either …
- derive from the
ViewComponent
class, - are decorated with the
[ViewComponent]
attribute, or - have a name that ends with the "ViewComponent" suffix.
We'll choose the base class approach because ViewComponent
provides a bunch of helper methods that we'll be calling to return and render a chunk of HTML. In a folder named "Components", we'll create a new C# class:
public class Navigation : ViewComponent
{
}
If you'd rather make the view component even more explicit by appending the "ViewComponent" suffix to the class name, you can additionally decorate the class with the ViewComponent
attribute to specify an unsuffixed component name:
[ViewComponent(Name = "Navigation")]
public class NavigationViewComponent : ViewComponent
{
}
We'll also add a special Invoke
method that returns an IViewComponentResult
to be rendered. Similar to MVC controllers, the Content
helper method handed down from the ViewComponent
base class accepts a string and simply returns its value:
public IViewComponentResult Invoke()
{
return Content("Navigation");
}
Another method provided by the ViewComponent
class is Json
, which serializes a given object and returns its JSON representation. And then, there's View
, which we'll look at in a minute, but first, let's see how we can render our view component.
#Rendering a View Component
Within our Razor views, we can use the Component
helper and its Invoke
method to render view components. The first argument (which is required) represents the name of the component, "Navigation"
in our case. The remaining arguments (which are optional) represent parameters that our component's Invoke
method might accept. In this case, we don't pass any further arguments because our Navigation
component doesn't accept any:
@Component.Invoke("Navigation")
However, we're passing a magic string here. We can get more compile-time safety by replacing the hardcoded string literal with a nameof()
expression that references our view component's class name:
@Component.Invoke(nameof(Navigation))
In order for the Navigation
class to be found, we'll have to add its namespace to the list of namespaces imported within all our Razor views. Open up the _ViewImports.cshtml
view within the "Views" folder or create it if it doesn't exist yet. In there, add the following line with the namespace used in your project:
@using ViewComponents.Components
You should now see the string "Navigation" appear. Of course, we don't want to return a hardcoded simple string from our Navigation
view component. Instead, we'd like to render a full-blown Razor view.
#Returning Views from View Components
Similar to controllers in MVC, the ViewComponent
base class offers a View
helper method for returning views. That method looks for a Razor view in these two locations:
- Views/Shared/Components/{ComponentName}/Default.cshtml
- Views/{ControllerName}/Components/{ComponentName}/Default.cshtml
If no explicit view name is specified, ASP.NET MVC 6 assumes the view to be named Default.cshtml
. That convention can be overridden by passing the view name as a string to the viewName
parameter of the View
method.
I recommend you put your view components underneath the Shared folder, even if you don't use them multiple times.
Here's a simple view that renders a given list of navigation items:
@model Navigation.ViewModel
<nav>
<ul>
@foreach (var navigationItem in Model.NavigationItems)
{
<li>
<a href="@navigationItem.TargetUrl">@navigationItem.Name</a>
</li>
}
</ul>
</nav>
Let's now create the view model classes for the navigation items and instantiate a view model that is then passed to the above Default.cshtml
view.
#Adding a View Model
In the spirit of high cohesion within our component, these view model classes are defined as nested classes of our Navigation
class. You could, of course, declare them elsewhere if you so choose. Nothing fancy here, really:
public class Navigation : ViewComponent
{
public class ViewModel
{
public IList<ItemViewModel> NavigationItems { get; }
public ViewModel(IList<ItemViewModel> navigationItems)
{
NavigationItems = navigationItems;
}
}
public class ItemViewModel
{
public string Name { get; }
public string TargetUrl { get; }
public ItemViewModel(string name, string targetUrl)
{
Name = name;
TargetUrl = targetUrl;
}
}
// ...
}
Within the Invoke
method, we'll now create an array of navigation items and pass it to a new view model instance:
public IViewComponentResult Invoke()
{
var navigationItems = new[]
{
new ItemViewModel("Home", Url.RouteUrl(RouteNames.Home)),
new ItemViewModel("Contact", Url.RouteUrl(RouteNames.Contact)),
new ItemViewModel("About", Url.RouteUrl(RouteNames.About))
};
var viewModel = new ViewModel(navigationItems);
return View(viewModel);
}
Note that we can use the Url
helper within our view component to generate a URL for a given route name. Also note that I've created a extracted a simple RouteNames
class that defines all route names, again using nameof
:
public static class RouteNames
{
public const string About = nameof(About);
public const string Contact = nameof(Contact);
public const string Home = nameof(Home);
}
The Startup.Configure
method also retrieves the route names from this class. Again, we get more compile-time safety for stringly-typed APIs and a much nicer IntelliSense experience:
app.UseMvc(routes =>
{
routes.MapRoute(RouteNames.Home, "", new { controller = "Home", action = "Index" });
routes.MapRoute(RouteNames.About, "about", new { controller = "Home", action = "About" });
routes.MapRoute(RouteNames.Contact, "contact", new { controller = "Home", action = "Contact" });
});
If you now run the application, you should see a list of navigation items linking to the specified route URLs. Sweet!
#Asynchronous View Components
Since the entire ASP.NET Core stack is asynchronous from top to bottom, view components can be asynchronous as well. Instead of an Invoke
method, you'll have to implement the InvokeAsync
method and return a Task<IViewComponentResult>
.
Imagine we're loading our navigation items from a database. An IO- or network-bound operation, database calls are the perfect use case for async
and await
:
public async Task<IViewComponentResult> InvokeAsync()
{
var navigationItems = await LoadNavigationItemsFromDatabase();
var viewModel = new ViewModel(navigationItems);
return View(viewModel);
}
The call to Component.Invoke
in our Razor has to be updated as well:
@await Component.InvokeAsync(nameof(Navigation))
Child actions in ASP.NET MVC 5 or earlier versions never fully supported asynchronicity, thereby making it impossible to properly perform asynchronous operations within them. That aspect has gotten a lot easier with ASP.NET MVC 6 and asynchronous view components.
#Dependency Injection Within View Components
ASP.NET Core has dependency injection built into the core of the stack. Therefore, we can have dependencies injected into the constructor of view components. Powerful stuff!
Let's assume we want to add a "Debug" item to our navigation that links to a debug screen showing various pieces of information useful during development (application settings, user claims, size of all cookies, …). Of course, we only want this item to be visible in hosting environments named "Development". Using dependency injection, we can inspect the hosting environment like this:
public class Navigation : ViewComponent
{
// Nested classes
// ...
private readonly IHostingEnvironment _environment;
public Navigation(IHostingEnvironment environment)
{
_environment = environment;
}
public IViewComponentResult Invoke()
{
var navigationItems = new List<ItemViewModel>
{
new ItemViewModel("Home", Url.RouteUrl(RouteNames.Home)),
new ItemViewModel("Contact", Url.RouteUrl(RouteNames.Contact)),
new ItemViewModel("About", Url.RouteUrl(RouteNames.About))
};
if (_environment.IsDevelopment())
{
var debugItem = new ItemViewModel("Debug", "/debug");
navigationItems.Add(debugItem);
}
var viewModel = new ViewModel(navigationItems);
return View(viewModel);
}
}
Pretty cool, isn't it?
#Summary
ASP.NET MVC 6 introduces view components, a component-oriented mixture of child actions and partial views. They can return various content, including Razor views, JSON, or plain text. View components can be rendered synchronously or asynchronously. Finally, they can integrate with the dependency injection system of ASP.NET Core through constructor injection.