Introduction to ASP.NET Core part 21: view components

Introduction

In the previous post we looked at partial views. Partial views have been around for a long time in ASP.NET MVC and they work in much the same way in .NET Core MVC. Partial views are a means of factoring out parts of a view into smaller, reusable and more manageable units. Partial views can also have their own dependencies. So they behave just like “full” views but are meant to be rendered within a parent view.

In this post we’ll look at an alternative to partial views called view components. View components are new in this version of MVC.

View components are also documented on the official .NET Core guide here. We’ll look try out most of it in this post. We’ll be working in our demo application DotNetCoreBookstore.

View components

View components are sort of a small version of a full MVC cycle. They have a controller and a view that follow a naming convention but are rendered within a parent view just like partial views. Since view components have their own controllers they can also have their own dependencies and parameters. However, they are not “open” controllers that can be directly invoked from a URL like /books/details . They are hidden MVC elements that are normally invoked from within a parent view using a new helper class called “Component”.

We’ll start by building a view component for the book details. In other words we’ll create the view component equivalent of the partial view from the previous post.

View component controllers are by convention placed in a folder called ViewComponents. So go ahead and insert a new folder called ViewComponents to the root of the web project. View component controllers are normal C# classes that follow a naming convention just like MVC controllers do. The easiest way to automatically register a view component controller is to derive from the ViewComponent base class in the Microsoft.AspNetCore.Mvc namespace and give it a name that ends with “ViewComponent”.

We’ll add a new C# class called BookDetailsViewComponent into the ViewComponents folder. A VC controller must have a function called Invoke that returns an IViewComponentResult object. The Invoke method can have 0, 1 or more parameters. We’ll see in a bit how the caller can specify the values for the Invoke method parameters.

The BookDetailsViewComponent class is very simple:

using DotNetCoreBookstore.Models;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace DotNetCoreBookstore.ViewComponents
{
    public class BookDetailsViewComponent : ViewComponent
    {
		public IViewComponentResult Invoke(BookDetailsViewModel bookDetailsViewModel)
		{
			return View("Details", bookDetailsViewModel);
		}
    }
}

The Invoke function has a parameter of type BookDetailsViewModel which is passed into a view called Details.

View component views must be placed in folders that also follow a naming convention. The book details view component will be used with books only, i.e. it won’t be shared with other controllers. The folder naming rule is the following for this case: Views/[ControllerName]/Components/[ComponentNameWithoutViewComponentAttachedToItsName]/[NameOfView.cshtml]. In our example we have to add a Details.cshtml view file to the Views/Books/Components/BookDetails folder:

Register a view component view in .NET Core project

The view component Details.cshtml is identical to the _BookDetails.cshtml partial view from the previous post with the exception of having to add a using statement:

@using DotNetCoreBookstore.Models
@model BookDetailsViewModel

<table>
    <thead>
        <tr>
            <th>Author</th>
            <th>Title</th>
            <th>Genre</th>
            <th>Pages</th>
            <th>Price</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>@Model.Author</td>
            <td>@Model.Title</td>
            <td>@Model.Genre</td>
            <td>@Model.NumberOfPages</td>
            <td>@Model.Price</td>
        </tr>
    </tbody>
</table>

We’re now ready to invoke the component from the Details view of BooksController:

@model BookDetailsViewModel

@{
    ViewBag.Title = "Details of " + @Model.Title;
}

<h1>Details of @Model.Title using a partial view</h1>
<div>
    @Html.Partial("_BookDetails", Model)
</div>

<h1>Details of @Model.Title using a view component</h1>
<div>
    @await Component.InvokeAsync("BookDetails", new { bookDetailsViewModel = @Model})
</div>

@Html.ActionLink("Back to list", "Index")

Component.InvokeAsync is an asynchronous method so we have to apply the await keyword for it to be rendered properly. The InvokeAsync function requires the name of the view component without the “ViewComponent” name extension. We can also supply the input parameters in an anonymous object. It will be passed into the Invoke method of the BookDetailsViewComponent object.

Run the application and navigate to a detail page like /books/details/1. You’ll see that the component view is rendered as expected.

Object dependencies in view components

In this example we’ll see that view component controller can also have their abstract object dependencies just like normal MVC controllers. Currently we have a section called FunnyMessage in _Layout.cshtml:

<div>
        @RenderSection("FunnyMessage", false)
</div>

We’ll add a common message to all pages that use the layout. It will no longer be the responsibility of the individual views to render this section, it will be handled by a view component in the layout view.

