Features for AdminLTE with ASP.NET Core

AdminLTE Dasboard and Control Panel Template

Based on my previous titled “Integrating AdminLTE with ASP.NET Core“, I want to add new cool common features for AdminLTE with ASP.NET Core. The source code of this template is on GitHub.

Adding Navigation

So, let’s add some navigation menu to our _MainNavigation.cshtml. What we need to do is simple. Remove the static paths and add add links to our controller methods. For now, we will add two navigation items:

  • Home will be linked to Home/Index
  • Privacy page will be linked to Home/Privacy
<aside class="main-sidebar sidebar-dark-primary elevation-4">
    <a href="~/Home" class="brand-link">
        <img src="~/img/AdminLTELogo.png" alt="AdminLTE Logo" 
             class="brand-image img-circle elevation-3"
             style="opacity: .8">
        <span class="brand-text font-weight-light">AdminLTE 3</span>
    </a>
    <div class="sidebar">
        <div class="user-panel mt-3 pb-3 mb-3 d-flex">
            <div class="image">
                <img src="~/img/user2-160x160.jpg" class="img-circle elevation-2" alt="User Image">
            </div>
            <div class="info">
                <a href="#" class="d-block">Enrico Rossini</a>
            </div>
        </div>
        <nav class="mt-2">
            <ul class="nav nav-pills nav-sidebar flex-column" 
                data-widget="treeview" role="menu" data-accordion="false">
                <li class="nav-item ">
                    <a asp-controller="Home" asp-action="Index" class="nav-link">
                        <i class="nav-icon fas fa-home"></i>
                        <p>
                            Home
                        </p>
                    </a>
                </li>
                <li class="nav-item ">
                    <a asp-controller="Home" asp-action="Privacy" class="nav-link">
                        <i class="nav-icon fas fa-lock"></i>
                        <p>
                            Privacy
                        </p>
                    </a>
                </li>
            </ul>
        </nav>
    </div>
</aside>

So, consider the following lines:

  • 3 and 10 – image reference fix.
  • 19 – You can see for the navigation Item named “Home”, we are linking to the controller “Home” and Index actions.
  • 27 – For the Privacy page, we like to the Home Controller and Privacy method.

Run the application and check.

Aside bar changed with Home and Privacy
Aside bar changed with Home and Privacy

So that’s working too. But there is one detail that we are missing. Navigation Indicator. There is no indication in our sidebar about the currently viewed page. For now, since we have just two navigation item, it’s quite fine to check the URL and understand which page we are at right now. But this is not ideal. Let’s fix it first.

Navigation Indicator

For this to work, we need data from our ASP.NET Core regarding the current controller and action method. And based on this we need to change the class of the corresponding navigation item to active. Active means the current page.

Add a new folder in the root of the project. Name it Helpers. Under it, add a new NavigationIndicatorHelper class.

public static class NavigationIndicatorHelper
{
    public static string MakeActiveClass(this IUrlHelper urlHelper, string controller,string action)
    {
        try
        {
            string result = "active";
            string controllerName = urlHelper.ActionContext.RouteData.Values["controller"].ToString();
            string methodName = urlHelper.ActionContext.RouteData.Values["action"].ToString();
            if (string.IsNullOrEmpty(controllerName)) return null;
            if (controllerName.Equals(controller, StringComparison.OrdinalIgnoreCase))
            {
                if (methodName.Equals(action, StringComparison.OrdinalIgnoreCase))
                {
                    return result;
                }
            }
            return null;
        }
        catch (Exception)
        {
            return null;
        }
    }
}

Here is what this helper class does. It has an extension method with the URLHelper. By this way, you can invoke it in the cshtml page too. It takes in the controller and the action method name and checks it with the current route data. If they match, we return a string “active”, else null.

Go to _MainNavigation.cshtml. Modify / Add these lines

@using static AdminLTE.MVC.Helpers.NavigationIndicatorHelper;
.
.
<a asp-controller="Home" asp-action="Index" 
   class="nav-link @Url.MakeActiveClass("home","index")">
.
.
<a asp-controller="Home" asp-action="Privacy" 
   class="nav-link @Url.MakeActiveClass("home","privacy")">

Run the application. Now, we have a working navigation indicator.

New home page with navigation indicator
New home page with navigation indicator

Breadcrumbs

A breadcrumbs is a type of secondary navigation scheme that reveals the user’s location in a website or Web application. The term comes from the Hansel and Gretel fairy tale in which the two title children drop breadcrumbs to form a trail back to their home. Just like in the tale, breadcrumbs in real-world applications offer users a way to trace the path back to their original landing point.

