View components
View or download sample code ((xref:)how to download)
Introducing view components
New to ASP.NET Core MVC, view components are similar to partial views, but they are much more powerful. View components don’t use model binding, and only depend on the data you provide when calling into it. A view component:
- Renders a chunk rather than a whole response
- Includes the same separation-of-concerns and testability benefits found between a controller and view
- Can have parameters and business logic
- Is typically invoked from a layout page
View components are intended anywhere you have reusable rendering logic that is too complex for a partial view, such as:
- Dynamic navigation menus
- Tag cloud (where it queries the database)
- Login panel
- Shopping cart
- Recently published articles
- Sidebar content on a typical blog
- A login panel that would be rendered on every page and show either the links to log out or log in, depending on the log in state of the user
A view component consists of two parts: the class (typically derived from ViewComponent) and the result it returns (typically a view). Like controllers, a view component can be a POCO, but most developers will want to take advantage of the methods and properties available by deriving from ViewComponent
.
Creating a view component
This section contains the high-level requirements to create a view component. Later in the article, we’ll examine each step in detail and create a view component.
The view component class
A view component class can be created by any of the following:
- Deriving from ViewComponent
- Decorating a class with the
[ViewComponent]
attribute, or deriving from a class with the[ViewComponent]
attribute - Creating a class where the name ends with the suffix ViewComponent
Like controllers, view components must be public, non-nested, and non-abstract classes. The view component name is the class name with the “ViewComponent” suffix removed. It can also be explicitly specified using the ViewComponentAttribute.Name
property.
A view component class:
Fully supports constructor dependency injection
Does not take part in the controller lifecycle, which means you can’t use filters in a view component
View component methods
A view component defines its logic in an InvokeAsync
method that returns an IViewComponentResult
. Parameters come directly from invocation of the view component, not from model binding. A view component never directly handles a request. Typically, a view component initializes a model and passes it to a view by calling the View
method. In summary, view component methods:
- Define an
InvokeAsync
method that returns anIViewComponentResult
- Typically initializes a model and passes it to a view by calling the
ViewComponent
View
method - Parameters come from the calling method, not HTTP, there is no model binding
- Are not reachable directly as an HTTP endpoint, they are invoked from your code (usually in a view). A view component never handles a request
- Are overloaded on the signature rather than any details from the current HTTP request
View search path
The runtime searches for the view in the following paths:
- Views/<controller_name>/Components/<view_component_name>/<view_name>
- Views/Shared/Components/<view_component_name>/<view_name>
The default view name for a view component is Default, which means your view file will typically be named Default.cshtml. You can specify a different view name when creating the view component result or when calling the View
method.
We recommend you name the view file Default.cshtml and use the Views/Shared/Components/<view_component_name>/<view_name> path. The PriorityList
view component used in this sample uses Views/Shared/Components/PriorityList/Default.cshtml for the view component view.
Invoking a view component
To use the view component, call the following inside a view:
@Component.InvokeAsync("Name of view component", <anonymous type containing parameters>)
The parameters will be passed to the InvokeAsync
method. The PriorityList
view component developed in the article is invoked from the Views/Todo/Index.cshtml view file. In the following, the InvokeAsync
method is called with two parameters:
[!code-cshtmlMain]
1: @using ViewComponentSample.Models
2: @model IEnumerable<TodoItem>
3:
4: <h2>ToDo</h2>
5: <table>
6: <tr>
7: <th>
8: @Html.DisplayNameFor(model => model.IsDone)
9: </th>
10: <th>
11: @Html.DisplayNameFor(model => model.Priority)
12: </th>
13: <th>
14: @Html.DisplayNameFor(model => model.Name)
15: </th>
16: <th></th>
17: </tr>
18: @foreach (var item in Model)
19: {
20: <tr>
21: <td>
22: @Html.DisplayFor(modelItem => item.IsDone)
23: </td>
24: <td>
25: @Html.DisplayFor(modelItem => item.Priority)
26: </td>
27: <td>
28: @Html.DisplayFor(modelItem => item.Name)
29: </td>
30: </tr>
31: }
32: </table>
33:
34: <div>
35: @await Component.InvokeAsync("PriorityList", new { maxPriority = 4, isDone = true })
36: </div>
Invoking a view component as a Tag Helper
For ASP.NET Core 1.1 and higher, you can invoke a view component as a (xref:)Tag Helper:
[!code-cshtmlMain]
1: @using ViewComponentSample.Models
2: @addTagHelper *, ViewCompFinal
3: @model IEnumerable<TodoItem>
4:
5: <h2>ToDo</h2>
6:
7: <table>
8: <tr>
9: <th>
10: @Html.DisplayNameFor(model => model.IsDone)
11: </th>
12: <th>
13: @Html.DisplayNameFor(model => model.Priority)
14: </th>
15: <th>
16: @Html.DisplayNameFor(model => model.Name)
17: </th>
18: <th></th>
19: </tr>
20:
21: @foreach (var item in Model)
22: {
23: <tr>
24: <td>
25: @Html.DisplayFor(modelItem => item.IsDone)
26: </td>
27: <td>
28: @Html.DisplayFor(modelItem => item.Priority)
29: </td>
30: <td>
31: @Html.DisplayFor(modelItem => item.Name)
32: </td>
33: </tr>
34: }
35: </table>
36: <div>
37: <vc:priority-list max-priority="2" is-done="false">
38: </vc:priority-list>
39: </div>
Pascal-cased class and method parameters for Tag Helpers are translated into their lower kebab case. The Tag Helper to invoke a view component uses the <vc></vc>
element. The view component is specified as follows:
<vc:[view-component-name]
parameter1="parameter1 value"
parameter2="parameter2 value">
</vc:[view-component-name]>
Note: In order to use a View Component as a Tag Helper, you must register the assembly containing the View Component using the @addTagHelper
directive. For example, if your View Component is in an assembly called “MyWebApp”, add the following directive to the _ViewImports.cshtml
file:
@addTagHelper *, MyWebApp
You can register a View Component as a Tag Helper to any file that references the View Component. See (xref:)Managing Tag Helper Scope for more information on how to register Tag Helpers.
The InvokeAsync
method used in this tutorial:
[!code-cshtmlMain]
1: @using ViewComponentSample.Models
2: @model IEnumerable<TodoItem>
3:
4: <h2>ToDo</h2>
5: <table>
6: <tr>
7: <th>
8: @Html.DisplayNameFor(model => model.IsDone)
9: </th>
10: <th>
11: @Html.DisplayNameFor(model => model.Priority)
12: </th>
13: <th>
14: @Html.DisplayNameFor(model => model.Name)
15: </th>
16: <th></th>
17: </tr>
18: @foreach (var item in Model)
19: {
20: <tr>
21: <td>
22: @Html.DisplayFor(modelItem => item.IsDone)
23: </td>
24: <td>
25: @Html.DisplayFor(modelItem => item.Priority)
26: </td>
27: <td>
28: @Html.DisplayFor(modelItem => item.Name)
29: </td>
30: </tr>
31: }
32: </table>
33:
34: <div>
35: @await Component.InvokeAsync("PriorityList", new { maxPriority = 4, isDone = true })
36: </div>
In Tag Helper markup:
[!code-cshtmlMain]
1: @using ViewComponentSample.Models
2: @addTagHelper *, ViewCompFinal
3: @model IEnumerable<TodoItem>
4:
5: <h2>ToDo</h2>
6:
7: <table>
8: <tr>
9: <th>
10: @Html.DisplayNameFor(model => model.IsDone)
11: </th>
12: <th>
13: @Html.DisplayNameFor(model => model.Priority)
14: </th>
15: <th>
16: @Html.DisplayNameFor(model => model.Name)
17: </th>
18: <th></th>
19: </tr>
20:
21: @foreach (var item in Model)
22: {
23: <tr>
24: <td>
25: @Html.DisplayFor(modelItem => item.IsDone)
26: </td>
27: <td>
28: @Html.DisplayFor(modelItem => item.Priority)
29: </td>
30: <td>
31: @Html.DisplayFor(modelItem => item.Name)
32: </td>
33: </tr>
34: }
35: </table>
36: <div>
37: <vc:priority-list max-priority="2" is-done="false">
38: </vc:priority-list>
39: </div>
In the sample above, the PriorityList
view component becomes priority-list
. The parameters to the view component are passed as attributes in lower kebab case.
Invoking a view component directly from a controller
View components are typically invoked from a view, but you can invoke them directly from a controller method. While view components do not define endpoints like controllers, you can easily implement a controller action that returns the content of a ViewComponentResult
.
In this example, the view component is called directly from the controller:
[!code-csharpMain]
1: using System.Linq;
2: using Microsoft.AspNetCore.Mvc;
3: using ViewComponentSample.Models;
4: using System.Threading.Tasks;
5: using Microsoft.EntityFrameworkCore;
6:
7: namespace ViewComponentSample.Controllers
8: {
9: public class ToDoController : Controller
10: {
11: private readonly ToDoContext _ToDoContext;
12:
13: public ToDoController(ToDoContext context)
14: {
15: _ToDoContext = context;
16: }
17:
18: public IActionResult Index()
19: {
20: var model = _ToDoContext.ToDo.ToList();
21: return View(model);
22: }
23: #region snippet_IndexVC
24: public IActionResult IndexVC()
25: {
26: return ViewComponent("PriorityList", new { maxPriority = 3, isDone = false });
27: }
28: #endregion
29:
30: public async Task<IActionResult> IndexFinal()
31: {
32: return View(await _ToDoContext.ToDo.ToListAsync());
33: }
34:
35: public IActionResult IndexNameof()
36: {
37: return View(_ToDoContext.ToDo.ToList());
38: }
39:
40: public IActionResult IndexFirst()
41: {
42: return View(_ToDoContext.ToDo.ToList());
43: }
44: }
45: }
Walkthrough: Creating a simple view component
Download, build and test the starter code. It’s a simple project with a Todo
controller that displays a list of Todo items.
Add a ViewComponent class
Create a ViewComponents folder and add the following PriorityListViewComponent
class:
[!code-csharpMain]
1: //#define V1
2: #if V1
3: #region snippet1
4: using Microsoft.AspNetCore.Mvc;
5: using Microsoft.EntityFrameworkCore;
6: using System.Collections.Generic;
7: using System.Linq;
8: using System.Threading.Tasks;
9: using ViewComponentSample.Models;
10:
11: namespace ViewComponentSample.ViewComponents
12: {
13: public class PriorityListViewComponent : ViewComponent
14: {
15: private readonly ToDoContext db;
16:
17: public PriorityListViewComponent(ToDoContext context)
18: {
19: db = context;
20: }
21:
22: public async Task<IViewComponentResult> InvokeAsync(
23: int maxPriority, bool isDone)
24: {
25: var items = await GetItemsAsync(maxPriority, isDone);
26: return View(items);
27: }
28: #region snippet2
29: private Task<List<TodoItem>> GetItemsAsync(int maxPriority, bool isDone)
30: {
31: return db.ToDo.Where(x => x.IsDone == isDone &&
32: x.Priority <= maxPriority).ToListAsync();
33: }
34: #endregion
35: }
36: }
37: #endregion
38:
39: #endif
Notes on the code:
- View component classes can be contained in any folder in the project.
- Because the class name PriorityListViewComponent ends with the suffix ViewComponent, the runtime will use the string “PriorityList” when referencing the class component from a view. I’ll explain that in more detail later.
The
[ViewComponent]
attribute can change the name used to reference a view component. For example, we could have named the classXYZ
and applied theViewComponent
attribute:- The
[ViewComponent]
attribute above tells the view component selector to use the namePriorityList
when looking for the views associated with the component, and to use the string “PriorityList” when referencing the class component from a view. I’ll explain that in more detail later. - The component uses dependency injection to make the data context available.
InvokeAsync
exposes a method which can be called from a view, and it can take an arbitrary number of arguments.The
InvokeAsync
method returns the set ofToDo
items that satisfy theisDone
andmaxPriority
parameters.
Create the view component Razor view
Create the Views/Shared/Components folder. This folder must be named Components.
Create the Views/Shared/Components/PriorityList folder. This folder name must match the name of the view component class, or the name of the class minus the suffix (if we followed convention and used the ViewComponent suffix in the class name). If you used the
ViewComponent
attribute, the class name would need to match the attribute designation.Create a Views/Shared/Components/PriorityList/Default.cshtml Razor view: [!code-cshtmlMain]
1: @model IEnumerable<ViewComponentSample.Models.TodoItem>
2:
3: <h3>Priority Items</h3>
4: <ul>
5: @foreach (var todo in Model)
6: {
7: <li>@todo.Name</li>
8: }
9: </ul>
The Razor view takes a list of
TodoItem
and displays them. If the view componentInvokeAsync
method doesn’t pass the name of the view (as in our sample), Default is used for the view name by convention. Later in the tutorial, I’ll show you how to pass the name of the view. To override the default styling for a specific controller, add a view to the controller-specific view folder (for example Views/Todo/Components/PriorityList/Default.cshtml).If the view component is controller-specific, you can add it to the controller-specific folder (Views/Todo/Components/PriorityList/Default.cshtml).
Add a
div
containing a call to the priority list component to the bottom of the Views/Todo/index.cshtml file:[!code-cshtmlMain]
1: @using ViewComponentSample.Models
2: @model IEnumerable<TodoItem>
3:
4: <h2>ToDo</h2>
5:
6: <table>
7: <tr>
8: <th>
9: @Html.DisplayNameFor(model => model.IsDone)
10: </th>
11: <th>
12: @Html.DisplayNameFor(model => model.Priority)
13: </th>
14: <th>
15: @Html.DisplayNameFor(model => model.Name)
16: </th>
17: <th></th>
18: </tr>
19:
20: @foreach (var item in Model)
21: {
22: <tr>
23: <td>
24: @Html.DisplayFor(modelItem => item.IsDone)
25: </td>
26: <td>
27: @Html.DisplayFor(modelItem => item.Priority)
28: </td>
29: <td>
30: @Html.DisplayFor(modelItem => item.Name)
31: </td>
32: </tr>
33: }
34: </table>
35: <div >
36: @await Component.InvokeAsync("PriorityList", new { maxPriority = 2, isDone = false })
37: </div>
The markup @await Component.InvokeAsync
shows the syntax for calling view components. The first argument is the name of the component we want to invoke or call. Subsequent parameters are passed to the component. InvokeAsync
can take an arbitrary number of arguments.
Test the app. The following image shows the ToDo list and the priority items:
You can also call the view component directly from the controller:
[!code-csharpMain]
1: using System.Linq;
2: using Microsoft.AspNetCore.Mvc;
3: using ViewComponentSample.Models;
4: using System.Threading.Tasks;
5: using Microsoft.EntityFrameworkCore;
6:
7: namespace ViewComponentSample.Controllers
8: {
9: public class ToDoController : Controller
10: {
11: private readonly ToDoContext _ToDoContext;
12:
13: public ToDoController(ToDoContext context)
14: {
15: _ToDoContext = context;
16: }
17:
18: public IActionResult Index()
19: {
20: var model = _ToDoContext.ToDo.ToList();
21: return View(model);
22: }
23: #region snippet_IndexVC
24: public IActionResult IndexVC()
25: {
26: return ViewComponent("PriorityList", new { maxPriority = 3, isDone = false });
27: }
28: #endregion
29:
30: public async Task<IActionResult> IndexFinal()
31: {
32: return View(await _ToDoContext.ToDo.ToListAsync());
33: }
34:
35: public IActionResult IndexNameof()
36: {
37: return View(_ToDoContext.ToDo.ToList());
38: }
39:
40: public IActionResult IndexFirst()
41: {
42: return View(_ToDoContext.ToDo.ToList());
43: }
44: }
45: }
Specifying a view name
A complex view component might need to specify a non-default view under some conditions. The following code shows how to specify the “PVC” view from the InvokeAsync
method. Update the InvokeAsync
method in the PriorityListViewComponent
class.
[!code-csharpMain]
1: //#define Final
2: #if Final
3:
4: using Microsoft.AspNetCore.Mvc;
5: using Microsoft.EntityFrameworkCore;
6: using System.Collections.Generic;
7: using System.Linq;
8: using System.Threading.Tasks;
9: using ViewComponentSample.Models;
10:
11: namespace ViewComponentSample.ViewComponents
12: {
13: public class PriorityListViewComponent : ViewComponent
14: {
15: private readonly ToDoContext db;
16:
17: public PriorityListViewComponent(ToDoContext context)
18: {
19: db = context;
20: }
21:
22: private Task<List<TodoItem>> GetItemsAsync(int maxPriority, bool isDone)
23: {
24: return db.ToDo.Where(x => x.IsDone == isDone &&
25: x.Priority <= maxPriority).ToListAsync();
26: }
27: #region snippet1
28: public async Task<IViewComponentResult> InvokeAsync(
29: int maxPriority, bool isDone)
30: {
31: string MyView = "Default";
32: // If asking for all completed tasks, render with the "PVC" view.
33: if (maxPriority > 3 && isDone == true)
34: {
35: MyView = "PVC";
36: }
37: var items = await GetItemsAsync(maxPriority, isDone);
38: return View(MyView, items);
39: }
40: #endregion
41: }
42: }
43:
44: #endif
Copy the Views/Shared/Components/PriorityList/Default.cshtml file to a view named Views/Shared/Components/PriorityList/PVC.cshtml. Add a heading to indicate the PVC view is being used.
[!code-cshtmlMain]
1: @model IEnumerable<ViewComponentSample.Models.TodoItem>
2:
3: <h2> PVC Named Priority Component View</h2>
4: <h4>@ViewBag.PriorityMessage</h4>
5: <ul>
6: @foreach (var todo in Model)
7: {
8: <li>@todo.Name</li>
9: }
10: </ul>
Update Views/TodoList/Index.cshtml:
[!code-cshtmlMain]
1: @using ViewComponentSample.Models
2: @model IEnumerable<TodoItem>
3:
4: <h2>ToDo</h2>
5: <table>
6: <tr>
7: <th>
8: @Html.DisplayNameFor(model => model.IsDone)
9: </th>
10: <th>
11: @Html.DisplayNameFor(model => model.Priority)
12: </th>
13: <th>
14: @Html.DisplayNameFor(model => model.Name)
15: </th>
16: <th></th>
17: </tr>
18: @foreach (var item in Model)
19: {
20: <tr>
21: <td>
22: @Html.DisplayFor(modelItem => item.IsDone)
23: </td>
24: <td>
25: @Html.DisplayFor(modelItem => item.Priority)
26: </td>
27: <td>
28: @Html.DisplayFor(modelItem => item.Name)
29: </td>
30: </tr>
31: }
32: </table>
33:
34: <div>
35: @await Component.InvokeAsync("PriorityList", new { maxPriority = 4, isDone = true })
36: </div>
Run the app and verify PVC view.
If the PVC view is not rendered, verify you are calling the view component with a priority of 4 or higher.
Examine the view path
- Change the priority parameter to three or less so the priority view is not returned.
- Temporarily rename the Views/Todo/Components/PriorityList/Default.cshtml to 1Default.cshtml.
Test the app, you’ll get the following error:
An unhandled exception occurred while processing the request. InvalidOperationException: The view 'Components/PriorityList/Default' was not found. The following locations were searched: /Views/ToDo/Components/PriorityList/Default.cshtml /Views/Shared/Components/PriorityList/Default.cshtml EnsureSuccessful
- Copy Views/Todo/Components/PriorityList/1Default.cshtml to Views/Shared/Components/PriorityList/Default.cshtml.
- Add some markup to the Shared Todo view component view to indicate the view is from the Shared folder.
Test the Shared component view.
Avoiding magic strings
If you want compile time safety, you can replace the hard-coded view component name with the class name. Create the view component without the “ViewComponent” suffix:
[!code-csharpMain]
1: // Used at the very end with Avoiding magic strings
2: //#define no_suffix
3: #if no_suffix
4: #region snippet1
5: using Microsoft.AspNetCore.Mvc;
6: using Microsoft.EntityFrameworkCore;
7: using System.Collections.Generic;
8: using System.Linq;
9: using System.Threading.Tasks;
10: using ViewComponentSample.Models;
11:
12: namespace ViewComponentSample.ViewComponents
13: {
14: public class PriorityList : ViewComponent
15: {
16: private readonly ToDoContext db;
17:
18: public PriorityList(ToDoContext context)
19: {
20: db = context;
21: }
22:
23: public async Task<IViewComponentResult> InvokeAsync(
24: int maxPriority, bool isDone)
25: {
26: var items = await GetItemsAsync(maxPriority, isDone);
27: return View(items);
28: }
29: private Task<List<TodoItem>> GetItemsAsync(int maxPriority, bool isDone)
30: {
31: return db.ToDo.Where(x => x.IsDone == isDone &&
32: x.Priority <= maxPriority).ToListAsync();
33: }
34: }
35: }
36: #endregion
37: #endif
Add a using
statement to your Razor view file, and use the nameof
operator:
[!code-cshtmlMain]
1: @using ViewComponentSample.Models
2: @using ViewComponentSample.ViewComponents
3: @model IEnumerable<TodoItem>
4:
5: <h2>ToDo nameof</h2>
6: <!-- Markup removed for brevity. -->
7: <table>
8: <tr>
9: <th>
10: @Html.DisplayNameFor(model => model.IsDone)
11: </th>
12: <th>
13: @Html.DisplayNameFor(model => model.Priority)
14: </th>
15: <th>
16: @Html.DisplayNameFor(model => model.Name)
17: </th>
18: <th></th>
19: </tr>
20: @foreach (var item in Model)
21: {
22: <tr>
23: <td>
24: @Html.DisplayFor(modelItem => item.IsDone)
25: </td>
26: <td>
27: @Html.DisplayFor(modelItem => item.Priority)
28: </td>
29: <td>
30: @Html.DisplayFor(modelItem => item.Name)
31: </td>
32: </tr>
33: }
34: </table>
35:
36: <div>
37:
38: @await Component.InvokeAsync(nameof(PriorityList), new { maxPriority = 4, isDone = true })
39: </div>
Additional Resources
|