SignalR Server-Side Timer

John Holland .NET, Technology Snapshot Leave a Comment

Recently, I had the fairly simple task of using SignalR to push out to logged-in users the dreaded impending “Site Maintenance” message.

The Product Owners wanted to keep it simple and straightforward, since it would not be something they would be doing all that often. That said, when they would need to do it, they needed it to be easy.

It was determined that using “AppSettings” in the “Web.config” would be the most straightforward approach, as it would be as easy as editing a text file. They wanted to be able to enter a date and time and then have that value be used in determining when to notify users of the maintenance window. Of course during the course of development, many other configuration options were defined and so the below reflects that, so you can benefit from our discovery in this blog post.

Using the AppSettings would allow them the ability to control the notification themselves without the assistance of a developer or require an entire user interface to be designed and built to support an activity that is not expected to occur that often.

Background

I already had SignalR implemented within the application so the necessary building blocks were already in place for me to jump right in without needing to be concerned with that aspect.

I started by adding the “AppSettings” values needed in the “Web.config.” Below are those settings and I am hopeful my comments above each explains clearly enough each one’s purpose.

Again, this is where the client will be making changes so I wanted to make sure they were commented well. I do realize there is a raging debate between those who believe comments are useful and those that believe they are not, or maybe better stated, that your code should be done in a way that comments are not required. I would rather refrain from discussing that here as it’s been debated to death already.

So any way, here are those “AppSettings”:

Web.config
--------------	
<!-- MAINTENANCE MESSAGING SETTINGS -->

<!-- DATETIME THAT MAINTENANCE WILL OCCUR. LEAVE BLANK FOR NO MAINTENANCE -->
<!-- EXAMPLE: 03/23/2015 12:00 PM -->
<add key="MAINTENANCE_DATETIME" value="" />

<!-- NUMBER OF MINUTES PRIOR TO THE ABOVE MAINTENANCE_DATETIME, TO BEGIN NOTIFYING USERS OF MAINTENANCE -->
<add key="MAINTENANCE_NOTIFICATION_THRESHOLD" value="120" />

<!-- NUMBER OF MINUTES PRIOR TO THE ABOVE MAINTENANCE_DATETIME, TO REDIRECT USERS AND NOT ALLOW LOGIN -->
<add key="MAINTENANCE_LOGIN_REDIRECT" value="2" />

<!-- FINAL NOTIFICAITON THRESHOLD: NUMBER OF MINUTES PRIOR TO MAINTEANCE_DATETIME, TO GIVE FINAL WARNING. -->
<add key="MAINTENANCE_FINAL_NOTIFICATION_THRESHOLD" value="1" />

<!-- * TO FORCE LINEBREAKS IN DISPLAY, USE (WHICH EQUALS ) AS YOU HAVE TO ENCODE HTML TAGS -->
<!-- * ##DATETIME## AND ##REMAINING_MINUTES## ARE TOKENS AND WILL BE POPULATED WITH APPROPRIATE VALUES -->
<!-- INITIAL NOTIFICATION MESSAGE TO USERS OF IMPENDING MAINTENANCE OCCURRING. -->
<add key="MAINTENANCE_INITIAL_NOTIFICATION_MESSAGE" value="Application will be brought down for maintenance at ##DATETIME##." />

<!-- FINAL NOTIFICATION MESSAGE TO USERS OF IMPENDING MAINTENANCE OCCURRING -->
<add key="MAINTENANCE_FINAL_NOTIFICATION_MESSAGE" value="Application will be going down for maintenance in ##REMAINING_MINUTES##, please save your work!" />

<!-- MAINTENANCE OCCURRING MESSAGE: MESSAGE TO BE RETURNED WHEN MAINTENANCE IS OCCURRING -->
<add key="MAINTENANCE_OCCURRING_MESSAGE" value="The Application is offline for maintenance at this time." />

Now to build the Timer.

I found the below statement on this page.

The official and recommended approach to run scheduled background work in ASP.NET (if there is no other, more robust way, like having an explicit Windows service or maybe a Windows Azure worker role) is to implement an IRegisteredObject with the ASP.NET runtime and hook it up at application start.

So in the API’s “Global.asax.cs”, I added the following to the “Application_Start()”:

// REGISTER THE HUBTIMER USED FOR MAINTENANCE MESSAGING

System.Web.Hosting.HostingEnvironment.RegisterObject(new ICHubTimer());

Below is the “ICHubTimer” object. I left a few debug lines in but commented out. When getting started, I find using them makes it easier to ensure the processing is occurring in the intended manner.

The “MaintenanceRepository” that is referenced below, is used simply to return the applicable AppSettings values using static methods. I have not included that code here but I’m sure you can create it without much direction. This lays the groundwork for making it more robust in the future if it is determined to use a database rather than the “Web.config” AppSettings to store the values.

Hubs/ICHubTimer.cs
----------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Hosting;

