Most templated web sites have content that's repeated across multiple pages. This site includes a tag cloud, a list of recent comments and an archive listing. These items or widgets appear on every page of the site. The simplest way to manage this in traditonal ASP.NET MVC without repeating code everywhere is to use Child Actions - controller action methods decorated with the [ChildActionOnly]
attribute called from within a view or layout file via the Html.Action
helper. Or, if the content of your widget doesn't require any interaction with the server (e.g. because you already have the data it needs), you can use a partial view invoked within the view or layout by the Html.RenderPartial
helper method. View Components are an alternative to both of these approaches.
A View component is a class that inherits from ViewComponent
and implements an Invoke
or InvokeAsync
method. The return type of the method will depend on what you want to render to the browser, but most likely, you will make use of a Razor View file to render templated content, so your Invoke
or InvokeAsync
method will return an IViewComponentResult
. You could also return a string
or an HtmlString
, for example. Here is an example of a very simple View Component:
public class ContactViewComponent : ViewComponent { public IViewComponentResult Invoke() { return View(); } }
The component class can be placed anywhere in the structure of an MVC application, but the recommendation is that you create a folder called ViewComponents to house them in. There is no logic in this component. All it does is return a view, but which view? The MVC framework will search two locations:
/Views/[CurrentController]/Components/[NameOfComponent]/Default.cshtml /Views/Shared/Components/[NameOfComponent]/Default.cshtml
The first location is for controller-specific views, and is not likely to be used very often. It is potentially useful if you want to provide a different view for the same component depending on the controller that owns the current action being invoked. The second location is much more likely to be used. You create a folder called Components in the Views/Shared folder, and then create a subfolder named after the component. The convention is that the name of the component is the class name without the "ViewComponent" suffix - or in this example, "Contact".
Here is this content of the Default.cshtml file:
@model ViewComponentTest.ViewModels.Contact.ContactViewModel <form method="post" asp-action="contact" asp-controller="home"> <fieldset> <legend>Contact</legend> Email: <input asp-for="Email" /><br /> Name: <input asp-for="Name" /><br /> Message: <textarea asp-for="Message"></textarea><br /> <input type="submit" value="Submit" class="btn btn-default" /> </fieldset> </form>
It's a standard strongly typed partial view, although it doesn't have to be strongly typed. In this intance, its model is a ContactViewModel
designed to help create with the creation of a contact form based on TagHelpers.
The content is rendered as a result of a call to Component.InvokeAsync()
from within a view or layout, with the name of the component being passed as a parameter:
@await Component.InvokeAsync("Contact")
At the time of writing, RC2 is not yet ready, so there are other methods available for rendering components, including Component.Invoke
, Component.Render
and so on. These are being removed in RC2. If you don't want to suffer breaking changes, stick solely with the asynchronous version of Invoke
.
The next example is a bit more complex. It demonstrates how to create a view component that's responsible for displaying data obtained from an external source such as a database:
public class BestSeller : ViewComponent { private IBookService _service; public BestSeller(IBookService service) { _service = service; } public async Task<IViewComponentResult> InvokeAsync(int numberToTake) { var mostPopular = await _service.MostPopular(numberToTake); return View(mostPopular); } }
The view component is named BestSeller (without the "ViewComponent" suffix"). It has a dependency on an instance of an IBookService
, which is injected into the constructor and is resolved by the ASP.NET Core Dependency Injection framework. This example features the asynchronous InvokeAsync
method, and accepts a parameter representing the number of items to select from the database. This represents one of the major improvements over Child Actions, which do not support asynchronous. Here is an implementation of the BookService
that makes use of the in-memory version of Entity Framework (which is designed for testing and demos):
public class BookService : IBookService { private BookContext _context; public BookService(BookContext context) { _context = context; var books = new List<Book> { new Book {BookId = 1, Author = "Kate Atkinson", Title = "A God In Ruins", Price = 7.99m }, new Book {BookId = 2, Author = "Renee Knight", Title = "Disclaimer", Price = 7.99m }, new Book {BookId = 3, Author = "James Patterson", Title = "Private Sydney", Price = 7.99m }, new Book {BookId = 4, Author = "Michael Punke", Title = "The Revenant", Price = 7.99m }, new Book {BookId = 5, Author = "Celia Imrie", Title = "Not Quite Nice", Price = 7.99m }, new Book {BookId = 6, Author = "Harlan Coben", Title = "The Stranger", Price = 7.99m }, new Book {BookId = 7, Author = "Emma Donoghue", Title = "Room", Price = 8.99m }, new Book {BookId = 8, Author = "Laura Barnett", Title = "The Versions of Us", Price = 7.99m } }; _context.Books.AddRange(books); _context.SaveChangesAsync(); } public async Task<List<Book>> MostPopular(int numberToTake) { return await _context.Books.OrderBy(b => b.BookId).Take(numberToTake).ToListAsync(); } }
And finally, here is the view, located in /Views/Shared/Components/BestSeller
@model List<Book> <h3>Best sellers</h3> <ul style="padding-left:0;list-style-type:none;"> @foreach (var book in Model) { <li>@book.Title - @book.Author (@book.Price.ToString("c"))</li> } </ul>
When this component is called from within the view, the argument representing the number to take is passed along with the name of the component:
@await Component.InvokeAsync("BestSeller", 5)
From RC2, this behavour will change so that the parameter representing the number to take will be passed as either a dictionary or an anonymous type:
@await Component.InvokeAsync("BestSeller", new { numberToTake = 5 })
And the result is injected into the view output:
Summary
This article looks at View Components, a new means for managing partial page content within ASP.NET Core MVC web sites. It shows what they are and how to use them. It also discusses a couple of upcoming changes when RC 2 is available.