ASP.NET MVC Tooltip with Web API, Bootstrap Popover, jQuery UI Dialog

Password policy

Introduction

In this article, I will share tips on how to dynamically add a help/tooltip / ScreenTip icon next to form elements such as textbox, label, paragraph, button, etc. A message/tip/text will be displayed when the user clicks on the icon either through the jQuery UI modal or Bootstrap popover, depending on the setting. All the text will be stored in a centralized location and will be retrieved on demand by using a Web API call. The idea was inspired by the following criteria:

  1. Most tooltips will close or disappear after the focus leaves the icon or form element when the user starts to type or after a few seconds. I want a tooltip that will stay open on click so that the user can take their time reading through the message, look up other information if necessary, and then enter the data into the form element. The user can drag or resize the tooltip if using jQuery UI Modal and dismiss the tooltip by pressing the x button.
  2. The tooltip contents should come from a database and be accessed by using Web API. One of my goals is to build an Administrator interface to manage the tooltip metadata in one location instead of going through every single page. Web API is being used so that the common metadata and service can be exposed to other applications in the future.
  3. Easy to integrate. Assuming the Web API is ready and the scripts are being referenced correctly, all you have to do is just add an attribute to the form element, two attributes if you want to use the Bootstrap popover feature.

Initially, the idea was to create the solution using jQuery UI only, but then I decided to add some flavor to it by throwing in the Bootstrap Popover. The caveat of the popover is that the user will not be able to drag or resize the tooltip dialog. Is up to the developer to decide if they want to display both or one of the other tooltip dialog modals.

Shown in Listing 1 and Listing 2 are examples of how to add a tooltip icon using jQuery UI and Bootstrap popover respectively.

Listing 1

<input type="text" class="form-control" data-yourtooltipctrl="lookupKey1" />

Listing 2

<input type="text" class="form-control" data-yourtooltipctrl="lookupKey2" data-yourtooltiptype="1" /> 

Implementation Web API

The Web API in this article is very straightforward and listed in Listing 3. For the sake of simplification, the API return results were hardcoded from a data source. In reality, the result should originate from a data source/repository. In this demo, the Web application will be using the POST method to get the tooltip metadata by key. There is nothing wrong with using the GET method; the goal is to demonstrate how to pass in the AntiForgeryToken and multiple parameters to the API using the POST method. Refer to the article Asp.net MVC Warning Banner using Bootstrap and AngularUI Bootstrap for more details on how AntiForgeryToken is being implemented and utilized by the application. There will be a JavaScript section below to show how to pass in multiple parameters and AntiForgeryToken through the AJAX post.

Listing 3

public class ToolTipController : BaseApiController
{
    //this is just a sample, in reality, the data should be originated from a repository
    IList<tooltip> toolTips = new List<tooltip>
    {
        new ToolTip { Id = 1, Key ="registerEmail", Title = "Email",
            Description ="Enter your Email Address", LastUpdatedDate = DateTime.Now },
        new ToolTip { Id = 2, Key ="registerPassword", Title = "Password policy",
            Description ="some policy...", LastUpdatedDate = DateTime.Now }
    };

    [Route("{key}")]
    public IHttpActionResult Get(string key)
    {
        var toolTip = toolTips.FirstOrDefault((p) => p.Key.ToLower() == key.ToLower());
        if (toolTip == null)
        {
            return NotFound();
        }
        return Ok(toolTip);
    }

    [HttpPost]
    [Route("GetWithPost")]
    [AjaxValidateAntiForgeryToken]
    public IHttpActionResult GetWithPost(Dummy dummy)
    {
        var toolTip = toolTips.FirstOrDefault((p) => p.Key == dummy.Key);
        if (toolTip == null)
        {
            return NotFound();
        }
        return Ok(toolTip);
    }
}

Listing 4

shows the contents of the BaseApiController. This base class is inherited from the ApiController and contains structs. The struct type is being used here instead of class because there are only a couple of properties, and it is short-lived. Please feel free to modify it to a class later to fulfill your requirements.

Listing 5

public class BaseApiController : ApiController
{
    public struct Dummy
    {
        public string Key { get; set; }
        public string Other { get; set; }
    }
}

Web API Enabling Cross-Origin Requests (CORS)

Technically, the Web API can be hosted together with the Web Application or separately. In this sample application, the Web API and Web Application are being decoupled. That being said, the Web and API application could be hosted on a different subdomain or domain. By design, the web browser security same-origin policy will prevent a web page from making AJAX requests to another domain or subdomain. However, this issue can be overcome by Enabling Cross-Origin Requests (CORS) to explicitly allow cross-origin requests from the Web to the API application.

