ASP.NET8 – View-Component primer
We show reusable code on how to create ASP.NET8 View-Components.
Abstract: We show how to create ASP.NET8 View-Components, which are a more powerful form of component architecture than popular Partial-Views. An example with reusable code is shown.
1 ASP.NET8 View-Components are more powerful than Partial-Views
Simply speaking, ASP.NET8 View-Components are similar to Partial-Views, but enable a higher level of componentization and functionality encapsulation. They are intended for ASP.NET8 MVC or Razor Pages architecture.
ASP.NET8 View-Components are intended for the creation of reusable components that have View/Render functionality, and whose complexity is greater than normal Partial-Views.
Key advantage over ASP.NET Partial-Views is that View-Components have a component part that is server-side executed and can access all the application architecture mechanisms, like Database Access Layer (for example EF) independently from the Host page where they are hosted. That enables quite elaborate and complex processing that can be done and componentized into a reusable form.
ASP.NET8 View-Components receive parameters from the calling method (host page or even controller directly) and not HTTP request. But, of course, nothing prevents the creation of View-Components that receive HttpRequest as a parameter that will be explicitly passed to the component.
View components in ASP.NET Core are well defined in the article [1], and I will not repeat it all here. The plan is to provide working C# sample code (“Primer”) on how view components typically can be used. Reusable code with a simple example is shown below. I added a decent quantity of comments, so it should be self-explanatory how they work.
2 Final Result
Here we show the final result. There is ASP.NET8 host page has 2 components and 4 invocations shown on the picture. Components are colored deliberately to show where they are. In a real app, of course, coloring would not be used. Here is what this example shows:
- Div-0. Control in Host ASP.NET8 page
- Div-1. View-Component-1 CustomerSearch, which is Async component, invoked with a method
- Div-2. View-Component-1 CustomerSearch, which is Async component, invoked with Tag Helper
- Div-3. View-Component-2 CustomerSearch2, which is Sync component, invoked with a method
- Div-4. View-Component-2 CustomerSearch2, which is Sync component, invoked with Tag Helper
3 Source Code for this Example
The source code is well commented, and it should be self-explanatory.
3.1 C# part
HomeController.cs
The controller provides three endpoints: Index, Test1, and Error.
It uses dependency injection for logging.
It returns views for each action, optionally passing models.
The Test1 action is specifically for testing a ViewComponent with a model.
CustomersSearchViewComponent.cs
This code defines an asynchronous View Component for a Razor Pages project that displays a list of customers. It uses dependency injection to receive a logger, accepts an optional model with parameters, prepares a view model with those parameters and a hardcoded customer list, and then renders a view with that data. The component is designed to be reusable and modular for customer search or selection features. Asynchronous.
CustomersSearch2ViewComponent.cs
This code defines a synchronous View Component for a Razor Pages project that displays a list of customers. It uses dependency injection to receive a logger, accepts an optional model with parameters, prepares a view model with those parameters and a hardcoded customer list, and then renders a view with that data. The component is designed to be reusable and modular for customer search or selection features, and operates synchronously rather than asynchronously.
//HomeController.cs=========================================
namespace Example1.Controllers
{
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}
public IActionResult Index()
{
return View();
}
//in this action we are testing the ViewComponent
public IActionResult Test1(Test1_ViewModel model)
{
return View(model);
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}
// CustomersSearchViewComponent_Model.cs ==========================
namespace Example1.ViewComponents.Models
{
public class CustomersSearchViewComponent_Model
{
public string? Parameter1String { get; set; } = null;
public int? Parameter2Int { get; set; } = null;
public string? IdOfTargetInputField { get; set; } = null;
}
}
// CustomersSearchViewComponent_ViewModel.cs ==========================
namespace Example1.ViewComponents.Models
{
public class CustomersSearchViewComponent_ViewModel
{
public string? Parameter1String { get; set; } = null;
public int? Parameter2Int { get; set; } = null;
public string? IdOfTargetInputField { get; set; } = null;
//this should come from DB in real application
public SelectList? ListOfCustomers { get; set; }
}
}
//CustomersSearch2ViewComponent.cs===================================================
namespace Example1.ViewComponents
{
//it should inherit from ViewComponent class
//this is Sync version of ViewComponent
public class CustomersSearch2ViewComponent : ViewComponent
{
private readonly ILogger<CustomersSearch2ViewComponent>? _logger;
// We are testing Dependency Injection (DI) to inject the logger
public CustomersSearch2ViewComponent(ILogger<CustomersSearch2ViewComponent> logger)
{
//note how useful is this, we are passing in this constructor
//into view-component objects that are part of app DI container
//you can pass here any form of reference to DataBase, for example
//like EF DbContext or similar
_logger = logger;
}
//The Invoke method for the View component
public IViewComponentResult Invoke(
CustomersSearchViewComponent_Model? model = null
)
{
//this is neat, this parameters in CustomersSearchViewComponent_Model
//are passed to the view-component from host form
{
//testing that DI worked - logger
string methodName = $"Type: {System.Reflection.MethodBase.GetCurrentMethod()?.DeclaringType?.FullName}, " +
$"Method: InvokeAsync; ";
_logger?.LogWarning(methodName);
}
//preparing view-model
CustomersSearchViewComponent_ViewModel viewModel = new();
viewModel.Parameter1String = model?.Parameter1String;
viewModel.Parameter2Int = model?.Parameter2Int;
viewModel.IdOfTargetInputField = model?.IdOfTargetInputField;
{
//this should come from DB in real application
SelectList list1 = new SelectList
(new List<SelectListItem>
{
new SelectListItem
{
Text = "John - 111",
Value = "111"
},
new SelectListItem
{
Text = "Mark - 222",
Value = "222"
},
new SelectListItem
{
Text = "Novak - 333",
Value = "333"
},
}, "Value", "Text");
viewModel.ListOfCustomers = list1;
}
//now render component view
return View("Default", viewModel);
}
}
}
//CustomersSearchViewComponent.cs===================================================
namespace Example1.ViewComponents
{
//it should inherit from ViewComponent class
//this is Async version of ViewComponent
public class CustomersSearchViewComponent : ViewComponent
{
private readonly ILogger<CustomersSearchViewComponent>? _logger;
// We are testing Dependency Injection (DI) to inject the logger
public CustomersSearchViewComponent(ILogger<CustomersSearchViewComponent> logger)
{
//note how useful is this, we are passing in this constructor
//into view-component objects that are part of app DI container
//you can pass here any form of reference to DataBase, for example
//like EF DbContext or similar
_logger = logger;
}
//The Invoke method for the View component
public async Task<IViewComponentResult> InvokeAsync(
CustomersSearchViewComponent_Model? model=null
)
{
//this is neat, this parameters in CustomersSearchViewComponent_Model
//are passed to the view-component from host form
{
//testing that DI worked - logger
string methodName = $"Type: {System.Reflection.MethodBase.GetCurrentMethod()?.DeclaringType?.FullName}, " +
$"Method: InvokeAsync; ";
_logger?.LogWarning(methodName);
}
await Task.Delay(0); // Simulate some async work
//preparing view-model
CustomersSearchViewComponent_ViewModel viewModel = new();
viewModel.Parameter1String = model?.Parameter1String;
viewModel.Parameter2Int = model?.Parameter2Int;
viewModel.IdOfTargetInputField = model?.IdOfTargetInputField;
{
//this should come from DB in real application
SelectList list1 = new SelectList
(new List<SelectListItem>
{
new SelectListItem
{
Text = "John - 111",
Value = "111"
},
new SelectListItem
{
Text = "Mark - 222",
Value = "222"
},
new SelectListItem
{
Text = "Novak - 333",
Value = "333"
},
}, "Value", "Text");
viewModel.ListOfCustomers= list1;
}
//now render component view
return View("Default",viewModel);
}
}
}
3.2 Razor (.cshtml) part
Test1.cshtml
This Razor view demonstrates how to use two View Components—one asynchronous and one synchronous—by invoking them both with the Component.InvokeAsync method and with tag helpers. The page includes a simple form for entering a customer ID and displays the results of each View Component in separate sections, showing different ways to pass parameters and render reusable UI blocks in a Razor Pages project. The layout uses Bootstrap for styling and organization.
CustomersSearch\Default.cshtml
This Razor view is the UI for the CustomersSearch asynchronous View Component. It displays parameters passed from the host form, shows a dropdown list of customers, and includes a button that, when clicked, copies the selected customer ID from the dropdown into a specific input field on the main form using JavaScript. The layout uses Bootstrap classes for styling and organization.
CustomersSearch2\Default.cshtml
This Razor view defines the UI for the synchronous CustomersSearch2 View Component. It displays parameters received from the host form, shows a dropdown list of customers, and provides a button that copies the selected customer ID from the dropdown into a specific input field on the main form using JavaScript. The component uses Bootstrap classes for layout and visual styling.
<!--Test1.cshtml --------------------------------------------------->
@using Example1.Models.Home;
@using Example1.ViewComponents;
@using Example1.ViewComponents.Models;
@using Example1
@using Example1.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Example1
@addTagHelper *, Example1.TagHelpers
@model Test1_ViewModel
@{
ViewData["Title"] = "Test1";
<!--Title --------------------------------------------------->
<h5 class="bg-primary text-left p-1 mt-1">
<span class="d-inline-block ms-2">
@ViewData["Title"]
</span>
</h5>
<!-- Flexbox row1 -->
<div class="d-flex" style="width:1100px">
<!-- Div 0-------------------------------------------------->
<div class="m-3 p-3">
<fieldset class="border rounded-3 p-3 bg-light shadow" style="width:500px">
<legend class="float-none w-auto px-3 border bg-light rounded-3 ">
Div - 0
</legend>
<!-- Form ----------------------------------------------------------------- -->
<form id="form1" method="post" class="row">
<div class="form-group">
<label asp-for="ContractOwnerCustomer">
Customer Id
</label>
<input id="ContractOwnerCustomerId" class="form-control" asp-for="ContractOwnerCustomer" />
</div>
</form>
<hr />
<!--Buttons ----------------------------------------------------- -->
<div>
<button type="submit" form="form1" class="btn btn-primary mt-3 me-2 float-end"
href=''>
Submit
</button>
</div>
</fieldset>
</div>
</div>
<!-- Flexbox row2 -->
<div class="d-flex" style="width:1100px">
<!-- Div 1----------------------------------------------- -->
<div class="m-3 p-3">
<h5 class="bg-secondary text-center p-1">
Div1 - CustomersSearch, Async, invoked with method
</h5>
@{
CustomersSearchViewComponent_Model myModel1 = new CustomersSearchViewComponent_Model();
myModel1.Parameter1String = "Div1-Async, invoked with method";
myModel1.Parameter2Int = 11111;
myModel1.IdOfTargetInputField = "ContractOwnerCustomerId";
//so, here we are using the ViewComponent
@await Component.InvokeAsync("CustomersSearch", new
{
model = myModel1,
}) ;
}
</div>
<!-- Div 2----------------------------------------------- -->
<div class="m-3 p-3">
<h5 class="bg-secondary text-center p-1">
Div2 - CustomersSearch, Async, invoked with Tag Helper
</h5>
@{
CustomersSearchViewComponent_Model myModel2 = new CustomersSearchViewComponent_Model();
myModel2.Parameter1String = "Div2-Async, invoked with Tag Helper";
myModel2.Parameter2Int = 22222;
myModel2.IdOfTargetInputField = "ContractOwnerCustomerId";
}
<vc:customers-search model=myModel2>
</vc:customers-search>
</div>
</div>
<!-- Flexbox row3-->
<div class="d-flex" style="width:1100px">
<!-- Div 3----------------------------------------------- -->
<div class="m-3 p-3">
<h5 class="bg-secondary text-center p-1">
Div3 - CustomersSearch2, Sync, invoked with method
</h5>
@{
CustomersSearchViewComponent_Model myModel3 = new CustomersSearchViewComponent_Model();
myModel3.Parameter1String = "Div3-Sync, invoked with method";
myModel3.Parameter2Int = 33333;
myModel3.IdOfTargetInputField = "ContractOwnerCustomerId";
//so, here we are using the ViewComponent
//it looks strange, Sync component invoked with async method
//but they want it this way
@await Component.InvokeAsync("CustomersSearch2", new
{
model = myModel3,
}) ;
}
</div>
<!-- Div 4----------------------------------------------- -->
<div class="m-3 p-3">
<h5 class="bg-secondary text-center p-1">
Div4 - CustomersSearch2, Sync, invoked with Tag Helper
</h5>
@{
CustomersSearchViewComponent_Model myModel4 = new CustomersSearchViewComponent_Model();
myModel4.Parameter1String = "Div4- Sync, invoked with Tag Helper";
myModel4.Parameter2Int = 44444;
myModel4.IdOfTargetInputField = "ContractOwnerCustomerId";
}
<vc:customers-search2 model=myModel4>
</vc:customers-search2>
</div>
<!-- ----------------------------------------------- -->
</div>
}
<!--CustomersSearch\Default.cshtml -------------------------------->
@using Example1.ViewComponents.Models
@model CustomersSearchViewComponent_ViewModel
<!--Visually, this div (color bg-info) is the component--->
<div id="ViewComponents1" class="bg-info p-3" style="width:400px">
<!--Title --------------------------------------------------->
<h5 class="bg-primary text-center p-1 m-3">
CustomersSearch - Async ViewComponent
</h5>
<p>
Proof-of-concept, parameter from Host Form
<br/> Parameter1String: @Model.Parameter1String
</p>
<p>
Proof-of-concept, parameter from Host Form
<br /> Parameter2Int: @Model.Parameter2Int
</p>
<fieldset class="border rounded-3 p-3 m-2 shadow">
<label>
Proof-of-concept, list of customers from DB
</label>
<select id="customerSelect2" class="form-select"
asp-items="@Model.ListOfCustomers">
</select>
<a class="btn btn-secondary mt-3 float-end"
onclick="copyInputTextTo(this)">
Copy Selected Customer ID to Form
</a>
</fieldset>
</div> <!-- End of <div id="ViewComponents1" -->
<script>
// This function will export selection value from the ViewComponent
// to the main form and copy it to the input field
function copyInputTextTo(anchor) {
var root = anchor.closest('fieldset');
var target = document.getElementById("@Model.IdOfTargetInputField");
var source = root.querySelector('#customerSelect2') ;
if (source && target) {
target.value = source.value;
}
return false;
}
</script>
<!--CustomersSearch2\Default.cshtml -------------------------------->
@using Example1.ViewComponents.Models
@model CustomersSearchViewComponent_ViewModel
<!--Visually, this div (color bg-warning) is the component--->
<div id="ViewComponents1" class="bg-warning p-3" style="width:400px">
<!--Title --------------------------------------------------->
<h5 class="bg-primary text-center p-1 m-3">
CustomersSearch2 - Sync ViewComponent
</h5>
<p>
Proof-of-concept, parameter from Host Form
<br /> Parameter1String: @Model.Parameter1String
</p>
<p>
Proof-of-concept, parameter from Host Form
<br /> Parameter2Int: @Model.Parameter2Int
</p>
<fieldset class="border rounded-3 p-3 m-2 shadow">
<label>
Proof-of-concept, list of customers from DB
</label>
<select id="customerSelect2" class="form-select"
asp-items="@Model.ListOfCustomers">
</select>
<a class="btn btn-secondary mt-3 float-end"
onclick="copyInputTextTo(this)">
Copy Selected Customer ID to Form
</a>
</fieldset>
</div> <!-- End of <div id="ViewComponents1" -->
<script>
// This function will export selection value from the ViewComponent
// to the main form and copy it to the input field
function copyInputTextTo(anchor) {
var root = anchor.closest('fieldset');
var target = document.getElementById("@Model.IdOfTargetInputField");
var source = root.querySelector('#customerSelect2') ;
if (source && target) {
target.value = source.value;
}
return false;
}
</script>
3.3 Location of files in the project
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}
/Pages/Shared/Components/{View Component Name}/{View Name}
/Areas/{Area Name}/Views/Shared/Components/{View Component Name}/{View Name}4 Conclusion
View Components in ASP.NET8 is are higher form of component architecture, and I found it practical. It takes a while to get used to them and learn how to create/use them, but they are definitely useful to any experienced ASP.NET programmer.
5 References
[1] View components in ASP.NET Core
https://learn.microsoft.com/en-us/aspnet/core/mvc/views/view-components?view=aspnetcore-9.0
[2] View Components in ASP.NET Core MVC
https://dotnettutorials.net/lesson/view-components-in-asp-net-core-mvc/































