Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Using ASP.NET Core Tag Helpers for Image Layout

DZone's Guide to

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.

· Web Dev Zone ·
Free Resource

Bugsnag monitors application stability, so you can make data-driven decisions on whether you should be building new features, or fixing bugs. Learn more.

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 gives 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 standard ASP.NET MVC Core Tag Helpers to create layouts for our site based on an image's dimensions.

There are two points which make 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!

Monitor application stability with Bugsnag to decide if your engineering team should be building new features on your roadmap or fixing bugs to stabilize your application.Try it free.

Topics:
web dev ,asp.net core ,web application development ,tag helpers

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}