Breadcrumbs in AdminLTE with ASP.NET Core
Breadcrumbs in AdminLTE with ASP.NET Core

I want a system for maximum flexibility to create custom breadcrumbs also not strictly related to the url. For example, I want to create my breadcrumbs for every action in every controller with the possibility to add words and links and give the user a nice navigation.

My implementation has two parts:

  • an attribute to add on top of actions
  • a function to generate breadcrumbs for the UI

Attribute

Attributes provide a way of associating information with code in a declarative way. They can also provide a reusable element that can be applied to a variety of targets.

Consider the [Obsolete] attribute. It can be applied to classes, structs, methods, constructors, and more. It declares that the element is obsolete. It’s then up to the C# compiler to look for this attribute, and do some action in response. If you want to know more about Attributes, there is a tutorial in the Microsoft documentation.

Also, an important feature to consider is multilanguage. Often, I have to create resources .RESX for different languages and then I want the ability to use the translate words from the resources in the breadcrumbs.

[Breadcrumb("YourString")]
[Breadcrumb(typeof(YourResourcesRESX), "ResourceKey")]
[Breadcrumb(typeof(YourResourcesRESX), "ResourceKey", "Controller", "Action")]
[Breadcrumb(typeof(YourResourcesRESX), "ResourceKey", "Controller", "Action", true)]

The functions I’m going to include are:

  • embedded text (line 1): add a new breadcrumbs with the specified string
  • text from resources (line 2): the text from this piece of breadcrumbs comes from resources. For that, I want to specified the resources’s typeof and the key
  • text from resources and link to an action (line 3): in this case, I want to read the text from the resource and add a link to a specific action in a controller
  • text from resources and link to an action with parameters (line 4): in this case, I want to read the text from the resource and add a link to a specific action in a controller adding the url parameters. This is very useful too

For that, the implementation of the attribute is in the following code. First, I have to create a new ResourceHelper to read a specific key in a resource file.

public class ResourceHelper
{
    public static string GetResourceLookup(Type resourceType, string resourceName)
    {
        if ((resourceType != null) && (resourceName != null))
        {
            PropertyInfo property = resourceType.GetProperty(resourceName, 
                         BindingFlags.Public | BindingFlags.Static);
            if (property == null)
            {
                return string.Empty;
            }
            if (property.PropertyType != typeof(string))
            {
                return string.Empty;
            }

            return (string)property.GetValue(null, null);
        }
        return null;
    }
}

Then, I can implement the attribute as the following code to read the breadcrumb configuration.

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

namespace AdminLTEWithASPNETCore.Attributes
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | 
     AttributeTargets.Assembly, AllowMultiple = true)]
    public class BreadcrumbAttribute : Attribute
    {
        public BreadcrumbAttribute(string label, 
                                   string controller = "", 
                                   string action = "", 
                                   bool passArgs = false)
        {
            Label = label;
            ControllerName = controller;
            ActionName = action;
            PassArguments = passArgs;
        }

        public BreadcrumbAttribute(Type resourceType, 
                                   string resourceName, 
                                   string controller = "", 
                                   string action = "", 
                                   bool passArgs = false)
        {
            Label = ResourceHelper.GetResourceLookup(resourceType, resourceName);
            ControllerName = controller;
            ActionName = action;
            PassArguments = passArgs;
        }

        public string Label { get; }
        public string ControllerName { get; }
        public string ActionName { get; }
        public bool PassArguments { get; }
    }
}

Render the Breadcrumbs for the UI

Now that we added the path for the breadcrumbs we want to create, we have to implement a function for the UI to show the breadcrumbs with the style to match AdminLTE.

First, I’m going to create a model to save the label to display and the link: I’m calling this model BreadcrumbItem.

public class BreadcrumbItem
{
    public string URL { get; set; }
    public string Label { get; set; }
}

Now, I’m going to create an extension for IUrlHelper to generate a fully qualified URL to an action method by using the specified action name, controller name and route values.

public static class UrlExtensions
{
    public static string AbsoluteAction(
        this IUrlHelper url,
        string actionName,
        string controllerName,
        object routeValues = null)
    {
        if (url != null)
            return url.Action(actionName, controllerName, routeValues,                    
                       url.ActionContext.HttpContext.Request.Scheme);
        else
            return string.Empty;
    }
}

Next steps are to create some function to display the breadcrumbs based on the settings from the attribute BreadcrumbAttribute saved in BreadcrumbItem.

The first simple implementation is creating a function with based data.

