Right now we are hard coding the buttons in the Categories section of the Home/Index view. We have hard coded buttons for CARS, TRUCKS, and JEEPS.
But, when we designed the database, we broke out the category names into a table called VehicleType with the idea that an admin should be able to enter a new category into the database and the application would dynamically be able to render a button for that new category.
In order to accomplish making the category buttons dynamic, we are going to refactor the application to use a View Component to render the buttons using the categories from the database.
What is a View Component
A View Component in ASP.NET Core MVC is like a mini-controller + view, but much more lightweight.
It’s used when you need to render a chunk of UI that’s reusable and has some logic behind it — but you don’t want to use a full controller and action.
Some key points about View Components are:
- They don’t handle HTTP requests like controllers do.
- They return a piece of HTML (a partial view), not a full page.
- They have their own logic (like getting data from a database) and their own view (cshtml).
- They are invoked from a view, not from a URL.
We are going to use a View Component here because the model that will contain a list of Categories or VehicleTypes is not really related to the main model of the application.
Create the VehicleType repository
When we create the Categories view component, we are going to need a way to retrieve all of the category names from the database stored in the VehicleType table. So the first thing we need to do is create a VehicleType repository.
We are going to create the VehicleType repository the same way we did for the Vehicle repository. We need to create an interface, define an implementation for the interface, and then register the new repository as a service in the add services section of Program.cs.
Create the IVehicleTypeRepository interface
In the Models\Repositories folder of the FredsCars project, add a new interface named IVehicleTypeRepository and modify it with the code shown below.
FredsCars\Models\Repositories\IVehicleTypeRepository.cs
namespace FredsCars.Models.Repositories
{
public interface IVehicleTypeRepository
{
IQueryable<VehicleType> VehicleTypes { get; }
}
}
Define the implimentation
In the Models\Repositories folder of the FredsCars project, add a new class named EFVehicleTypeRepository and modify it with the code shown below.
FredsCars\Models\Repositories\EFVehicleTypeRepository.cs
using FredsCars.Data;
namespace FredsCars.Models.Repositories
{
public class EFVehicleTypeRepository : IVehicleTypeRepository
{
private FredsCarsDbContext _context;
public EFVehicleTypeRepository(FredsCarsDbContext context)
{
_context = context;
}
public IQueryable<VehicleType> VehicleTypes => _context.VehicleTypes;
}
}
Register the service
In the Program class, register the new service in the services container.
FredsCars\Program.cs
... existing code ...
// Add Services
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<FredsCarsDbContext>(opts =>
{
opts.UseSqlServer(
builder.Configuration["ConnectionStrings:FredsCarsMvcConnection"]
);
});
builder.Services.AddScoped<IVehicleRepository, EFVehicleRepository>();
builder.Services.AddScoped<IVehicleTypeRepository, EFVehicleTypeRepository>();
... existing code ...
I won’t go into detail to describe the code in the above three subsections since we covered them all thoroughly when creating the Vehicle repo.
Create the View Component
Add a folder to the root of the FredsCars project called Components. This is the conventional place to put view components. Within the new Components folder create a class called CategoriesComponent.