Add a new C# class called FunnyMessageViewComponent to the ViewComponents folder:

using DotNetCoreBookstore.Dependencies;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace DotNetCoreBookstore.ViewComponents
{
    public class FunnyMessageViewComponent : ViewComponent
    {
		private readonly IStringFormatter _stringFormatter;

		public FunnyMessageViewComponent(IStringFormatter stringFormatter)
		{
			if (stringFormatter == null) throw new ArgumentNullException("String formatter!");
			_stringFormatter = stringFormatter;
		}

		public IViewComponentResult Invoke(object objectToFormat)
		{
			return View("Show", _stringFormatter.FormatMe(objectToFormat));
		}
	}
}

We’re planning to use this component in many views so it’s a good idea to share it. The folder naming convention in this case is Shared/Components/[ComponentNameWithoutTheViewComponentNameExtension]/[ViewName.cshtml]. In our case it will be Shared/Components/FunnyMessage/Show.cshtml:

Register a shared view component view in .NET Core project

Show.cshtml is very simple:

@model String

<div>
    <h1>A message using a view component</h1>
    <p>@Model</p>
</div>

Here’s how to invoke it from _Layout.cshtml:

<body>
    <div>
        @RenderBody()
    </div>
    <div>
        @RenderSection("FunnyMessage", false)
    </div>
    <div>
        <p>@StringFormatter.FormatMe(new { Message = "Hello from the Layout view using dependency injection", Warning = "Use this technique sparingly"})</p>
    </div>
    <div>
        @await Component.InvokeAsync("FunnyMessage", new { objectToFormat = new { Message = "Invoking a view component from the layout...", TimeUtc = DateTime.UtcNow } })
    </div>
</body>

Navigate to /books and you’ll see the rendered message. The abstract IStringFormatter dependency of FunnyMessageViewComponent was resolved in Startup.cs in a previous part of this series:

services.AddSingleton<IStringFormatter, JsonStringFormatter>();

An asynchronous invoke method in the view component

It can happen that the view component object needs to call an asynchronous function that returns a Task. Here’s a FunnyMessageAsyncViewComponent class which is the same FunnyMessageViewComponent but the message is formatted asynchronously. We pretend that message formatting is a resource heavy task that should be completed on a different thread:

using DotNetCoreBookstore.Dependencies;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace DotNetCoreBookstore.ViewComponents
{
    public class FunnyMessageAsyncViewComponent : ViewComponent
    {
		private readonly IStringFormatter _stringFormatter;

		public FunnyMessageAsyncViewComponent(IStringFormatter stringFormatter)
		{
			if (stringFormatter == null) throw new ArgumentNullException("String formatter!");
			_stringFormatter = stringFormatter;
		}

		public async Task<IViewComponentResult> InvokeAsync(object objectToFormat)
		{
			string formatted = await Task.Factory.StartNew(() => { return _stringFormatter.FormatMe(objectToFormat); });
			return View("Show", formatted);
		}

	}
}

Note that the function to implement is not Invoke anymore but InvokeAsync.

We’ll need another Show view in the Shared/Components/FunnyMessageAsync folder to adhere to the naming conventions:

Register a shared asynchronous view component view in .NET Core project

This Show.cshtml has the following body:

@model String

<div>
    <h1>A message using an asyncronous view component</h1>
    <p>@Model</p>
</div>

Here’s how it can be invoked from _Layout.cshtml:

<div>
        @await Component.InvokeAsync("FunnyMessageAsync", new { objectToFormat = new { Message = "Invoking an async view component from the layout...", TimeUtc = DateTime.UtcNow } })
</div>

The asynchronous version will be rendered just like the synchronous one.

We’ll continue in the next post.

View the list of MVC and Web API related posts here.

Advertisement

About Andras Nemes
I'm a .NET/Java developer living and working in Stockholm, Sweden.

3 Responses to Introduction to ASP.NET Core part 21: view components

  1. Vojtech Cizinsky says:

    Another great article. By the way there is a few typos in the part around BookDetails. The ‘ViewController’ should be actually ‘ViewComponent’ there I believe.

  2. naveen says:

    awesome….awesome series on asp.net core ….please continue these articles..

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Elliot Balynn's Blog

A directory of wonderful thoughts

Software Engineering

Web development

Disparate Opinions

Various tidbits

chsakell's Blog

WEB APPLICATION DEVELOPMENT TUTORIALS WITH OPEN-SOURCE PROJECTS

Once Upon a Camayoc

Bite-size insight on Cyber Security for the not too technical.

%d bloggers like this: