Using ASP.NET Core Tag Helpers for Image Layout

Different image sizes in HTML can be a pain in your layouts. In today's post, we create an ASP.NET Tag Helper to display multiple layouts based on an image's size.

Written by Jonathan "JD" Danylko • Last Updated: • MVC •
Woman sitting down looking at multiple works of art on a wall.

User-generated content is always tough to gauge when setting up HTML layouts especially when working with remote images.

If an image is a set size, the layout is simple, but what if it's a different size every time? We want our layout to always look good.

If you want to create a layout based on an image's size, there isn't a way to extract the dimensions from a remote image.

Or is there?

Today, we'll write a custom Tag Helper in ASP.NET Core and show how to display different layouts based on an image's dimensions.

What are Tag Helpers?

In the early days of ASP.NET MVC Tag Helpers, they were Html Helpers.

We've even created Html Helpers on the blog like creating visual alerts or a generic Html Helper for creating an update link in a grid.

Tag Helpers in ASP.NET MVC Core are a little different. They act like standard HTML elements, but can transform into full-blown layouts.

Along with our Html Helpers, we've also created Tag Helpers in the past to create a scheduled link and build smart links.

The power of Tag Helpers give your content builders a shot in the arm with their layouts. When your users add these custom Tag Helper elements to their content, the server-side runs the tag helper's code and generates something different.

Remote Image Dimensions

This still doesn't get us past our immediate problem of determining a remote image's dimensions.

After looking around, I found a Stack Overflow post explaining this exact problem.

It seems reddit already figured out this type of pre-loading images for their layouts.

I've included these ImageUtilities in this project and I consider them to be the secret sauce for getting our remote image's size without downloading the entire image.

Building our ImageLayout Tag Helper

With that obstacle out of the way, we can turn our attention to our Tag Helper.

For starters, we need an element. We'll start with an image-layout tag.

[HtmlTargetElement("image-layout")] 
public class ImageLayout : TagHelper

The HtmlTargetElement attribute explains what the tag looks like. For our ImageLayout class, we'll inherit from the TagHelper class.

Next, we need a way to pass in our image's URL. Since the img element has a src attribute, we might as well copy that into our design as well.

[HtmlAttributeName("src")]
public string ImageUrl { get; set; }

So anytime we have the attribute src, it will populate the value in ImageUrl.

Using placeholder.com, our HTML will look like this:

<image-layout src="http://via.placeholder.com/50x50" />

We're Partially There

When working with Html Helpers, we need a ViewContext to load partials. It's the same with Tag Helpers.

Back in 2016 when I created an A/B test Tag Helper, I learned about ASP.NET Core and how dependency injection is automatically available right out of the box.

[ViewContext]
[HtmlAttributeNotBound]
public ViewContext ViewContext { get; set; }

Place this inside your Tag Helper class and you automatically get an instance of a ViewContext through property injection.

However, we also need an HtmlHelper instance to load our partials.

This is achieved through constructor injection.

private readonly IHtmlHelper _html;

public
 ImageLayout(IHtmlHelper helper) {     _html = helper; }

This gives us our HtmlHelper instance.

Now we can process our ImageLayout.

Building the Content

Our ProcessAsync method contains the bulk of our ImageLayout. We override the base method of TagHelper to achieve this.

public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
    // Contextualize the ViewContext
    ((IViewContextAware) _html).Contextualize(ViewContext);

    // Make sure we don't have any tags associated with this TagHelper.     output.TagName = String.Empty;
    // DI'd into the constructor     IHtmlContent content = null;
    // If we don't have an image, return the noImage.cshtml     if (String.IsNullOrEmpty(ImageUrl))     {         content = await _html.PartialAsync("noimage");     }     else     {         var uri = new Uri(ImageUrl);         var imageSize = ImageUtilities.GetWebDimensions(uri);
        // only 250px          if (imageSize.Width <= 300 && imageSize.Height <= 300)         {             content = await _html.PartialAsync("smallimage", uri);         }          // Image larger than 700px         else if (imageSize.Width >= 700)         {             content = await _html.PartialAsync("largeimage", uri);         }     }

    output.Content.SetHtmlContent(content); }

Next, we need to Contextualize our ViewContext, reset the tagname, and initialize the HtmlContent.

If our ImageUrl is null or empty, we load the noimage.cshtml. If we do have an image, we use our ImageUtilities to determine the size of our image without loading it.

Once we get dimensions, we determine which partial view to render.

For the partials, notice how we pass in the Uri as our model? The image Uri is passed to our partial to display in our HTML.

For example, our smallimage.cshtml will look like this:

@model Uri

<
div class="alert alert-success">     <div class="row">         <div class="col-md-1">             <img src="@Model.AbsoluteUri" alt="Small Image" title="Small Image" />         </div>         <div class="col-md-11">             <h4>Small image Layout</h4>         </div>     </div> </div>

and our largeimage.cshtml looks like this:

@model Uri

<
div class="alert alert-info">     <h4>Large Image Layout</h4>     <img src="@Model.AbsoluteUri" alt="Large Image" title="Large Image" /> </div>

Putting it all Together

With everything we discussed, our ImageLayout Tag Helper is finished.

using System;
using System.Threading.Tasks;
using ImageTagHelperExample.Utilities;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace ImageTagHelperExample.TagHelpers
{
    [HtmlTargetElement("image-layout")] 
    public class ImageLayout : TagHelper
    {
        [HtmlAttributeName("src")]
        public string ImageUrl { get; set; }

        [ViewContext]         [HtmlAttributeNotBound]         public ViewContext ViewContext { get; set; }
        private readonly IHtmlHelper _html;
        public ImageLayout(IHtmlHelper helper)         {             _html = helper;         }
        public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)         {             // Contextualize the ViewContext             ((IViewContextAware) _html).Contextualize(ViewContext);
            // Make sure we don't have any tags associated with this TagHelper.             output.TagName = String.Empty;
            // DI'd into the constructor             IHtmlContent content = null;
            // If we don't have an image, return the noImage.cshtml             if (String.IsNullOrEmpty(ImageUrl))             {                 content = await _html.PartialAsync("noimage");             }             else             {                 var uri = new Uri(ImageUrl);                 var imageSize = ImageUtilities.GetWebDimensions(uri);
                // only 250px                  if (imageSize.Width <= 300 && imageSize.Height <= 300)                 {                     content = await _html.PartialAsync("smallimage", uri);                 }                  // Image larger than 700px                 else if (imageSize.Width >= 700)                 {                     content = await _html.PartialAsync("largeimage", uri);                 }             }
            output.Content.SetHtmlContent(content);         }     } }

In our Index.cshtml, we need to define that we're using our new ImageLayout TagHelper. This is defined at the top by using the addTagHelper directives.

@model ImageViewModel
@addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers"
@addTagHelper "*, ImageTagHelperExample"

The code is available at the GitHub repository.

Conclusion

We've covered how to use a standard ASP.NET MVC Core Tag Helpers to create layouts for our site based on an image's dimensions.

There are two points which makes this a slick technique:

  1. You can get the dimensions of an image without loading the entire image.
  2. You can change the layout of each image by changing the Partial HTML. No compiling is required unless you have other image sizes with partial views. Instead of thinking programmatically, think declaratively.

This technique can be expanded upon for greater flexibility and maintainability.

Think of your TagHelpers/HtmlHelpers as conduits pointing to other partial views and all you have to do is modify HTML instead of recompiling C# code every time there's a change.

Have you extended TagHelpers to build out entire components from one single tag? What's your favorite Tag Helper you've built so far? Post your comments below and let's discuss!

ASP.NET 8 Best Practices on Amazon

ASP.NET 8 Best Practices by Jonathan Danylko


Reviewed as a "comprehensive guide" and a "roadmap to excellence" with over 120 Best Practices for ASP.NET Core 8, Jonathan's first book by Packt Publishing explores proven techniques for every phase of the SDLC.

Learn industry-standard concepts to improve your coding, debugging, and deployment of ASP.NET Core websites.

Order now on Amazon.com button

Picture of Jonathan "JD" Danylko

Jonathan "JD" Danylko is an author, web architect, and entrepreneur who's been programming for over 30 years. He's developed websites for small, medium, and Fortune 500 companies since 1996.

He currently works at Insight Enterprises as an Architect.

When asked what he likes to do in his spare time, he replies, "I like to write and I like to code. I also like to write about code."

comments powered by Disqus