SignalR started off as an OSS side project by Damien Edwards and David Fowler at Microsoft but soon it snowballed into a popular abstraction for persistent connections in .NET. With the release of Visual Studio Web Update 2012.2, SignalR was formally rolled into a part of ASP.NET and is now an open source component that is fully supported by Microsoft.
In the past, we have explored interesting ways to use SignalR here on DotNetCurry. Our first SignalR post demonstrated how to use SignalR with the excellent Diff-Patch-Match library by Neil Fraser and build a collaborative ‘Review’ functionality where two people or more people can review the same post and view changes made by each other, in real time.
With the release of Windows 8 and Office 2013 came One Note that had similar real-time ‘review’ functionality.
Today we will use SignalR, ASP.NET MVC’s Hot Towel application template and the JavaScript Diff-Patch-Match library to build a single page application that will broadcast changes to a ‘Note’ to all who are watching it.
The Concept
SignalR as we know helps us maintain persistent connections between a server and a client. The Server is typically an IIS hosted ASP.NET application, but it could very well be a Katana hosted or Mono Hosted non-IIS server. There is a wide array of clients supported, some official, some community built. The JavaScript client is official. Today we are going to use it with the JavaScript client.
Our application will be a Note taking application where users can share the URL of their note with others who can then watch changes being made in real time.
Hot Towel and Single Page Apps
The Hot Towel application template was created by noted .NET Community member John Papa. If you are a subscriber to the DNC Magazine you would have got a chance to read through Fanie Reynders excellent introduction to the Hot Towel SPA article. If not, we strongly suggest you go through it. We’ll cover only data access here today. We leave navigation and routing for another day.
The Hot Towel Template
This Visual Studio template was created by John Papa to provide all the moving pieces required to build a complete Single Page application in one template. To understand ‘All Pieces’ let’s look at this diagram that I snagged from John Papa’s hour long tutorial at the DotNetConf. You can see the full video on YouTube here.
As you can see, John has highlighted the features of each SPA template currently available. His idea behind Hot Towel was to have something that you can get up and running with quickly without having to ‘delete’ anything in the template. In that respect, all the templates in blue have Sample implementation showing a simple ToDo app. So if you want to use any of those templates, you have to take the code for the ToDo app out first. However the templates in Orange are empty templates with all the ingredients in and ready to start as in you don’t have to delete anything to get started.
So Hot Towel template essentially uses Knockout for Data binding, provides Rich Data support via BreezeJS, has Navigation and History built in and is based off DurandalJS SPA framework by Rob Eisenberg.
The SignalR Application Setup
To keep things simple today, we’ll add a single Note to the Database via SQL query; the Note will store three pieces of information
- Name
- Note Content
- Date Updated
For brevity, we’ll ignore Authentication and Authorization for now. The data will be stored in SQL Server LocalDB and we’ll use Entity Framework for persisting data.
Getting Started
We start off with an ASP.NET MVC 4 Web Application using .NET Framework 4.5 and select the “Hot Towel Single Page Application” project template.
Once the project is setup, let’s spend a few minutes understanding the project layout.
The Project Layout
As seen in the image below, the most apparent ‘new’ folder is the App folder that is not usually associated with a normal MVC Project.
The App Folder
The App folder for an SPA application is basically where your client side logic lies. It’s like a project inside a project. It contains the ViewModel (JavaScript), the Views (html) as well as Services like logging, DataContext and initialization. Note that almost all the ViewModel JS files have a corresponding view. Nav and Footer are two sub-views who use the view model of their parent that is shell.js.
The Durandal subfolder contains its dependencies, composition code, plugins and other bootstrap code that we can use as-is for the moment.
The App_Start Folder
The App_Start folder has the additional BreezeWebApiConfig.cs that uses the MVC WebActivator hooks to initialize a BreezeApi route. The HotTowelConfig.cs Registers all the Bundles whereas the HotTowelRouteConfig registers the HotTowel Route as the default by inserting it before the MVC Route. (The WebActivator kicks in before the Application_Start is invoked).
public static void RegisterHotTowelPreStart() {
// Preempt standard default MVC page routing to go to HotTowel Sample
System.Web.Routing.RouteTable.Routes.MapRoute(
name: "HotTowelMvc",
url: "{controller}/{action}/{id}",
defaults: new
{
controller = "HotTowel",
action = "Index",
id = UrlParameter.Optional
}
);
}
The First Run
With this rudimentary idea of what the various project components are, let’s change the App\Views\nav.html’s header from HotTowel SPA to CollaboratR. Run the Application. You should see a view similar to the following:
Including SignalR
Now that we have our SPA going, let’s include SignalR in the mix and test it out.
Installing SignalR
1. In the Package Manager Console type in the following to install SignalR.
PM> install-package Microsoft.Aspnet.SignalR
2. Once all the dependencies and the SignalR library is installed, add the highlighted code to the beginning of the RegisterHotTowelPreStart method in the App_Start\HotTowelRouteConfig.cs Application_Start in the Global.asax to call
public static void RegisterHotTowelPreStart()
{
System.Web.Routing.RouteTable.Routes.MapHubs();
// Preempt standard default MVC page routing to go to HotTowel Sample
System.Web.Routing.RouteTable.Routes.MapRoute(
name: "HotTowelMvc",
url: "{controller}/{action}/{id}",
defaults: new
{
controller = "HotTowel",
action = "Index",
id = UrlParameter.Optional
}
);
}
3. Next we create a bundle for the jquery.signalR script and add it to the bundle collection. Update the code in AppStart\BundleConfig to add the following bundle
bundles.Add(new ScriptBundle("~/scripts/signalr").Include(
"~/Scripts/jquery.signalR-{version}.js"));
4. For ease of use, we’ll add the script references to the index.cshtml but you can put it in any sub-page if you want to. At the bottom of the page add the following (the sequence is important)
@Scripts.Render("~/scripts/signalr")
<script src="~/signalr/hubs"></script>
5. Next we’ll add a SignalR Hub file to the Controllers folder.
This adds the following Hub class
public class CollaboratorHub : Hub
{
public void Hello()
{
Clients.All.hello("Ping " + DateTime.Now.ToString());
}
}
We have updated the message being sent out from “Hello” to “Ping “ + DateTime.Now.ToString() so that we can some easily distinguishable changes on the client when a new message is sent out.
Updating View Model for a test run
To test SignalR out, we will update the home.js and home.html. We need to do make the following changes
- Update the ViewModel with a sendMessageAction property that’s bound to the click event of a button on the view, and a messageRecieved property that’s bound to a label in the view.
var vm = {
activate: activate,
title: 'All Notes',
sendMessageAction: sendMessage,
messageReceived: ko.observable()
};
- Initialize the SignalR client and implement the sendMessage method
function sendMessage()
{
logger.log("sending message to server");
sig.server.hello();
}
var sig = $.connection.collaboratorHub;
$.connection.hub.start().done(function ()
{
vm.sendMessageAction;
});
- Finally we implement a function to receive the ‘Hello’ ping from the server and update the ViewModel
sig.client.hello = function (what)
{
vm.messageReceived(what);
logger.log('Server says: ' + what, null, 'home', true);
}
return vm;
Updating View for the test run
We add a Button whose click event is bound to the sendMessageAction in the ViewModel and a label to show the ping message from the server.
<div class="container">
<button class="btn" data-bind="click: sendMessageAction">
Send a Hello to All clients
</button>
<label data-bind="text: messageReceived">
No Messages Received
</label>
</div>
The Test Run
Now run the application and open it in two browsers. Click on the Send Message… button and see the same message being updated on both clients
Cool! SignalR is working along with the Hot Towel framework. Let’s move on to our real requirement.
The Implementation
We start off by creating our Data Entity - Note and the required plumbing to Save and Retrieve it from database. Once that is done, we’ll update the Home.html to show the notes. Here the Text field will be an editor where we’ll do the SignalR magic to show updates on multiple browsers in Real-Time.
The Data Layer
1. In the Models folder we add a new class called Note with the following properties.
public class Note
{
public int Id { get; set; }
public string Name { get; set; }
public string Text { get; set; }
public DateTime Updated { get; set; }
}
2. Next we add another class in the Models folder called CollaboratrDbContext. This is the context class
public class CollaboratrDbContext : DbContext
{
public CollaboratrDbContext()
: base(nameOrConnectionString: "CollaboratRDbConnection")
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
Database.SetInitializer<CollaboratrDbContext>(
new DropCreateDatabaseIfModelChanges<CollaboratrDbContext>());
}
public DbSet<Note> Notes { get; set; }
}
3. Finally we add the following connection string in the web.config’s <connectionString> section.
<add name=" CollaboratRDbConnection"
connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=CollaboratRDb;Integrated
Security=SSPI;AttachDBFilename=|DataDirectory|\CollaboratRDb.mdf"
providerName="System.Data.SqlClient" />
With this our DataLayer is setup. Now to setup the Services.
The Web API Service
Now that our Data entity and DBContext is setup, time to setup the service that will retrieve all the data as well as save new Data.
The BreezeController
In the Controllers folder, we add a new class called BreezeController as follows and decorate it with an attribute [Breeze.WebApi.BreezeController]. In the class below, we see an EFContextProvider class which is essentially a wrapper for dealing with JSon inputs. Notice the super compact implementation where we have only SaveChanges method for save new or updated Notes. Other two methods are for fetching MetaData or an IQueryable for all the Notes in the system.
[Breeze.WebApi.BreezeController]
public class BreezeController : ApiController
{
readonly EFContextProvider<CollaboratrDbContext> _contextProvider =
new EFContextProvider<CollaboratrDbContext>();
[HttpGet]
public string Metadata()
{
return _contextProvider.Metadata();
}
[HttpPost]
public SaveResult SaveChanges(JObject saveBundle)
{
return _contextProvider.SaveChanges(saveBundle);
}
[HttpGet]
public IQueryable<Note> Notes()
{
return _contextProvider.Context.Notes;
}
}
This sets up the Server side. Over to the client.
Updating the Client
We start with the Home.js file and update the ViewModel as follows:
1. We declare the vm as an empty object. In the define method, we pass the logger, the router and the app path for reference.
2. Next we declare the service path in the serviceName variable. This corresponds to the BreezeController web API Controller we had defined earlier.
3. We then initialize the ViewModel vm with the notes, title, selectedNote and activate methods.
var vm = {};
define(['services/logger', 'durandal/app', 'durandal/plugins/router'], function (logger)
{
// service name is route to the Web API controller
var serviceName = 'api/Breeze';
// manager is the service gateway and cache holder
var manager = new breeze.EntityManager(serviceName);
// define the viewmodel
vm = {
notes: ko.observableArray(),
show: ko.observable(false),
title: 'My Notes',
selectedNote: ko.observable(),
activate: activate
};
4. Next we have the activate method which in turn queries the database and gets the data for us
function activate()
{
return getNotes();
}
return vm;
//#region private functions
// get Notes asynchronously
// returning a promise to wait for
function getNotes()
{
logger.log("querying Notes");
var query = breeze.EntityQuery.from("Notes");
return manager
.executeQuery(query)
.then(querySucceeded)
.fail(queryFailed);
// reload vm.notes with the results
function querySucceeded(data)
{
logger.log("queried Notes");
vm.notes(data.results);
vm.show(true); // show the view
}
};
function queryFailed(error)
{
logger.log("Query failed: " + error.message);
}
});
This concludes the internal methods. We’ll revisit the home.js to update the SignalR logic, but first we’ll update the UI in home.html
1. The HTML is rather simple. It uses KO’s data binding syntax to bind the list of notes populated in the ViewModel.
<section>
<h2 class="page-title" data-bind="text: title"></h2>
</section>
<div>
<ul data-bind="foreach: notes">
<li style="list-style: none">
<div style="border: solid 1px gray; padding:10px">
<h1>
<label data-bind="text: Name"></label>
</h1>
<label data-bind="text: Updated"></label>
<br />
<textarea data-bind="value: Text" class="note-text" rows="5" style="width:90%"></textarea><br />
</div>
</li>
</ul>
</div>
2. Updating client to use SignalR and Diff Match Patch library. We can install the Diff Match Patch library we need from Nuget as follows
PM> Install-Package Google.DiffMatchPatch.Js
3. Once installed, we update the BundleConfig.cs to include the above JS file. Since the SignalR bundle is already included in the Shell, we don’t need to do anything special.
bundles.Add(new ScriptBundle("~/scripts/signalr").Include(
"~/Scripts/jquery.signalR-{version}.js",
"~/Scripts/diff_match_patch.js"));
We update the Home.js again as follows:
var sig = $.connection.collaboratorHub;
$.connection.hub.start();
$(document).on('keyup', '.note-text', null, function ()
{
var note = ko.dataFor(this);
vm.selectedNote = note;
sig.server.patch(this.value);
});
sig.client.patch = function (newText)
{
var dmp = new diff_match_patch();
dmp.Match_Distance = 1000;
dmp.Match_Threshold = 0.5;
dmp.Patch_DeleteThreshold = 0.5;
var source = vm.notes()[0].Text();
var patches = dmp.patch_make(source, newText);
var dest = "";
var results = dmp.patch_apply(patches, source);
vm.notes()[0].Text(results[0]);
}
4. Finally we need the corresponding method in the Hub class, so we update the CollaboratoRHub.cs to get the updatedText as an input parameter. It is then sent back to all other connections except for the sender. The ‘Others’ dynamic property on which we are calling the ‘Patch’ ensures that SIgnalR doesn’t call back the client from where the change originated.
public void Patch(string updatedText)
{
Clients.Others.Patch(updatedText);
}
With the Diff and Patch logic in, we are ready to roll. First we insert a row of data in our database
Next we run the app.
We open a second browser with the same URL. Now as we update one Note, the other one gets updated. Proof that the diff-match-patch is actually patching, is by typing in both the screens to see changes being merged at both ends.
Awesome right? Well that’s a wrap for today.
Conclusion
Today we saw a mashup created using the Hot Towel Single Page App template for ASP.NET MVC4, along with SignalR and a nifty little Diff-Match-Patch library, to get realtime and concurrent updates to a document.
We also saw how to retrieve data from a database in the Hot Towel SPA template. Will anyone of you be interested to see how to do complete CRUD operations and upgrade the simple text editor to a Rich Text Editor. If yes, let me know in the comments section.
Download the entire source code of this article (Github)
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!
Was this article worth reading? Share it with fellow developers too. Thanks!
Suprotim Agarwal, MCSD, MCAD, MCDBA, MCSE, is the founder of
DotNetCurry,
DNC Magazine for Developers,
SQLServerCurry and
DevCurry. He has also authored a couple of books
51 Recipes using jQuery with ASP.NET Controls and
The Absolutely Awesome jQuery CookBook.
Suprotim has received the prestigious Microsoft MVP award for Sixteen consecutive years. In a professional capacity, he is the CEO of A2Z Knowledge Visuals Pvt Ltd, a digital group that offers Digital Marketing and Branding services to businesses, both in a start-up and enterprise environment.
Get in touch with him on Twitter @suprotimagarwal or at LinkedIn