“ASP.NET SignalR is a new library for ASP.NET developers that makes it incredibly simple to add real-time web functionality to your applications.” as stated on the official site of SignalR.
Recently I needed to find a solution that supports push-like notifications for a SharePoint 2010 web-based administration tool that should run in the background but should send notifications occasionally to the user started the request. Most of the forum discussions suggested SignalR, and the information and samples I found about this library on the web (like the above-mentioned official site or these tutorials) were really convincing.
Probably the most difficult part of the SharePoint 2010 – SignalR integration is the different .NET Framework version they use (.NET Framework 3.5, that is Common Language Runtime (CLR) 2.0 vs. .NET Framework 4.5, that is CLR 4.0). To bridge this gap we should include some kind of out-of-process communication channel (typically HTTP-based) that mediates between the parties.
Unfortunately, most of the SharePoint-related samples are either jokes (like the April Fools post about SharePointR exactly 2 years ago), outdated (for example using SignalR 1.x, like this solution), and / or based on SharePoint 2013 (like the posts here, here or this CodePlex solution).
To to make a long story short, I decided to create my own implementation for SharePoint 2010 using SignalR 2.0, and base the implementation on a WCF service as opposed to this HTTP Handler-based solution.
Solution Architecture
My solution contains four projects:
TaskNotifierService is an IIS hosted WCF service that acts as the SignalR Hub as well. It is a CLR 4.0 process.
SPTaskNotifier is a SharePoint 2010 application (that means CLR 2.0). There is an event receiver in this project, that calls the WCF service in the TaskNotifierService if a new task is assigned to a user. There are some simple JavaScript functions as well that support the SignalR integration on the client side, like registering the client and displaying notifications if a task was assigned to the current user.
TaskNotifierTestWeb is a simple web application. The project is based on the .NET Framework 4.5, but it is irrelevant as we use only client side code in an .html page. The goal of the project is to simplify testing the JavaScript code that interacts with SignalR without involving the SharePoint deployment process (like IISRESET etc.).
TaskNotifierTestConsole is a console application based on the .NET Framework 4.5. Again, the framework version is not important, any version that supports WCF would be OK. This application can be used to send test messages to the WCF host, and thus test the code that creates the WCF client proxy and calls the WCF service methods. Using this project we can test the functionality without SharePoint as well. If your goal is just to send test messages, but you are not interested in the code itself, you can use the WCF Test Client of Visual Studio either (more about that later).
Below is the architecture of these components including the possible connections between them. The core and indispensable component is the TaskNotifierService. The other components are optional. For example you can use either the TaskNotifierTestConsole, the SPTaskNotifier or the WCF Test Client to sent messages (e.g. call the WCF service method) to the TaskNotifierService, and it will forward the messages to the adequate client, let it be either the TaskNotifierTestWeb or the SPTaskNotifier.
TaskNotifierService
We start our code review with the core component, the TaskNotifierService. We define the data and service contracts of our WCF service in this project.
The TaskNotification is the data contract of the communication. It includes the Title of the new task, the LoginName of the user the task is assigned to and the URL of the display form of the task item.
- [DataContract]
- public class TaskNotification
- {
- // the login name of the user the task is assigned to
- [DataMember]
- public string LoginName { get; set; }
- // the url of the task display form
- [DataMember]
- public string Url { get; set; }
- // the title of the task
- [DataMember]
- public string Title { get; set; }
- }
The ITaskNotifierService interface is the service contract, including only a single method called TaskCreated. This method has a single parameter of type TaskNotification.
- [ServiceContract]
- public interface ITaskNotifierService
- {
- [OperationContract]
- void TaskCreated(TaskNotification taskNotification);
- }
The TaskNotifierService class implements the ITaskNotifierService interface. In the implementation of the TaskCreated method we first get an instance of the current ConnectionManager then get the hub context via its GetHubContext<TaskNotifierHub> method. Finally we collects the client connections using the contextfor the user the task was assigned to (taskNotification.LoginName) and call the addTaskMessage method of these connections including the task title and URL as parameters (taskNotification.Title and taskNotification.Url):
- using System;
- using System.Diagnostics;
- using Microsoft.AspNet.SignalR;
- using Microsoft.AspNet.SignalR.Infrastructure;
- namespace TaskNotifierService
- {
- // NOTE: In order to launch WCF Test Client for testing this service, please select TaskNotifierService.svc or TaskNotifierService.svc.cs at the Solution Explorer and start debugging.
- // NOTE: WCF Test Client issue with '\' in LoginName. Escape it as '\\', like 'domain\\user' instead of 'domain\user'
- // http://connect.microsoft.com/VisualStudio/feedback/details/632374/wcf-test-client-sends-incorrect-null-value-as-string-type-value-for-wrongly-escaped-entered-values
- public class TaskNotifierService : ITaskNotifierService
- {
- public void TaskCreated(TaskNotification taskNotification)
- {
- Debug.Print("Task with title '{0}' was created.", taskNotification.Title);
- IConnectionManager connectionManager = GlobalHost.ConnectionManager;
- var context = connectionManager.GetHubContext<TaskNotifierHub>();
- context.Clients.Group(taskNotification.LoginName.ToUpper()).addTaskMessage(taskNotification.Title, taskNotification.Url);
- }
- }
- }
Note: We call the ToUpper method on the LoginName to make it case-insensitive, see the same for the Context.User.Identity.Name in the OnConnected method of the TaskNotifierHub below.
Our TaskNotifierHub class is a subclass of a SignalR Hub class. Whenever a new client is connected to the hub, we determine the user name corresponding the connection and assign the connection ID to the group identified by the user name.
- using System;
- using System.Threading.Tasks;
- using Microsoft.AspNet.SignalR;
- namespace TaskNotifierService
- {
- [Authorize]
- public class TaskNotifierHub : Hub
- {
- public override Task OnConnected()
- {
- string name = Context.User.Identity.Name.ToUpper();
- Groups.Add(Context.ConnectionId, name);
- return base.OnConnected();
- }
- }
- }
The Startup class is responsible for the startup of our service hub. In its Configuration method we set up the configuration settings that enable accessing the hub from an external web site (see CORS, Cross-Origin Resource Sharing) and finally start the SignalR pipeline.
- using Microsoft.AspNet.SignalR;
- using Microsoft.Owin;
- using Microsoft.Owin.Cors;
- using Owin;
- [assembly: OwinStartup(typeof(TaskNotifierService.Startup))]
- namespace TaskNotifierService
- {
- public class Startup
- {
- public void Configuration(IAppBuilder app)
- {
- // Branch the pipeline here for requests that start with "/signalr"
- app.Map("/signalr", map =>
- {
- // Setup the CORS middleware to run before SignalR.
- // By default this will allow all origins. You can
- // configure the set of origins and/or http verbs by
- // providing a cors options with a different policy.
- map.UseCors(CorsOptions.AllowAll);
- var hubConfiguration = new HubConfiguration
- {
- // You can enable JSONP by uncommenting line below.
- // JSONP requests are insecure but some older browsers (and some
- // versions of IE) require JSONP to work cross domain
- EnableJSONP = true
- };
- // Run the SignalR pipeline. We're not using MapSignalR
- // since this branch already runs under the "/signalr"
- // path.
- map.RunSignalR(hubConfiguration);
- });
- }
- }
- }
TaskNotifierTestConsole
The second project, TaskNotifierTestConsole is a simple WCF client to test the WCF service hosted in IIS (see TaskNotifierService above), so a Service reference to the TaskNotifierService was added to this project.
- static void Main(string[] args)
- {
- // TODO Update the login name to match the user's login name that is authenticated in the browser
- SendTaskNotification("CONTOSO\\Administrator", "Test Task Title", "http://site/task");
- }
In the SendTaskNotification method we create a WCF proxy instance and call its TaskCreated method with the test parameters.
- private static void SendTaskNotification(string loginName, string title, string url)
- {
- TaskNotifierServiceClient sc = TaskNotifierProxy;
- sc.TaskCreated(new TaskNotification
- {
- LoginName = loginName,
- Title = title,
- Url = url
- });
- }
The configuration of the proxy (WCF bindings, etc.) are set from code, see TaskNotifierProxy property later at the TaskCreated event receiver of the SPTaskNotifier project. Of course, you could set the same values from a configuration file as well.
TaskNotifierTestWeb
The third project in the solution is the TaskNotifierTestWeb project. In this project we have a single page, default.htm that includes the communication JavaScript methods. We first include the references to the required JavaScript libraries, set up the hub URL, declare a proxy to reference the hub. Next a function is created that the hub can call to send notifications. In this case we simply append the information as a span to the current HTML page. Finally the connection is started.
- <!DOCTYPE html>
- <html>
- <head>
- <title>Task Notifier Test Page</title>
- </head>
- <body>
- <div class="container">
- <ul id="discussion">
- </ul>
- </div>
- <!–Script references. –>
- <!–TODO: Update the URLs to match your current configuration –>
- <!–Reference the jQuery library. –>
- <script src="http://localhost:57800/Scripts/jquery-1.6.4.min.js" ></script>
- <!–Reference the SignalR library. –>
- <script src="http://localhost:57800/Scripts/jquery.signalR-2.0.2.min.js"></script>
- <!–Reference the autogenerated SignalR hub script. –>
- <script src="http://localhost:57800/signalr/hubs"></script>
- <!–Add script to update the page and send messages.–>
- <script type="text/javascript">
- $(function () {
- $.connection.hub.url = "http://localhost:57800/signalr";
- // Declare a proxy to reference the hub.
- var taskNotifier = $.connection.taskNotifierHub;
- // Create a function that the hub can call to send notifications.
- taskNotifier.client.addTaskMessage = function (title, url) {
- var anchor = $('<a />').attr('href', url).attr('target', '_blank').text(title);
- var anchorText = $('<span />').append(anchor).html();
- var text = "A task called '" + anchorText + "' was assigned to you.";
- $('#discussion').append(text).append('<br />');
- };
- // Start the connection.
- $.connection.hub.start();
- });
- </script>
- </body>
- </html>
Important, that Windows Authentication should be enabled for the web site, otherwise the hub cannot determine the user name on the client connection.
SPTaskNotifier
The SPTaskNotifier is a simple SharePoint project that includes an event receiver that is triggered whenever a new item is created in a Tasks list.
First, we register our event handler to all lists created from the Tasks list template:
- <?xml version="1.0" encoding="utf-8"?>
- <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
- <Receivers ListTemplateId="107">
- <Receiver>
- <Name>TaskCreated</Name>
- <Type>ItemAdded</Type>
- <Assembly>$SharePoint.Project.AssemblyFullName$</Assembly>
- <Class>SPTaskNotifier.TaskCreated</Class>
- <SequenceNumber>10000</SequenceNumber>
- </Receiver>
- </Receivers>
- </Elements>
In the ItemAdded method of the TaskCreated class we determine the Title and the owner of the new task, as well as the URL of the display form, and call the SendTaskNotification method with this parameters.
- public override void ItemAdded(SPItemEventProperties properties)
- {
- Debug.Print("SPTaskNotifier.TaskCreated started.");
- try
- {
- SPListItem task = properties.ListItem;
- if (task != null)
- {
- SPFieldUserValue userFieldValue = new SPFieldUserValue(properties.Web, task[SPBuiltInFieldId.AssignedTo] as string);
- if ((userFieldValue != null) && (userFieldValue.User != null))
- {
- string url = string.Format("{0}{1}?ID={2}", properties.Web.Url, properties.List.DefaultDisplayFormUrl, task.ID);
- this.SendTaskNotification(userFieldValue.User.LoginName, task.Title, url);
- }
- else
- {
- Debug.Print("SPTaskNotifier.TaskCreated: No user assigned to task ID={0}", task.ID);
- }
- }
- else
- {
- Debug.Print("SPTaskNotifier.TaskCreated: No task found");
- }
- base.ItemAdded(properties);
- }
- catch (Exception ex)
- {
- Debug.Print("SPTaskNotifier.TaskCreated exception: {0}\r\n{1}", ex.Message, ex.InnerException);
- }
- Debug.Print("SPTaskNotifier.TaskCreated finished.");
- }
The SendTaskNotification method is very similar to the version we saw in the case of the TaskNotifierTestConsole project above, but includes a few lines that help debugging and tracing.
- private void SendTaskNotification(string loginName, string title, string url)
- {
- Debug.Print("SPTaskNotifier.SendTaskNotification started. loginName='{0}'; title='{1}'; url='{2}'",
- loginName, title, url);
- TaskNotifierServiceClient sc = TaskNotifierProxy;
- sc.TaskCreated(new TaskNotification
- {
- LoginName = loginName,
- Title = title,
- Url = url
- });
- Debug.Print("SPTaskNotifier.SendTaskNotification finished.");
- }
Since we call the TaskNotifierService, we should add a Service reference to this project as well. The WCF client proxy is configured dynamically from code.
- private static TaskNotifierServiceClient TaskNotifierProxy
- {
- get
- {
- {
- var binding = new BasicHttpBinding
- {
- Name = "taskNotifierBinding",
- HostNameComparisonMode = HostNameComparisonMode.StrongWildcard,
- MessageEncoding = WSMessageEncoding.Text,
- UseDefaultWebProxy = true,
- AllowCookies = false,
- BypassProxyOnLocal = false,
- Security =
- {
- Mode = BasicHttpSecurityMode.TransportCredentialOnly,
- Transport =
- {
- ClientCredentialType = HttpClientCredentialType.None,
- ProxyCredentialType = HttpProxyCredentialType.None
- },
- Message =
- {
- ClientCredentialType = BasicHttpMessageCredentialType.UserName
- }
- }
- };
- // TODO update the URL to match your current configuration
- EndpointAddress remoteAddress = new EndpointAddress("http://localhost:57800/TaskNotifierService.svc");
- TaskNotifierServiceClient client = new TaskNotifierServiceClient(binding, remoteAddress);
- client.ClientCredentials.Windows.ClientCredential = CredentialCache.DefaultNetworkCredentials;
- return client;
- }
- }
- }
The SharePoint project includes not only the event receiver, but two JavaScript files as well. We reference the first one, the TaskNotifierLoader.js from a CustomAction element.
- <?xml version="1.0" encoding="utf-8"?>
- <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
- <CustomAction Location="ScriptLink" ScriptSrc="/_layouts/SPTaskNotifier/TaskNotifierLoader.js" Sequence="900"/>
- </Elements>
TaskNotifierLoader.js support dynamic loading of the necessary jQuery and SignalR libraries as well as our other .js file, the TaskNotifier.js.
- // TODO update the URL to match your current configuration
- var hostURLSignalR = 'http://localhost:57800/';
- // Script references.
- // Reference the jQuery library.
- document.write('<script type="text/javascript" src="' + hostURLSignalR + 'Scripts/jquery-1.6.4.min.js"></script>');
- // Reference the SignalR library.
- document.write('<script type="text/javascript" src="' + hostURLSignalR + 'Scripts/jquery.signalR-2.0.2.min.js"></script>');
- // Reference the autogenerated SignalR hub script.
- document.write('<script type="text/javascript" src="' + hostURLSignalR + 'signalr/hubs"></script>');
- // load our custom script as well
- document.write('<script type="text/javascript" src="/_layouts/SPTaskNotifier/TaskNotifier.js"></script>');
TaskNotifier.js performs the same SignalR hub-related tasks as we saw at the default.htm of the TaskNotifierTestWeb project above, but in this case we display the notification as a standard SharePoint status message.
- $(function () {
- $.connection.hub.url = hostURLSignalR + "/signalr";
- // Declare a proxy to reference the hub.
- var taskNotifier = $.connection.taskNotifierHub;
- // Create a function that the hub can call to broadcast messages.
- taskNotifier.client.addTaskMessage = function (title, url) {
- var anchor = $('<a />').attr('href', url).attr('target', '_blank').text(title);
- var anchorText = $('<span />').append(anchor).html();
- var text = "A task called '" + anchorText + "' was assigned to you.";
- var statusId = SP.UI.Status.addStatus("New task:", text);
- makeStatusClosable(statusId);
- };
- // Start the connection.
- $.connection.hub.start();
- });
- function makeStatusClosable(statusId) {
- var status = $('#' + statusId);
- $(status).html('<a title="Close status message" href="" onclick="javascript:SP.UI.Status.removeStatus(\'' + statusId + '\');javascript:return false;">x</a> ' + $(status).html());
- }
Testing the Solution
Be sure you start the TaskNotifierService project first. As I mentioned, it is the core, without that the other components won’t work.
Next, you can start the UI components, that means opening either SharePoint or the test web project in the browser. If you start the UI first, the client can’t register itself at the SignalR hub so it won’t receive any notifications later.
You can test the SignalR hub and the web UI (either the test project or the SharePoint solution) using the WCF Test Client.
You can select the TaskCreated method, set the parameter values you would like to pass to the method, and invoke the method.
A few important things to note regarding this tool:
You should use a double backslash to separate the domain and user names in the LoginName parameter, as there is a bug in the WCF Test Client.
You might get an error message when you start the WCF Test Client:
Error: Cannot obtain Metadata
In this case you should read the full error message as it may have a rather trivial solution like the lack of free memory in this case:
Memory gates checking failed because the free memory (xxxxxxxx bytes) is less than 5% of total memory. As a result, the service will not be available for incoming requests. To resolve this, either reduce the load on the machine or adjust the value of minFreeMemoryPercentageToActivateService on the serviceHostingEnvironment config element.
Here is a sample output of calling the TaskCreated method from the WCF Test Client with the parameters displayed on the screenshot above using the test web site (see TaskNotifierTestWeb project):
And the result of the same call in SharePoint:
Alternatively, you can test the functionality from the TaskNotifierTestConsole project as well. The following screenshots display the result of calling the TaskCreated method from the TaskNotifierTestConsole project in the case of the test web project:
And in the case of the SharePoint solution:
Note: The user can close the SharePoint status messages as described in my former post.
Of course, the main goal of this post and the sample is to illustrate, how to use SignalR 2.0 from SharePoint, so let’s see how it works. In this case, I suggest you to start two separate browser tabs, one for the Task list and an other one with an arbitrary page on the same SharePoint site. The reason of this is that when you create a new task, the page is reloaded and you might miss the notification. You should receive the notification in the other browser window.
So let’s create a new task and assign it to the current user.
A new status message will be displayed in the other browser tab.
Clicking on the hyperlink will open the display form of the task just created.
Hopefully you can apply the techniques illustrated here to add real-time functionality easily to your SharePoint applications via the SignalR libraries.
You can download the source code of the sample application from here.