Modify the contents of the CategoriesComponent class with the code shown below.
FredsCars\Components\CategoriesComponent.cs
using FredsCars.Models.Repositories;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace FredsCars.Components
{
public class CategoriesComponent : ViewComponent
{
private IVehicleTypeRepository _repo;
public CategoriesComponent(IVehicleTypeRepository repo)
{
_repo = repo;
}
public async Task<IViewComponentResult> InvokeAsync()
{
ViewBag.CurrentCategory = RouteData?.Values["category"];
return View(await _repo.VehicleTypes
.AsNoTracking()
.Select(x => x.Name)
.ToListAsync());
}
}
}
In the code above we get an instance of the new IVehicleType repository through DI and assign it to a private variable named _repo.
We then define an asynchronous method named InvokeAsync which returns a Task of IViewComponentResult
. When a view component is called from a view file its Invoke or InvokeAsync method is called.
Within the Invoke method we use the ViewBag object with a dynamic property named CurrentCategory to capture the current category from the current route. We need to do this because the view component will not have access to the CurrentCategory property in the VehiclesListViewModel model coming down from the Home controller in the main application. This is a main consideration when deciding whether or not to use a view component. How much of the main model do you need in this component? View Components are best used when the data represented in its model are not related to the main application.
Next, in the return statement we return a View. As the model for the view we use the VehicleType repo and project the sequence of VehicleTypes into an IQueryable of VehicleType names using the Select method. We could end the statement with the Select method but in order to take make the call asynchronous we make a call to ToListAsync
.
Also notice we use the AsNoTracking
method in the LINQ query to turn off change tracking for entities and increase performance.
Call the Categories view component
Next, let’s set up the call to the view component from the Index view of the Home controller. Modify the Index view with the code shown below.
FredsCars\Views\Home\Index.cshtml
<!-- Categories -->
<div class="col-4 col-md-3 col-lg-2"
style="border-right: 2px solid black">
<div class="d-grid gap-2 button-grid">
<vc:categories-component />
</div>
</div>
In the razor code above we have replaced all of the hard coded category buttons with a call to the view component.
At this point, if you restart the application and navigate to https://localhost:40443, you will see an InvalidOperationException exception message:
InvalidOperationException: The view 'Components/CategoriesComponent/Default' was not found. The following locations were searched:
/Views/Home/Components/CategoriesComponent/Default.cshtml
/Views/Shared/Components/CategoriesComponent/Default.cshtml
A regular view or partial view is searched for in either the Views/Shared folder or the Views/[name of controller handling the request] folder.
A view component’s searchable locations tack on a Components folder and a subfolder named after the view component class after the normal locations and by convention the default name of the view is Default.cshtml.
Unlike the _VehicleTableRowResult
partial view we created in the last module for Vehicle table rows, I believe this view component is reusable. We are just laying out a series of HTML anchor elements made to look like buttons with Bootstrap. The parent div making the call to the view component it is nested in is telling it to display the buttons as a Bootstrap display grid with a gap of two.
<div class="d-grid gap-2 button-grid">
<vc:categories-component />
</div>
So these buttons can be rendered in a variety of ways depending on instruction from a parent div element. And, this view component can go in the Views/Shared folder rather then the Views/Home folder.
Create the partial view for the Categories view component
Let’s create the folder structure representing the reusable path for a view component’s default partial view.
In the Views/Shared folder of the FredsCars project create a new folder named Components. In the new Components folder create another folder called CategoriesComponent. In the Views/Shared/Components/CategoriesComponent folder create a new view named Default.cshml.