Listing 6

Shows the code in the Global.asax file to enable the CORS. The purpose of the SetAllowOrigin method is to check if the request URL is in the white list, if yes, set the Access-Control-Allow-Origin header value with the URL value. This response header will signal to the web browser that the requested domain/URL is permitted to access the resources on the server. In addition, before sending the actual requests, the web browser will issue a "preflight request" to the server by using the HTTP Options method. The response from the server will tell the browser what methods and headers are allowed by the request. In the current example, the X-Requested-With and request verification token were added to the header. The former header is utilized by the AngularJS $http service function AJAX Request, and the latter is the anti-forgery token value from the web application. Check out this article Enabling Cross-Origin Requests in ASP.NET Web API 2 to learn more about how CORS and Preflight Requests work.

Listing 7

internal void SetAllowOrigin(string url)
{
    // Get the allow origin URL from app settings
    string allowOrigin = System.Configuration.ConfigurationManager.AppSettings["AllowWebApiCallURL"];
    if (allowOrigin.Split(';').Select(s => s.Trim().ToLower()).Contains(url.ToLower()))
    {
        HttpContext.Current.Response.Headers.Remove("Access-Control-Allow-Origin");
        // http://domain.com or * to allow all callers
        HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", url.ToLower());
    }
}

protected void Application_BeginRequest(object sender, EventArgs e)
{
    SetAllowOrigin(HttpContext.Current.Request.Headers["Origin"] == null ?
        string.Format("{0}://{1}", HttpContext.Current.Request.Url.Scheme, HttpContext.Current.Request.Url.Authority) :
        HttpContext.Current.Request.Headers["Origin"]);

    if (HttpContext.Current.Request.HttpMethod == "OPTIONS")
    {
        HttpContext.Current.Response.AddHeader("Access-Control-Allow-Methods", "GET, PUT, POST");
        HttpContext.Current.Response.AddHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, content-type, Accept, requestverificationtoken");
        HttpContext.Current.Response.End();
    }
}

Web API Machine key

As mentioned previously, in this example, the Web and API applications were both being hosted on a separate domain. The POST method in the API is expecting an anti-forgery token from the requester. For the Web API to decrypt the token, both applications must use the same set of machine keys. Here is the link http://www.developerfusion.com/tools/generatemachinekey/ to generate machine keys if your applications needed one.

Your simple tooltip script

On page load, the client script will find and iterate through all the HTML elements with data-yourtooltipctr data attribute. During the loop, an image button will be created and placed next to the element. The data-yourtooltipid attribute value in the button will be utilized by the Web API to retrieve the tooltip contents. Its value is generated from data-yourtooltipctr data attribute, so make sure the value is unique. The purpose of the data-yourtooltiptype attribute is to flag if the tooltip will be displayed using jQuery or Bootstrap. If the attribute is present, the tooltip will be displayed using Bootstrap Popover, else use the jQuery UI Dialog plugin. Refer to Listing 6 for more detailed information. The image can be replaced by modifying the image source "/content/info_16x16.png".

Listing 8

$('[data-yourtooltipctrl]').each(function (index) {
    var toolTipType = '';
    // Get tooltip type
    if ($(this).data('yourtooltiptype')) {
        toolTipType = $(this).data('yourtooltiptype');
    }
    var container = $("<a href='#' class='yourToolTipLink' data-yourtooltiptype='" + toolTipType
        + "' data-yourtooltipid='" + $(this).data('yourtooltipctrl')
        + "'><img alt='Click for detail' src='/content/info_16x16.png'/>")
        .css({
            cursor: 'help',
            'padding-left': '2px'
        });
    $(this).css("display", "inline-block");
    if ($(this).is("label")) {
        $(this).append(container);
    } else {
        $(this).after(container);
    }
});

Listing 9

Shows the code logic for the image button/tooltip icon click event triggered by yourToolTipLink class selector. Initially, the logic will close all the previously open tooltip dialogs. Then, it will utilize the jQuery AJAX function to POST to the Web API. The API takes two parameters, namely Key and Other. The Key value is retrieved from the yourtooltipid data attribute. The Other parameter holds a dummy value. As mentioned previously, the Web API will use the key to retrieve the tooltip contents. The AJAX function will also utilize the beforeSend function to include the AntiForgeryToken in the request header.

