Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ASP.NET Core 2.0 MVC View Components

0.00/5 (No votes)
28 Aug 2017 1  
How to reuse parts of web pages using view components in ASP.NET Core MVC. Continue reading...

Problem

How to reuse parts of web pages using view components in ASP.NET Core MVC.

Solution

In an empty project, update Startup class to add services and middleware for MVC:

public void ConfigureServices(
            IServiceCollection services)
        {
            services.AddScoped<IAddressFormatter, AddressFormatter>();
            services.AddMvc();
        }

        public void Configure(
            IApplicationBuilder app,
            IHostingEnvironment env)
        {
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }

Add a model to display in the view:

public class EmployeeViewModel
    {
        public int Id { get; set; }
        public string Firstname { get; set; }
        public string Surname { get; set; }
    }

Add a controller with action method returning ViewResult:

public IActionResult Index()
        {
            var model = new EmployeeViewModel
            {
                Id = 1,
                Firstname = "James",
                Surname = "Bond"
            };
            return View(model);
        }

Add parent view Index.cshtml:

@using Fiver.Mvc.ViewComponents.Models.Home
@model EmployeeViewModel

<div style="border: 1px solid black; margin: 5px">
    <h2>Employee Details (view)</h2>

    <p>Id: @Model.Id</p>
    <p>Firstname: @Model.Firstname</p>
    <p>Surname: @Model.Surname</p>

    @await Component.InvokeAsync("Address", new { employeeId = Model.Id })
</div>

@await Component.InvokeAsync("UserInfo")

Add view component’s model:

public class AddressViewModel
    {
        public int EmployeeId { get; set; }
        public string Line1 { get; set; }
        public string Line2 { get; set; }
        public string Line3 { get; set; }
        public string FormattedValue { get; set; }
    }

Add view component’s class:

[ViewComponent(Name = "Address")]
    public class AddressComponent : ViewComponent
    {
        private readonly IAddressFormatter formatter;

        public AddressComponent(IAddressFormatter formatter)
        {
            this.formatter = formatter;
        }

        public async Task<IViewComponentResult> InvokeAsync(int employeeId)
        {
            var model = new AddressViewModel
            {
                EmployeeId = employeeId,
                Line1 = "Secret Location",
                Line2 = "London",
                Line3 = "UK"
            };
            model.FormattedValue = 
                  this.formatter.Format(model.Line1, model.Line2, model.Line3);
            return View(model);
        }
    }

Add view component’s view Default.cshtml:

@using Fiver.Mvc.ViewComponents.Models.Home
@model AddressViewModel

<div style="border: 1px dashed red; margin: 5px">
    <h3>Address Details (view component in Views/Home)</h3>

    <p>Employee: @Model.EmployeeId</p>
    <p>Line1: @Model.Line1</p>
    <p>Line2: @Model.Line2</p>
    <p>Line3: @Model.Line3</p>
    <p>Full Address: @Model.FormattedValue</p>
</div>

Discussion

View Components are special type of views that are rendered inside other views. They are useful for reusing parts of a view or splitting a large view into smaller components.

Unlike Partial Views, View Components do not rely on Controllers. They have their own class to implement the logic to build component’s model and razor view page to display HTML/CSS.

I like to think of them as mini-controllers, although this is not strictly correct but helps conceptualise their usage. Unlike controllers, they do not handle HTTP requests or have controller lifecycle, which means they can’t rely on filters or model binding.

View Components can utilise dependency injection, which makes them powerful and testable.

Creating

There are few ways to create View Components, I’ll discuss the most commonly used (and best in my view) option.

  1. Create a class (anywhere in your project) and inherit from ViewComponent abstract class.
    • Name of the class, by convention, ends with ViewComponent.
  2. Create a method called InvokedAsync() that returns Task.
    • This method can take any number of parameters, which will be passed when invoking the component (see Invoking section below).
  3. Create model e.g. via database, etc.
  4. Call IViewComponentResult by calling the View() method of base ViewComponent. You could pass your model to this method.
    • Optionally, you could specify the name of razor page (see Discovery section below).

The base ViewComponent class gives access to useful details (via properties) like HttpContext, RouteData, IUrlHelper, IPrincipal and ViewData.

Invoking

View Components can be invoked by either:

  1. Calling @await Component.InvokeAsync(component, parameters) from the razor view.
  2. Returning ViewComponent(component, parameters) from a controller.

Here “component” is a string value refering to the component class.

InvokeAsync() method can take any number of parameters and is passed using anonymous object when invoking the View Component.

Below is an example of the second option above, notice that the second action method doesn’t work because the razor page for the component is not under controller’s views folder:

public class ComponentsController : Controller
    {
        public IActionResult UserInfo()
        {
            // works: this component's view is in Views/Shared
            return ViewComponent("UserInfo");
        }

        public IActionResult Address()
        {
            // doesn't works: this component's view is NOT in Views/<controller>
            return ViewComponent("Address", new { employeeId = 5 });
        }
    }

view components sln

Discovery

MVC will search for the razor page for View Component in the following sequence:

  1. Views/[controller]/Components/[component]/[view].cshtml
  2. Views/Shared/Components/[component]/[view].cshtml

Here matches either:

  1. Name of the component class, minus the ViewComponent suffix if used.
  2. Value specified in [ViewComponent] attribute applied to component class.

Also [view]  by default is Default.cshtml, however can be overwritten by returning a different name from the component class. Below, the component returns a view named Info.cshtml:

public class UserInfoViewComponent : ViewComponent
    {
        public async Task<IViewComponentResult> InvokeAsync()
        {
            var model = new UserInfoViewModel
            {
                Username = "james@bond.com",
                LastLogin = DateTime.Now.ToString()
            };
            return View("info", model);
        }
    }

jQuery

You could access View Components via jQuery as well. To do so, enable the use of Static Files in Startup:

public void Configure(
            IApplicationBuilder app,
            IHostingEnvironment env)
        {
            app.UseStaticFiles();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }

Add jQuery script file to wwwroot and use it in a page:

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>ASP.NET Core View Components</title>

    <script src="~/js/jquery.min.js"></script>

</head>
<body>
    <div>
        <h1>ASP.NET Core View Components</h1>

        <input type="button" id="GetViewComponent" value="Get View Component" />

        <div id="result"></div>
    </div>
    <script>
        $(function () {
            $("#GetViewComponent").click(function () {

                $.ajax({
                    method: 'GET',
                    url: '@Url.Action("UserInfo", "Components")'
                }).done(function (data, statusText, xhdr) {
                    $("#result").html(data);
                }).fail(function (xhdr, statusText, errorText) {
                    $("#result").text(JSON.stringify(xhdr));
                });

            });
        });
    </script>
</body>
</html>

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here