This tutorial explains how to show users a message in the MessageCenter GUI from the .Net parts of the system, such as the Event System, Templates or Custom Resolvers using a Microsoft open source product called SignalR.
Providing feedback to end users is important in our web applications. The Tridion GUI uses JavaScript and the MessageCenter to provide feedback about operations such as publishing and copying items. Using a GUI Extension a developer can also display messages to the MessageCenter with JavaScript. However, we have not been able to display an information message in the MessageCenter from other parts of Tridion that use .Net, such as a C# template, the Event System, or a Custom Resolver - until now.
This tutorial explains how to show users a message in the MessageCenter GUI from the .Net parts of the system, such as the Event System, Templates or Custom Resolvers using a Microsoft open source product called SignalR. The GUI Extension is compatible with Tridion 2011 and uses the MessageCenter to display feedback when an error has occurred or pages have been added to the Publishing Queue.
The goal is to send messages from the event system on the publisher to notify a user when the publish transaction they have initiated is completed. This means they do not have to keep checking the publishing queue, and can continue working until their pages are published and ready for viewing.
Read on to discover how you can hook into the MessageCenter from any C# code on the CMS.
We will use a simple use case to illustrate the technology and walk through the code: Inform users in the Message Center when publish actions that they initiated have completed.
This is an in-depth tutorial split into 4 parts:
- Create the SignalR Server
- Creating the SignalR Server and Client for the Tridion MessageCenter in 6 steps (~25 minutes)
- Adding the Tridion GUI as a Client
- Setting up the Event System SignalR .Net client
Diagram courtesy of Will Price
The solution uses the SignalR .NET library to create a service that listens for and broadcasts messages to the MessageCenter using a GUI Extension. Find out more about SignalR on the Microsoft SignalR page or watch one of the SignalR presentations. In this example we use the SignalR Persistent Connection.
If you would like to read more about SignalR I would suggest the great free e-book ASP.NET SignalR, Incredibly simple real-time features for your web apps by Jose M. Aguilar.
Part 1: Creating SignalR Server
In Visual Studio 2010 create a new C# 'ASP.NET Web Application' and call it 'Signalr4Tridion' using the .Net Framework 4.0. This will be used as our SignalR server.
In VS 2012, change the .Net Framework version to 4.0.
SignalR does not require any configuration in the web.config. However, you may need to remove some default New Project web.config settings if you are using VS 2010. Open the Web.config and delete the following config sections if you have them:
<connectionStrings>, <authentication>, <membership>, <profile>, <roleManager>
The VS 2010 web.config file should look like this:
<?xml version="1.0"?>
<!--
For more information on how to configure your ASP.NET application, please visit
http://go.microsoft.com/fwlink/?LinkId=169433
-->
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.0" />
</system.web>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
</configuration>
The next step is to add SignalR to our Web Application. The best way to download the library is to use the NuGet Package Manager (http://nuget.org/). If you don't have it installed, go to the NuGet website and install it before continuing.
NuGet has a console window and this is the easiest way to download new packages. Open the NuGet Package Manager Console from the View menu, Other Windows, Package Manager Console. Then, type 'Install-Package Microsoft.AspNet.SignalR'. The console command downloads all the files for the SignalR Server and SignalR JavaScript client and adds them as references in our project. We've now got all the external dependencies in our project for SignalR.
Part 2: Creating the SignalR Server and Client for the Tridion MessageCenter in 6 steps (~25 minutes)
- In Solution Explorer, create a 'SignalrServer' folder that will contain the PersistentConnection implementation
- In the 'SignalrServer' folder add a new Class and call it 'MessageCenterChannel', and add the following code:
using System;
using Microsoft.AspNet.SignalR;
using System.Threading.Tasks;
namespace Signalr4Tridion.SignalrServer
{
public class MessageCenterChannel : PersistentConnection
{
protected override Task OnReceived(IRequest request, string connectionId, string data)
{
int i;
var pars = data.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries);
// connection is joining a group. This is used to send messages to specific users.
// 1 user per group
if (pars.Length == 2 && pars[0].ToLower() == "join")
{
return this.Groups.Add(connectionId, pars[1]);
}
else if (pars.Length == 2 && pars[0].ToLower() == "leave") // remove the user from the group. Not used now.
{
return this.Groups.Remove(connectionId, pars[1]);
}
else if ((i = data.IndexOf(":")) > -1) // groupid:message
{
//send a message to a group (which is 1 user)
var groupName = data.Substring(0, i);
var message = data.Substring(i + 1);
return this.Groups.Send(groupName, message);
}
else
{
// Broadcast data to all clients
return Connection.Broadcast(data);
}
}
}
} - Open Global.asax and add the code to the Application_Start method:
void Application_Start(object sender, EventArgs e)
{
RouteTable.Routes.MapConnection<MessageCenterChannel>("channel", "/channel",
new ConnectionConfiguration { EnableCrossDomain = true });
} - Create a sample webpage SignalR client for testing
Open the Default.aspx file, remove all the sample code and paste the following SignalR code. The code below creates a new SignalR Client connection to the SignalR server and sends (broadcasts) the text in the form field to the server. The server then broadcasts it to any connected Signalr client, including itself.
<html>
<head>
<script src="Scripts/jquery-1.6.4.js" type="text/javascript"></script>
<script src="Scripts/jquery.signalR-1.1.2.js" type="text/javascript"></script>
<script type="text/javascript">
$(function () {
var connection = $.connection('/channel');
connection.received(function (data) {
$('#messages').prepend('<li>' + data + '</li>');
});
connection.start().done(function () {
$("#broadcast").click(function () {
connection.send($('#msg').val());
});
});
});
</script>
</head>
<!-- Sample Code from https://github.com/SignalR/SignalR/wiki/QuickStart-Persistent-Connections -->
<body>
<form>
<input type="text" id="msg" />
<input type="button" id="broadcast" value="broadcast" />
<ul id="messages">
</ul>
</form>
</body>
</html>
Warning: Confirm the filename of the jquery and signalr libraries in the script tag. There is a good chance when you read this article it is newer (and different) than the sample code below. Look in your Scripts folder and drag the current SignalR and jQuery file into your Default.aspx page. - Testing SignalR Server
Run the project in debug mode and type something in the text box. It should appear under the text box in an li tag. Excellent. SignalR is working.
Debugging: If there is no li element displayed then the connection between the Signalr Client and Server is not working. Open your JavaScript debug console in Chrome or Firefox and see what error messages are there.
Testing, part 2:: Now let's fire up another client. Open a different browser window with the same page. Type a message in the second window. Now, go back to the first page and notice how your second page's message appears there. Cool.
This is the main idea of SignalR - clients can send or 'broadcast' messages to all other connected clients. Using the SignalR JavaScript library (as above) we can create 2 connected web page clients. However, SignalR also provides support for C# clients and we can also use the Microsoft .Net Signalr Client library for this. There's even an SignalR Obj-C client for OSX and iOS and a SignalR Android client. In our example we'll use the .Net SignalR client library inside a Tridion Event System application to communicate with Signalr. - Deploy to IIS
Before we move on let's deploy our SignalrServer Web Application to IIS on the Tridion CMS. This is important because we will have a fixed URL for our SignalrServer and will be able to access it from our GUI Extension later.
- Create a new Folder on the Webserver (for example, c:\inetpub\wwwroot) for the IIS solution 'Signalr4Tridion'
- Deploy the Visual Studio code. I prefer to use the 'Publish' option in Visual Studio for deploying the SignalR Server solution to the IIS folder and use the File Copy action.
You should have the following folders / files there:
1. bin
2. Scripts
3. Default.aspx
4. Global.asax
5. packages.config
6. Web.config - Open IIS, create a new Website 'Signalr4Tridion' and set the port to 8123. If you use a different port please change 8123 in this example to your port number.
Testing the SignalR IIS Web App
Finally, we're able to test our SignalrServer. Go to your browser and type http://localhost:8123/Default.aspx (or whatever port you used). Do you see the list item when selecting 'broadcast'? Good. Try opening up another browser window with the same URL and see messages shown the 2 SignalR clients. Next we'll add a GUI extension as a SignalR JavaScript client, and show the notifications in the Message Center.
* Debug: If it's not working and you see the error below in the JavaScript console then make sure it is running inside its' own App Pool with .NET 4:
http://localhost:8123/notify/negotiate?_=1353060751433 404 Not Found
Part 3: Adding the Tridion GUI as a Client
The next step is to create a Tridion GUI Extension and set it up as a SignalR JavaScript client. This uses the SignalR JavaScript client library and will create a connection to our SignalrServer and listen for messages. In this example we're using the Persistent Connection implementation of SignalR.
- Create the GUI Extension Folder Structure. On the Tridion CMS Server, create a new folder in Tridion\web\WebUI\Editors and call it Signalr4TridionMC.
- In Signalr4TridionMC create a new folder called 'js' and copy the jQuery and SignalR JavaScript files from the SignalR Server Deployed code (C:\inetpub\wwwroot\Signalr4Tridion\Scripts) to the '/Signalr4Tridion/js' folder. The latest versions at time of writing are jquery.signalR-1.1.3.js and jquery-1.6.4.js, but are likely to be changed when you read this.
- Create a new file called notification.js (in the js folder) that will connect to SignalR and send the notification to the MessageCenter. Paste the code below and update the port # if needed.
var $j = jQuery.noConflict();
var userid = GetUserID();
var connection = $j.connection('http://localhost:8123/channel');
connection.start()
.done(function() {
connection.send('join ' + userid);
connection.received(function (data) {
// Show MessageCenter Message
$messages.registerNotification(data);
console.log('message received, ' + userid + ':' + data);
});
})
.fail(function() {
console.log('error connecting to signalr');
});
function GetUserID() {
var id = Tridion.UI.UserSettings.getJsonUserSettings(true).User["@ID"];
var pos = id.indexOf('-');
id = id.substr(pos + 1);
pos = id.indexOf('-');
id = id.substr(0, pos);
return id;
}Explanation:
We first make a connection to the SignalR server with:
var connection = $j.connection('http://localhost:8123/channel');
The '/channel' in the URL is defined in the global.asax file here:
RouteTable.Routes.MapConnection<MessageCenterChannel>("channel", "/channel",
Then, if we have a connection we send a 'join' message. This is to create our own private channel from the SignalR server to our GUI (and not other users using Tridion at this moment). We use a userid for this, but it could be any unique string.
connection.send('join ' + userid);
On receiving a message from the SignalR Server we call the Tridion GUI method to instantiate the popup:
$messages.registerNotification(data); - Create a new config file in the Signalr4TridionMC folder, Signalr4TridionMC.config, in the Editors '\Signalr4TridionMC' folder. * Note the filenames in the cfg:fileset node.
<?xml version="1.0"?>
<Configuration xmlns="http://www.sdltridion.com/2009/GUI/Configuration/Merge" xmlns:cfg="http://www.sdltridion.com/2009/GUI/Configuration" xmlns:ext="http://www.sdltridion.com/2009/GUI/extensions" xmlns:cmenu="http://www.sdltridion.com/2009/GUI/extensions/ContextMenu">
<resources cache="true">
<cfg:extensiongroups>
<cfg:extensiongroup name="Signalr4TridionMC.Group">
<cfg:extension target="Tridion.Web.UI.Editors.CME.Views.Dashboard">
<cfg:insertafter>Signalr4TridionMC.Resources</cfg:insertafter>
</cfg:extension>
</cfg:extensiongroup>
</cfg:extensiongroups>
<cfg:groups>
<cfg:group name="Signalr4TridionMC.Resources">
<cfg:fileset>
<cfg:file type="script" id="jQuery16">/js/jquery-1.6.4.js</cfg:file>
<cfg:file type="script" id="signalr112">/js/jquery.signalR-1.1.3.js</cfg:file>
<cfg:file type="script" id="notificationMc">/js/notification.js</cfg:file>
</cfg:fileset>
</cfg:group>
</cfg:groups>
</resources>
<definitionfiles/>
<extensions>
<ext:dataextenders/>
<ext:editorextensions />
</extensions>
<commands />
<contextmenus/>
<localization></localization>
<settings>
<defaultpage />
<navigatorurl />
<editurls/>
<listdefinitions/>
<itemicons/>
<theme>
<path></path>
<resourcegroup />
</theme>
<resourceextensions>
<resourceextension>Signalr4TridionMC.Group</resourceextension>
</resourceextensions>
<customconfiguration></customconfiguration>
</settings>
</Configuration>
Note: If the jQuery or SignalrR versions are different, please update the config file paths above. - Open IIS , Add a new Virtual Directory with Alias 'Signalr4TridionMC', in 'SDL Tridion 2011/WebUI/Editors'. The directory is the one created in the previous step.
- Enable the extension in the System.Config (located at \Tridion\web\WebUI\WebRoot\Configuration)
<editor name="Signalr4TridionMC">
<installpath>C:\Program Files (x86)\Tridion\web\WebUI\Editors\Signalr4TridionMC</installpath>
<configuration>Signalr4TridionMC.config</configuration>
<vdir>Signalr4TridionMC</vdir>
</editor> - Flush the browser cache by increasing the modification number in the System.config file For example,
<server version="6.1.0.55920" modification="1"> - Test the GUI Extension
1. Open Tridion
2. Open the SignalR Test Default.aspx in a new tab (we deployed this earlier to IIS at http://localhost:8123/).
3. Type a message in the textbox.
4. Open the Tridion GUI and see if the message appears. If it does then you've successfully set up the SignalR server and Tridion GUI Extension client. Congratulations!
For fun, we could also test out groups:
1.In the SignalR textbox, type 'join 11' (where 11 is your UserID from the Tridion/Administration/User Management, and URI is tcm:0-11-65552). You won't get any response from the GUI or LI items.
Now type '11:Private message' and only that user sees the message. If you have access to 2 user accounts, you should not see it on the other browser. This is the format SignalR uses to send messages to groups (even if you belong to a group of 1).
* If your GUI is empty, double-check your js filenames, your URL to the Signalr4Tridion website, and also your IIS Website name. To see the problem open the javascript console and copy / paste a line like this to your browser to get the real error message: http://localhost/WebUI/Editors/CME/Views/Dashboard/Dashboard_v6.1.0.55920.3_.aspx?mode=js
* Also, try this url, http://localhost:8123/channel. You should receive 'Protocol error: Unknown transport.'. This is a good error message and means everything is fine.
Part 4: Setting up the Event System SignalR .Net client
Now to have some fun sending messages from the Event System with the SignalR .NET Client.
- Create a new Project in Visual Studio, Windows, Class Library - 'EventSystemSignalr'.
Add these references from the Tridion/bin/client folder:
Tridion.Common
Tridion.ContentManager
Tridion.ContentManager.Common
Tridion.ContentManager.Publishin - Install the SignalR client from the NuGet Package Manager Console, View Menu, Other Windows, Package Manager Console):
Install-Package Microsoft.AspNet.SignalR.Client - Create a Handler for the PublishTransaction SaveEventArgs. Here we will connect to the SignalrServer and notify it that a PublishTransaction has finished.
using System;
using Microsoft.AspNet.SignalR.Client;
using Tridion.ContentManager;
using Tridion.ContentManager.ContentManagement;
using Tridion.ContentManager.Extensibility;
using Tridion.ContentManager.Extensibility.Events;
using Tridion.ContentManager.Publishing;
namespace EventSystemSignalr
{
[TcmExtension("Publish Transaction Notification Event Handler")]
public class PublishNotificationHandler : TcmExtension
{
public PublishNotificationHandler()
{
EventSystem.Subscribe<PublishTransaction, SaveEventArgs>(TransactionSaved, EventPhases.TransactionCommitted);
EventSystem.Subscribe<Component, SaveEventArgs>(ComponentSaved, EventPhases.TransactionCommitted);
}
public void TransactionSaved(PublishTransaction transaction, SaveEventArgs args, EventPhases phases)
{
string message = GetMessage(transaction);
if (!String.IsNullOrEmpty(message))
{
var connection = new Connection("http://localhost:8123/channel");
connection.Start().Wait();
int userId = GetUserId(transaction);
connection.Send("join " + userId);
connection.Send(userId + ":" + message);
connection.Stop();
}
}
public void ComponentSaved(Component component, SaveEventArgs args, EventPhases phases)
{
var connection = new Connection("http://localhost:8123/channel");
connection.Start().Wait();
connection.Send("Component " + component.Title + ", hello from SignalR ");
connection.Stop();
}
private int GetUserId(PublishTransaction tridionObject)
{
TcmUri userUri = tridionObject.Creator.Id;
return userUri.ItemId;
}
private string GetMessage(PublishTransaction transaction)
{
string message = "";
if (transaction.State == PublishTransactionState.Success)
{
if (transaction.Items.Count == 1)
{
message = "Publishing " + transaction.Items[0].Title + " completed.";
}
}
else if (transaction.State == PublishTransactionState.Failed)
{
if (transaction.Items.Count == 1)
{
message = "Publishing " + transaction.Items[0].Title + " failed.";
}
}
return message;
}
}
} - Compile and copy the DLLs (including the SignalR and related DLLs) to the Tridion/bin folder. Open the services.msc console, stop these services: Tridion Content Manager Service Host and Tridion Content Manager Publisher and Shut down COM+. Now copy the DLLs.
- Register the new Event System in the Tridion ContentManager.config file located at Tridion\Config. (Tridion 2011 does not require any MMC snap-in for 2011 events). If you get the 'Please check if this file is open in another program' error when saving in your text editor, open your editor as an 'Administrator' and try again.
<eventSystem maxThreadCount="5" threadTimeout="30" threadNamePrefix="EventSystem" />
<extensions>
<add assemblyFileName="C:\Program Files\Tridion\bin\EventSystemSignalr.dll"/>
</extensions> - Test the Event by publishing a page (or pages). The Notification message will show in the Tridion GUI and it will quickly be archived since it is an information message.
Congratulations - no more need of checking and refreshing the Publish Queue - just relax and wait for the good news.
Troubleshooting:
1. Check the Port # in the C# Event System and URL of the SignalR server
2. Go to the SignalR test page on the server (textbox with list items) and see if it raises the message in the Tridion GUI. This confirms the SignalR GUI Extension is working. If that is working then your Event System client is not setup correctly. If your Publisher is on a different machine you will need to deploy the Event System there.
There is also a test event for the ComponentSave that shows a message in the MessageCenter. The intention of this code is to quickly debug SignalR in case your Publisher is on a different machine. You can remove the code calling the ComponentSave event by commenting out this line: 'EventSystem.Subscribe<Component'
Summary
SignalR is the magic that helps our backend .Net CMS talk to the frontend JavaScript GUI - something that was very difficult to do before SignalR. We can now show users informative messages from Templates, Event System or a Custom Resolver and improve our system usability.
Taking it further
Thats about as far as we have time for now, here are some brief thoughts on how you could take this further:
- Extend the example to allow you to show an error/warning/notification depending on whether your transaction failed/warned/completed successfully. We could just extend the message format from {userid}:{message} to {userid}:{messagetype}:{message}
- Hook notifications into templates - if you want you could use similar code in a .NET TBB. We tried this, but noticed that to get it working you need to have the SignalR client dll in the GAC - which involved downloading the source and recompiling with a strong naming key.
- Make your service tidy up the connections. Currently the groups created in your service will just add more and more connections, but never clear them down. You should track the connections and the groups they go it, and ensure when a connection is closed you remove it from the corresponding group.
- Hook notifications into tridion groups - by altering the group concept and using the Tridion API to work out what groups a user is in you could broadcast notifications to a whole set of users. In this way you could create a notify reviewers button on the staging website.
- Persist the notifications. Of course, if you happen to close your browser before the publish transaction is complete, you will never get the notification, as you happened to be offline when it was broadcast. By persisting the notifications in a queue, we could wait until there are active connections for a user, before broadcasting the notifications.