private static string BuildBreadcrumb(BreadcrumbItem bs)
{
    string link = "<li class='breadcrumb-item'>";

    if (!string.IsNullOrEmpty(bs.URL))
        link += $"<a href='{bs.URL}'>{bs.Label}</a>";
    else
        link += bs.Label;

    link += "</li>";

    return link;
}

Easy. Now, a bit complicate function based on BreadcrumbAttribute, IUrlHelper and parameters from the query url.

private static string BuildBreadcrumb(BreadcrumbAttribute bs,
                                      IUrlHelper urlHelper,
                                      string parameters)
{
    string link = "<li class='breadcrumb-item'>";

    if (!string.IsNullOrEmpty(bs.ActionName) && !string.IsNullOrEmpty(bs.ControllerName))
        link += $"<a href='{urlHelper.AbsoluteAction(bs.ActionName, bs.ControllerName)} " +
                $"{(bs.PassArguments ? parameters : string.Empty)}'>{bs.Label}</a>";
    else
        link += bs.Label;

    link += "</li>";

    return link;
}

The last function is definitely the most complicated one. In this function, I read the current controller, action and query url and verify is that there is a controller and action and I create for that a url, invoking IUrlHelperFactory.

The first breadcrumb is always the home page in HomeController and Index action. Then, based on the attributes BreadcrumbAttribute and the List<BreadcrumbItem> I’m creating the final breadcrumbs. The breadcrumbs are formed by ol html tag.

public static IHtmlContent BuildBreadcrumbNavigation(this IHtmlHelper helper, 
                                                List<BreadcrumbItem> breadcrumbExtras)
{
    IHtmlContentBuilder rtn = null;

    string controllerName = helper.ViewContext.RouteData.Values["controller"].ToString();
    string actionName = helper.ViewContext.RouteData.Values["action"].ToString();
    string query = helper.ViewContext.HttpContext.Request.QueryString.Value;

    var type = typeof(Microsoft.AspNetCore.Mvc.Controller);
    var controllersTypes = AppDomain.CurrentDomain.GetAssemblies()
        .SelectMany(s => s.GetTypes())
        .Where(p => p.IsSubclassOf(type))
        .ToList();

    var urlHelperFactory = helper.ViewContext.HttpContext
               .RequestServices.GetRequiredService<IUrlHelperFactory>();
    var urlHelper = urlHelperFactory.GetUrlHelper(helper.ViewContext);
    var homeLink = urlHelper.Action("Index", "Home");
    var breadcrumb = new HtmlContentBuilder()
               .AppendHtml("<ol class='breadcrumb float-sm-right'><li class='breadcrumb-item'>")
               .AppendHtml($"<a href='{homeLink}'><i class='fas fa-home'></i></a>")
               .AppendHtml("</li>");

    var controllerType = controllersTypes
                             .FirstOrDefault(x => x.Name == $"{controllerName}Controller");
    if (controllerType != null)
    {
        var breadcrumbControllerAttributes = controllerType
                            .GetCustomAttributes<BreadcrumbAttribute>();
        if (breadcrumbControllerAttributes != null && breadcrumbControllerAttributes.Any())
        {
            foreach (var bs in breadcrumbControllerAttributes)
            {
                breadcrumb.AppendHtml(BuildBreadcrumb(bs, urlHelper, query));
            }
        }

        if (breadcrumbExtras != null && breadcrumbExtras.Any())
        {
            foreach (var be in breadcrumbExtras)
            {
                breadcrumb.AppendHtml(BuildBreadcrumb(be));
            }
        }
        else
        {
            var actionProp = controllerType.GetTypeInfo().GetMethods()
                .FirstOrDefault(x => x.Name == actionName && 
                        x.CustomAttributes.Any(y => y.AttributeType == typeof(BreadcrumbAttribute)));
            var breadcrumbMethodsAttributes = actionProp?.GetCustomAttributes<BreadcrumbAttribute>();

            if (breadcrumbMethodsAttributes != null && breadcrumbMethodsAttributes.Any())
            {
                foreach (var bs in breadcrumbMethodsAttributes)
                {
                    breadcrumb.AppendHtml(BuildBreadcrumb(bs, urlHelper, query));
                }
            }
        }
    }
    rtn = breadcrumb.AppendHtml("</ol>");

    return rtn;
}

Display the Breadcrumbs in the UI

Finally, after all of this, we can display the breadcrumbs in the UI. For calling the function, we have to add this line in the _Layout.cshtml.

<div class="col-sm-6"> 
@Html.BuildBreadcrumbNavigation((List<BreadcrumbExtra>)ViewBag.BreadcrumbExtras)
</div>

