While developing our new web application, we wanted to add a menu component that is dynamically generated based on the current route and parameters.
I initially looked into the concept of partials in ASP.NET Core, and while these are great for reusing static markup, they’re not so great for building dynamic, data-driven content such as a dynamic menu.
Where your requirement is to reuse dynamic and / or data driven content, then the correct design approach is to use a ViewComponent. From the Microsoft documentation
After looking at Partial Views, and View Components, I found that ViewComponents don’t have to depend on data already existing. For example, you can simply make an asynchronous call to a server side method like so:
@await Component.InvokeAsync("MenuItems", 1234)
According to the Microsoft documentation, view components are similar to partial views, but they’re much more powerful. View components don’t necessarily use model binding, and only depend on the data provided when called. A view component:
- Renders a chunk rather than a whole response.
- Includes the same separation-of-concerns and testability benefits found between a controller and view.
- Can have parameters and business logic.
- Is typically invoked from a layout page.
View components are intended anywhere you have reusable rendering logic that’s too complex for a partial view, such as:
- Dynamic navigation menus
- Tag cloud (where it queries the database)
- Login panel
So now, our menu tree structure is handled by a ViewComponent. All the business logic for building a user-specific menu is contained within the ViewComponent, and the ViewComponent returns the menu tree structure. This is then displayed by the Razor Page that calls the ViewComponent. When you call a view component method, you don’t have to pass parameters, and you don’t have to pass a view model. However, with Partials, you need to pass data (a view model), at the time you want to render the Partial view, so you need to have your data ready before hand, making it tightly coupled to your existing view(s).
There are also many other benefits such as:
- Encapsulate the underlying business logic for a Razor Page in a separate component
- Allows the business logic to be unit-tested
- Allow for the UI component to be reused across different forms, essentially acting like an independent view
- Leads to cleaner code with separation of concerns
Here is the View Component itself:
namespace WebApplication1.Controllers
{
public class MenuViewComponent : ViewComponent
{
private readonly MenuHelper _cbaMenuHelper = new MenuHelper();
public async Task<IViewComponentResult> InvokeAsync(int testId)
{
//testId is optional, and can be passed in when you call InvokeAsync
//Parse current URL to determine which menu item to highlight:
string baseUrl = Request.Scheme + "://" + Request.Host.Value;
var httpRequestFeature = Request.HttpContext.Features.Get<IHttpRequestFeature>();
var uri = httpRequestFeature.RawTarget;
var id = HttpContext.Request.Query["id"].ToString(); //Parse Query String
var menuItems = await _cbaMenuHelper.GetAllMenuItems(baseUrl + uri, id); //Can pass in testId here if required.
return View("_MenuPartial", _cbaMenuHelper.GetMenu(menuItems, null));
}
}
}
The ViewComponent calls other methods to actually generate the menu. In this case it’s just a list of Parent -> child mapped menu objects which is included here for your reference:
public class Menu
{
public Guid ID { get; set; }
public Guid? ParentID { get; set; }
public string Content { get; set; }
public string IconClass { get; set; }
public string Url { get; set; }
public string SelectedStyle { get; set; } // If you want a style vs a class;
public string SelectedClass { get; set; }
public string OnClick { get; set; }
public long Order { get; set; }
}
Once you have your ‘hierarchical’ list of Menu items, just return it to your view as shown inside the InvokeAsync()
method. In this case, the view, just recursively displays the menu items according to the parent child relationships within your menu records:
@foreach (var menu in Model)
{
if (menu.Children.Any())
{
<li>
<a href="#" class="tree-menu-header">
<i class="@menu.IconClass"></i><span>@menu.Content</span>
</a>
<ul class="menu vertical nested is-active">
<partial name="Components/Menu/_MenuPartial" model="menu.Children" />
</ul>
</li>
}
else
{
<li class="@menu.SelectedClass">
@{
if (string.IsNullOrEmpty(menu.Url))
{
<a onclick="@menu.OnClick">
<i class="@menu.IconClass"></i><span style="@menu.SelectedStyle">@menu.Content</span>
</a>
}
else
{
<a href="@menu.Url">
<i class="@menu.IconClass"></i><span style="@menu.SelectedStyle;">@menu.Content</span>
</a>
}
}
</li>
}
}
This is returned wrapped inside an instance of IViewComponentResult
, which is one of the supported result types returned from a ViewComponent.
This is what the call to invoke the ViewComponent from the layout page looks like:
@await Component.InvokeAsync("Menu", new { cbaId = ViewBag.Id })
I’ve used the ViewBag
in this case to pass an ID to the ViewComponent, so there is some context of what to display. You can also see within the ViewComponent that I retrieve the current Route, and use it, along with the Id above to determine what menu items to load.
The end result looks like this:
You can see the ordering is correct, and along with using Font Awesome icons, a nice collapsible menu is created. Clicking on the Create New button runs custom Javascript to most likely create something:
The rest of the items are route Url’s, all created from the MenuHelper class. I used the basic ASP.NET core application as a starting point and ‘wrapped’ my menu components around it.
The source code is available for this article at: https://github.com/IntrinsicInnovation/CoreMenuBuilder
I created the entire dynamic menuing system very quickly. And, it’s essentially independent of other views and partial views within your system. This can be reused simply by cutting and pasting all of my code into your application. This is a real world example that solves very real problems with modern application development. Having independent easily testable modules like this ensures your applications are far less likely to cause issues in a production environment.