View Components in Razor Pages

View Components perform a similar role to Tag Helpers and Partial Pages. They generate HTML and are designed to represent reusable snippets of UI that can help break up and simplify complex layouts, or that can be used in multiple pages. View components are recommended instead of partial pages or tag helpers whenever any form of logic is required to obtain data for inclusion in the resulting HTML snippet, specifically calls to an external resource such as a file, database or web service. View components also lend themselves to unit testing.

View components are particularly useful for data-driven features in a layout page where there is no related page model or controller class. Examples of use cases for view components include data-driven menus, tag clouds and shopping basket widgets.

Elements of a view component

View components consist of a class file and a .cshtml view file. The class file contains the logic for generating the model. It can be thought of as a mini-controller, just as the Razor PageModel file is considered to be a controller. The view file contains the template used to generate the HTML to be plugged in to the page that hosts the view component.

The class file must conform to the following rules:

  • It must derive from the ViewComponent class
  • It must have "ViewComponent" as a suffix to the class name or it must be decorated with the [ViewComponent] attribute (or derive from a class that's decorated with the [ViewComponent] attribute)
  • It must implement a method named Invoke with a return type of IViewComponentResult (or InvokeAsync returning Task<IViewComponentResult> if you need to call asynchronous methods). Typically, this is satisfied by a return View(...) statement in the method.

By default, the view file is named default.cshtml. You can specify an alternative name for the view file by passing it to the return View(...) statement. The view file's placement within the application's file structure is important, because the framework searches pre-defined locations for it:

/Pages/Shared/Components/<component name>/Default.cshtml
/Views/Shared/Components/<component name>/Default.cshtml

The framework will also search by walking up the directory tree from the location of the calling page until it reaches the root Pages folder. This is the same search pattern as for partials.

The component name is the name of the view component class without the ViewComponent suffix (if it is applied). For a Razor Pages only site, the recommended location for view component views is the /Pages/Shared/Components/ directory. This is especially the case if you are using Areas, which you will do if you use the Identity UI. The path that begins with /Views should only really be used if you are creating a hybrid Razor Pages/MVC application.

Walkthrough

The following walkthrough will result in two example view components being created. One will call into an external web service to obtain a list of people, and will display their names. The other will take a parameter representing the ID of one person whose details will be obtained from an external web service and then displayed in a widget.

The service APIs used in this example are hosted at JSONPlaceholder which provides free JSON APIs for development and testing purposes.

The view components will not be responsible for making calls to the external APIs. This task will be performed in a separate service class which will be injected into the view components via the built-in Dependency Injection framework.

  1. Create a new Razor Pages site named RazorPages using Visual Studio or the command line.

  2. Add a new C# class file named Domain.cs to the root folder of the application and replace any existing content with the following:

    namespace RazorPages
    {
        public class User
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public string UserName { get; set; }
            public string Email { get; set; }
            public string Phone { get; set; }
            public string Website { get; set; }
            public Address Address { get; set; }
            public Company Company { get; set; }
        }
    
        public class Address
        {
            public string Street { get; set; }
            public string Suite { get; set; }
            public string City { get; set; }
            public string Zipcode { get; set; }
            public Geo Geo { get; set; }
        }
        public class Company
        {
            public string Name { get; set; }
            public string Catchphrase { get; set; }
            public string Bs { get; set; }
        }
        public class Geo
        {
            public float Lat { get; set;}
            public float Lng { get; set; }
        }
    }
    

    These classes map to the structure of the objects represented by the JSON provided by the API being used for this example.

  3. Add a new folder named Services to the root folder
    Services folder

  4. Add a new C# class file named IUserService.cs to the Services folder. Replace any existing content with the following code:

    using System.Collections.Generic;
    using System.Threading.Tasks;
    
    namespace RazorPages.Services
    {
        public interface IUserService
        {
            Task<List<User>> GetUsersAsync();
            Task<User> GetUserAsync(int id);
        }
    }
    

    This is the interface that specifies the operations offered by the service

  5. Add another new C# class file named UserService.cs to the Services folder and replace any existing content with the following:

    using Newtonsoft.Json;
    using System.Collections.Generic;
    using System.Net.Http;
    using System.Threading.Tasks;
    
    namespace RazorPages.Services
    {
        public class UserService : IUserService
        {
            public async Task<List<User>> GetUsersAsync()
            {
                using (var client = new HttpClient())
                {
                    var endPoint = "https://jsonplaceholder.typicode.com/users";
                    var json = await client.GetStringAsync(endPoint);
                    return JsonConvert.DeserializeObject<List<User>>(json);
                }
            }
    
            public async Task<User> GetUserAsync(int id)
            {
                using (var client = new HttpClient())
                {
                    var endPoint = $"https://jsonplaceholder.typicode.com/users/{id}";
                    var json = await client.GetStringAsync(endPoint);
                    return JsonConvert.DeserializeObject<User>(json);
                }
            }
        }
    }
    
    

    This class represents an implementation of the IUserService interface.

  6. Add a folder named ViewComponents to the root of the application. Then add a new file to that folder, name it UsersViewComponent.cs and replace any existing content with the following:

    using Microsoft.AspNetCore.Mvc;
    using RazorPages.Services;
    using System.Threading.Tasks;
    
    namespace RazorPages.ViewComponents
    {
        public class UsersViewComponent : ViewComponent
        {
            private IUserService _userService;
    
            public UsersViewComponent(IUserService userService)
            {
                _userService = userService;
            }
    
            public async Task<IViewComponentResult> InvokeAsync()
            {
                var users = await _userService.GetUsersAsync();
                return View(users);
            }
        }
    }
    

    This is the code part of the view component. It makes use of the built-in dependency injection system to resolve the implementation of IUserService which is injected into the constructor of the view component class. The InvokeAsync method obtains a List<User> from the service and passes it to the view.

  7. Create a folder named Components in the Pages folder and then add another folder named Users to the newly created Components folder. Add a new file named default.cshtml to the Users folder. The resulting folder and file hierarchy should look like this:

    ViewComponents view file

  8. Replace the code in default.cshtml with the following:

    @model List<RazorPages.User>
    <h3>Users</h3>
    <ul>
        @foreach (var user in Model)
        {
            <li>@user.Name</li>
        }
    </ul>
    

    This is the view, and completes the view component. Notice that the view accepts a model of type List<User> via the @model directive, which is type passed to the view from the InvokeAsync method.

  9. Open the Startup.cs file and add using RazorPages.Services; to the using directives at the top of the file. Then amend the ConfigureServices method so that the code looks like this:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
        services.AddTransient<IUserService, UserService>();
    }
    

    This step registers the IUserService with the dependency injection system, and specifies that UserService is the actual implementation to use.

  10. Open the Layout.cshtml file and locate the content between the <nav> section and the <environment> tag helper that currently looks like this:

    <div class="container body-content">
    
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; 2017 - RazorPages</p>
        </footer>
    </div>
    

    Change the content as follows:

    <div class="container body-content">
        <div class="col-md-9">
            @RenderBody()
        </div>
        <div class="col-md-3">
            @await  Component.InvokeAsync("Users")
        </div>
    </div>
    <hr />
    <footer class="container">
        <p>&copy; 2017 - RazorPages</p>
    </footer>
    

    This converts the layout for the site to 2 columns, with page content displayed in the left hand column and a Users widget displayed in the right hand column. It uses the Component.InvokeAsync method to render the output of the view component to the page. The string which is passed to the method represents the name of the view component (the class name without the "ViewComponent" suffix).

  11. Run the site to ensure that all is working. The list of users should appear on the right hand side of every page:

    User View Component

