Skip to content

How to use View Controllers in Umbraco

Published: at 10:03 PM (5 min read)

My guide to how to share HTML markup where the params are not coming from the same Model, so a View Partial did not make as much sense.

Table of contents

Open Table of contents

Background

Having not used Umbraco since before it was moved to dotnet and reading that Partial Macros were being deprecated in version 14, I was at a bit of a loss as to how to define a bit of HTML that I could use in many places now that Macros were not an option (and something my old projects used heavily).

The use case here is that I would like to be able to define a placeholder image on my Root page that is then used anywhere that I have image code. I had tried to use a View Partial and pass in the params as part of the ViewDataDictionary but did not like that I had then lost the ability to base the params on a type.

Prerequisite

For this demo I have an Umbraco 13 app scaffolded out using the templates and the dotnet CLI. I have a root content node type of Home and an Image content type where I have added the AltText field as a textbox.

The C# Class

What the docs don’t seem to make clear, and nor do any other blog posts I could find on the topic, is that the class file can be defined anywhere in the project. For me I created a directory in the root of my project called ViewComponents and will be putting them in there.

Scaffolding

I was also having a lot of issues where I did not have a namespace defined in the file. I am using VS Code as I use it for all my other development, so I would recommend looking for an extension that will scaffold out the class for you (I used kreativ-software.csharpextensions).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Nix.ViewComponents.Image
{
    public class ImageViewComponent
    {
        public async Task<IViewComponentResult> InvokeAsync()
        {
            return View();
        }
    }
}

There are a few different ways to get the class to be picked up, but I went for suffixing my class name with ViewComponent. It will need to implement the ViewComponent interface and define either a Invoke or InvokeAsync method.

using Microsoft.AspNetCore.Mvc;

namespace Nix.ViewComponents.Image
{
    public class ImageViewComponent : ViewComponent
    {
        public async Task<IViewComponentResult> InvokeAsync()
        {
            return View();
        }
    }
}

To get data from Umbraco we need to use the UmbracoHelper package. We can inject this in the constructor.

using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Web.Common;

namespace Nix.ViewComponents.Image
{
    public class ImageViewComponent : ViewComponent
    {

        private readonly UmbracoHelper _umbracoHelper;
        public ImageViewComponent(UmbracoHelper umbracoHelper)
        {
            _umbracoHelper = umbracoHelper;
        }
        public async Task<IViewComponentResult> InvokeAsync()
        {
            return View();
        }

    }
}

Getting the Props in

Now we have the base structure in, we can get into our use case, if an image is not set, get the selected one from the root content page.

Firstly, I am going to pass in the properties I want from an image to the InvokeAsync method. To do this I am going to create subclasses with the props Url, AltText, CssClasses and CropName. As everything but the CropName will need to be passed into the View, I am going to inherit from the Type that I will send to the view to add the CropName.

using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Web.Common;

namespace Nix.ViewComponents.Image
{
    public class ImageViewComponent : ViewComponent
    {

        private readonly UmbracoHelper _umbracoHelper;
        public ImageViewComponent(UmbracoHelper umbracoHelper)
        {
            _umbracoHelper = umbracoHelper;
        }
        public async Task<IViewComponentResult> InvokeAsync(ImageViewComponentModelDto props)
        {
            var url = props.Url;
            var altText = props.AltText;
            return View(new ImageViewComponentModel() { Url = url, AltText = altText, CssClasses = props.CssClasses });
        }

    }

    public class ImageViewComponentModel
    {
        public required String Url { get; set; }
        public required String AltText { get; set; }
        public required String CssClasses { get; set; }
    }

    public class ImageViewComponentModelDto : ImageViewComponentModel
    {
        public required String CropName { get; set; }
    }
}

Now we are in a position to get the root content node and get the Media Picker defined with an alias of placeholderImage, then check if the props passed in are valid, and if not use the placeholder.

// Removed for brevity
        public async Task<IViewComponentResult> InvokeAsync(ImageViewComponentModelDto props)
        {
            var home = _umbracoHelper.ContentAtRoot().FirstOrDefault() as Home;
            var img = home?.PlaceholderImage;
            var content = img?.Content as Image
            var url = String.IsNullOrEmpty(props.Url) ? img.GetCropUrl(props.CropName) : props.Url;
            var altText = String.IsNullOrEmpty(props.AltText) ? content?.AltText : props.AltText;
            return View(new ImageViewComponentModel() { Url = url, AltText = altText, CssClasses = props.CssClasses });
        }
// Removed for brevity

We need the home variable to be able to cast it to use strong typing to get the placeholderImage, then we need to drill into this to get the cropUrl and the altText, that both come from different levels. If the placeholder image is needed then we are using the same crop value to make sure it matches and the css classes are passed straight through.

The View

This needs to live under a specific directory with a specific filename, I have placed my .cshtml file called Default, in /Views/Components/Image (again look up the Microsoft docs for the options).

All the logic has been moved up to the Class so the file is only 2 lines long.

<!-- /Views/Components/Image/Default.cshtml -->
@model Nix.ViewComponents.Image.ImageViewComponentModel

<img src="@Model.Url" alt="@Model.AltText" class="@Model.CssClasses">

Using it in other Views

Now we have a reusable component, lets use it to either show the selected image, or the placeholder, for a bootstrap card header image.

@using Nix.ViewComponents.Image
@{
  var crop = "authorCard";
}

@await Component.InvokeAsync("Image", new ImageViewComponentModel()
{
  Url = author.CardIamge.GetCropUrl(crop),
  AltText = author.CardIamge.Content.AltText,
  CropName = crop,
  CssClasses = "card-image-top"
})