Modify the Default.cshtml file with the code below.
FredsCars\Views\Shared\Components\CategoriesComponent\Default.cshtml
@model IEnumerable<string>
<a asp-controller="Home"
asp-action="Index"
asp-route-category=""
asp-route-pageNumber="1"
class="btn @(ViewBag.CurrentCategory == null
? "btn-primary"
: "btn-outline-primary button")">
<b>ALL</b>
</a>
@foreach (string category in Model ??
Enumerable.Empty<string>())
{
<a asp-controller="Home"
asp-action="Index"
asp-route-category="@category"
asp-route-pageNumber="1"
class="btn @(ViewBag.CurrentCategory == category
? "btn-primary"
: "btn-outline-primary button")">
<b>@category.ToUpper()</b>
</a>
}
The razor code above starts off by declaring the model of the partial view an IEnumerable of string (IEnumeralbe<string>
) which will contain the VehicleType names sent down by the CategoriesComponent
class’ InvokeAsync method.
Next, an anchor tag helper lays out the ALL category button and sets the controller route value to Home, action to Index, category to null, and pageNumber to 1. This is identical to what was previously hard coded in the Home/Index view. A slight change comes when we add the Bootstrap classes with the HTML class attribute. When using the ternary operator to decide whether to add the btn-primary class or the btn-outline-primary class we compare ViewBag.CurrentCategory
rather than Model.CurrentCategory
to null to check if no category has been selected by the user.
<a asp-controller="Home"
asp-action="Index"
asp-route-category=""
asp-route-pageNumber="1"
class="btn @(ViewBag.CurrentCategory == null
? "btn-primary"
: "btn-outline-primary button")">
<b>ALL</b>
</a>
After the ALL button comes a foreach loop to iterate through the VehcileType names using category as the variable name in the loop and an anchor tag helper to lay out the dynamic category button names from the repository. The dynamic anchor tag helpers are almost identical to the more static ALL button layed out in the same file except the category route value uses the category value from the forloop’s current iteration and in the ternary operator’s decision on which Bootstrap classes to add compares ViewBag.CurrentCategory to the current iteration’s category value rather than null. Also the text value of the anchor tag helper uses the string.ToUpper()
method to show the text of the dynamic button in all upper case.
@foreach (string category in Model ??
Enumerable.Empty<string>())
{
<a asp-controller="Home"
asp-action="Index"
asp-route-category="@category"
asp-route-pageNumber="1"
class="btn @(ViewBag.CurrentCategory == category
? "btn-primary"
: "btn-outline-primary button")">
<b>@category.ToUpper()</b>
</a>
If you restart and run the application it looks and behaves exactly as before. We have just refactored and migrated the category button’s html, C#, and razor code to a reusable and testable View Component.
Unit Test the Categories View Component
To complete this module we need to unit test the Categories view component. We need to test that it can return a list of categories and that it can pass down the currently selected category.
On the root of the FredsCars.Tests project, create a new folder called Components and in the new Components folder create a class named CategoriesComponentTests.cs
Unit Test the view component can return categories
To test that the categories view component can correctly return categories from a repository, create a test called Can_Return_Categories
. Modify the CategoriesComponentTests class with the code below.
FredsCars.Tests\Components\CategoriesComponentTests.cs
using FredsCars.Components;
using FredsCars.Models;
using FredsCars.Models.Repositories;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using MockQueryable;
using Moq;
namespace FredsCars.Tests.Components
{
public class CategoriesComponentTests
{
[Fact]
public async Task Can_Return_Categories()
{
// Arrange
// 1 - create a List<T> with test items
var vehicleTypes = new List<VehicleType>
{
new VehicleType
{
Id = 1,
Name = "Cat1"
},
new VehicleType
{
Id = 1,
Name = "Cat2"
},
new VehicleType
{
Id = 1,
Name = "Cat3"
}
};
// 2 - build mock using MockQueryable.Moq extension
var mockVehicleTypesIQueryable = vehicleTypes.BuildMock();
// 3 - build mock IVehicleRepository
Mock<IVehicleTypeRepository> mockVehicleTypeRepo =
new Mock<IVehicleTypeRepository>();
mockVehicleTypeRepo.Setup(mvtr => mvtr.VehicleTypes).Returns(mockVehicleTypesIQueryable);
CategoriesComponent result = new (mockVehicleTypeRepo.Object);
// Act
string[] results =
((IEnumerable<string>?)(await result.InvokeAsync()
as ViewViewComponentResult)?.ViewData?.Model
?? Enumerable.Empty<string>()).ToArray();
// Assert
Assert.True(Enumerable.SequenceEqual(new string[] { "Cat1", "Cat2", "Cat3" }, results));
}
}
}
In the code above, we created a test called Can_Return_Categories
to test if the Categories view component can indeed return categories from a repository correctly.
The Assert section is the same as in many unit tests we have written up to this point except that rather than instantiate the Home controller and call the Index method, here we create an instance of a CategoriesComponent
object named result. And in the Act section we call its InvokeAsync
method.
We are used to seeing the return View()
statement in a controller return a ViewResult type object.
The return View()
statement in a view component returns an object of type, unfortunately and confusingly named, ViewViewComponentResult
. So we have to convert the result of the view component’s InvokeAsync method to that type in order to get the Model and convert the model into an array.
// Act
string[] results =
((IEnumerable<string>?)(await result.InvokeAsync()
as ViewViewComponentResult)?.ViewData?.Model
?? Enumerable.Empty<string>()).ToArray();
Then we assert that the results contained in the model match the test data; Cat1, Cat2, and Cat3. We use the Enumerable.SequenceEqual
method to check that a string array containing the string values “Cat1”, “Cat2”, and “Cat3” match the results of calling the view component’s InvokeAsync
method. The SequenceEqual
method returns a bool value we feed to an Assert.True
statement as the condition to evaluate as either true or false.
Unit Test the view component can pass down selected category
For the second unit test we need to make sure the Categories view component can pass down to the partial view what the currently selected category is so that the view will be able to highlight the correct button.
Add the Can_Return_SelectedCategory test using the code shown below.
FredsCars.Tests\Components\CategoriesComponentTests.cs
using FredsCars.Components;
using FredsCars.Models;
using FredsCars.Models.Repositories;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Routing;
using MockQueryable;
using Moq;
namespace FredsCars.Tests.Components
{
public class CategoriesComponentTests
{
[Fact]
public async Task Can_Return_Categories()
{
... existing code ...
}
[Fact]
public async Task Can_Return_SelectedCategory()
{
// Arrange
string selectedCategory = "Trucks";
// 1 - create a List<T> with test items
var vehicleTypes = new List<VehicleType>
{
new VehicleType
{
Id = 1,
Name = "Cars"
},
new VehicleType
{
Id = 1,
Name = "Trucks"
},
new VehicleType
{
Id = 1,
Name = "J"
}
};
// 2 - build mock using MockQueryable.Moq extension
var mockVehicleTypesIQueryable = vehicleTypes.BuildMock();
// 3 - build mock IVehicleRepository
Mock<IVehicleTypeRepository> mockVehicleTypeRepo =
new Mock<IVehicleTypeRepository>();
mockVehicleTypeRepo.Setup(mvtr => mvtr.VehicleTypes).Returns(mockVehicleTypesIQueryable);
CategoriesComponent target = new CategoriesComponent(mockVehicleTypeRepo.Object);
target.ViewComponentContext = new ViewComponentContext
{
ViewContext = new ViewContext
{
RouteData = new RouteData()
}
};
target.RouteData.Values["category"] = selectedCategory;
// Action
string? result =
(string?)(await target.InvokeAsync()
as ViewViewComponentResult)?.ViewData?["CurrentCategory"];
// Assert
Assert.Equal(selectedCategory, result);
}
}
}
In the unit test above, make sure to add the two new namespaces:
// For ViewContext
using Microsoft.AspNetCore.Mvc.Rendering;
// For RouteData
using Microsoft.AspNetCore.Routing;
The Arrange section is once again similar to many tests we have written so far but before the buildup of the test data and the mock repo we create a string variable called selectedCategory and assign it the value “Trucks”.
After the buildup in the Arrange section we instantiate an instance of CategoriesComponent, pass the mocked VehicleType repo to its constructor and assign it to a variable named target.
We then create a view context for the Categories view component so that it can hold the route data we need to get a selected category from the category URL segment. To do this we assign the ViewComponentContext property of the Categories view component a new ViewComponentContext using the new operator and set ViewComponentContext’s ViewContext object property to a new view context using auto property syntax. And finally we instantiate a new RouteData object for the view context.
target.ViewComponentContext = new ViewComponentContext
{
ViewContext = new ViewContext
{
RouteData = new RouteData()
}
};
The last thing we do in the Arrange section is set a value in the RouteData.Values
data dictionary with the key “category” to the selectedCategory variable with a value of “Trucks”. This will simulate grabbing the “Trucks” string from a URL.
target.RouteData.Values["category"] = selectedCategory;
In the Action section we call the InvokeAsync method of the view component, convert the result to a ViewViewComponent result, and grab the CurrentCategory dynamic property of the ViewBag object which gets set in the Invoke method of view component.
// Action
string? result =
(string?)(await target.InvokeAsync()
as ViewViewComponentResult)?.ViewData?["CurrentCategory"];
In the Assert section we verify that the value of selectedCategory equals the result of the Invoke method call’s ViewData["CurrentCategory"]
value.
Assert.Equal(selectedCategory, result);
What’s Next
In this point in the application we have achieved a significant milestone. We have finished the main page of the application and developed features for filtering vehicles by category, paging, and sorting.
In the next several modules we will complete the CRUD operations (Create, Retrieve, Update, and Delete) by creating Create, Update (or Edit), and Delete pages for the web application. The main page implements the Retrieve operation. And we will create a Details page which also implements Retrieve.