Taghelper and passing parameters

The second example will demonstrate the use of a tag helper instead of calling the Component.InvokeAsync method. It will also demonstrate passing parameters to the view component.

  1. Add a new C# class file to the ViewComponents folder and name it UserViewComponent.cs. Replace any existing content with the following:

    using Microsoft.AspNetCore.Mvc;
    using RazorPages.Services;
    using System.Threading.Tasks;
    
    namespace RazorPages.ViewComponents
    {
        public class UserViewComponent : ViewComponent
        {
            private IUserService _userService;
    
            public UserViewComponent(IUserService userService)
            {
                _userService = userService;
            }
    
            public async Task<IViewComponentResult> InvokeAsync(int id)
            {
                var user = await _userService.GetUserAsync(id);
                return View(user);
            }
        }
    }
    

    This is the code part of the view component. The only difference between this component and the previous one is that the InvokeAsync method expects a parameter of type int to be passed, which is then passed to the service method.

  2. Add a new folder to the /Pages/Components folder named User. Add a Razor file to the folder named default.cshtml. The structure of the Components folder should now look like this:

    user view component

  3. Replace any existing content in the new default.cshtml file with the following:

    @model RazorPages.User
    
    <h3>Featured User</h3>
    <div>
        <strong>@Model.Name</strong><br />
        @Model.Website<br />
        @Model.Company.Name<br />
    </div>
    
  4. Open the ViewImports.cshtml file and add the following line to the existing code:

    @addTagHelper *, RazorPages
    

    This registers the view component tag.

  5. Replace the call to @await Component.InvokeAync("Users") in the layout file with the following:

    <vc:user id="new Random().Next(1, 10)"></vc:user>
    

    The name of the view component is specified in the tag helper, along with the parameter for the InvokeAsync method. In this case, the Random class is used to return any number from 1-10 each time the component is invoked, resulting in a user being selected randomly each time the page is displayed.

Note that the name of the component and its parameters must be specified in kebab case in the tag helper. Kebab case takes upper case characters and replaces them with their lowercase version, prefixing those that appear within the word with a hyphen. So a MainNavigation view component becomes main-navigation in the tag helper. A productId parameter name becomes product-id.

  1. Run the application to test that the component is working, and refresh a few times to see different users' details being displayed:

    user view component

If you prefer to use the Component.InvokeAsync method, parameters are passed as a second argument:

@await Component.InvokeAsync("User", new Random().Next(1, 10))

Optional Parameters

From .NET 6, the view component tag helper supports optional parameters. Consequently, you no longer need to specify a value for an optional parameter when adding a view component to a page. For example, suppose that our UserViewComponent took an optional boolean parameter to indicate whether full details of the user are required for display:

public async Task<IViewComponentResult> InvokeAsync(int id, bool showDetails = false)
{
    var user = showDetails ? 
        await _userService.GetUserWithDetailsAsync(id) :
        await _userService.GetUserAsync(id);
    return View(user);
}

Prior to .NET 6, you needed to specify a value for the show-details parameter despite it having a default value. Now you don't:

<vc:user id="new Random().Next(1, 10)" />
Last updated: 6/9/2022 11:14:13 AM

© 2018 - 2024 - Mike Brind.
All rights reserved.
Contact me at Mike dot Brind at Outlook.com