If you want to see the result, I have published the full source code on GitHub.

Gravatar

Gravatar images may be requested just like a normal image, using an IMG tag. To get an image specific to a user, you must first calculate their email hash. All URLs on Gravatar are based on the use of the hashed value of an email address. Images and profiles are both accessed via the hash of an email, and it is considered the primary way of identifying an identity within the system. Also, you find documentation how to implement Gravatar in your application in the official website.

Trim leading and trailing whitespace from an email address

  • Force all characters to lower-case
  • md5 hash the final string

Features

  • Size
  • Default image (404, mystery-man, identicon, monsterid, wavatar, retro)
  • Custom default image (url)
  • Force default image
  • Ratings
  • Secure requests (HTTPS) is automatic (but can be forced)

Implementation

The GravatarHelpers returns an URL for the image. You can use this url in an IMG tag. I show the simple code to use in the next paragraph.

The enums GravatarRating and GravatarDefaultImage are coming from the Gravatar documentation. GravatarUrl returns the image url for an email.

public static class GravatarHelpers
{
    public static string Gravatar(this HtmlHelper html,
                                  string email,
                                  int? size = null,
                                  GravatarRating rating = GravatarRating.Default,
                                  GravatarDefaultImage defaultImage = 
                                  GravatarDefaultImage.MysteryMan,
                                  object htmlAttributes = null)
    {
        string url = GravatarUrl(email, size, rating, defaultImage);

        var tag = new TagBuilder("img");
        tag.MergeAttributes(new RouteValueDictionary(htmlAttributes));
        tag.Attributes.Add("src", url.ToString());

        if (size != null)
        {
            tag.Attributes.Add("width", size.ToString());
            tag.Attributes.Add("height", size.ToString());
        }

        return tag.ToString();
    }

    public static string GravatarUrl(string email, int? size = null,
                                     GravatarRating rating = GravatarRating.Default,
                                     GravatarDefaultImage defaultImage = 
                                     GravatarDefaultImage.MysteryMan)
    {
        var url = new StringBuilder("//www.gravatar.com/avatar/", 90);
        url.Append(GetEmailHash(email));

        var isFirst = true;
        Action<string, string> addParam = (p, v) =>
        {
            url.Append(isFirst ? '?' : '&');
            isFirst = false;
            url.Append(p);
            url.Append('=');
            url.Append(v);
        };

        if (size != null)
        {
            if (size < 1 || size > 512)
                throw new ArgumentOutOfRangeException("size", size,
                   "Must be null or between 1 and 512, inclusive.");
            addParam("s", size.Value.ToString());
        }

        if (rating != GravatarRating.Default)
            addParam("r", rating.ToString().ToLower());

        if (defaultImage != GravatarDefaultImage.Default)
        {
            if (defaultImage == GravatarDefaultImage.Http404)
                addParam("d", "404");
            else if (defaultImage == GravatarDefaultImage.Identicon)
                addParam("d", "identicon");
            if (defaultImage == GravatarDefaultImage.MonsterId)
                addParam("d", "monsterid");
            if (defaultImage == GravatarDefaultImage.MysteryMan)
                addParam("d", "mm");
            if (defaultImage == GravatarDefaultImage.Wavatar)
                addParam("d", "wavatar");
        }

        return url.ToString();
    }

    private static string GetEmailHash(string email)
    {
        if (email == null)
            return new string('0', 32);

        email = email.Trim().ToLower();

        var emailBytes = Encoding.ASCII.GetBytes(email);
        var hashBytes = new MD5CryptoServiceProvider().ComputeHash(emailBytes);

        var hash = new StringBuilder();
        foreach (var b in hashBytes)
            hash.Append(b.ToString("x2"));
        return hash.ToString();
    }
}

public enum GravatarRating
{
    Default,
    G,
    Pg,
    R,
    X
}

public enum GravatarDefaultImage
{
    Default,
    Http404,
    MysteryMan,
    Identicon,
    MonsterId,
    Wavatar
}

How to use it

<img src="@GravatarHelpers.GravatarUrl(userName)" class="img-circle elevation-2" alt="User Image">

Final result

After all those changes, we have an awesome template project to start with. The look and feel is like the following image. The basic implementation of AdminLTE with ASP.NET Core is in the other post.

Final version of the project template with active links, breadcrumbs and Gravatar
Final version of the project template with active links, breadcrumbs and Gravatar

The complete code is on GitHub. If you have any questions, please use our forum or open a ticket on GitHub.