ASP.NET8 View Components Set New Bar for Web UI Reusability

by Mark PelfMay 23rd, 2025
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

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.

People Mentioned

Mention Thumbnail

Coin Mentioned

Mention Thumbnail
featured image - ASP.NET8 View Components Set New Bar for Web UI Reusability
Mark Pelf HackerNoon profile picture
0-item

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=========================================
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  --------------------------------------------------->
@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();
                myModel1.Parameter1String = "Div3-Sync, invoked with method";
                myModel1.Parameter2Int = 33333;
                myModel1.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 = myModel1,
                    })        ;
            }
        </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();
                myModel2.Parameter1String = "Div4- Sync, invoked with Tag Helper";
                myModel2.Parameter2Int = 44444;
                myModel2.IdOfTargetInputField = "ContractOwnerCustomerId";
            }

            <vc:customers-search2 model=myModel2>
            </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

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/

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks