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.
- Create a class (anywhere in your project) and inherit from
ViewComponent abstract
class.
- Name of the class, by convention, ends with
ViewComponent
.
- 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).
- Create model e.g. via database, etc.
- 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:
- Calling
@await Component.InvokeAsync(component, parameters)
from the razor view.
- 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()
{
return ViewComponent("UserInfo");
}
public IActionResult Address()
{
return ViewComponent("Address", new { employeeId = 5 });
}
}
Discovery
MVC will search for the razor page for View Component in the following sequence:
- Views/[controller]/Components/[component]/[view].cshtml
- Views/Shared/Components/[component]/[view].cshtml
Here matches either:
- Name of the component class, minus the
ViewComponent
suffix if used.
- 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>