using System.Timers;
using Microsoft.AspNet.SignalR;
using System.Web.Script.Serialization;
using project.DataAccess.Repositories;

namespace project.API
{
    public class ICHubTimer : IRegisteredObject
    {
        private readonly IHubContext _hub;
        private Timer _timer;
        private int _timerInterval;

        public ICHubTimer()
        {
            //System.Diagnostics.Debug.WriteLine("HUBTIMER: CONSTRUCTOR");

            // GET HUB CONTEXT
            _hub = GlobalHost.ConnectionManager.GetHubContext();
                      
            // IN MILLISECONDS (15000 = 15 SECONDS, 30000 = 30 SECONDS, 60000 = 60 SECONDS)
            _timerInterval = 15000;

            // START THE TIMER
            StartTimer();
        }
        
        private void StartTimer()
        {
            //System.Diagnostics.Debug.WriteLine("HUBTIMER: STARTTIMER");

            // SET TIMER UP WITH INTERVAL
            _timer = new Timer(_timerInterval);

            // ADD HANDLER TO TIMER.ELAPSED EVENT
            _timer.Elapsed += OnTimerElapsed;

            // START THE TIMER GOING
            _timer.Start();
        }

        private void OnTimerElapsed(Object source, ElapsedEventArgs e)
        {
            //System.Diagnostics.Debug.WriteLine("HUBTIMER: ELAPSED");

            // IF MAINTENANCE DATETIME IS AN ACTUAL DATETIME VALUE
            DateTime maintenanceDatetime;
            if (DateTime.TryParse(MaintenanceRepository.MaintenanceDatetime(), out maintenanceDatetime))
            {
                // DETERMINE HOW MANY MINUTES ARE REMAINING UNTIL MAINTENANCE IS TO OCCUR
                var minutesRemaining = Convert.ToInt32(maintenanceDatetime.Subtract(DateTime.Now).TotalMinutes);

                // INDICATES THE NUMBER OF MINUTES FROM THE MAINTENANCE OCCURRING TO NOTIFY USERS OF THE MAINTEANCE. 
                // THIS WILL MAKE IT SO WE AVOID NOTIFYING USERS OF MAINTENANCE TO START NEXT WEEK OR SO.
                var notificationThreshold = MaintenanceRepository.MaintenanceNotificationThreshold();

                // IF MINUTESREMAINING IS WITHIN THE NOTIFICATION THRESHOLD
                if (minutesRemaining<= notificationThreshold)
                {
                    // GET THE FINAL NOTIFICATION THRESHOLD
                    var finalNotficationThreshold = MaintenanceRepository.FinalNotificationThreshold();

                    // CREATE AND POPULATE DICTIONARY TO PASS NAME/VALUES TO CLIENT
                    Dictionary<string, string> msg = new Dictionary<string, string>();
                    msg.Add("mr", minutesRemaining.ToString());
                    msg.Add("nt", notificationThreshold.ToString());
                    msg.Add("fnt", finalNotficationThreshold.ToString());

                    // DETERMINE IF DATE IS DAYLIGHTSAVINGS OR STANDARD
                    var tz = TimeZoneInfo.Local;
                    var mdt = maintenanceDatetime.ToString("MM/dd/yyyy h:mm tt");
                    mdt += " " + (tz.IsDaylightSavingTime(maintenanceDatetime) ? tz.DaylightName : tz.StandardName);

                    // DETERMINE WHICH MESSAGE TO RETURN.  USE INITIAL NOTIFICATION MESSAGE BY DEFAULT
                    var notficationMessage = MaintenanceRepository.InitialNotificationMessage();
                    if (minutesRemaining <= finalNotficationThreshold) notficationMessage = MaintenanceRepository.FinalNotificationMessage(); // REPLACE SOME TOKENS notficationMessage = notficationMessage.Replace("##DATETIME##", mdt); // FORM APPROPRIATE REMAINING MINUTES STRING var remaining_minutes_str = minutesRemaining.ToString() + " minute" + (minutesRemaining > 1 ? "s" : "");
                    notficationMessage = notficationMessage.Replace("##REMAINING_MINUTES##", remaining_minutes_str);

                    msg.Add("nm", notficationMessage);

                    // SERIALIZE AS JSON AND BROADCAST TO ALL CLIENTS
                    string json = new JavaScriptSerializer().Serialize(msg);
                    _hub.Clients.All.maintenanceOccurring(json);
                }
            }
        }

        public void Stop(bool immediate)
        {
            _timer.Dispose();
            HostingEnvironment.UnregisterObject(this);
        }
    }
}

Below is JavaScript code that loads when the application is loaded in the browser.

The very first thing I wanted to do is find out if the maintenance is currently occurring, or if it is within the window of 2 minutes before maintenance is to begin, in order to prevent users from logging on. So I make a call to our API which does the evaluation and returns true or false. If true, we need to redirect the user to the Maintenance page.

Next are all of the variables and supporting client-side methods to be used by the method that is to be called from the server. The client uses ExtJS for its SPA framework so you will see it used within the functionality.

