Marius Schulz
Marius Schulz
Front End Engineer

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.