Strongly Typed Icon Fonts in ASP.NET MVC

One of my resolutions for 2014 is to blog more. I tend to get on a roll where I’ll write three or four articles only to eventually get distracted by my day job and forget about the blog until the next ice age. Hopefully it’ll stick this time, but we’ll see.

With that, I’d like to write about a technique for working with icon fonts, which have been steadily gaining in popularity. I love icon fonts. They allow me to package up a whole bunch of simple glyphs and pictograms, use them on my site or application without too much fuss on nearly every browser, and let me control presentation attributes such as color, size, etc. I especially like the recent trend of web-based tools for building custom icon fonts from an available library of glyphs (I tend to use Fontastic, but I’ve also had good luck with IcoMoon and FlatIcon).

Most icon fonts, including those from the services mentioned above, provide a CSS file with styles you can use to insert specific icons from the font using a friendly name. Most of these style sheets either use an HTML5 data- declaration and/or a CSS class with a :before selector to insert the requested character in the appropriate font face before the target element. For example, the top of the CSS file I get with the font I use from Fontastic looks like:

@charset "UTF-8";

@font-face {
  font-family: "back-office";
  src:url("fonts/back-office.eot");
  src:url("fonts/back-office.eot?#iefix") format("embedded-opentype"),
    url("fonts/back-office.woff") format("woff"),
    url("fonts/back-office.ttf") format("truetype"),
    url("fonts/back-office.svg#back-office") format("svg");
  font-weight: normal;
  font-style: normal;
}

[data-icon]:before {
  font-family: "back-office" !important;
  content: attr(data-icon);
  font-style: normal !important;
  font-weight: normal !important;
  font-variant: normal !important;
  text-transform: none !important;
  speak: none;
  line-height: 1;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

[class^="icon-"]:before,
[class*=" icon-"]:before {
  font-family: "back-office" !important;
  font-style: normal !important;
  font-weight: normal !important;
  font-variant: normal !important;
  text-transform: none !important;
  speak: none;
  line-height: 1;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.icon-address-book:before {
  content: "a";
}
.icon-alert:before {
  content: "b";
}

Ignore the @font-face declaration and the two blocks after it (the first sets up common styles when using a data-icon attribute and the second sets up common styles for icon- classes). The important thing is the last part of the file that declares the CSS classes “icon-address-book” and “icon-alert”. You typically use these in your HTML (or Razor) as:

<span>
    <i class="icon-alert"></i> Oh, no! There's an alert icon preceding this text!
</span>

This requires that you use the name of the CSS class directly in your view. That is a “magic string”, and I hate magic strings. What if you refactor the icon font to remove a particular icon (or worse yet, one of your team members commits a new icon font without particular icons – it’s happened to me)? What if you want to automatically find all of the uses of a particular icon? What if you want to pass around or store an icon without resorting to strings? All of these can be solved by using something other than a string literal to represent a particular icon.

The first step is figuring out what we’re going to use instead. I like enums for this purpose because they’re strongly-typed and not interchangeable. But there’s one small problem: if I want to refer to my icons with a friendly name, I still have to store the CSS class name somewhere. Unfortunately in C# enums can’t have string values. A workaround is to store the CSS class names as a System.ComponentModel.DescriptionAttribute. Then we can just use the following extension method to get the value of the DescriptionAttribute whenever we need it (just put in any static class):

public static string GetDescription(this Enum value)
{
    FieldInfo fieldInfo = value.GetType().GetField(value.ToString());
    DescriptionAttribute description = fieldInfo.GetCustomAttribute(false);
    return description == null ? value.ToString() : description.Description;
}

I use this technique a lot outside this particular font icon problem and it works very well for attaching string values to an enum member. This lets us create an enum type that looks like this:

public enum Icon
{
    [Description("icon-address-book")]
    AddressBook,
    [Description("icon-alert")]
    Alert
}

Then, if we create a couple of HTML helpers (these can just go in any static class):

public static MvcHtmlString Icon(this HtmlHelper helper, Icon icon)
{
    return MvcHtmlString.Create(icon.Html());
}

// Not really an HTML helper, but included here anyway
public static string Html(this Icon icon)
{
    if (icon == Util.Icon.None) return string.Empty;
    return string.Format("<i class='{0}'></i> ", icon.GetDescription());
}

We can write the following in our view (this assumes Razor, but the technique should work in any view engine that exposes the HtmlHelper class and supports extension methods):

<span>
    @Html.Icon(Icon.Alert) Oh, no! There's an alert icon preceding this text!
</span>

This successfully eliminated our magic string. But we still have to create the Icon enum and manually match it to the CSS from the icon font. Luckily, there is an easy way to create the enum automatically using T4 templates. Generally I like to limit the number of T4 templates I have in my projects. They eat up time in the build cycle and can get out of sync with the current build causing some hard to diagnose and often frustrating bugs. But in cases like this, T4 is perfect. Just drop the following into your project and name it “Icons.tt”.

<#@ template language="C#" hostSpecific="true" #>
<#@ assembly name="System.Core" #> 
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<# Process(); #>
<#+
    readonly Regex regex = new Regex(@"^\.icon-(.*)\:before \{$", RegexOptions.Compiled | RegexOptions.Multiline);

    public void Process()
    {
        WriteLine("using System.ComponentModel;");
        WriteLine("");
        WriteLine("namespace Sipc.BackOffice.Util");
        WriteLine("{");
        WriteLine("\tpublic enum Icon");
        WriteLine("\t{");
        string css = System.IO.File.ReadAllText(Host.ResolvePath("..\\Content\\icons\\styles.css"));
        foreach (Match match in regex.Matches(css))
        {
            WriteLine("\t\t[Description(\"icon-" + match.Groups[1].Value + "\")]");
            WriteLine("\t\t" + String.Join(null, match.Groups[1].Value.Split(new char[]{'-'}, StringSplitOptions.RemoveEmptyEntries)
                .Select(x => (char.IsDigit(x[0]) ? ("_" + x[0]) : char.ToUpper(x[0]).ToString()) + x.Substring(1))) + ",");            
        }
        WriteLine("\t\t[Description(\"\")]");
        WriteLine("\t\tNone");
        WriteLine("\t}");
        WriteLine("}");
    }
#>

You’ll need to adjust the path to the CSS file so it matches your project. You may also need to tweak the regular expression and/or the string processing (which basically just tries to strip the “icon-“ part of the CSS class, remove the hyphens, and title case the rest). However, this should give you enough to start with. Once it’s in the project and you rebuild, you’ll get a C# source file with an Icon enum that has values and corresponding DescriptionAttributes for every class in the CSS. Then when you go to update or change your icon font, just drop the new CSS file on top of the old one and rerun the T4 template (or rebuild the project).

Advertisements