Finally there is the method “maintenanceOccurring” to be called from the server. It will take in a JSON object to allow us as much flexibility as possible with future development, and I just like using JSON as the data transfer mechanism.

Based on the values that were determined server-side, it presents a kind of countdown for the user. The user is shown a message telling them that the maintenance will be occuring and is told to log out and save their work. On the last step the user is essentially forced off and redirected to the maintenance page. The maintenance page will display the message defined by the client which could provide an estimated time of completion or some other information if they so choose.

// SEE IF MAINTENANCE IS OCCURING BEFORE PROCEEDING ANY FURTHER
// AND REDIRECT IF NECESSARY.
$.get('/api/v1/Maintenance/LoginRedirect', function (result) {
    if (result === true) {
        window.onbeforeunload = null;
        location.href = "/Maintenance";
    }
});


// MAINTENANCE MESSAGING
var maintenanceNotified = false;
var maintenanceNotifiedFinal = false;

// BLINKING
var originalTitle;
var blinkTitle;
var blinkTitleLogicState = false;

function startTitleBlinking(title) {
    originalTitle = document.title;
    blinkTitle = title;
    blinkTitleIteration();
}

function blinkTitleIteration() {
    if (blinkTitleLogicState == false) {
        document.title = blinkTitle;
    }
    else {
        document.title = originalTitle;
    }
    blinkTitleLogicState = !blinkTitleLogicState;
    blinkTitleHandler = setTimeout(blinkTitleIteration, 500);
}

function stopTitleBlinking() {
    if (blinkTitleHandler) {
        clearTimeout(blinkTitleHandler);
    }
    document.title = originalTitle;
}

function resetMaintenanceVars() {
    maintenanceNotified = false;
    maintenanceNotifiedFinal = false;
}

function maintenancePopup(reasonText) {

    startTitleBlinking('**MAINTENANCE**');

    Ext.TaskManager.stopAll();
    window.onbeforeunload = windowBeforeUnloadClone;

    var message = '';
    if (!Ext.isEmpty(reasonText)) {
        message = reasonText;
    }
    message += 'Press OK to continue.';
    
    Ext.Msg.show({
        title: 'Maintenance Notification!',
        msg: message,
        modal: true,
        buttons: Ext.Msg.OK,
        width: 400,
        closable: false,
        icon: Ext.MessageBox.WARNING,
        fn: function (buttonId) {
            if (buttonId === "ok") {
                stopTitleBlinking();
            }
        }
    });
}

...

Further down in the .js file, along with the other SignalR methods:

// METHOD CALLED FROM SERVER-SIDE CODE
$.connection.iCHub.client.maintenanceOccurring = function (jsonStr) {
	console.log("SignalR: maintenanceOccuring: " + jsonStr);

	// DECODE INTO A JSON OBJECT
	var obj = Ext.JSON.decode(jsonStr);
	var minutes_remaining = parseInt(obj.mr);
	var notification_threshold = parseInt(obj.nt);
	var final_notification_threshold = parseInt(obj.fnt);
	var notification_message = obj.nm;
	
	// IF WITHIN THE NOTIFICATION THRESHOLD AND USER HAS NOT SEEN NOTIFICATION YET
	if (minutes_remaining <= notification_threshold && maintenanceNotified == false) {
		maintenancePopup(notification_message);
		maintenanceNotified = true;
	}
	// ELSE IF WITHIN THE FINAL NOTIFCATION THRESHOLD AND USER HAS NOT SEE NOTIFICATION YET
	else if (minutes_remaining <= final_notification_threshold && maintenanceNotifiedFinal == false) {
		maintenancePopup(notification_message);
		maintenanceNotifiedFinal = true;
	}
	// ELSE IF IT IS TIME TO SEND TO MAINTENANCE PAGE
	else if (minutes_remaining <= 0) {
		window.onbeforeunload = null;
		location.href = MAINTENANCE_PAGE;
	}
};

Conclusion

This is just one more use for SignalR. I am hopeful other opportunities will present themselves to make use of this technology that appears to have no limit to its potential uses. As I said in my first SignalR post, although my implementation may have some unique conditions (such as using ExtJS), nothing done above can’t be done with straight JavaScript and SignalR.

That has to be one of SignalRs best benefits: it doesn’t matter what JavaScript framework you choose to use on the front end, or no framework at all. As long as you use JavaScript in some aspect of your .NET application, SignalR can be useful in your application.

I am hopeful that this will help someone who is looking at possibly integrating SignalR into their current application. I would tell you to do it and don’t look back, as the more you use SignalR, the more uses you will find for SignalR.

— John Holland, asktheteam@keyholesoftware.com


About the Author
John Holland

John Holland

John Holland is an experienced developer and architect who has been designing and developing database applications since 1987 and Internet development since 1995. Expertise surrounding application design and development focuses on technologies C#, ASP.NET, and PHP.


Share this Post

Leave a Reply