If the request succeeds, the logic will populate the dialog content. The modal will be displayed through jQuery UI Modal or Bootstrap Popover, depending on if the data-your tooltip type attribute is specified in the HTML element. The challenging part of this script is that we have to maintain the API URL and can’t simply use a relative path because the API is being hosted on a different domain. If that is a problem, my suggestion is to modify this script to read the API URL from a global variable. The global variable should be populated from the code behind it. Here is an article sharing some examples of how to pass the Server-side data to JavaScript or remember to update the URL when deploying the application to different environments.

Listing 10

$(".yourToolTipLink").on('click', function (e) {
    e.stopPropagation();
    e.preventDefault();
    var o = $(this);
    var toolTipTypeSpecified = $(this).attr('data-yourtooltiptype');
    // Close the dialog or tooltip, to make sure only one at a time
    $(".yourToolTipLink").not(this).popover('hide');
    if ($("#yourTooltipPanel").dialog('isOpen') === true) {
        $("#yourTooltipPanel").not(this).dialog('close');
    }
    var Dummy = {
        "Key": $(this).data('yourtooltipid'),
        "Other": "dummy to show Posting multiple parameters to API"
    };
    jQuery.ajax({
        type: "POST",
        url: "http://localhost:47503/api/tooltip/GetWithPost",
        data: JSON.stringify(Dummy),
        dataType: "json",
        contentType: "application/json;charset=utf-8",
        beforeSend: function (xhr) {
            xhr.setRequestHeader('RequestVerificationToken', $("#antiForgeryToken").val());
        },
        headers: {
            Accept: "application/json;charset=utf-8",
            "Content-Type": "application/json;charset=utf-8"
        },
        accepts: {
            text: "application/json"
        },
        success: function (data) {
            if (toolTipTypeSpecified) {
                o.popover({
                    placement: 'right',
                    html: true,
                    trigger: 'manual',
                    title: data.Title + '<a href="#" class="close" data-dismiss="alert">×</a>',
                    content: '<div class="media"><div class="media-body"><p>' + data.Description + '</p></div></div>'
                }).popover('toggle');
                $('.popover').css({ 'width': '100%' });
            } else {
                $("#yourTooltipPanel p").html(data.Description);
                $("span.ui-dialog-title").text(data.Title);
                $("#yourTooltipPanel").dialog("option", "position", {
                    my: "left top",
                    at: "left top",
                    of: o
                }).dialog("open");
            }
        }
    });
});

How to Integrate?

Assuming your Web API is ready and running. Add all the styles and JavaScript references as indicated in Figure 1 to the _Layout page. It seems like a lot, but your application should have most of it already, for example, jQuery and Bootstrap. The _AntiforgeryToken.cshtml partial view contains a hidden field to hold the anti-forgery token. The _WebToolTip.cshtml partial view contains a div element with id="yourTooltipPanel" and a JavaScript reference to yourSimpleTooltip.js file. Update the Web API URL in the JavaScript to point to your Web API.

Figure 1

Add bootstrap

While writing this section, I noticed that the Antiforgerytoken could be a pain to integrate with other platforms such as ASP.NET Webform, PHP, Classic ASP, etc. In the sample project, I created a sample HTML page to demonstrate how to utilize this tooltip script without the token. I created another API method that doesn’t care about the token and cloned the script to yourSimpleTooltipNoToken.js file. This file excludes the beforeSend function in the jQuery AJAX post. I didn’t make it dynamic to avoid complications. Refer to Figure 2.

Figure 2

Add bootstrap

Point of Interest

In my previous project, when adding a new element to a form, I always set the position to absolute and calculate the x and y position dynamically. Let's assume I’m adding an icon next to a textbox element. There is a flaw in this method because when you resize the screen, the icon position will be out of sync, as indicated in Figure 3. You have to refresh the browser to refresh the icon position so that it will appear next to the textbox again. In this project, I use the jQuery after method to insert the icon after the form element. That way, the icon will always stick with the element during screen resize.

Figure 3

Email address

Conclusion

I hope someone will find this information useful and it will make your programming job easier. If you find any bugs or disagree with the contents or want to help improve this article, please drop me a line, and I'll work with you to correct it. I would suggest downloading the demo and exploring it to grasp the full concept because I might miss some important information in this article. Please contact me if you want to help improve this article, and use the following link to report any issues: https://github.com/bryiantan/SimpleLibrary/issues.

Resources


Similar Articles