Passing a ViewModel by Attribute to ASP.NET Core ViewComponents

Written on December 28, 2018

ASP.NET Core provides the concept of View components and the intro from the official docs reads quite straight forward:

View components are similar to partial views, but they're much more powerful. View components don't use model binding, and only depend on the data provided when calling into it. This article was written using ASP.NET Core MVC, but view components also work with Razor Pages.

Yet, it was a bit of confusing when trying to use View components with Tag Helpers and passing fragments of the parent view model to the View Component by attributes.

First, lets take a look at what we're building:

As you can see, there are two "sections" named "Books" and "Services" and both have child elements.

In order keep some structure and separate concerns, we don't want to render everything in the main view. The structure, we want to establish, is this:

Index-View of HomeController
 - PageSection ("Books")
  - SectionItems (List<SectionItem>)
    - SectionItem (Title "Kubernetes")

Our complete ViewModel class structure is this - being loaded from a database or a headless CMS:

public class IndexViewModel
{
    public List<PageSection> Sections { get; set; }
}

public class PageSection
{
    public string Title { get; set; }
    public List<SectionItem> SectionItems { get; set; }
}

public class SectionItem
{
    public string Text { get; set; }
}

For the sake of simplicity, I'll just create it inline in HomeController.cs:

public class HomeController : Controller
{
    public IActionResult Index()
    {
        var indexViewModel = new IndexViewModel
        {
            Sections = new List<PageSection>
            {
                new PageSection
                {
                    Title = "Books",
                    SectionItems = new List<SectionItem>
                    {
                        new SectionItem { Text = "ASP.NET Core in Action" },
                        new SectionItem { Text = "Node.js in Action"}
                    }
                },
                new PageSection
                {
                    Title = "Services",
                    SectionItems = new List<SectionItem>
                    {
                        new SectionItem { Text = "ASP.NET Core Development"},
                        new SectionItem { Text = "Node.js Development"},
                        new SectionItem { Text = "Kubernetes"}
                    }
                }
            }
        };
        return View(indexViewModel);
    }
}

In the end, our Index.cshtml for the HomeController will be this:

@model IndexViewModel
@addTagHelper *, ViewComponentsSample

@foreach (var pageSection in Model.Sections)
{
    <vc:page-section section="@pageSection"></vc:page-section>
}

The Tag Helper to invoke a view component uses the <vc></vc> element and it added by the addTagHelper directive.

Also, Pascal-cased class and method parameters for Tag Helpers are translated into their lower kebab case - this will be important very soon.

The first View component we'll implement, obviously is the PageSection View component (remember Tag Helpers and the kebab casing convention? 😉)

The path of the Components HTML is ~/Views/Shared/Components/PageSection/Default.cshtml and this is the markup:

@model PageSection
@addTagHelper *, ViewComponentsSample

<h3>@Model.Title</h3>
<vc:section-items section-items="@Model.SectionItems"></vc:section-items>

This component also uses the @addTagHelper directive and it defines a ViewModel of type PageSection which has been a fragment of the IndexViewModel from above.

Besides rendering the Section title it also calls another View component and passes it's own SectionItems (kebab-casing again) to the child View component.

The code behind for these two View components is inside the ~/ViewComponents directory and their code looks like this:

PageSection.cs:

public class PageSection : ViewComponent
{
    public IViewComponentResult Invoke(Models.PageSection section)
    {
        return View("~/Views/Shared/Components/PageSection/Default.cshtml", section);
    }
}

SectionItems.cs:

public class SectionItems : ViewComponent
{
    public IViewComponentResult Invoke(List<Models.SectionItem> sectionItems)
    {
        return View("~/Views/Shared/Components/SectionItems/Default.cshtml", sectionItems);
    }
}

There are some similarities both classes share:

  • They derive from the ViewComponent base class
  • They implement the Invoke method which returns a IViewComponentResult
  • They pass the model to the Views cshtml Template

And now we have some conventions ("magic" 🤙) to understand here to get things working:

  • The Invoke method is called by thevc TagHelper
  • The name (and of course the type) of the parameter(s) passed as Tag Helper attributes have to match the name and type of the Invoke method - and kebab casing is your friend again.

You can nest View components at arbitrary levels, so I added another one to finally list the SectionItems.

This is it's template:

@model List<ViewComponentsSample.Models.SectionItem>
@addTagHelper *, ViewComponentsSample
<ul>
    @foreach (var item in Model)
    {
        <vc:section-item text="@item.Text"></vc:section-item>
    }
</ul>

And this is the code behind:

@model string
<li>@Model</li>

Once you get it, it's pretty cool 😎

The sample can be found on GitHub.