Building a Collaborative Drawing Application - Pro ASP.NET SignalR: Real-Time Communication in .NET with SignalR 2.1 (2014)

Pro ASP.NET SignalR: Real-Time Communication in .NET with SignalR 2.1 (2014)

Chapter 10. Building a Collaborative Drawing Application

This chapter develops a collaborative drawing application with in-depth detail. For this application, there are application requirements that need to be met, as follows:

· Interactively draw in real time

· Chat in real time

· Restrict access to authenticated users

· Scale easily on demand

The project will consist of a server that will be able to support multiple types of clients. Hosting of the project will be done in Microsoft Azure because it has all the necessary PaaS to support all the required features. To ensure that the server can scale the project, we will be able to run as multiple instances with load balancing and use Azure Redis Cache as a backplane.

Project Overview

In this project, we will create a collaborative chat and drawing application. The application lets you create your own canvas/chat room, which we’ll refer to later in the code as a canvas room. Inside the canvas room, you’ll have a canvas and chat session that is shared with only the people present in the canvas room. You will see people who join and leave the canvas room in the users online section.

An important thing to note about the canvas room is the way to draw. You have your choice of one of four brush actions that must be selected: brush, eraser, fill, and clear all. There is also a choice of eight colors and sizes. None of the drawing actions occurs until you click on the canvas. The color is important only if you’re using the brush or fill, and the size is important only if you are using the brush or the eraser.

Image Note Throughout this chapter, we often describe a large class, section by section, to give you a breakdown of how it works. We then add the complete class to the project as a single step to ensure that the step-by-step instructions are clear and easy to follow.

Now that we have gone over the requirements of the project, let’s get started by developing the server.

Developing the Server

The server in this project will be the main hub for all the clients. We will go over the following components and technologies and show how they are used:

· SignalR

· Web API

· OWIN hosting

· OWIN security cookies

· Unity

· Azure Cloud Services (worker role)

· Microsoft SQL database

· Azure Redis Cache

As we go through each component or technology, we will add it into our server solution. To keep the naming simple, we’ll use GroupBrush as the solution name and prefix for the projects. There will be six projects in the solution: BL, DL, Entity, Web, Cloud, and Worker. To get the server development started we follow these steps to set up the solution and projects:

1. Create a new solution named GroupBrush using the Blank Solution template.

2. Add a class library project named GroupBrush.Web.

3. Add a class library project named GroupBrush.BL.

4. Add a class library project named GroupBrush.DL.

5. Add a class library project named GroupBrush.Entity.

6. Remove any unneeded Class1.cs files that were added with the class libraries.

7. Add a new Windows Azure Cloud Service project named GroupBrush.Cloud and a worker role that will create a project that should be named GroupBrush.Worker.

Now that we have the projects added to our solution, let’s quickly go over what each is used for. The GroupBrush.Web project contains the OWIN components and Unity. GroupBrush.BL contains the business logic encapsulated into services. Many of those services will depend on the next project, GroupBrush.DL, which contains the database logic. The Web, BL, and DL projects all reference their entity objects from the GroupBrush.Entity project. The GroupBrush.Cloud project is the project for cloud configuration, and the GroupBrush.Workerproject will host the application in a Worker role on Azure.

Now that we have created the solution and have a general idea of what each project is, we can get started going over the components and the related code. The first component is SignalR, which enables the server to provide real-time interactivity.

Enabling Real-Time Interactivity Using SignalR

Real-time interactivity is the critical requirement that makes the project attractive. We add this interactivity by using a SignalR Hub that will provide the real-time connection between the server and client.

We’ll add a hub to our project by creating a class named CanvasHub that derives from the Hub class, using the definition shown in Listing 10-1. We will add this class to the project shortly.

Listing 10-1. CanvasHub Class Definition

public class CanvasHub : Hub

To add some extra functionality to our class, we’ll inject a couple of services into the class constructor that will provide user and canvas services. The rest of the CanvasHub class will consist of a couple of helper functions, connection events, and server-side methods.

Before implementing the SignalR code, the projects folder structure is implemented by following these steps:

1. Run the following command from the Package Manager Console for the Default package of GroupBrush.Web and GroupBrush.Worker:

Install-Package Microsoft.AspNet.SignalR.Core

2. Add a solution folder named Hubs to the GroupBrush.Web project.

3. Add a solution folder named Users to the GroupBrush.BL and GroupBrush.DL projects.

4. Add a solution folder named Canvases to the GroupBrush.BL and GroupBrush.DL projects.

5. Add a solution folder named Storage to the GroupBrush.BL project.

With the structure and dependencies out of the way, we’ll look first at the constructor in Listing 10-2. The constructor is very simple; it takes the IUserService and ICanvasRoomService parameters and assigns them to their respective private variables.

Listing 10-2. Constructor and Private Variables of the CanvasHub Class

private IUserService _userService;
private ICanvasRoomService _canvasRoomService;
public CanvasHub(IUserService userService, ICanvasRoomService canvasRoomService)
{
_userService = userService;
_canvasRoomService = canvasRoomService;
}

The constructor’s first dependency is the IUserService shown in Listing 10-3, which provides the signatures for user services. These services provide the creation of a user, validation of a user login, and retrieval of a user’s name from an ID. The hub uses only theGetUserNameFromId method in this interface.

Listing 10-3. IUserService Interface

public interface IUserService
{
int? CreateAccount(string userName, string password);
bool ValidateUserLogin(string userName, string password, out int? userId);
string GetUserNameFromId(int id);
}

The second dependency is the ICanvasRoomService interface that provides canvas room services shown in Listing 10-4. The services provided by this interface allow brush actions performed by the user to be added to the canvas room, data retrieval to synchronize to a last known command position, and ability to add/remove users from the canvas room.

Listing 10-4. ICanvasRoomService Interface

public interface ICanvasRoomService
{
CanvasBrushAction AddBrushAction(string canvasId, CanvasBrushAction brushData);
CanvasSnapshot SyncToRoom(string canvasId, int currentPosition);
void AddUserToCanvas(string canvasId, string id);
void RemoveUserFromCanvas(string canvasId, string id);
}

The implementation for these services will be added later in the chapter. Next, add the interfaces with these steps:

1. Add an interface named IUserService to the GroupBrush.BL project in the Users folder with the contents of Listing 10-3.

2. Add an interface named ICanvasRoomService to the GroupBrush.BL project in the Canvases folder with the contents of Listing 10-4.

After the CanvasHub class constructor, there are two helper methods, two events, and four server-side methods in this class that we will discuss before adding them into our project. The two helper methods are GetCanvasIdFromQueryString and GetUserNameFromContext. They are added so that common code specific to the CanvasHub class is not repeated in multiple places in the class.

The first helper method, GetCanvasIdFromQueryString shown in Listing 10-5, looks in the query string for the key canvasid; if it is found and is valid, Guid will return that canvasid.

Listing 10-5. GetCanvasIdFromQueryString Helper Method

private string GetCanvasIdFromQueryString()
{
Guid validationGuid = Guid.Empty;
string groupId = Context.QueryString["canvasid"];
if (!string.IsNullOrWhiteSpace(groupId) && Guid.TryParse(groupId,out validationGuid))
{
return groupId;
}
throw new ArgumentException("Invalid Canvas Id");
}

The second helper method, GetUserNameFromContext shown in Listing 10-6, gets the username of the current user from the request. This is accomplished by retrieving the ID from Context.Request.User.Identity.Name, which is the current request’s identity objectName property that is populated by an OWIN security middleware (discussed later in the chapter). Once this ID is obtained, it is passed into the IUserService to look up a username for that ID. If the name is found, it is returned; otherwise, the method returns an empty string for the username.

Listing 10-6. GetUserNameFromContext Helper Method

private string GetUserNameFromContext()
{
string strUserId = Context.Request.User.Identity.Name;
int userId = 0;
if (int.TryParse(strUserId, out userId))
{
return _userService.GetUserNameFromId(userId);
}
return string.Empty;
}

Next are the OnConnected and OnDisconnected events shown in Listing 10-7 and 10-8, respectively. These events override the base class events and have three main actions: to add or remove the user from the canvas, to add or remove the connection ID from the canvas group, and to notify all canvas group members of the user connecting or disconnecting.

Listing 10-7. OnConnected Event

public override Task OnConnected()
{
_canvasRoomService.AddUserToCanvas(GetCanvasIdFromQueryString(), GetUserNameFromContext());
Groups.Add(Context.ConnectionId.ToString(), GetCanvasIdFromQueryString());
Clients.Group(GetCanvasIdFromQueryString()).UserConnected(GetUserNameFromContext());
return base.OnConnected();
}

Listing 10-8. OnDisconnected Event

public override Task OnDisconnected(bool stopCalled)
{
_canvasRoomService.RemoveUserFromCanvas(GetCanvasIdFromQueryString(), GetUserNameFromContext());
Groups.Remove(Context.ConnectionId.ToString(), GetCanvasIdFromQueryString());
Clients.Group(GetCanvasIdFromQueryString()).UserDisconnected(GetUserNameFromContext());
return base.OnDisconnected(stopCalled);
}

The last parts of the CanvasHub class are the four server-side methods, which are methods that are publicly exposed to the SignalR clients.

MoveCursor, shown in Listing 10-9, takes two parameters: the cursor x and y values of the current user. The method sends a client message of MoveOtherCursor to all the members of the canvas group with the current user’s x and y cursor coordinates and username.

Listing 10-9. MoveCursor Method

public void MoveCursor(double x, double y)
{
Clients.Group(GetCanvasIdFromQueryString()).MoveOtherCursor(GetUserNameFromContext(), x, y);
}

The second method, SendChatMessage shown in Listing 10-10, takes one parameter: the message to send. The method prepends the username and a colon to the message and sends it to all the members of the canvas group by the UserChatMessage client method.

Listing 10-10. SendChatMessage Method

public void SendChatMessage(string message)
{
Clients.Group(GetCanvasIdFromQueryString()).UserChatMessage(GetUserNameFromContext() + ": " + message);
}

The third method, SendDrawCommand shown in Listing 10-10, takes one parameter, CanvasBrushAction, which is defined in Listing 10-12. It contains the brush information and the affected positions of the brush, as defined in the Position class in Listing 10-13. The first step of this method is to save the information provided by the parameter by calling the AddBrushAction method on the ICanvasRoomService service. Once the information is saved, we can send the parameter information to all the canvas group users by using theDrawCanvasBrushAction client method.

Listing 10-11. SendDrawCommand Method

public void SendDrawCommand(CanvasBrushAction brushData)
{
CanvasBrushAction canvasBrushAction = _canvasRoomService.AddBrushAction(GetCanvasIdFromQueryString(), brushData);
Clients.Group(GetCanvasIdFromQueryString()).DrawCanvasBrushAction(canvasBrushAction);
}

Listing 10-12. CanvasBrushAction Entity Class

public class CanvasBrushAction
{
public int Sequence { get; set; }
public Int64 ClientSequenceId { get; set; }
public int Type { get; set; }
public string Color { get; set; }
public int Size { get; set; }
public List<Position> BrushPositions { get; set; }
}

Listing 10-13. Position Entity Class

public class Position
{
public double X { get; set; }
public double Y { get; set; }
}

The last method, SyncToRoom shown in Listing 10-14, takes in one parameter: the client’s last known drawing history position. It returns all the canvas brush data as a CanvasSnapshot, defined in Listing 10-15, from the value of the position parameter to the latest entry in storage. The information is retrieved by using the SyncToRoom method on the ICanvasRoomService service and is returned only to the calling user.

Listing 10-14. SyncToRoom Method

public CanvasSnapshot SyncToRoom(int currentPosition)
{
return _canvasRoomService.SyncToRoom(GetCanvasIdFromQueryString(), currentPosition);
}

Listing 10-15. CanvasSnapshot Entity Class

public class CanvasSnapshot
{
public string CanvasName { get; set; }
public string CanvasDescription { get; set; }
public List<string> Users { get; set; }
public List<CanvasBrushAction> Actions { get; set; }
}

The CanvasHub class, shown in Listing 10-16, is the main SignalR class for our project. It will provide the real-time functionality for the drawing, chatting, and online user state of the canvas room.

Listing 10-16. Complete CanvasHub Class

public class CanvasHub : Hub
{
private IUserService _userService;
private ICanvasRoomService _canvasRoomService;
public CanvasHub(IUserService userService, ICanvasRoomService canvasRoomService)
{
_userService = userService;
_canvasRoomService = canvasRoomService;
}
private string GetCanvasIdFromQueryString()
{
Guid validationGuid = Guid.Empty;
string groupId = Context.QueryString["canvasid"];
if (!string.IsNullOrWhiteSpace(groupId) && Guid.TryParse(groupId,out validationGuid))
{
return groupId;
}
throw new ArgumentException("Invalid Canvas Id");
}
private string GetUserNameFromContext()
{
string strUserId = Context.Request.User.Identity.Name;
int userId = 0;
if (int.TryParse(strUserId, out userId))
{
return _userService.GetUserNameFromId(userId);
}
return string.Empty;
}
public override Task OnConnected()
{
_canvasRoomService.AddUserToCanvas(GetCanvasIdFromQueryString(), GetUserNameFromContext());
Groups.Add(Context.ConnectionId.ToString(), GetCanvasIdFromQueryString());
Clients.Group(GetCanvasIdFromQueryString()).UserConnected(GetUserNameFromContext());
return base.OnConnected();
}
public override Task OnDisconnected(bool stopCalled)
{
_canvasRoomService.RemoveUserFromCanvas(GetCanvasIdFromQueryString(), GetUserNameFromContext());
Groups.Remove(Context.ConnectionId.ToString(), GetCanvasIdFromQueryString());
Clients.Group(GetCanvasIdFromQueryString()).UserDisconnected(GetUserNameFromContext());
return base.OnDisconnected(stopCalled);
}
public void MoveCursor(double x, double y)
{
Clients.Group(GetCanvasIdFromQueryString()).MoveOtherCursor(GetUserNameFromContext(), x, y);
}
public void SendChatMessage(string message)
{
Clients.Group(GetCanvasIdFromQueryString()).UserChatMessage(GetUserNameFromContext() + ": " + message);
}
public void SendDrawCommand(CanvasBrushAction brushData)
{
CanvasBrushAction canvasBrushAction = _canvasRoomService.AddBrushAction(GetCanvasIdFromQueryString(), brushData);
Clients.Group(GetCanvasIdFromQueryString()).DrawCanvasBrushAction(canvasBrushAction);
}
public CanvasSnapshot SyncToRoom(int currentPosition)
{
return _canvasRoomService.SyncToRoom(GetCanvasIdFromQueryString(), currentPosition);
}
}
Now that we have discussed the sections that make up the CanvasHub class, they need to be added to our project by following these steps:

1. Add a class named CanvasBrushAction to the GroupBrush.Entity project with the contents of Listing 10-12.

2. Add a class named Position to the GroupBrush.Entity project with the contents of Listing 10-13.

3. Add a class named CanvasSnapshot to the GroupBrush.Entity project with the contents of Listing 10-15.

4. Add a class named CanvasHub to the GroupBrush.Web project under the Hubs folder with the contents of Listing 10-16.

The services for CanvasHub provide the internal workings that allow the canvas room and user data to be retrieved and persisted. (We’ll go over user service implementation in the next section and focus on the canvas data in this section.) ICanvasRoomService is implemented in this project as a class named CanvasRoomService, as shown in Listing 10-17.

Listing 10-17. CanvasRoomService Class Definition

public class CanvasRoomService : ICanvasRoomService

CanvasRoomService needs additional dependencies that will be injected into the constructor, as seen in Listing 10-18, as it was with CanvasHub. The dependencies are represented by the IMemStorage and IGetCanvasDescriptionData interfaces, which are defined inListings 10-19 and 10-20, respectively. As with the constructor in CanvasHub, the dependencies will be assigned to local variables in the constructor.

Listing 10-18. Constructor and Private Variables for CanvasRoomService

IMemStorage _ memStorage;
IGetCanvasDescriptionData _getCanvasDescriptionData;
public CanvasRoomService(IMemStorage memStorage, IGetCanvasDescriptionData getCanvasDescriptionData)
{
_ memStorage = memStorage;
_getCanvasDescriptionData = getCanvasDescriptionData;
}

Listing 10-19. IMemStorage Interface

public interface IMemStorage
{
CanvasBrushAction AddBrushAction(string canvasId, CanvasBrushAction brushData);
List<CanvasBrushAction> GetBrushActions(string canvasId, int currentPosition);
List<string> GetCanvasUsers(string canvasId);
void AddUserToCanvas(string canvasId, string id);
void RemoveUserFromCanvas(string canvasId, string id);
string GetUserName(int id);
void StoreUserName(int id, string userName);
}

Listing 10-20. IGetCanvasDescriptionData Interface

public interface IGetCanvasDescriptionData
{
CanvasDescription GetCanvasDescription(Guid canvasId);
}

Listing 10-21 shows that the logic for most of the methods of the CanvasRoomService is to call the corresponding method on the IMemStorage dependency. The exception to this is the SyncToRoom method, which calls the IGetCanvasDescriptionData dependency to retrieve a CanvasDescription object (defined in Listing 10-22), and the IMemStorage dependency to retrieve the canvas data. The data from these three calls are combined into one class and returned. The storage logic is fairly abstracted from this class because we want to provide two different storage implementations: an in-memory storage that works for only one instance and a Redis-based memory storage that allows scaling to multiple instances.

Listing 10-21. Complete CanvasRoomService Class

public class CanvasRoomService : ICanvasRoomService
{
IMemStorage _memStorage;
IGetCanvasDescriptionData _getCanvasDescriptionData;
public CanvasRoomService(IMemStorage memStorage, IGetCanvasDescriptionData getCanvasDescriptionData)
{
_memStorage = memStorage;
_getCanvasDescriptionData = getCanvasDescriptionData;
}
public CanvasBrushAction AddBrushAction(string canvasId, CanvasBrushAction brushData)
{
return _memStorage.AddBrushAction(canvasId, brushData);
}
public CanvasSnapshot SyncToRoom(string canvasId, int currentPosition)
{
CanvasDescription canvasDescription = _getCanvasDescriptionData.GetCanvasDescription(Guid.Parse(canvasId));
List<CanvasBrushAction> actions = new List<CanvasBrushAction>();
actions = _memStorage.GetBrushActions(canvasId, currentPosition);
List<string> users = new List<string>();
users = _memStorage.GetCanvasUsers(canvasId);
return new CanvasSnapshot() { Users = users, Actions = actions, CanvasName = canvasDescription.Name, CanvasDescription = canvasDescription.Description };
}
public void AddUserToCanvas(string canvasId, string id)
{
_memStorage.AddUserToCanvas(canvasId, id);
}

public void RemoveUserFromCanvas(string canvasId, string id)
{
_memStorage.RemoveUserFromCanvas(canvasId, id);
}
}

Listing 10-22. CanvasDescription Entity Class

public class CanvasDescription
{
public string Name { get; set; }
public string Description { get; set; }
}

Before we go into more detail about that, let’s catch up with what we have done so far:

1. Added an interface named IMemStorage to the GroupBrush.BL project under the Storage folder with the contents of Listing 10-19.

2. Added an interface named IGetCanvasDescriptionData to the GroupBrush.DL project under the Canvases folder with the contents of Listing 10-20.

3. Added a class named CanvasRoomService to the GroupBrush.BL project under the Canvases folder with the contents of Listing 10-21.

4. Added a class named CanvasDescription to the GroupBrush.Entity project with the contents of Listing 10-22.

There are two possible implementations for the IMemStorage interface, depending on the number of instances we want to deploy. We discuss the in-memory solution here and the Redis implementation in the “Scaling the Server” section later in the chapter.

The first requirement for this class, which we call MemoryStorage, is to implement the IMemStorage, as shown in Listing 10-23. Unlike most of the other classes we have covered so far, this class does not have any dependencies injected in the constructor. However, it does have four ConcurrentDictionary collections that we will use to persist the canvas room data (see Listing 10-24).

Listing 10-23. MemoryStorage Class Definition

public class MemoryStorage : IMemStorage

Listing 10-24. MemoryStorage Private Variables

private ConcurrentDictionary<string, int> canvasTransactions = new ConcurrentDictionary<string, int>();
private ConcurrentDictionary<string, ConcurrentBag<CanvasBrushAction>> canvasActions = new ConcurrentDictionary<string, ConcurrentBag<CanvasBrushAction>>();
private ConcurrentDictionary<string, ConcurrentDictionary<string,string>> canvasUsers = new ConcurrentDictionary<string, ConcurrentDictionary<string,string>>();
private ConcurrentDictionary<int, string> userNames = new ConcurrentDictionary<int, string>();

The class contains seven public methods that satisfy the IMemStorage interface contract. Two methods, AddBrushAction and GetBrushActions, are used specifically for canvas room data; three methods are for users in a canvas room; and two methods are for username data.

The AddBrushAction method shown in Listing 10-25 stores client brush actions for a specific canvas room into memory. For drawings to look correct, we need to ensure that brush actions are applied in order. To ensure this order, we need to store the sequence as a unique number for each CanvasBrushAction. So in the top of the method, we check to see whether we have a canvasTransactions entry for the canvas ID. If there is no existing entry, we add one, starting at 0.

Listing 10-25. AddBrushAction Method

public CanvasBrushAction AddBrushAction(string canvasId, CanvasBrushAction brushData)
{
if (!canvasTransactions.ContainsKey(canvasId))
{
canvasTransactions[canvasId] = 0;
}
int transactionNumber = canvasTransactions[canvasId] = canvasTransactions[canvasId]++;
if (!canvasActions.ContainsKey(canvasId))
{
canvasActions[canvasId] = new ConcurrentBag<CanvasBrushAction>();
}
brushData.Sequence = transactionNumber;
canvasActions[canvasId].Add(brushData);
return brushData;
}

We take the entry, increment it by 1, and use it as the transactionNumber variable. Now that we have a transaction, we have to see whether there is a canvasActions entry to store the client brush actions. If there is no entry, we add a new entry ofConcurrentBag<CanvasBrushAction> to the collection.

Next we add transactionNumber to brushData. We now add the updated brushData to the canvasActions entry. Finally we return the updated brushData.

The second method, GetBrushActions shown in Listing 10-26, gets the brush actions that have occurred for a specific canvas since a position in the canvas history. The method starts by creating a List<CanvasBrushAction> named actions to store the returned brush action entries. The next part of the method checks to see whether a canvasActions collection entry exists for the canvasId parameter. If the entry exists, all entries that are greater than or equal to the currentPosition parameter are added to the actions collection, and that collection is then sorted by sequence number. Finally, regardless of whether an entry existed, the actions collection is returned.

Listing 10-26. GetBrushActions Method

public List<CanvasBrushAction> GetBrushActions(string canvasId, int currentPosition)
{
List<CanvasBrushAction> actions = new List<CanvasBrushAction>();
if (canvasActions.ContainsKey(canvasId))
{
ConcurrentBag<CanvasBrushAction> storedActions = canvasActions[canvasId];
actions.AddRange(storedActions.Where(x => x.Sequence >= currentPosition));
actions.Sort(new Comparison<CanvasBrushAction>((a, b) => { return a.Sequence.CompareTo (b.Sequence); }));
}
return actions;
}

The third method, GetCanvasUsers shown in Listing 10-27, gets the current list of usernames in a canvas. This method also starts with creating a List<string> named returnValue, which will be returned at the end of the method. All the logic for this method depends on there being an entry in the canvasUsers collection. If there is an entry found, the collection is enumerated, and the username data that is stored as the key is added to a HashSet to ensure uniqueness. The returnValue collection is then overridden with a new list generated from theHashSet. Finally, the returnValue parameter is returned.

Listing 10-27. GetCanvasUsers Method

public List<string> GetCanvasUsers(string canvasId)
{
List<string> returnValue = new List<string>();
if (canvasUsers.ContainsKey(canvasId))
{
HashSet<string> uniqueList = new HashSet<string>();
ConcurrentDictionary<string, string> users = canvasUsers[canvasId];
foreach (KeyValuePair<string, string> user in users)
{
uniqueList.Add(user.Key);
}
returnValue = uniqueList.ToList<string>();
}
return returnValue;
}

The fourth method, AddUserToCanvas shown in Listing 10-28, adds users to a canvas room. This method starts by checking for the existence of an entry in the canvasUsers collection. If an entry does not exist, it adds a new ConcurrentDictionary<string,string> to the canvasUsers collection for the canvasId parameter. This ensures that the lookup in canvasUsers by canvasId always returns a ConcurrentDictionary<string, string> for the specified canvas. The returned collection is checked for the existence of the idparameter. If it does not exist in the collection, it is added to the collection.

Listing 10-28. AddUserToCanvas Method

public void AddUserToCanvas(string canvasId, string id)
{
if (!canvasUsers.ContainsKey(canvasId))
{
canvasUsers[canvasId] = new ConcurrentDictionary<string, string>();
}
ConcurrentDictionary<string, string> users = canvasUsers[canvasId];
if (!users.ContainsKey(id))
{
users[id] = id;
}
}

The fifth method, RemoveUserFromCanvas shown in Listing 10-29, removes users from a canvas room. It starts by checking for the existence of an entry in the canvasUsers collection. If an entry does exist and contains a key for the id parameter, the method attempts to remove the id from the entry.

Listing 10-29. RemoveUserFromCanvas Method

public void RemoveUserFromCanvas(string canvasId, string id)
{
if (canvasUsers.ContainsKey(canvasId))
{
ConcurrentDictionary<string, string> users = canvasUsers[canvasId];
if (users.ContainsKey(id))
{
string tempValue = null;
users.TryRemove(id, out tempValue);
}
}
}

The methods GetUserName and StoreUserName are simple methods shown in Listing 10-30 to store and retrieve usernames based on the user ID. These methods are not dependent on a canvasId. The GetUserName method checks the userNames collection for an ID; if it exists, it returns the username; otherwise, it returns null. The StoreUserName method stores the username in the collection at the key specified.

Listing 10-30. GetUserName and StoreUserName Methods

public string GetUserName(int id)
{
if (userNames.ContainsKey(id))
{
return userNames[id];
}
return null;
}
public void StoreUserName(int id, string userName)
{
userNames[id] = userName;
}

Now let’s add this class to our project with the following step:

1. Add a class named MemoryStorage to the GroupBrush.BL project under the Storage folder with the contents of Listing 10-31.

Listing 10-31. Complete MemoryStorage Class

public class MemoryStorage : IMemStorage
{
private ConcurrentDictionary<string, int> canvasTransactions = new ConcurrentDictionary<string, int>();
private ConcurrentDictionary<string, ConcurrentBag<CanvasBrushAction>> canvasActions = new ConcurrentDictionary<string, ConcurrentBag<CanvasBrushAction>>();
private ConcurrentDictionary<string, ConcurrentDictionary<string,string>> canvasUsers = new ConcurrentDictionary<string, ConcurrentDictionary<string,string>>();
private ConcurrentDictionary<int, string> userNames = new ConcurrentDictionary<int, string>();

public CanvasBrushAction AddBrushAction(string canvasId, CanvasBrushAction brushData)
{
if (!canvasTransactions.ContainsKey(canvasId))
{
canvasTransactions[canvasId] = 0;
}
int transactionNumber = canvasTransactions[canvasId] = canvasTransactions[canvasId]++;
if (!canvasActions.ContainsKey(canvasId))
{
canvasActions[canvasId] = new ConcurrentBag<CanvasBrushAction>();
}
brushData.Sequence = transactionNumber;
canvasActions[canvasId].Add(brushData);
return brushData;
}
public List<CanvasBrushAction> GetBrushActions(string canvasId, int currentPosition)
{
List<CanvasBrushAction> actions = new List<CanvasBrushAction>();
if (canvasActions.ContainsKey(canvasId))
{
ConcurrentBag<CanvasBrushAction> storedActions = canvasActions[canvasId];
actions.AddRange(storedActions.Where(x => x.Sequence >= currentPosition));
}
actions.Sort(new Comparison<CanvasBrushAction>((a, b) => { return a.Sequence.CompareTo(b.Sequence); }));
return actions;
}
public List<string> GetCanvasUsers(string canvasId)
{
List<string> returnValue = new List<string>();
if (canvasUsers.ContainsKey(canvasId))
{
HashSet<string> uniqueList = new HashSet<string>();
ConcurrentDictionary<string, string> users = canvasUsers[canvasId];
foreach (KeyValuePair<string, string> user in users)
{
uniqueList.Add(user.Key);
}
returnValue = uniqueList.ToList<string>();
}
return returnValue;
}
public void AddUserToCanvas(string canvasId, string id)
{
if (!canvasUsers.ContainsKey(canvasId))
{
canvasUsers[canvasId] = new ConcurrentDictionary<string, string>();
}
ConcurrentDictionary<string, string> users = canvasUsers[canvasId];
if (!users.ContainsKey(id))
{
users[id] = id;
}
}
public void RemoveUserFromCanvas(string canvasId, string id)
{
if (canvasUsers.ContainsKey(canvasId))
{
ConcurrentDictionary<string, string> users = canvasUsers[canvasId];
if (users.ContainsKey(id))
{
string tempValue = null;
users.TryRemove(id, out tempValue);
}
}
}
public string GetUserName(int id)
{
if (userNames.ContainsKey(id))
{
return userNames[id];
}
return null;
}
public void StoreUserName(int id, string userName)
{
userNames[id] = userName;
}
}

The final class for the SignalR implementation is the GetCanvasDescriptionData class shown in Listing 10-32. This class provides the data layer access to retrieve the CanvasDescription data. This class has a dependency of a string in the constructor, which we will use to inject the connection string.

Listing 10-32. GetCanvasDescriptionData Class

public class GetCanvasDescriptionData : IGetCanvasDescriptionData
{
private string _connectionString;
public GetCanvasDescriptionData(string connectionString)
{
_connectionString = connectionString;
}
public CanvasDescription GetCanvasDescription(Guid canvasId)
{
CanvasDescription returnValue = null;
using(SqlConnection connection = new SqlConnection(_connectionString))
using (SqlCommand command = new SqlCommand())
{
command.Connection = connection;
command.CommandText = "dbo.GetCanvasDescription";
command.CommandType = System.Data.CommandType.StoredProcedure;
SqlParameter prmCanvasId = new SqlParameter("@CanvasId", SqlDbType.UniqueIdentifier) { Direction = ParameterDirection.Input, Value = canvasId };
SqlParameter prmCanvasName = new SqlParameter("@CanvasName", SqlDbType.NVarChar, 100) { Direction = ParameterDirection.Output};
SqlParameter prmCanvasDescription = new SqlParameter("@CanvasDescription", SqlDbType.NVarChar, 100) { Direction = ParameterDirection.Output };
command.Parameters.Add(prmCanvasId);
command.Parameters.Add(prmCanvasName);
command.Parameters.Add(prmCanvasDescription);
connection.Open();
command.ExecuteNonQuery();
returnValue = new CanvasDescription();
if (prmCanvasName != null && prmCanvasName.Value != DBNull.Value && prmCanvasName.Value is string)
{
returnValue.Name = (string)prmCanvasName.Value;
}
if (prmCanvasDescription != null && prmCanvasDescription.Value != DBNull.Value && prmCanvasDescription.Value is string)
{
returnValue.Description = (string)prmCanvasDescription.Value;
}
}
return returnValue;
}
}

The method is a very straightforward data layer class: it creates a new SqlConnection and SqlCommand inside of using statements that will properly dispose of the classes and close the connection. The SqlConnection is created with the connection string that was injected.

The next part of the class sets up the parameters of the command with the connection, the stored procedure name, and the type of command to be executed. We add the canvasId as a SQL input parameter and create an output SQL parameter for the canvas name and canvas description. Once everything is set up, we open the connection and execute the query. A CanvasDescription return value is created, and the Name and Description values are set if the CanvasName and CanvasDescription parameters come back with proper values and types. This return value is then returned.

This class needs to be added to our project with the following steps:

1. Add a class named GetCanvasDescriptionData to the GroupBrush.DL project under the Canvases folder with the contents of Listing 10-32.

2. In GroupBrush.BL, add a project reference to the GroupBrush.Entity project.

3. In GroupBrush.DL, add a project reference to the GroupBrush.Entity project.

4. In GroupBrush.BL, add a project reference to the GroupBrush.DL project.

5. In GroupBrush.Web, add a project reference to the GroupBrush.BL project.

6. In GroupBrush.Web, add a project reference to the GroupBrush.Entity project.

We have now created the classes that support the real-time functionality, but we still need a way to provide other data that is not real-time and is not called very often. To do that, we can use the Web API to provide the needed endpoints to create and join canvases.

Adding API Endpoints

Sometimes we do not have a connection to a hub or want to create one, but we still need to access data from the application. Instead of creating a hub connection every time, we need data to hold a hub connection open for a few requests. We can use Web API endpoints for this data access.

In this section, we will add the endpoints for creating and joining a canvas. To create these endpoints, we have to add a little more to our project. The following steps add more folder structure and install the needed dependencies:

1. Add a solution folder named Public to the GroupBrush.Web project.

2. Add a solution folder named Controllers to the Public folder in the GroupBrush.Web project.

3. Run the following command from the Package Manager Console for the Default package of the GroupBrush.Web and GroupBrush.Worker projects:

Install-Package Microsoft.AspNet.WebApi.Owin

4. Run the following command from the Package Manager Console for the Default package of the GroupBrush.Web and GroupBrush.Worker projects:

Install-Package Microsoft.AspNet.WebApi.SelfHost

The first place to start with the Web API endpoints is the controller class, which derives from the ApiController class that lets us create a Web API endpoint. We will create a class named CanvasController that derives from ApiController (see Listing 10-33).

Listing 10-33. CanvasController Class

public class CanvasController : ApiController
{
ICanvasService _canvasService;
public CanvasController(ICanvasService canvasService)
{
_canvasService = canvasService;
}

[Route("api/canvas")]
[HttpPost]
public Guid? CreateCanvas(CanvasDescription canvasDescription)
{
Guid? canvasId = null;
if (canvasDescription != null)
{
canvasId = _canvasService.CreateCanvas(canvasDescription);
}
return canvasId;
}

[Route("api/canvas")]
[HttpPut]
public Guid? LookUpCanvas(CanvasName canvasName)
{
Guid? canvasId = null;
if (canvasName != null)
{
canvasId = _canvasService.LookUpCanvas(canvasName.Name);
}
return canvasId;
}
}

Like many other classes, this class has dependencies injected in the constructor. The dependency that is injected into the constructor of the CanvasController class is the ICanvasService interface in Listing 10-34, which will allow us to create and join canvases from Web API endpoints. This injected dependency is set to a private variable in the CanvasController class.

Listing 10-34. ICanvasService Interface

public interface ICanvasService
{
Guid? CreateCanvas(CanvasDescription canvasDescription);
Guid? LookUpCanvas(string canvasName);
}

The controller is fairly simple and contains only two methods, CreateCanvas and LookUpCanvas, which return data from the injected service.

Because we are using attribute routing for our Web API controller, both of these methods are accessible through the URL hostname + /api/canvas. But to give our API more of rest feel, we limit access to these methods using the HttpPost and HttpPut attributes. These attributes allow access only if the HTTP verb in the request is Post or Put. (The Post and Put verbs are for creating and joining, respectively.)

A type mapping will occur behind the scenes to populate the data that is passed into the methods. Sometimes the data is ambiguous, and the controller cannot map value types properly. We are giving it a little help with the LookUpCanvas method. In this method, we tell the controller that we’re expecting a type of CanvasName, which is just a single string (see Listing 10-35).

Listing 10-35. CanvasName Entity Class

public class CanvasName
{
public string Name;
}

Now that we know the controller just calls the ICanvasService interface, we need to look at the implementation. For the implementation, there is a class named CanvasService (shown in Listing 10-36), which derives from the ICanvasService interface. It also has two dependencies injected into the constructor: ICreateCanvasData and ILookUpCanvasData (shown in Listings 10-37 and 10-38, respectively). These dependencies are the interfaces for accessing the database to create or join a canvas, respectively, which set private variables for use by the class.

Listing 10-36. CanvasService class

public class CanvasService : ICanvasService
{
ICreateCanvasData _createCanvasData;
ILookUpCanvasData _lookUpCanvasData;
public CanvasService(ICreateCanvasData createCanvasData,ILookUpCanvasData lookUpCanvasData)
{
_createCanvasData = createCanvasData;
_lookUpCanvasData = lookUpCanvasData;
}
public Guid? CreateCanvas(CanvasDescription canvasDescription)
{
Guid? canvasId = null;
if (canvasDescription != null && !string.IsNullOrWhiteSpace(canvasDescription.Name) && !string.IsNullOrWhiteSpace(canvasDescription.Description))
{
canvasId = _createCanvasData.CreateCanvas(canvasDescription.Name, canvasDescription.Description);
}
return canvasId;
}
public Guid? LookUpCanvas(string canvasName)
{
Guid? canvasId = null;
if(!string.IsNullOrWhiteSpace(canvasName))
{
canvasId = _lookUpCanvasData.LookUpCanvas(canvasName);
}
return canvasId;
}
}

Listing 10-37. ICreateCanvasData Interface

public interface ICreateCanvasData
{
Guid? CreateCanvas(string canvasName, string Description);
}

Listing 10-38. ILookUpCanvasData Interface

public interface ILookUpCanvasData
{
Guid? LookUpCanvas(string canvasName);
}

The logic for the methods in this class checks to make sure the data passed into them is valid by not being null or an empty string. If the data is valid, they call the respective data layer services and return the values from those services.

The CreateCanvasData class in Listing 10-39 implements the ICreateCanvasData interface that our project uses to create a new canvas. This class is very simple: it has the connection string injected in the constructor, which sets the connection string to a local variable. TheCreateCanvas method takes two parameters, canvasName and canvasDescription, which it passes directly into the stored procedure. If the return value is 0, which means successful creation of a canvas, and a valid Guid is returned for the canvasId parameter, the canvasIdparameter is returned, which signals a successful canvas creation. If the return value is not 0 or the canvasId parameter is null, the canvas was not created, and a null is returned for the Guid value.

Listing 10-39. CreateCanvasData Class

public class CreateCanvasData : ICreateCanvasData
{
private string _connectionString;
public CreateCanvasData(string connectionString)
{
_connectionString = connectionString;
}
public Guid? CreateCanvas(string canvasName, string description)
{
Guid? returnValue = null;
using(SqlConnection connection = new SqlConnection(_connectionString))
using (SqlCommand command = new SqlCommand())
{
command.Connection = connection;
command.CommandText = "dbo.CreateCanvas";
command.CommandType = System.Data.CommandType.StoredProcedure;
SqlParameter prmReturnValue = new SqlParameter("@ReturnValue", SqlDbType.Int) { Direction = ParameterDirection.ReturnValue };
SqlParameter prmCanvasName = new SqlParameter("@CanvasName", SqlDbType.NVarChar, 100) { Direction = ParameterDirection.Input, Value = canvasName };
SqlParameter prmCanvasDescription = new SqlParameter("@CanvasDescription", SqlDbType.NVarChar, 255) { Direction = ParameterDirection.Input, Value = description };
SqlParameter prmCanvasId = new SqlParameter("@CanvasId", SqlDbType.UniqueIdentifier) { Direction = ParameterDirection.Output };
command.Parameters.Add(prmReturnValue);
command.Parameters.Add(prmCanvasName);
command.Parameters.Add(prmCanvasDescription);
command.Parameters.Add(prmCanvasId);
connection.Open();
command.ExecuteNonQuery();
if (prmReturnValue != null && prmReturnValue.Value != DBNull.Value && prmReturnValue.Value is int && (int)prmReturnValue.Value == 0)
{
if (prmCanvasId != null && prmCanvasId.Value != DBNull.Value && prmCanvasId.Value is Guid)
{
returnValue = (Guid)prmCanvasId.Value;
}
}
}
return returnValue;
}
}

The LookUpCanvasData class shown in Listing 10-40 implements the ILookUpCanvas data interface that our project uses to join a canvas. This class is very simple: the connection string is injected in the constructor, which sets the connection string to a local variable. When theLookUpCanvas method is called, it will pass the canvasName parameter to the database to see whether a canvas exists with that name. If the return value is 0 (which means a canvas was successfully found and a valid Guid is returned for the canvasId parameter), the value of thecanvasId parameter is returned; otherwise, a null value is returned.

Listing 10-40. LookUpCanvasData Class

public class LookUpCanvasData : ILookUpCanvasData
{
private string _connectionString;
public LookUpCanvasData(string connectionString)
{
_connectionString = connectionString;
}
public Guid? LookUpCanvas(string canvasName)
{
Guid? returnValue = null;
using(SqlConnection connection = new SqlConnection(_connectionString))
using (SqlCommand command = new SqlCommand())
{
command.Connection = connection;
command.CommandText = "dbo. LookUpCanvas";
command.CommandType = System.Data.CommandType.StoredProcedure;
SqlParameter prmReturnValue = new SqlParameter("@ReturnValue", SqlDbType.Int) { Direction = ParameterDirection.ReturnValue };
SqlParameter prmCanvasName = new SqlParameter("@CanvasName", SqlDbType.NVarChar, 100) { Direction = ParameterDirection.Input, Value = canvasName };
SqlParameter prmCanvasId = new SqlParameter("@CanvasId", SqlDbType.UniqueIdentifier) { Direction = ParameterDirection.Output };
command.Parameters.Add(prmReturnValue);
command.Parameters.Add(prmCanvasName);
command.Parameters.Add(prmCanvasId);
connection.Open();
command.ExecuteNonQuery();
if (prmReturnValue != null && prmReturnValue.Value != DBNull.Value && prmReturnValue.Value is int && (int)prmReturnValue.Value == 0)
{
if (prmCanvasId != null && prmCanvasId.Value != DBNull.Value && prmCanvasId.Value is Guid)
{
returnValue = (Guid)prmCanvasId.Value;
}
}
}
return returnValue;
}
}

Now is a good time to update our project to include the classes we discussed. We can accomplish this with the following steps:

1. Add the ICanvasService interface with the contents in Listing 10-34 to the GroupBrush.BL project in the Canvases folder.

2. Add the CanvasController class with the contents in Listing 10-33 to the GroupBrush.Web project in the Controllers folder nested under the Public folder.

3. Add the CanvasName class with the contents in Listing 10-35 to the GroupBrush.Entity project.

4. Add the ICreateCanvasData interface with the contents in Listing 10-36 to the GroupBrush.DL project in the Canvases folder.

5. Add the ILookUpCanvasData interface with the contents in Listing 10-37 to the GroupBrush.DL project in the Canvases folder.

6. Add the CanvasService class with the contents in Listing 10-38 to the GroupBrush.BL project in the Canvases folder.

7. Add the CreateCanvasData class with the contents in Listing 10-39 to the GroupBrush.DL project in the Canvases folder.

8. Add the LookUpCanvasData class with the contents in Listing 10-40 to the GroupBrush.DL project in the Canvases folder.

The next step is to add users to authenticate with. We can set up user registration and authentication access via the ASP.NET Web API, which we do in the next section.

Securing the Server

To secure the server, we add two Web API controllers, which allow us to register and authenticate users. Both of these controllers have the IUserService interface shown in Listing 10-41 injected into them. Both controllers also derive from the ApiController class. The dependency injection will provide the needed user services to create an account and validate a user’s login, and the ApiController class will allow these classes to implement Web API endpoints.

Listing 10-41. IUserService Interface

public interface IUserService
{
int? CreateAccount(string userName, string password);
bool ValidateUserLogin(string userName, string password, out int? userId);
string GetUserNameFromId(int id);
}

We have to add some required dependencies with the following step:

1. Run the following command from the Package Manager Console for the Default package of GroupBrush.Web and GroupBrush.Worker.

Install-Package Microsoft.Owin.Security.Cookies

The first controller class is the RegisterController class shown in Listing 10-42. This class has a single endpoint represented by the CreateUser method. The purpose of this method is to create a new user and return success message with a logged-in cookie. The endpoint is accessed by the URL of hostname + /public/api/user. What is important about this URL is that after we lock down the server, only the entries with a root of public will be accessible without a valid cookie.

Listing 10-42. RegisterController Class

public class RegisterController : ApiController
{
IUserService _userService;
public RegisterController(IUserService userService)
{
_userService = userService;
}

[Route("public/api/user")]
[HttpPost]
public HttpResponseMessage CreateUser(User user)
{
HttpResponseMessage response = new HttpResponseMessage();
response.StatusCode = System.Net.HttpStatusCode.Forbidden;
if (user != null)
{
int? userId = null;
userId = _userService.CreateAccount(user.UserName, user.Password);
if(userId.HasValue && userId.Value > -1)
{
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, userId.ToString()));
OwinHttpRequestMessageExtensions.GetOwinContext(this.Request).Authentication.SignIn(identity);
response.StatusCode = System.Net.HttpStatusCode.OK;
response.Content = new StringContent("Success");
}
}
return response;
}
}

The CreateUser method takes an entity class of User (defined in Listing 10-43) so that the Web API will bind the data from the request to a usable object. Once we have the user object, we use the user service to validate the username and password. If the user service returns a positive user ID, we use that as an indicator of a successful account creation and return a message of "Success" with a logged-in cookie. If the user is null or a negative user ID is returned, we’ll return an HTTP status code of Forbidden. (The logic of creating the cookie will be examined when we discuss the login endpoint).

Listing 10-43. User Entity Class

public class User
{
public string UserName { get; set; }
public int UserId { get; set; }
public string Password { get; set; }
}

Now that we can register users, we need to create some endpoints to log in, log off, and validate the current cookie. To consolidate our login endpoints to one controller, we’ll create a LoginController class, which will have the IUserService dependency injected, as shown inListing 10-44. We’ll use this user service to validate login requests.

Listing 10-44. LoginController Class Constructor

IUserService _userService;
public LoginController(IUserService userService)
{
_userService = userService;
}

Looking at the login endpoint/method in Listing 10-45, we see that this endpoint will take a Post request from the URL hostname + /public/api/login. This method uses the UserLogin entity shown in Listing 10-46 to get the needed login information. If the user service can validate the login from the data in the UserLogin object, it will return a message of "Success" with a logged-in cookie. If the UserLogin object is not valid or the user service could not validate the login, an HTTP status code of Unauthorized is returned.

Listing 10-45. Login Method

[Route("public/api/login")]
[HttpPost]
public HttpResponseMessage Login(UserLogin login)
{
HttpResponseMessage response = new HttpResponseMessage();
response.StatusCode = System.Net.HttpStatusCode.Unauthorized;
if (login != null)
{
int? userId;
if (_userService.ValidateUserLogin(login.UserName, login.Password, out userId))
{
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, userId.ToString()));
OwinHttpRequestMessageExtensions.GetOwinContext(this.Request).Authentication.SignIn(identity);
response.StatusCode = System.Net.HttpStatusCode.OK;
response.Content = new StringContent("Success");
}
}
return response;
}

Listing 10-46. UserLogin Entity

public class UserLogin
{
public string UserName { get; set; }
public string Password { get; set; }
}

In the event of a successful login, a new ClaimsIdentity for cookies is created. Next, a claim with the key of "Name" and data of the user ID is added to ClaimsIdentity, which is passed in to the SignIn method of OWIN authentication middleware. For this example, the authentication middleware is Microsoft OWIN security cookies. So the calling SignIn method will create a cookie that is returned in the response.

Sometimes a client might have a cookie, but that does not mean that the cookie is still valid. To allow the client to see whether the login is still valid, we expose the URL hostname + /public/api/loginStatus for Get requests. If the server can validate the cookie in the request, a message of "loggedIn" is returned; otherwise, a message of "loggedOut" is returned (see Listing 10-47).

Listing 10-47. GetLoginStatus Method

[Route("public/api/loginStatus")]
[HttpGet]
public string GetLoginStatus()
{
if (User != null && User.Identity.IsAuthenticated)
{
return "loggedIn";
}
else
{
return "loggedOut";
}
}

When it is time for a user to log out, there is an endpoint that is available for Post requests at hostname + /public/api/logout. This endpoint tells the OWIN cookie security to sign out the current request, which gets rid of the cookie by logging the user out. Once this is complete, a message of "Success" is returned (see Listing 10-48).

Listing 10-48. Logout Method

[Route("public/api/logout")]
[HttpPost]
public string Logout()
{
OwinHttpRequestMessageExtensions.GetOwinContext(this.Request).Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType);
return "Success";
}

With all the components together, you see the output of the complete LoginController class in Listing 10-49. For a more detailed look at what is occurring in the register and login controllers, we use the UserService class.

Listing 10-49. Complete LoginController Class

public class LoginController : ApiController
{
IUserService _userService;
public LoginController(IUserService userService)
{
_userService = userService;
}
[Route("public/api/loginStatus")]
[HttpGet]
public string GetLoginStatus()
{
if (User != null && User.Identity.IsAuthenticated)
{
return "loggedIn";
}
else
{
return "loggedOut";
}
}

[Route("public/api/login")]
[HttpPost]
public HttpResponseMessage Login(UserLogin login)
{
HttpResponseMessage response = new HttpResponseMessage();
response.StatusCode = System.Net.HttpStatusCode.Unauthorized;
if (login != null)
{
int? userId;
if (_userService.ValidateUserLogin(login.UserName, login.Password, out userId))
{
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, userId.ToString()));
OwinHttpRequestMessageExtensions.GetOwinContext(this.Request).Authentication.SignIn(identity);
response.StatusCode = System.Net.HttpStatusCode.OK;
response.Content = new StringContent("Success");
}
}
return response;
}

[Route("public/api/logout")]
[HttpPost]
public string Logout()
{
OwinHttpRequestMessageExtensions.GetOwinContext(this.Request).Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType);
return "Success";
}
}

A critical service of the canvas, registration, and login classes is the UserService class, which allows creation, validation, and name look-up for users. All these actions depend on the database for persistent storage of this information.

This class has three data layer project interfaces and the IMemStorage interface injected in the constructor to provide this data. The data layer interfaces are listed in Listings 10-50, 10-51, and 10-52, which provide the CreateUser, ValidateUser, and GetUserName methods, respectively.

Listing 10-50. ICreateUserData Interface

public interface ICreateUserData
{
int? CreateUser(string userName, string password);
}

Listing 10-51. IValidateUserData Interface

public interface IValidateUserData
{
bool ValidateUser(string userName, string password, out int? userId);
}

Listing 10-52. IGetUserNameFromIdData Interface

public interface IGetUserNameFromIdData
{
string GetUserName(int id);
}

The class logic shown in Listing 10-53 is relatively simple: CreateAccount and ValidateUserLogin return the data passed through to the data layer interfaces. The GetUserName method is not much more complex; it checks the IMemStorage implementation for the username. If it does not exist or is an empty string, it will query the data layer for this information. If the username is found in the data layer, it is stored in the IMemStorage implementation and then returned from the method.

Listing 10-53. UserService Class

public class UserService : IUserService
{
ICreateUserData _createUserData;
IValidateUserData _validateUserData;
IGetUserNameFromIdData _getUserNameFromIdData;
IMemStorage _memStorage;

public UserService(ICreateUserData createUserData, IValidateUserData validateUserData, IGetUserNameFromIdData getUserNameFromIdData, IMemStorage memStorage)
{
_createUserData = createUserData;
_validateUserData = validateUserData;
_getUserNameFromIdData = getUserNameFromIdData;
_memStorage = memStorage;
}
public int? CreateAccount(string userName, string password)
{
return _createUserData.CreateUser(userName, password);
}
public bool ValidateUserLogin(string userName, string password, out int? userId)
{
return _validateUserData.ValidateUser(userName, password, out userId);
}
public string GetUserNameFromId(int id)
{
string userName = _memStorage.GetUserName(id);
if (string.IsNullOrWhiteSpace(userName))
{
userName = _getUserNameFromIdData.GetUserName(id);
if (!string.IsNullOrWhiteSpace(userName))
{
_memStorage.StoreUserName(id, userName);
}
}
return userName;
}
}

Now we discuss creating concrete class implementations of data layer interfaces. The first implementation is the CreateUserData class shown in Listing 10-54, which implements the ICreateUserData interface.

The class will have the connection string injected in the constructor and will pass the userName and password parameters to the database via the CreateUser stored procedure. The stored procedure returns two output parameters, ReturnValue and UserId, which are used to determine whether a user was created successfully. If the ReturnValue parameter is equal to 0, and the UserId parameter is a positive integer, the value from the UserId parameter is returned. If the ReturnValue parameter is equal to 0, and the UserId parameter is not populated, it will return a value of -1 to indicate an error. If ReturnValue is not valid, the method will return null.

Listing 10-54. CreateUserData Class

public class CreateUserData : ICreateUserData
{
private string _connectionString;
public CreateUserData(string connectionString)
{
_connectionString = connectionString;
}
public int? CreateUser(string userName, string password)
{
int? returnValue = null;
using(SqlConnection connection = new SqlConnection(_connectionString))
using (SqlCommand command = new SqlCommand())
{
command.Connection = connection;
command.CommandText = "dbo.CreateUser";
command.CommandType = System.Data.CommandType.StoredProcedure;
SqlParameter prmReturnValue = new SqlParameter("@ReturnValue", SqlDbType.Int) { Direction = ParameterDirection.ReturnValue };
SqlParameter prmName = new SqlParameter("@Name", SqlDbType.NVarChar, 100) { Direction = ParameterDirection.Input, Value = userName };
SqlParameter prmPassword = new SqlParameter("@Password", SqlDbType.NVarChar, 255) { Direction = ParameterDirection.Input, Value = password };
SqlParameter prmUserId = new SqlParameter("@UserId", SqlDbType.Int) { Direction = ParameterDirection.Output };
command.Parameters.Add(prmReturnValue);
command.Parameters.Add(prmName);
command.Parameters.Add(prmPassword);
command.Parameters.Add(prmUserId);
connection.Open();
command.ExecuteNonQuery();
if(prmReturnValue != null && prmReturnValue.Value != DBNull.Value && prmReturnValue.Value is int && (int)prmReturnValue.Value == 0)
{
if(prmUserId != null && prmUserId.Value != DBNull.Value && prmUserId.Value is int)
{
returnValue = (int)prmUserId.Value;
}
else
{
returnValue = -1;
}
}
}
return returnValue;
}
}

The ValidateUserData class shown in Listing 10-55 has the connection string injected in the constructor. The class will pass the userName and password parameters to the database via the ValidateUser stored procedure while expecting three output parameters:ReturnValue, ValidUser, and UserId. If the ValidUser parameter is equal to 1, and the UserId parameter is populated with a valid value, the out userId parameter is set with the value from the UserId parameter and the method returns true. Otherwise, the out userIdparameter is null and false is returned from the method.

Listing 10-55. ValidateUserData Class

public class ValidateUserData : IValidateUserData
{
private string _connectionString;
public ValidateUserData(string connectionString)
{
_connectionString = connectionString;
}
public bool ValidateUser(string userName, string password, out int? userId)
{
bool returnValue = false;
userId = null;
using(SqlConnection connection = new SqlConnection(_connectionString))
using (SqlCommand command = new SqlCommand())
{
command.Connection = connection;
command.CommandText = "dbo.ValidateUser";
command.CommandType = System.Data.CommandType.StoredProcedure;
SqlParameter prmName = new SqlParameter("@Name", SqlDbType.NVarChar, 100) { Direction = ParameterDirection.Input, Value = userName };
SqlParameter prmPassword = new SqlParameter("@Password", SqlDbType.NVarChar, 255) { Direction = ParameterDirection.Input, Value = password };
SqlParameter prmValidUser = new SqlParameter("@ValidUser", SqlDbType.Int) { Direction = ParameterDirection.Output };
SqlParameter prmUserId = new SqlParameter("@UserId", SqlDbType.Int) { Direction = ParameterDirection.Output };
command.Parameters.Add(prmName);
command.Parameters.Add(prmPassword);
command.Parameters.Add(prmValidUser);
command.Parameters.Add(prmUserId);
connection.Open();
command.ExecuteNonQuery();
if (prmValidUser != null && prmValidUser.Value != DBNull.Value && prmValidUser.Value is int && (int)prmValidUser.Value == 1)
{
if (prmUserId != null && prmUserId.Value != DBNull.Value && prmUserId.Value is int)
{
userId = (int)prmUserId.Value;
returnValue = true;
}
}
}
return returnValue;
}
}

The GetUserNameFromIdData class shown in Listing 10-56 has the connection string injected in the constructor. The class will pass the id parameter to the database via the GetUserName stored procedure. The procedure is expecting one output parameter of UserName. If theUserName parameter is a string value, it is returned for the method; otherwise, null is returned.

Listing 10-56. GetUserNameFromIdData Class

public class GetUserNameFromIdData : IGetUserNameFromIdData
{
private string _connectionString;
public GetUserNameFromIdData(string connectionString)
{
_connectionString = connectionString;
}
public string GetUserName(int id)
{
string returnValue = null;
using (SqlConnection connection = new SqlConnection(_connectionString))
using (SqlCommand command = new SqlCommand())
{
command.Connection = connection;
command.CommandText = "dbo.GetUserName";
command.CommandType = System.Data.CommandType.StoredProcedure;
SqlParameter prmUserId = new SqlParameter("@UserId", SqlDbType.Int) { Direction = ParameterDirection.Input, Value = id };
SqlParameter prmUserName = new SqlParameter("@UserName", SqlDbType.NVarChar, 100) { Direction = ParameterDirection.Output };
command.Parameters.Add(prmUserName);
command.Parameters.Add(prmUserId);
connection.Open();
command.ExecuteNonQuery();
if (prmUserName != null && prmUserName.Value != DBNull.Value && prmUserName.Value is string)
{
returnValue = (string)prmUserName.Value;
}
}
return returnValue;
}
}

Now that we have covered all the logic to secure the server, let’s add it to our project with the following steps:

1. Add the RegisterController class with the contents in Listing 10-42 to the GroupBrush.Web project in the Controllers folder nested under the Public folder.

2. Add the User class with the contents in Listing 10-43 to the GroupBrush.Entity project.

3. Add the UserLogin class with the contents in Listing 10-46 to the GroupBrush.Entity project.

4. Add the LoginController class with the contents in Listing 10-49 to the GroupBrush.Web project in the Controllers folder nested under the Public folder.

5. Add the ICreateUserData interface with the contents in Listing 10-50 to the GroupBrush.DL project in the Users folder.

6. Add the IValidateUserData interface with the contents in Listing 10-51 to the GroupBrush.DL project in the Users folder.

7. Add the IGetUserNameFromIdData interface with the contents in Listing 10-52 to the GroupBrush.DL project in the Users folder.

8. Add the UserService class with the contents in Listing 10-53 to the GroupBrush.BL project in the Users folder.

9. Add the CreateUserData class with the contents in Listing 10-54 to the GroupBrush.DL project in the Users folder.

10.Add the ValidateUserData class with the contents in Listing 10-55 to the GroupBrush.DL project in the Users folder.

11.Add the GetUserNameFromIdData class with the contents in Listing 10-56 to the GroupBrush.DL project in the Users folder.

Now that we have all the components, we’ll show you how to set up the dependency resolver, which will allow the constructor dependency injection to have the implemented classes we intended.

Setting Up the Dependency Resolver

For this project, we will be using the Unity container. We add it to our project with the following steps:

1. Add a solution folder named Unity to the GroupBrush.Web project.

2. Run the following command from the Package Manager Console for the Default package of GroupBrush.Web and GroupBrush.Worker:

Install-Package Unity

For Unity to work in our project, we have to create an adapter to work with both the SignalR and Web API dependency resolver interfaces and a helper class to configure our container registrations. Both classes will be added to the GroupBrush.Web project.

Looking at the UnityDependencyResolver class definition shown in Listing 10-57 you can see that we’re deriving from the SignalR DefaultDependencyResolver and implementing the Web API IDependencyResolver interface, which enables us to have shared functionality. This works well in most cases, but it is missing the default registrations that the Web API would normally have from the DefaultDependencyResolver Web API.

Listing 10-57. UnityDependencyResolver Class Definition

public class UnityDependencyResolver : DefaultDependencyResolver,
System.Web.Http.Dependencies.IDependencyResolver

The next parts of the resolver are the constructor and private variables. Listing 10-58 shows two constructors. The first constructor is an empty constructor that uses the internal Unity container. The second constructor takes in a container that it will use for that class, which is necessary to provide functionality for the Web API BeginScope method (see Listing 10-61).

Listing 10-58. UnityDependencyResolver Constructor and Private Variables

IUnityContainer _container = new UnityContainer();
public UnityDependencyResolver()
{ }
public UnityDependencyResolver(IUnityContainer container)
{
_container = container;
}

Next are the overrides of the SignalR DefaultDependencyResolver. We use these methods to attempt to resolve or register using the Unity container.

In the resolve methods GetService and GetServices in Listing 10-59, there is not a friendly way to see whether Unity can resolve a type, so a try-catch statement must be used, falling back to the base class if the resolution fails.

Listing 10-59. UnityDependencyResolver SignalR DefaultDependencyResolver Overrides

public override object GetService(Type serviceType)
{
try
{
return _container.Resolve(serviceType);
}
catch
{
return base.GetService(serviceType);
}
}

public override IEnumerable<object> GetServices(Type serviceType)
{
try
{
List<object> services = _container.ResolveAll(serviceType).ToList();
object defaultService = GetService(serviceType);
if (defaultService != null) services.Add(defaultService);
return services;
}
catch
{
return base.GetServices(serviceType);
}
}

public override void Register(Type serviceType, IEnumerable<Func<object>> activators)
{
_container.RegisterType(serviceType, new InjectionFactory((c) =>
{
object returnObject = null;
foreach (Func<Object> activator in activators)
{
object tempObject = activator.Invoke();
if (tempObject != null)
{
returnObject = tempObject;
break;
}
}
return returnObject;
}));
base.Register(serviceType, activators);
}

The registration function adapts the DefaultDependencyResolver Register method to register the services in the Unity container and then in the base class. This is useful because if an object can be resolved out of the Unity container, it avoids the exception penalty of resolving it from the base class.

Every dependency injection container has its own syntax to provide registration, resolution, and container lifetime management. So to use some of the features of Unity, we created methods to expose the Unity methods shown in Listing 10-60. The first method allows us to register an interface to an implementation. The second method does the same as the first, but also allows us to control the LifeTimeManager of our registration. The third method allows us to register types to an activation function that we define.

Listing 10-60. Helpful Unity Registration Methods

public void RegisterType<TFrom,TTo>(params InjectionMember [] injectionMembers) where TTo : TFrom
{
_container.RegisterType<TFrom, TTo>(injectionMembers);
}
public void RegisterType<TFrom, TTo>(LifetimeManager lifetimeManager, params InjectionMember[] injectionMembers) where TTo : TFrom
{
_container.RegisterType<TFrom, TTo>(lifetimeManager, injectionMembers);
}
public override void Register(Type serviceType, Func<object> activator)
{
_container.RegisterType(serviceType, new InjectionFactory((c) => activator.Invoke()));
base.Register(serviceType, activator);
}
public void RegisterInstance<TInterface>(TInterface instance)
{
_container.RegisterInstance<TInterface>(instance);
}

The last part of DependencyResolver is the BeginScope method shown in Listing 10-61 that is added for Web API compatibility. It allows the creation of a child container per request that comes in. This allows unique registration and resolution per request. If the resolution does not succeed in the child container, it will bubble up to the parent containers to look for the resolution.

Listing 10-61. Web API IDependencyResolver Method

public IDependencyScope BeginScope()
{
return new UnityDependencyResolver(_container.CreateChildContainer());
}

Now that all the methods are together, you can see a complete DependencyResolver class like the one in Listing 10-62. To use the new resolver, we will configure the registration in a static class (discussed next).

Listing 10-62. Complete UnityDependencyResolver Class

public class UnityDependencyResolver : DefaultDependencyResolver, System.Web.Http.Dependencies.IDependencyResolver
{
IUnityContainer _container = new UnityContainer();
public UnityDependencyResolver()
{ }
public UnityDependencyResolver(IUnityContainer container)
{
_container = container;
}
public override object GetService(Type serviceType)
{
try
{
return _container.Resolve(serviceType);
}
catch
{
return base.GetService(serviceType);
}
}

public override IEnumerable<object> GetServices(Type serviceType)
{
try
{
List<object> services = _container.ResolveAll(serviceType).ToList();
object defaultService = GetService(serviceType);
if (defaultService != null) services.Add(defaultService);
return services;
}
catch
{
return base.GetServices(serviceType);
}
}

public override void Register(Type serviceType, IEnumerable<Func<object>> activators)
{
_container.RegisterType(serviceType, new InjectionFactory((c) =>
{
object returnObject = null;
foreach (Func<Object> activator in activators)
{
object tempObject = activator.Invoke();
if (tempObject != null)
{
returnObject = tempObject;
break;
}
}
return returnObject;
}));
base.Register(serviceType, activators);
}
public void RegisterType<TFrom,TTo>(params InjectionMember [] injectionMembers) where TTo : TFrom
{
_container.RegisterType<TFrom, TTo>(injectionMembers);
}
public void RegisterType<TFrom, TTo>(LifetimeManager lifetimeManager, params InjectionMember[] injectionMembers) where TTo : TFrom
{
_container.RegisterType<TFrom, TTo>(lifetimeManager, injectionMembers);
}
public override void Register(Type serviceType, Func<object> activator)
{
_container.RegisterType(serviceType, new InjectionFactory((c) => activator.Invoke()));
base.Register(serviceType, activator);
}
public void RegisterInstance<TInterface>(TInterface instance)
{
_container.RegisterInstance<TInterface>(instance);
}
public IDependencyScope BeginScope()
{
return new UnityDependencyResolver(_container.CreateChildContainer());
}
}

The UnityWireupConfiguration class in Listing 10-63 is a very simple registration for most types. But there are a few registrations that are different.

Listing 10-63. UnityWireupConfiguration Class

public class UnityWireupConfiguration
{
public static void WireUp(UnityDependencyResolver dependencyResolver)
{
string groupBrushSQLConnectionString = CloudConfigurationManager.GetSetting("GroupBrushDB");
dependencyResolver.RegisterType<IUserService, UserService>(new ContainerControlledLifetimeManager());
dependencyResolver.RegisterType<ICanvasService,CanvasService>();
dependencyResolver.RegisterType<ICanvasRoomService, CanvasRoomService>();
dependencyResolver.RegisterType<IMemStorage, MemoryStorage>(new ContainerControlledLifetimeManager());
dependencyResolver.Register(typeof(IGetUserNameFromIdData), () => new GetUserNameFromIdData(groupBrushSQLConnectionString));
dependencyResolver.Register(typeof(IGetCanvasDescriptionData), () => new GetCanvasDescriptionData(groupBrushSQLConnectionString));
dependencyResolver.Register(typeof(ICreateUserData), () => new CreateUserData(groupBrushSQLConnectionString));
dependencyResolver.Register(typeof(IValidateUserData), () => new ValidateUserData(groupBrushSQLConnectionString));
dependencyResolver.Register(typeof(ICreateCanvasData), () => new CreateCanvasData(groupBrushSQLConnectionString));
dependencyResolver.Register(typeof(ILookUpCanvasData), () => new LookUpCanvasData(groupBrushSQLConnectionString));
}
}

The first line of the WireUp method retrieves configuration settings from a CloudConfigurationManager that provides compatibility with App.Config and cloud configurations. These configuration values can then be passed to the registration as with the SQL connection string.

The second line provides a ContainerControlledLifeTimeManager to that registration. That means the object will live the entire life of the container, whereas the other registration types are re-created with every request.

Finally, the order in which types are registered is important; registrations that are dependent for other registrations should be registered first.

Once again, it is time to add the Unity container classes to our project with the following steps:

1. Add the UnityDependencyResolver class with the contents in Listing 10-62 to the GroupBrush.Web project in the Unity folder.

2. Add the UnityWireupConfiguration class with the contents in Listing 10-63 to the GroupBrush.Web project in the Unity folder.

3. Add a reference from the GroupBrush.Web project to the GroupBrush.DL project.

4. Add a reference to the Microsoft.WindowsAzure.Configuration assembly in the GroupBrush.Web project.

Next, we have to set up the OWIN pipeline so the application can handle the requests with the correct order and logic.

Setting Up the OWIN Pipeline

The StartUp class sets up the OWIN pipeline, which will handle every request that comes in. The StartUp class has a special assembly attribute to indicate that the OWIN pipeline should use this class for configuration. (You can see an example of this attribute in Listing 10-64.) The configuration for our project has four setup areas: dependency resolver, authentication, Web API, and SignalR. Similar to the Unity setup, the order in which things are registered to IAppBuilder is important.

Listing 10-64. OwinStart Attribute

[assembly: OwinStartup(typeof(GroupBrush.Web.Startup))]

The first thing to set up in the StartUp configuration is the dependency resolver, which is done by using the three lines in Listing 10-65 and one line in Listing 10-67 (shown later). The first line creates the dependency resolver that we will use for SignalR and the Web API. We can configure it using the UnityWireupConfiguration static WireUp method, which is done in the second line. The third line configures SignalR to use the dependency resolver we created in the first line. (There is a fourth line of configuration needed for the Web API, which will be discussed in the Web API section).

Listing 10-65. Dependency Resolver Setup

var dependencyResolver = new UnityDependencyResolver();
UnityWireupConfiguration.WireUp(dependencyResolver);
GlobalHost.DependencyResolver = dependencyResolver;

The next thing to add to the StartUp class is the authentication shown in Listing 10-66. For authentication, create a configuration class for the cookie authentication. In this configuration, we want the default cookie authentication type and for any login/logout redirect to return to the root path. With our cookie configuration, we set up the first entry in the OWIN pipeline with the UseCookieAuthentication method. The next entry we add to the OWIN pipeline allows requests to the root or public folder to continue to the next middleware in the pipeline. If the request did not continue to the next middleware, and the user is null or unauthenticated, the request is stopped with a 401 response. Finally, if the request has made it this far, it is continued to the next middleware in the pipeline.

Listing 10-66. Authentication Setup

var options = new CookieAuthenticationOptions()
{
AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
LoginPath = new PathString("/"),
LogoutPath = new PathString("/")
};
app.UseCookieAuthentication(options);

app.Use(async (context, next) =>
{
if (context.Request.Path.Value.Equals("/")||
context.Request.Path.Value.StartsWith("/public", StringComparison.CurrentCultureIgnoreCase))
{
await next();
}
else if (context.Request.User == null || !context.Request.User.Identity.IsAuthenticated)
{
context.Response.StatusCode = 401;
}
else
{
await next();
}
});

After the authentication is set up, the Web API is the next middleware that we want to run. We use the setup in Listing 10-67 to create a configuration for the Web API middleware. In that configuration, we add the dependency resolver that we created in Listing 10-65 so the Web API will use that dependency resolver. Next, we tell the configuration to MapHttpAttributeRoutes, which will enable us to use the Web API attribute routing features. We add the Web API to the OWIN pipeline using the UseWebApi extension method.

Listing 10-67. Web API Setup

HttpConfiguration webApiConfiguration = new HttpConfiguration();
webApiConfiguration.DependencyResolver = dependencyResolver;
webApiConfiguration.MapHttpAttributeRoutes();
app.UseWebApi(webApiConfiguration);

Finally, we add SignalR middleware with one line, as demonstrated in Listing 10-68.

Listing 10-68. SignalR Setup

app.MapSignalR();

If you take the four sections in the correct order, you will end up with a complete StartUp class like Listing 10-69. But don’t forget the OwinStartUp attribute in Listing 10-64, which is critical for the application to know which configuration to use.

Listing 10-69. Complete StartUp Class

public class Startup
{
public void Configuration(IAppBuilder app)
{
var dependencyResolver = new UnityDependencyResolver();
UnityWireupConfiguration.WireUp(dependencyResolver);
GlobalHost.DependencyResolver = dependencyResolver;

var options = new CookieAuthenticationOptions()
{
AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
LoginPath = new PathString("/"),
LogoutPath = new PathString("/")
};
app.UseCookieAuthentication(options);

app.Use(async (context, next) =>
{
if (context.Request.Path.Value.Equals("/") || context.Request.Path.Value.StartsWith("/public", StringComparison.CurrentCultureIgnoreCase))
{
await next();
}
else if (context.Request.User == null || !context.Request.User.Identity.IsAuthenticated)
{
context.Response.StatusCode = 401;
}
else
{
await next();
}
});

HttpConfiguration webApiConfiguration = new HttpConfiguration();
webApiConfiguration.DependencyResolver = dependencyResolver;
webApiConfiguration.MapHttpAttributeRoutes();
app.UseWebApi(webApiConfiguration);

app.MapSignalR();
}
}

To add the StartUp configuration, we follow these steps:

1. Add an OWIN StartUp class to the root of the GroupBrush.Web project with the contents of Listing 10-69.

2. Ensure that the OwinStartUp attribute is included in the StartUp.cs file created in step 1 and is similar to Listing 10-64.

We have now created all the code that is necessary to run the server, but need an application to host it in. For this application, we will be hosting in an Azure cloud service worker role (discussed next).

Hosting the Server in Azure

For this project, the Microsoft Azure cloud is a great candidate to support all the platforms we need to have a scalable, reliable, and easily maintainable application. We will use the cloud services platform to host our application in a worker role.

We will cover the following five areas of the worker role that are essential to the project:

· Implementing the RoleEntryPoint class

· Creating the cloud service

· Configuring the worker role

· Testing our deployment locally

· Deploying our application to the cloud

After covering these five areas, we will have a fully deployed application running in a worker role.

Implementing the RoleEntryPoint Class

First, we want to implement the RoleEntryPoint class. Azure uses this class as an entry point to start, stop, and run the code in the worker role. When we created the GroupBrush.Worker project, it created a WorkerRole class that derives from the RoleEntryPoint class. We will use this class to host our application. When this is running in the cloud, we can start and stop our application using the OnStart and OnStop methods, respectively.

Because the WorkerRole class is already created, we will use it and add a few changes. The first change is to add a variable that will be responsible for disposing of the WebApp. The next change is starting the WebApp by calling the WebApp generic static Start method with our OWINStartUp class and configuration. The configuration provided is the endpoint from which the WebApp should be hosted. We determine the endpoint by looking for SignalREndpoint in the endpoints configured for the instance.

Finally, we want to have a way to stop the application; we do that by calling the dispose method on the IDisposable object that was returned from the start method. Once all this is complete, the code will look similar to Listing 10-70.

Listing 10-70. WorkerRole Class

public class WorkerRole : RoleEntryPoint
{
IDisposable _webApp = null;
public override void Run()
{
while (true)
{
Thread.Sleep(10000);
Trace.TraceInformation("Working");
}
}

public override bool OnStart()
{
ServicePointManager.DefaultConnectionLimit = 12;
RoleInstanceEndpoint signalREndpoint = null;
if (RoleEnvironment.CurrentRoleInstance.InstanceEndpoints.TryGetValue("SignalREndpoint", out signalREndpoint))
{
_webApp = WebApp.Start<Startup>(string.Format("{0}://{1}", signalREndpoint.Protocol, signalREndpoint.IPEndpoint));
}
else
{
throw new KeyNotFoundException("Could not find SignalREndpoint");
}
return base.OnStart();
}
public override void OnStop()
{
if (_webApp != null)
{
_webApp.Dispose();
}
base.OnStop();
}
}

Let’s add these changes to our project with the following steps:

1. Update the WorkerRole class in the GroupBrush.Worker project with the contents of Listing 10-70.

2. Add a reference from the GroupBrush.Worker project to the GroupBrush.Web project.

3. Run the following command from the Package Manager Console for the Default package of GroupBrush.Worker:

Install-Package Microsoft.Owin.Hosting
Install-Package Microsoft.Owin.Host.HttpListener

That is all the code it takes to start and stop our application in the cloud. Now that we have our code, we have to create a host in the cloud to run it.

Creating the Cloud Service

Because we already have code for a worker role, we need to create a cloud service on Azure to host it. There are numerous ways to create a cloud service, but we’ll use Server Explorer in Visual Studio to do it.

A cloud service can be created only once for a project by using the following steps:

1. Sign in to Visual Studio with the account that is associated to the Azure account.

2. Open the Server Explorer and navigate to Cloud Services, as shown in Figure 10-1.

image

Figure 10-1. Azure components in Server Explorer

3. Right-click on Cloud Services to display the context menu and then click Create Cloud Service, as shown in Figure 10-2.

image

Figure 10-2. Cloud Services context menu

4. Complete the following on the Create Cloud Service dialog box, as shown in Figure 10-3:

a. Choose the subscription that you want this cloud service to be associated with.

b. Enter a name for the cloud service.

c. Choose where you want this cloud service deployed.

5. Finally, click Create after all the correct information is entered.

image

Figure 10-3. Create Cloud Service dialog box

There should now be a cloud service ready to use in Azure. Now we need to create a database that will persist our data in the cloud.

Creating the Azure SQL Database

In our application, we need to persist data such as users and canvases, so we will use the Azure SQL database. Creating the database is very simple; we follow these steps:

1. Log in to http://portal.azure.com.

2. Click the New(+) button in the bottom left.

3. Click SQL Database (see Figure 10-4).

image

Figure 10-4. Creating a new SQL Database

4. In the SQL database tab, do the following, as shown in Figure 10-5:

a. Enter the database name.

b. Click the server button to configure the server.

image

Figure 10-5. Creating a new SQL database configuration

5. In the Server tab, click Create a New Server.

6. In the New Server tab, do the following:

a. Enter server name.

b. Enter server admin name.

c. Enter admin password.

d. Enter admin password again to confirm.

e. Click OK.

7. Click Create.

We store two sets of records for our application: users and canvases. So we need to create two tables in our database: one for users and the other for canvases.

The Users table consists of three values per record, as seen in Listing 10-71: the user ID, the name, and password of the user.

Listing 10-71. Users Table Create Script

USE [GroupBrush]
GO

CREATE TABLE [dbo].[Users](
[UserId] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY,
[Name] [nvarchar](100) NOT NULL,
[Password] [nvarchar](255) NOT NULL
) ON [PRIMARY]

GO

The Canvases table shown in Listing 10-72 is very simple as well. It contains the canvas table ID, canvas ID, canvas name, and canvas description.

Listing 10-72. Canvases Table Create Script

USE [GroupBrush]
GO

CREATE TABLE [dbo].[Canvases](
[CanvasTableId] [bigint] IDENTITY(1,1) NOT NULL PRIMARY KEY,
[CanvasId] [uniqueidentifier] NOT NULL,
[CanvasName] [nvarchar](100) NOT NULL,
[CanvasDescription] [nvarchar](100) NOT NULL
) ON [PRIMARY]

GO

To add these tables to our database, follow these steps:

1. Connect to the database using SQL Server Management Studio.

2. Run the complete script in Listing 10-71 to add the Users table.

3. Run the complete script in Listing 10-72 to add the Canvases table.

Image Note When using MS SQL in Azure, a table must have a primary key before you can use it.

Now that the tables are created, we can create the stored procedures to support the data layer functions. Because the stored procedures are simplified, we will not go into the details of each procedure and give only a short description:

· CreateUser, shown in Listing 10-73, creates a new user returning the user ID.

· ValidateUser, shown in Listing 10-74, validates the user login.

· GetUserName, shown in Listing 10-75, gets the username from the user ID.

· CreateCanvas, shown in Listing 10-76, creates a new canvas returning the canvas ID.

· LookUpCanvas, shown in Listing 10-77, gets the canvas ID of a canvas from the name.

· GetCanvasDescription, shown in Listing 10-78, gets the canvas name and description from the canvas ID.

Listing 10-73. CreateUser Stored Procedure

USE [GroupBrush]
GO

CREATE Procedure [dbo].[CreateUser](
@Name nchar(100),
@Password nchar(255),
@UserId int output
)

As
Begin

Declare @ReturnValue int = -1
If Exists(Select 1 From dbo.Users Where Name = @Name)
Begin
Set @ReturnValue = 1
End
Else
Begin
Declare @UserIds Table (userId int)
INSERT INTO [dbo].[Users]
([Name]
,[Password])
Output Inserted.UserId Into @UserIds(userId)
VALUES
(@Name
,@Password)
Select @ReturnValue = 0, @UserId = userId From @UserIds
End
Return @ReturnValue
End

GO

Listing 10-74. ValidateUser Stored Procedure

USE [GroupBrush]
GO

CREATE Procedure [dbo].[ValidateUser](
@Name nchar(100),
@Password nchar(255),
@UserId int = NULL output,
@ValidUser bit output
)

As
Begin
Select @UserId = UserId From dbo.Users Where Name = @Name and Password = @Password
If (@UserId Is Not Null)
Begin
Set @ValidUser = 1
End
Else
Begin
Set @ValidUser = 0
End
End


GO

Listing 10-75. GetUserName Stored Procedure

USE [GroupBrush]
GO

Create Procedure [dbo].[GetUserName](
@UserId Int,
@UserName NVarChar(100) Output
)

As
Begin

Select @UserName = Name From dbo.Users where UserId = @UserId

End
GO

Listing 10-76. CreateCanvas Stored Procedure

USE [GroupBrush]
GO

CREATE Procedure [dbo].[CreateCanvas](
@CanvasName nchar(100),
@CanvasDescription nchar(255),
@CanvasId UniqueIdentifier output
)

As
Begin

Declare @ReturnValue int = -1
If Exists(Select 1 From dbo.Canvases Where CanvasName = @CanvasName)
Begin
Set @ReturnValue = 1
End
Else
Begin
Declare @CanvasIds Table (CanvasId UniqueIdentifier)
INSERT INTO [dbo].[Canvases]
([CanvasId]
,[CanvasName]
,[CanvasDescription])
Output Inserted.CanvasId Into @CanvasIds(CanvasId)
VALUES
(NewID()
,@CanvasName
,@CanvasDescription)
Select @ReturnValue = 0,
@CanvasId = CanvasId
From @CanvasIds
End
Return @ReturnValue
End

GO

Listing 10-77. LookUpCanvas Stored Procedure

USE [GroupBrush]
GO

CREATE Procedure [dbo].[LookUpCanvas](
@CanvasName nvarchar(100),
@CanvasId UniqueIdentifier = NULL output
)

As
Begin
Declare @ReturnValue int = -1
Select @CanvasId = CanvasId From dbo.Canvases Where CanvasName = @CanvasName
If @CanvasId Is Not Null
Begin
Set @ReturnValue = 0
End

Return @ReturnValue
End

GO

Listing 10-78. GetCanvasDescription Stored Procedure

USE [GroupBrush]
GO

Create Procedure [dbo].[GetCanvasDescription](
@CanvasId UniqueIdentifier,
@CanvasName NVarChar(100) Output,
@CanvasDescription NVarChar(100) Output
)

As
Begin

Select @CanvasName = CanvasName, @CanvasDescription = CanvasDescription
From dbo.Canvases
Where CanvasId = @CanvasId

End
GO

Now we know which stored procedures are needed, so let’s add them to our database with the following steps:

1. Connect to the database using SQL Server Management Studio.

2. Run the complete script in Listing 10-73 to add the CreateUser stored procedure.

3. Run the complete script in Listing 10-74 to add the ValidateUser stored procedure.

4. Run the complete script in Listing 10-75 to add the GetUserName stored procedure.

5. Run the complete script in Listing 10-76 to add the CreateCanvas stored procedure.

6. Run the complete script in Listing 10-77 to add the LookUpCanvas stored procedure.

7. Run the complete script in Listing 10-78 to add the GetCanvasDescription stored procedure.

Now with the database work complete, let’s move on to configuring the worker role.

Configuring the Worker Role

For the worker role to work correctly, we have to add some settings at runtime. These settings can also change, depending on the environment in which we are running. This section briefly covers the following configuration tabs: Settings, Endpoints, and Configuration. The configuration for a worker role can be found under the properties of that worker role (see Figure 10-6).

image

Figure 10-6. Worker role context menu

For application configuration, it is very common to have different settings, based on where the code is running. To accommodate for this, we can configure the configuration for All Configurations, Cloud, Local, or add our own service configuration. All Configurations applies the configuration to all configurations regardless of where it is running, but it is possible to overwrite in a specific configuration.

The Cloud configuration is used when the worker role is run on Azure. The Local configuration is used when the application is run locally for testing. Each configuration tab allows a Service Configuration setting for all the configuration values that the tab will let you configure.

The first tab is the Settings tab (see Figure 10-7), in which strings and connection strings can be added. It is a very simple configuration: for each entry, we give it a name, the type of entry it is, and the value. This tab stores simple settings or connection strings to databases that can be retrieved by name.

image

Figure 10-7. Azure worker role Settings tab

This configuration tab is where we set the connection string for the database with the following steps:

1. Go to the Settings tab in the worker role configuration.

2. Click Add Setting.

3. On the newly added setting, change the name to GroupBrushDB.

4. Change the type to String.

5. Change the value to be the value of the connection string of the database created in the previous section.

The next tab is the Endpoints tab shown in Figure 10-8. The settings are used to determine what endpoints are allowed for a worker role. The endpoints have a name, the type (direction), public port, private port, and SSL certificate name. We need to add an endpoint so that the worker has a port that is exposed to the Internet to take requests.

image

Figure 10-8. Azure worker role Endpoints tab

To add the endpoint, follow these steps:

1. Go to the Endpoints tab in the worker role configuration.

2. Click Add Endpoint.

3. On the newly added Endpoint entry, change the name to SignalREndpoint.

4. Change the type to Input.

5. Change the protocol to http.

6. Make the public and private ports 80.

7. Leave the SSL certificate name blank.

8. Press Ctrl+S to save your settings.

With the endpoint added, there is one last tab: the Configuration tab (see Figure 10-9). This is the tab used to choose the size and number of instances of the worker role that we want running by default.

image

Figure 10-9. Azure worker role Configuration tab

Although we did not cover all the possible configuration tabs available to the worker role, we discussed the most common ones. Now we have to test our deployment locally.

Testing Deployment Locally

Because the application is cloud-based, you might think that we need to deploy to the cloud to test it. With the Azure cloud service, this is not the case—we can use the Azure Compute Emulator that is available from Microsoft.

To test locally, set the GroupBrush.Cloud project as the StartUp project. Once this is set, we can run/debug the project using the Emulator by pressing F5. When the application is running, we see output similar to Figure 10-10 by right-clicking the Azure Emulator in the task notification area and then clicking Show Compute Emulator UI.

image

Figure 10-10. Azure Compute Emulator

With our application running locally, we now have to deploy it to the cloud.

Deploying an Application to the Cloud

We are finally ready to deploy our application to the cloud. We have implemented the code that will run the server, created the cloud service that will host the server, created the SQL database to persist the data for the server, configured the server environments, and tested the server locally. The deployment is very straightforward and simple to do. The deployment will generally take minutes to complete, so expect to wait when you need to deploy.

It is possible to deploy your service to a staging environment first and then move it to a “production” environment once you have validated that it works on staging. We will show how to directly deploy to “production,” which is the same as staging (it just requires selecting a different environment in the publishing steps). To deploy to “production,” follow these steps:

1. Right-click the GroupBrush.Cloud project and click Publish, as shown in Figure 10-11.

image

Figure 10-11. Cloud Service context menu

2. Sign in to the Azure account and click Next.

3. In the Publish Settings section shown in Figure 10-12, do the following:

a. Choose the cloud service that was set up earlier.

b. Change the environment to Production.

c. Click Next.

image

Figure 10-12. Publish Settings dialog box

4. Validate settings in the Publish Summary dialog box similar to Figure 10-13.

image

Figure 10-13. Publish summary dialog box

5. Click Publish.

Now that we have successfully published, our application should be live in the cloud. But what if it is very popular and needs to scale? Scaling the application is discussed next.

Scaling the Server

To scale our application beyond the one instance, we need to be able to address the following areas:

· Communication between instances

· In-memory storage

· Different authentication between machines

Once we have addressed these areas, the application should be able to scale up well.

The first issue to address is the communication between instances, which is critical to ensure that a message from a user on one instance is available on all other instances. To address this, we can use one of the many scaleout solutions that SignalR supports.

For this application, in which there are a lot of messages, and delay is very noticeable, the Redis scaleout solution is the best fit. The solution requires a dedicated server, but it is far better than the SQL or MessageBus solution for our application.

Azure recently added Redis Cache as a PaaS, which works well and is very easy to set up. To support scaling for our application, add the Redis Cache to Azure with the following steps:

1. Log in to http://portal.azure.com.

2. Click the New(+) button in the bottom left.

3. Click on Redis Cache.

4. Enter a name for the cache.

5. Choose the resource group of the existing application objects.

6. Choose the same location as the existing application.

7. Click Create.

We now have a Redis cache that is ready to use.

Moving on to the in-memory storage, which worked well when there was one instance, but does not work now because the data would be in memory on different instances. Because we are using Redis as our scaleout solution, we can also use it as shared memory between the instances.

To do this, we need to create a class that implements IMemStorage for Redis. So we create the RedisStorage class that derives from IMemStorage, as shown in Listing 10-79.

Listing 10-79. RedisStorage Class Definition

public class RedisStorage : IMemStorage

The RedisStorage constructor in Listing 10-80 has one dependency that is injected. The injected item is RedisConfiguration, defined in Listing 10-81, that provides the configuration for Redis.

Listing 10-80. RedisStorage Constructor

public RedisStorage(RedisConfiguration redisConfiguration)
{
_redisConfiguration = redisConfiguration;
_userNames = new Dictionary<int, string>();
}

Listing 10-81. RedisConfiguration Entity Class

public class RedisConfiguration
{
public string HostName { get; set; }
public string Password { get; set; }
public int Port { get; set; }
public bool UseRedis { get; set; }
public string EventKey { get; set; }
public RedisConfiguration(string hostName, string password, bool useRedis)
{
HostName = hostName;
Password = password;
UseRedis = useRedis;
Port = 6379;
EventKey = "GroupBrush";
}
}

Listing 10-82 shows the private variables for RedisStorage. There are four prefixes that are used to keep the keys unique between the types of objects. There is also an in-memory dictionary to store the username to prevent numerous lookups to the Redis server for username.

Listing 10-82. RedisStorage Private Variables

private const string TRANSACTION_PREFIX = "CanvasTransaction:";
private const string ACTION_PREFIX = "CanvasBrushAction:";
private const string USERS_PREFIX = "CanvasUsers:";
private const string USERNAMES_PREFIX = "CanvasUsernames:";
private readonly RedisConfiguration _redisConfiguration;
Dictionary<int, string> _userNames;

The first method is the AddBrushAction method, which connects to the Redis server to get the next transaction number for the canvas and uses it as the sequence for the CanvasBrushAction that is passed in. Next, it takes the CanvasBrushAction and serializes it into JSON and adds it to the canvas list on the Redis server. Finally, it returns the CanvasBrushAction object.

Listing 10-83. AddBrushAction Method

public CanvasBrushAction AddBrushAction(string canvasId, CanvasBrushAction brushData)
{
int transactionNumber = 0;
using (var conn = new RedisConnection(_redisConfiguration.HostName,_redisConfiguration.Port,password: _redisConfiguration.Password))
{
conn.Open();
var incrTask = conn.Hashes.Increment(0, TRANSACTION_PREFIX + canvasId, "transaction");
transactionNumber = (int)incrTask.Result;
}
brushData.Sequence = transactionNumber;
string serializedData = JsonConvert.SerializeObject(brushData);
using (var conn = new RedisConnection(_redisConfiguration.HostName,_redisConfiguration.Port,password: _redisConfiguration.Password))
{
conn.Open();
conn.Lists.AddLast(0, ACTION_PREFIX + canvasId, serializedData);
}
return brushData;
}

The GetBrushActions method shown in Listing 10-84 contacts the Redis server and asks for all CanvasBrushActions that have been stored from the current position to the latest entry. If there are results returned from Redis, it deserializes each JSON object and adds it to the list of actions to return. These actions are then sorted by sequence number. Finally these actions are returned.

Listing 10-84. GetBrushActions Method

public List<CanvasBrushAction> GetBrushActions(string canvasId, int currentPosition)
{
List<CanvasBrushAction> actions = new List<CanvasBrushAction>();
string[] storedActions = null;
using (var conn = new RedisConnection(_redisConfiguration.HostName,_redisConfiguration.Port,password: _redisConfiguration.Password))
{
conn.Open();
var rangeTask = conn.Lists.RangeString(0, ACTION_PREFIX + canvasId, currentPosition, Int32.MaxValue);
storedActions = rangeTask.Result;
}
if (storedActions != null)
{
foreach (string storedAction in storedActions)
{
actions.Add(JsonConvert.DeserializeObject<CanvasBrushAction>(storedAction));
}
actions.Sort(new Comparison<CanvasBrushAction>((a, b) => { return a.Sequence.CompareTo(b.Sequence); }));
}
return actions;
}

The GetCanvasUser method in Listing 10-85 is very simple: it connects to the Redis server to get a list of users that have connected to that canvas. The method then puts all the users through a HashSet to get unique users, which it returns.

Listing 10-85. GetCanvasUsers Method

public List<string> GetCanvasUsers(string canvasId)
{

List<string> returnValue = new List<string>();
HashSet<string> uniqueList = new HashSet<string>();
using (var conn = new RedisConnection(_redisConfiguration.HostName,_redisConfiguration.Port,password: _redisConfiguration.Password))
{
conn.Open();
var getAllTask = conn.Sets.GetAllString(0, USERS_PREFIX + canvasId);
uniqueList = new HashSet<string>(getAllTask.Result.ToList());
}
returnValue = uniqueList.ToList<string>();
return returnValue;
}

The AddUserToCanvas method in Listing 10-86 is another simple method that adds the passed-in ID to the list of users on the Redis server for the specified canvas ID.

Listing 10-86. AddUserToCanvas Method

public void AddUserToCanvas(string canvasId, string id)
{
using (var conn = new RedisConnection(_redisConfiguration.HostName,_redisConfiguration.Port,password: _redisConfiguration.Password))
{
conn.Open();
conn.Sets.Add(0, USERS_PREFIX + canvasId, id);
}
}

The RemoveUserFromCanvas method in Listing 10-87 is similar to the previous method, but it removes the user instead of adding it.

Listing 10-87. RemoveUserFromCanvas Method

public void RemoveUserFromCanvas(string canvasId, string id)
{
using (var conn = new RedisConnection(_redisConfiguration.HostName,_redisConfiguration.Port,password: _redisConfiguration.Password))
{
conn.Open();
conn.Sets.Remove(0, USERS_PREFIX + canvasId, id);
}
}

The GetUserName and StoreUserName methods shown in Listing 10-88 use a combination of in-memory and RedisStorage to store the data. To prevent a lot of out-of-process lookups for username in the GetUserName method, the method looks at its internal username structure first to see whether it exists. If it does not, it connects to the Redis server to retrieve it. The StoreUserName method first stores the data on the Redis server and then stores it in memory.

Listing 10-88. GetUserName and StoreUserName Methods

public string GetUserName(int id)
{
string userName = null;
if (_userNames.ContainsKey(id))
{
userName = _userNames[id];
}
else
{
using (var conn = new RedisConnection(_redisConfiguration.HostName,_redisConfiguration.Port,password: _redisConfiguration.Password))
{
conn.Open();
var getTask = conn.Strings.GetString(0, USERNAMES_PREFIX + id.ToString());
userName = getTask.Result;
_userNames[id] = userName;
}
}
return userName;
}
public void StoreUserName(int id, string userName)
{
using (var conn = new RedisConnection(_redisConfiguration.HostName,_redisConfiguration.Port,password: _redisConfiguration.Password))
{
conn.Open();
conn.Strings.Set(0, USERNAMES_PREFIX + id.ToString(), userName).Wait();
}
_userNames[id] = userName;
}

Once all the parts of the RedisStorage class are put together, we see the complete class (see Listing 10-89).

Listing 10-89. Complete RedisStorage Class

public class RedisStorage : IMemStorage
{
private const string TRANSACTION_PREFIX = "CanvasTransaction:";
private const string ACTION_PREFIX = "CanvasBrushAction:";
private const string USERS_PREFIX = "CanvasUsers:";
private const string USERNAMES_PREFIX = "CanvasUsernames:";
private readonly RedisConfiguration _redisConfiguration;
Dictionary<int, string> _userNames;
public RedisStorage(RedisConfiguration redisConfiguration)
{
_redisConfiguration = redisConfiguration;
_userNames = new Dictionary<int, string>();
}
public CanvasBrushAction AddBrushAction(string canvasId, CanvasBrushAction brushData)
{
int transactionNumber = 0;
using (var conn = new RedisConnection(_redisConfiguration.HostName,_redisConfiguration.Port,password: _redisConfiguration.Password))
{
conn.Open();
var incrTask = conn.Hashes.Increment(0, TRANSACTION_PREFIX + canvasId, "transaction");
transactionNumber = (int)incrTask.Result;
}
brushData.Sequence = transactionNumber;
string serializedData = JsonConvert.SerializeObject(brushData);
using (var conn = new RedisConnection(_redisConfiguration.HostName,_redisConfiguration.Port,password: _redisConfiguration.Password))
{
conn.Open();
conn.Lists.AddLast(0, ACTION_PREFIX + canvasId, serializedData);
}
return brushData;
}
public List<CanvasBrushAction> GetBrushActions(string canvasId, int currentPosition)
{
List<CanvasBrushAction> actions = new List<CanvasBrushAction>();
string[] storedActions = null;
using (var conn = new RedisConnection(_redisConfiguration.HostName,_redisConfiguration.Port,password: _redisConfiguration.Password))
{
conn.Open();
var rangeTask = conn.Lists.RangeString(0, ACTION_PREFIX + canvasId, currentPosition, Int32.MaxValue);
storedActions = rangeTask.Result;

}
if (storedActions != null)
{
foreach (string storedAction in storedActions)
{
actions.Add(JsonConvert.DeserializeObject<CanvasBrushAction>(storedAction));
}
actions.Sort(new Comparison<CanvasBrushAction>((a, b) => { return a.Sequence.CompareTo(b.Sequence); }));
}
return actions;
}
public List<string> GetCanvasUsers(string canvasId)
{

List<string> returnValue = new List<string>();
HashSet<string> uniqueList = new HashSet<string>();
using (var conn = new RedisConnection(_redisConfiguration.HostName,_redisConfiguration.Port,password: _redisConfiguration.Password))
{
conn.Open();
var getAllTask = conn.Sets.GetAllString(0, USERS_PREFIX + canvasId);
uniqueList = new HashSet<string>(getAllTask.Result.ToList());
}
returnValue = uniqueList.ToList<string>();
return returnValue;
}
public void AddUserToCanvas(string canvasId, string id)
{
using (var conn = new RedisConnection(_redisConfiguration.HostName,_redisConfiguration.Port,password: _redisConfiguration.Password))
{
conn.Open();
conn.Sets.Add(0, USERS_PREFIX + canvasId, id);
}
}

public void RemoveUserFromCanvas(string canvasId, string id)
{
using (var conn = new RedisConnection(_redisConfiguration.HostName,_redisConfiguration.Port,password: _redisConfiguration.Password))
{
conn.Open();
conn.Sets.Remove(0, USERS_PREFIX + canvasId, id);
}
}
public string GetUserName(int id)
{
string userName = null;
if (_userNames.ContainsKey(id))
{
userName = _userNames[id];
}
else
{
using (var conn = new RedisConnection(_redisConfiguration.HostName,_redisConfiguration.Port,password: _redisConfiguration.Password))
{
conn.Open();
var getTask = conn.Strings.GetString(0, USERNAMES_PREFIX + id.ToString());
userName = getTask.Result;
_userNames[id] = userName;
}
}
return userName;
}
public void StoreUserName(int id, string userName)
{
using (var conn = new RedisConnection(_redisConfiguration.HostName,_redisConfiguration.Port,password: _redisConfiguration.Password))
{
conn.Open();
conn.Strings.Set(0, USERNAMES_PREFIX + id.ToString(), userName).Wait();
}
_userNames[id] = userName;
}
}

With RedisStorage complete, we have taken care of the second issue about in-memory. So the final issue to address is different authentication between machines.

Whenever you deploy an application instance, it is created with a unique key. So if you are deploying more than one instance, each instance has its own unique key. Each time a cookie is encrypted on the server, it will be encrypted with a key that other instances cannot decrypt.

To correct this, we can create a class that will encrypt and decrypt the keys the same way on every instance. The AesDataProtector class implements IDataProtector (see Listing 10-90). For this class, we inject in the constructor the password and salt that we want it to use. The constructor then uses a built-in function to return bytes we can use as the key and initial vector (IV).

Listing 10-90. AesDataProtector Class

public class AesDataProtector : IDataProtector
{
private byte [] _IV;
private byte [] _key;
public AesDataProtector(string password, string salt)
{
Rfc2898DeriveBytes key = new Rfc2898DeriveBytes(password, Encoding.ASCII.GetBytes(salt));
_key = key.GetBytes(256 / 8);
_IV = key.GetBytes(128 / 8);
}

public byte[] Protect(byte[] userData)
{
byte[] encrypted;
using (AesCryptoServiceProvider aesAlg = new AesCryptoServiceProvider())
{
aesAlg.Key = _key;
aesAlg.IV = _IV;
ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
using (MemoryStream msEncrypt = new MemoryStream())
{
using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
{
csEncrypt.Write(userData, 0, userData.Length);
csEncrypt.FlushFinalBlock();
encrypted = msEncrypt.ToArray();
}
}
}
return encrypted;
}

public byte[] Unprotect(byte[] protectedData)
{
byte[] output = null;
using (AesCryptoServiceProvider aesAlg = new AesCryptoServiceProvider())
{
aesAlg.Key = _key;
aesAlg.IV = _IV;

ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);

using (MemoryStream msDecrypt = new MemoryStream(protectedData))
{
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
{
byte[] buffer = new byte[8];
using (MemoryStream msOutput = new MemoryStream())
{
int read;
while ((read = csDecrypt.Read(buffer, 0, buffer.Length)) > 0)
{
msOutput.Write(buffer, 0, read);
}
output = msOutput.ToArray();
}
}
}
}
return output;
}
}

There are two functions, Protect and Unprotect, which are very simple and do an encryption on the data in the Protect function and a decryption on the data in the Unprotect function.

To use the new AesDataProtector class, we must provide a provider class that implements the IDataProtectionProvider interface (see Listing 10-91). The class is constructed so that an AesDataProtector is to be injected in the constructor when the Create method is called to return this AesDataProtector.

Listing 10-91. AesDataProtectionProvider Class

public class AesDataProtectionProvider : IDataProtectionProvider
{
AesDataProtector _dataProtector;
public AesDataProtectionProvider(AesDataProtector dataProtector)
{
_dataProtector = dataProtector;
}
public IDataProtector Create(params string[] purposes)
{
return _dataProtector;
}
}

With the scaling limitations addressed, let’s add the scaling to our project with the following steps:

1. Run the following command from the Package Manager Console for the Default package of GroupBrush.Worker and GroupBrush.Web:

Install-Package Microsoft.AspNet.SignalR.Redis

2. Run the following command from the Package Manager Console for the Default package of GroupBrush.BL:

Install-Package BookSleeve -version x.y.z

a. Make sure the installed version is the same as the other projects.

Install-Package Newtonsoft.Json -version x.y.z

b. Make sure the installed version is the same as the other projects.

3. Add the RedisConfiguration class with the contents of Listing 10-81 in the GroupBrush.Entity project.

4. Add the RedisStorage class with the contents of Listing 10-89 under the Storage folder in the GroupBrush.BL project.

5. Add a DataProtectors solution folder to the GroupBrush.BL project.

6. Add the AesDataProtector class with the contents of Listing 10-90 under the DataProtectors folder in the GroupBrush.BL project.

7. Add the AesDataProtectionProvider class with the contents of Listing 10-91 under the DataProtectors folder in the GroupBrush.BL project.

8. Add configuration settings to the GroupBrush.Worker role:

a. Add a setting named GroupBrushRedisHostname with the value of the host name of the Redis server (the host name under the properties menu for Azure Redis Cache).

b. Add a setting named GroupBrushRedisPassword with the value of the password of the Redis server (the primary under the keys menu for Azure Redis Cache).

c. Add a setting named UseRedis with the value of true for cloud configuration and false for local configuration.

9. Add the contents of Listing 10-92 to the top of the WireUp method in the UnityWireupConfiguration class.

Listing 10-92. Redis Configuration Settings

string groupBrushRedisHostname = CloudConfigurationManager.GetSetting("GroupBrushRedisHostname");
string groupBrushRedisPassword = CloudConfigurationManager.GetSetting("GroupBrushRedisPassword");
string strUseRedis = CloudConfigurationManager.GetSetting("UseRedis") ?? "false";
bool useRedis = bool.Parse(strUseRedis);
RedisConfiguration redisConfiguration = new RedisConfiguration(groupBrushRedisHostname,groupBrushRedisPassword,useRedis);
dependencyResolver.RegisterInstance<RedisConfiguration>(redisConfiguration);

10.Replace the line from Listing 10-93 with Listing 10-94 in the UnityWireupConfiguration class.

Listing 10-93. IMemStorage Setting to Replace

dependencyResolver.RegisterType<IMemStorage,
MemoryStorage>(new ContainerControlledLifetimeManager());

Listing 10-94. IMemStorage Replacement Setting

if (useRedis)
{
dependencyResolver.RegisterType<IMemStorage, RedisStorage>(new ContainerControlledLifetimeManager(), new InjectionConstructor(redisConfiguration));
}
else
{
dependencyResolver.RegisterType<IMemStorage, MemoryStorage>(new ContainerControlledLifetimeManager());
}

11.Add the logic in Listing 10-95 at the top of the Configuration method in StartUp.cs.

Listing 10-95. Redis Configuration Logic for StartUp.cs

string strUseRedis = CloudConfigurationManager.GetSetting("UseRedis") ?? "false";
bool useRedis = bool.Parse(strUseRedis);

12.Add the logic in Listing 10-96 between the UseWebApi and MapSignalR functions of the Configuration method in StartUp.cs.

Listing 10-96. Redis Configuration for SignalR in StartUp.cs

RedisConfiguration redisConfiguration = dependencyResolver.Resolve<RedisConfiguration>();
if (redisConfiguration.UseRedis)
{
GlobalHost.DependencyResolver.UseRedis(redisConfiguration.HostName, redisConfiguration.Port, redisConfiguration.Password, redisConfiguration.EventKey);
}

13.Update the instance count in the GroupBrush.Worker Configuration tab to increase the number of instances.

There is now a fully functioning server, so the next step is to create the clients that can connect to it.

Developing the Clients

To connect to the server, we will create a JavaScript client. Even though the clients will run on their own machines, we have to host the content for them on the server. To do this, we will use the OWIN Static Files middleware to serve up the content in the form of URLs that a normal web site would have. The content is stored in a specific structure, and the OWIN middleware needs to be added, so we complete the following steps before moving on:

1. Run the following command from the Package Manager Console for the Default package of GroupBrush.Web and GroupBrush.Worker.

Install-Package Microsoft.AspNet.SignalR.JS
Install-Package Microsoft.Owin.StaticFiles

2. Add a solution folder named Content to the GroupBrush.Web project.

3. Add a solution folder named Content to the Public folder in the GroupBrush.Web project.

4. Add a solution folder named Scripts to the Public folder in the GroupBrush.Web project.

5. Add a solution folder named Styles to the Public folder in the GroupBrush.Web project.

6. Add a solution folder named Styles to the GroupBrush.Web project.

7. Add the content in Listing 10-97 to the end of the Configuration method in the StartUp.cs file.

Listing 10-97. Options to Set Up the OWIN Static Files Middleware

var sharedOptions = new SharedOptions() { RequestPath = new PathString(string.Empty), FileSystem = new PhysicalFileSystem(".//public//content") };
app.UseDefaultFiles(new Microsoft.Owin.StaticFiles.DefaultFilesOptions(sharedOptions) { DefaultFileNames = new List<string>() { "index.html" } });
app.UseStaticFiles("/public");
app.UseStaticFiles("/content");
app.UseStaticFiles("/scripts");
app.UseStaticFiles("/styles");
app.UseStaticFiles(new StaticFileOptions(sharedOptions));

In Listing 10-97, the first line sets the option to redirect any request for the root document to the Public content folder. The next line sets the default document of index.html if the requested path does not contain a file. The remaining lines set up their respective paths to respond to requests for those paths.

Developing the Client Homepage

For our client, there are three views that the user will see: homepage signed out, homepage signed in, and canvas room page.

The first page users see is homepage signed out, as shown in Figure 10-14, which presents a page for visitors to create an account or sign in. Once users log in, they see a screen similar to Figure 10-15, which gives them the ability to sign out, create canvases, or join canvases. Both these views are the same index.html page in Listing 10-98, but with different .css classes applied. The last view is the canvas page that can be seen later in the chapter in Figure 10-16. In this view, the user can have real-time drawings and chat.

image

Figure 10-14. Signed-out index page

image

Figure 10-15. Signed-in index page

Listing 10-98. Index.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Group Brush</title>
<link rel="stylesheet" type="text/css" href="/public/styles/Index.css" />
<script src="/public/scripts/jquery-1.6.4.min.js"></script>
<script src="/public/scripts/Index.js"></script>
</head>
<body>
<div id="mainContent">
<div id="mainMessage">
<h1>Group Brush</h1>
<div id="loadingMessage">Loading...</div>
</div>
<div id="actionContent">
<div class="loggedOut" id="createAccountContent">
<div>
<span>Welcome to Group Brush</span><br />
<span>This site is for friends to draw together. Please Sign-In or Create an account.</span>
</div>
<div id="createAccount">
<span class="sectionTitle">Create account</span><br />
<span>User name:</span><input name="username" id="username" type="text" /><br />
<span>Password:</span><input name="password" id="password" type="password" /><br />
<span>Verify Password:</span><span id="passwordNotEqual" class="error">Passwords do not match</span><input name="verifyPassword" id="verifyPassword" type="password" />
<span id="createAccountError" class="error">Could not create account</span>
<button id="btnCreateAccount">Create Account</button>
</div>
</div>
<div class="loggedIn" id="canvasActionContent">
<div id="createCanvasContainer">
<span class="sectionTitle">Create shared canvas</span>
<span>Canvas name:</span><input name="canvasname" id="canvasname" type="text" /><br />
<span>Canvas Description:</span><input name="canvasdescription" id="canvasdescription" type="text" /><br />
<span id="createCanvasError" class="error">Could not create canvas</span>
<button id="btnCreateCanvas">Create Canvas</button>
</div>
<div id="joinCanvasContainer">
<span class="sectionTitle">Join shared canvas</span>
<span>Canvas name:</span><input name="canvasname" id="canvasname" type="text" /><br />
<span id="joinCanvasError" class="error">Could not join canvas</span>
<button id="btnJoinCanvas">Join Canvas</button>
</div>
</div>
</div>
</div>
<div id="userContent">
<div class="loggedOut">
<div id="loginWidget">
<div id="LogIn">
<form id="loginForm">
<span id="signIn">Sign In</span><br />
<span>User name:</span><input name="username" id="username" type="text" />
<span>Password:</span><input name="password" id="password" type="password" />
</form>
<span id="loginError" class="error">Could not login</span>
<button id="btnLogin">Login</button>
</div>
</div>
</div>
<div class="loggedIn">
<div id="LogOut">
<button id="btnLogout">Logout</button>
</div>
</div>
</div>
</body>
</html>

The HTML in Listing 10-98 shows the divs that are for the loggedIn and loggedOut states. The HTML and styling demonstrated in this chapter are simplified and not best practices, so we do not go into much detail for them.

Moving on to look at the .css classes in Listing 10-99, most of the styling is to set up the div containers for the various page functions. Both the loggedOut and loggedIn .css are set to display: none; this is done because a server-side loggedIn check in the JavaScript will set the correct state when the page loads.

Listing 10-99. Index.css

div.loggedOut{
display: none;
}
div.loggedIn{
display: none;
}
div#mainContent{
float: left;
}
div#userContent{
float: left;
}
div#mainMessage h1{
font-size: 72px;
}
span.error{
color:red;
display:none;
}
div#actionContent>div{
border: 1px solid black;
float:left;
}
div#actionContent div.loggedIn div{
height: 220px;
}
div#actionContent div.loggedOut div{
height: 300px;
}
div#actionContent div div{
width: 200px;
float: left;
border-left: 1px solid black;
border-right: 1px solid black;
}
div#actionContent div div span{
padding: 4px;
}
div#actionContent div div input{
display: block;
margin-left: 4px;
margin-top: 4px;
}
div#actionContent div div button{
display: block;
margin-left: 4px;
margin-top: 4px;
}
span.sectionTitle{
font-size:larger;
}

For our client to work, there is a lot of JavaScript that has to be in place. Even for a simple page like the homepage, it will have many helper functions, JavaScript events, and Ajax calls.

The homepage JavaScript has three helper functions in Listing 10-100: showAsLoggedIn, showAsLoggedOut, and openCanvas. The first two methods change the .css to show the loggedIn or loggedOut view. The third method is called once a canvas ID has been returned from the server so that the canvas page can be loaded. To support mobile devices that don’t allow pop-ups, we’ll check the window width and height. If both the measurements are below the threshold, the script will redirect the page; otherwise, it will open a new window to the new canvas.

Listing 10-100. Index.JS Helper Functions

function showAsLoggedIn() {
$('div.loggedIn').show();
$('div.loggedOut').hide();
}
function showAsLoggedOut() {
$('div.loggedOut').show();
$('div.loggedIn').hide();
}
function openCanvas(id) {
var canvasURL = "/Content/Canvas.html?canvasId=" + id;
if (window.innerWidth <= 800 && window.innerHeight <= 600) {
window.location.href = canvasURL;
}
else {
window.open(canvasURL, "_blank");
}
}

The rest of the JavaScript logic for the homepage occurs once we determine the page is fully loaded by the $(document).ready handler being called, which lets us know it is safe to bind to the events. The only event that we are interested in is the click event, to which we’ll add a handler for all the buttons on the page.

The buttons on the page are for logging in, creating an account, creating a canvas, and joining a canvas. There is an additional click handler added to every button to reset the error text .css in case it was displayed. Besides binding events in the ready method, we also do an Ajax call to determine whether the user is logged in, which occurs at the end of the ready function and can be seen at the end of Listing 10-106.

The first binding we’ll look at is the login button in Listing 10-101. Whenever the login button is clicked, the values for the username and password input fields are added to a JSON object as UserName and Password, respectively. Next, an Ajax call is made to the server to try to log in with the values from the JSON object. If the Ajax call is successful and returns with "success", the logged-in status is shown. If the call is successful and does not have "success" or the call fails, the logged-out page will be shown.

Listing 10-101. Login Button Logic

$('#btnLogin').click(function () {
var dataObject = { "UserName": $('form#loginForm input#username').val(), "Password": $('form#loginForm input#password').val() };
$.ajax({
url: '/public/api/login',
type: 'post',
contentType: "application/json",
data: JSON.stringify(dataObject),
success: function (data, status) {
if (status == "success" && data == "Success") {
showAsLoggedIn();
}
else {
$('span#loginError').show();
}
},
error: function (data) {
$('span#loginError').show();
showAsLoggedOut();
}
});
});

The next binding is the create account button in Listing 10-102. Whenever the button is clicked, the values for password and password verify input fields are compared. If they are not equal, and an error message is shown, the event is ended. If they are equal, the username and password input fields are added to a JSON object as UserName and Password, respectively. Next, an Ajax call is made to the server to create an account with the values from the JSON object. If the Ajax call is successful and returns with "success", the account creation was successful and the logged-in status is shown. If the call is successful and does not have "success" or the call fails, the logged-out page will be shown with an error message.

Listing 10-102. Create Account Button Logic

$('#btnCreateAccount').click(function () {
$('span#passwordNotEqual').hide();
if ($('div#createAccount input#password').val() != $('div#createAccount input#verifyPassword').val())
{
$('span#passwordNotEqual').show();
return;
}
var dataObject = {
"UserName": $('div#createAccount input#username').val(),
"Password": $('div#createAccount input#password').val()
};
$.ajax({
url: '/public/api/user',
type: 'post',
contentType: "application/json",
data: JSON.stringify(dataObject),
success: function (data, status) {
if (status == "success" && data == "Success") {
showAsLoggedIn();
}
else
{
$('span#createAccountError').show();
}
},
error: function (data) {
showAsLoggedOut();
$('span#createAccountError').show();
}
});
});

Now for the create canvas button binding in Listing 10-103. Whenever the button is clicked, the canvas name and canvas description input fields are added to a JSON object as Name and Description, respectively. Next an Ajax call is made to the server to create a canvas with the values from the JSON object. If the Ajax call is successful and returns with "success", the canvas creation was successful and the openCanvas function is called to open the canvas room page. If the call is successful and does not have "success" or the call fails, an error message is shown.

Listing 10-103. Create Canvas Button Logic

$('#btnCreateCanvas').click(function () {
var dataObject = {
"Name": $('div#createCanvasContainer input#canvasname').val(),
"Description": $('div#createCanvasContainer input#canvasdescription').val()
};
$.ajax({
url: '/api/canvas',
type: 'post',
contentType: "application/json",
data: JSON.stringify(dataObject),
dataType: "Json",
success: function (data, status) {
if (status == "success" && data != undefined) {
openCanvas(data)
}
else {
$('span#createCanvasError').show();
}
},
error: function (data) {
$('span#createCanvasError').show();
}
});
});

Next is the join canvas button binding in Listing 10-104. Whenever the button is clicked, the canvas name input field is added to a JSON object as Name. Then an Ajax call is made to the server to join a canvas with the values from the JSON object. If the Ajax call is successful and returns with "success", joining the canvas was successful and the openCanvas function is called to open the canvas room page. If the call is successful and does not have "success" or the call fails, an error message is shown.

Listing 10-104. Join Canvas Button Logic

$('#btnJoinCanvas').click(function () {
var dataObject = {
"Name": $('div#joinCanvasContainer input#canvasname').val()
};
$.ajax({
url: '/api/canvas',
type: 'put',
contentType: "application/json",
data: JSON.stringify(dataObject),
dataType: "Json",
success: function (data, status) {
if (status == "success" && data != undefined) {
openCanvas(data)
}
else {
$('span#joinCanvasError').show();
}
},
error: function (data) {
$('span#joinCanvasError').show();
}
});
});

The last button binding on the homepage is the logout button in Listing 10-105. Whenever the button is clicked, an Ajax call is made to the server to log out. If the Ajax call is successful, the logout page will be shown. Otherwise, there is no feedback if the call was unsuccessful.

Listing 10-105. Logout Button Logic

$('#btnLogout').click(function () {
$.ajax({
url: '/public/api/logout',
type: 'post',
success: function (data) {
showAsLoggedOut();
}
});
});

Listing 10-106. Complete Index.JS File

function showAsLoggedIn() {
$('div.loggedIn').show();
$('div.loggedOut').hide();
}
function showAsLoggedOut() {
$('div.loggedOut').show();
$('div.loggedIn').hide();
}
function openCanvas(id) {
var canvasURL = "/Content/Canvas.html?canvasId=" + id;
if (window.innerWidth <= 800 && window.innerHeight <= 600) {
window.location.href = canvasURL;
}
else {
window.open(canvasURL, "_blank");
}
}
$(document).ready(function () {
$('div#mainContent button').bind('click', function () { $('span.error').hide();})
$('#btnLogin').click(function () {
var dataObject = { "UserName": $('form#loginForm input#username').val(), "Password": $('form#loginForm input#password').val() };
$.ajax({
url: '/public/api/login',
type: 'post',
contentType: "application/json",
data: JSON.stringify(dataObject),
success: function (data, status) {
if (status == "success" && data == "Success") {
showAsLoggedIn();
}
else {
$('span#loginError').show();
}
},
error: function (data) {
$('span#loginError').show();
showAsLoggedOut();
}
});
});
$('#btnCreateAccount').click(function () {
$('span#passwordNotEqual').hide();
if ($('div#createAccount input#password').val() != $('div#createAccount input#verifyPassword').val())
{
$('span#passwordNotEqual').show();
return;
}
var dataObject = {
"UserName": $('div#createAccount input#username').val(),
"Password": $('div#createAccount input#password').val()
};
$.ajax({
url: '/public/api/user',
type: 'post',
contentType: "application/json",
data: JSON.stringify(dataObject),
success: function (data, status) {
if (status == "success" && data == "Success") {
showAsLoggedIn();
}
else
{
$('span#createAccountError').show();
}
},
error: function (data) {
showAsLoggedOut();
$('span#createAccountError').show();
}
});
});
$('#btnCreateCanvas').click(function () {
var dataObject = {
"Name": $('div#createCanvasContainer input#canvasname').val(),
"Description": $('div#createCanvasContainer input#canvasdescription').val()
};
$.ajax({
url: '/api/canvas',
type: 'post',
contentType: "application/json",
data: JSON.stringify(dataObject),
dataType: "Json",
success: function (data, status) {
if (status == "success" && data != undefined) {
openCanvas(data)
}
else {
$('span#createCanvasError').show();
}
},
error: function (data) {
$('span#createCanvasError').show();
}
});
});
$('#btnJoinCanvas').click(function () {
var dataObject = {
"Name": $('div#joinCanvasContainer input#canvasname').val()
};
$.ajax({
url: '/api/canvas',
type: 'put',
contentType: "application/json",
data: JSON.stringify(dataObject),
dataType: "Json",
success: function (data, status) {
if (status == "success" && data != undefined) {
openCanvas(data)
}
else {
$('span#joinCanvasError').show();
}
},
error: function (data) {
$('span#joinCanvasError').show();
}
});
});
$('#btnLogout').click(function () {
$.ajax({
url: '/public/api/logout',
type: 'post',
success: function (data) {
showAsLoggedOut();
}
});
});
$.ajax({
url: '/public/api/loginStatus',
type: 'get',
success: function (data,status,x) {
if (status == "success" && data == "loggedIn") {
showAsLoggedIn();
}
else {
showAsLoggedOut();
}
},
error: function (data) {
$('div.loggedOut').show();
},
complete: function () {
$('#loadingMessage').hide();
}
});
});

Now that have gone over the homepage section, let’s add the files to our project with the following steps:

1. Create an HTML page named index.html with the content of Listing 10-98 in the Content folder under the Public folder.

a. Update the version numbers for the scripts to be the same as the version in the Scripts folder.

b. Update the Copy To Output Directory property to Copy Always.

2. Create a .css page named index.css with the content of Listing 10-99 in the Styles folder under the Public folder.

a. Update the Copy To Output Directory property to Copy Always.

3. Create a JavaScript page named index.js with the content of Listing 10-106 in the Scripts folder under the Public folder.

a. Update the Copy To Output Directory property to Copy Always.

Next we will create the canvas room, in which real-time drawing and chat happens.

Developing the Client Canvas Room

The HTML for the canvas room is basically just the structure for demonstration purposes. Figure 10-16 shows that there is not much HTML in the room.

image

Figure 10-16. Canvas page

There are a couple of things to point out in the HTML, however: the stacked canvases, the status messaging, and data attributes on the brushes and colors. Listing 10-107 shows three canvases of the same size and position that have different z-indexes. We stack the canvases this way so that we can temporarily draw to these canvases and erase what is there without having to worry about redrawing what was on the canvas before. The other thing to look at in the bottom of the HTML in Listing 10-107 is the status messages that we display, depending on the connection state. The data attributes on the brushes and colors are used by the JavaScript to determine what brush to use and what color the brush should be.

Listing 10-107. Canvas.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>GroupBrush</title>
<link rel="stylesheet" type="text/css" href="/styles/Canvas.css" />
<script src="/public/scripts/jquery-1.6.4.min.js"></script>
<script src="/public/scripts/jquery.signalr-2.0.3.min.js"></script>
<script src="/signalr/hubs"></script>
<script src="/scripts/canvas.js"></script>
</head>
<body>
<div id="canvasContent" class="connectionContent">
<div id="mainContent">
<div id="canvasContainer">
<div style="position:relative; width: 600px;height: 400px;">
<canvas width="600" height="400" id="scratchCanvas" style="position:absolute; z-index: 3;"></canvas>
<canvas width="600" height="400" id="drawingCanvas" style="position:absolute; z-index: 2;"></canvas>
<canvas width="600" height="400" id="cursorCanvas" style="position: absolute; z-index: 1;"></canvas>
</div>
</div>
<div id="toolboxContainer">
<div class="brushes">
<div class="brush tool selected" data-brushtype="1">Brush</div>
<div class="brush tool" data-brushtype="2">Eraser</div>
<div class="brush tool" data-brushtype="3">Fill</div>
<div class="brush tool" data-brushtype="4">Clear All</div>
</div>
<div class="colors">
<div class="color tool selected" data-colorvalue="#FF0000" id="Red"> </div>
<div class="color tool" data-colorvalue="#FFA500" id="Orange"> </div>
<div class="color tool" data-colorvalue="#FFFF00" id="Yellow"> </div>
<div class="color tool" data-colorvalue="#00FF00" id="Green"> </div>
<div class="color tool" data-colorvalue="#0000FF" id="Blue"> </div>
<div class="color tool" data-colorvalue="#800080" id="Purple"> </div>
<div class="color tool" data-colorvalue="#A52A2A" id="Brown"> </div>
<div class="color tool" data-colorvalue="#000000" id="Black"> </div>
</div>
<div class="sizeContainer">
<span>Size:</span>
<select id="sizes">
<option value="1">1</option>
<option value="2">2</option>
<option value="4">4</option>
<option value="8">8</option>
<option value="16">16</option>
<option value="32">32</option>
<option value="64">64</option>
<option value="128">128</option>
</select>
</div>
</div>
</div>
<div id="sideContent">
<h1 id="CanvasName"></h1>
<span id="CanvasDescription"></span>
<div id="onlineUsersContainer">
<span>Users Online:</span>
<ul id="userList"></ul>
</div>
<div id="chatContainer">
<span>Chat Messages:</span>
<div id="chatMessagesContainer">
<ul id="chatMessages"></ul>
</div>
<div id="chatInputContainer">
<input type="text" id="chatInput" />
<button id="btnSendMessage">Send</button>
</div>
</div>
</div>
</div>
<div id="loadingCanvasContent" class="connectionContent">
<span>Connecting...</span>
</div>
<div id="syncingCanvasContent" class="connectionContent">
<span>Syncing...</span>
</div>
<div id="reloadCanvasContent" class="connectionContent">
<span>Connection problems </span>
<button id="btnReload">Reload</button>
</div>

</body>
</html>

Once again, the .css for this project is very basic, and there really isn’t much to the content in Listing 10-108 other than setting up the flow and containers of the page, brush colors, and status message display.

Listing 10-108. Canvas.css

div#mainContent{
float: left;
width: 600px;
height: 600px;
border: solid 2px black;
}
div#sideContent{
float: left;
width: 300px;
height: 600px;
}
div#canvasContainer{
border-bottom: 1px solid black;
}
div#toolboxContainer{
width: 600px;
height: 100px;
margin-top: 20px;
}
div#toolboxContainer{
width: 600px;
height: 100px;
}
div#toolboxContainer div.brushes {
width: 110px;
float: left;
}
div#toolboxContainer div.colors {
width: 430px;
float: left;
}
div#toolboxContainer div.colors div#Red {
background-color: #FF0000;
}
div#toolboxContainer div.colors div#Orange {
background-color: #FFA500;
}
div#toolboxContainer div.colors div#Yellow {
background-color: #FFFF00;
}
div#toolboxContainer div.colors div#Green {
background-color: #00FF00;
}
div#toolboxContainer div.colors div#Blue {
background-color: #0000FF;
}
div#toolboxContainer div.colors div#Purple {
background-color: #800080;
}
div#toolboxContainer div.colors div#Brown {
background-color: #A52A2A;
}
div#toolboxContainer div.colors div#Black {
background-color: #000000;
}
div#toolboxContainer div.actions {
width: 50px;
float: left;
}
div#toolboxContainer div.tool {
width: 50px;
height: 50px;
float: left;
border: 1px solid black;
text-align: center;
}
div#toolboxContainer div.selected {
border: 5px solid #AAAAAA;
width: 42px;
height: 42px;
}
div#onlineUsersContainer{
border: 1px solid black;
margin: 0px 10px 10px 10px;
height: 200px;
}
div#chatContainer{
border: 1px solid black;
margin: 10px;
height: 290px;
position: relative;
}
div#chatInputContainer{
position:absolute;
bottom: 0px;
}
div#sideContent ul li {
list-style: none;
}
div#sideContent span{
margin-left: 4px;
}
div#sideContent input{
margin-left: 4px;
width: 210px;
}
div#canvasContent{
display:none;
}
div#syncingCanvasContent{
display:none;
}
div#reloadCanvasContent{
display:none;
}
body{
touch-action:none;
}
canvas#scratchCanvas{
-ms-user-select:none;
}

The code for the client is very complex, so we will break it down into smaller related functions. As with the HTML, the JavaScript is for example purposes only and may not be using best practices.

Listing 10-109 shows the variables for the canvas room. The first variable, hubProxy, is used to store the proxy to the SignalR hub. The next variable, userList, is an array that is used to store the users displayed in the user list. The lastCursorPositionTime variable marks the last time the current user’s cursor was sent to the server. The lastDrawTime variable was the last time the draw positions were sent to the server. The lastSequence variable stores the latest draw sequence that has been received. The isBrushown and firstTouch variables keep track of whether a drag event is occurring. The brushPositions array stores the positions that need to be sent to the server. The otherBrushCursors array stores the username, position, and last update time of the other canvas room users’ cursors. The isDrawingContinuedvariable is for the Canvas object to know whether it needs to move the position context before it should draw. Finally, the cleanUpCursorCanvasTimer variable stores the setTimeout handle so that it can be cancelled.

Listing 10-109. Canvas.js Variables

var hubProxy = null;
var userList = [];
var lastCursorPositionTime = 0;
var lastDrawTime = 0;
var lastSequence = 0;
var isBrushDown = false;
var firstTouch = null;
var brushPositions = [];
var otherBrushCursors = [];
var isDrawingContinued = false;
var cleanUpCursorCanvasTimer = null;

Besides variables for the canvas room, there are also “objects” that can be passed around the script. The first object shown in Listing 10-110 is the Position object, which stores the x and y coordinates of a position. The next object, OtherBrush, stores the brush (cursor) name, position, and last update time. The last object, CurrentBrushData, retrieves the current selected color, size, and type from the HTML DOM and is returned as an “object” that can be sent to the server.

Listing 10-110. Canvas.js Objects

function Position(x, y) {
this.x = x;
this.y = y;
}
function OtherBrush(userName, x, y) {
this.UserName = userName;
this.x = x;
this.y = y;
this.updateTime = Date.now();
}
function CurrentBrushData() {
this.Color = $('div.color.tool.selected').data('colorvalue') || 1;
this.Size = $('select#sizes').val() || 1;
this.Type = $('div.brush.tool.selected').data('brushtype') || 1;
}

The next similar sets of logic are the helper methods shown in Listing 10-111. The getCanvasId method looks into the query string to find the canvas ID by looking for a query string key of 'canvasId'. If the canvas ID cannot be found in the query string, this function redirects the page back to the site root.

Listing 10-111. Canvas.js Helper Methods

function getCanvasId() {
var canvasId = null;
var queryString = window.location.search.substring(1);
var queryStringArray = queryString.split('&');
for (var x = 0; x < queryStringArray.length; x++) {
var keyValue = queryStringArray[x].split('=');
if (keyValue.length > 1) {
var queryKey = keyValue[0];
var queryValue = keyValue[1];
if (queryKey == 'canvasId') {
canvasId = queryStringArray[x];
}
}
}
if (canvasId == undefined || canvasId.length != 45) {
window.location('/');
}
return canvasId;
}

function connectionChange(contentToShow) {
$('div.connectionContent').hide();
if (contentToShow != undefined)
$(contentToShow).show();
}
$(document).ready(function () {
$('div.color.tool').click(function () {
$('div.color.tool').removeClass('selected');
$(this).addClass('selected');
});
$('div.brush.tool').click(function () {
$('div.brush.tool').removeClass('selected');
$(this).addClass('selected');
});
$('div#reloadCanvasContent button#btnReload').click(function () {
connect();
});
connect();
});

The connectionChange helper method is used whenever the viewing state of the canvas room needs to change. It hides all the major content div sections with a class name of 'connectionContent'. If it can find the content of the passed-in .css selector, it shows that content.

The last helper method is the ready function provided by JQuery when the document is fully loaded. We use this helper function to add a click binding for the color and brushes div containers that will change the selected color or brush. The helper also adds a click binding for the reload button that is displayed when the connection cannot connect. Finally, this function calls the connect method that connects the SignalR hub (discussed next).

The connect method is one of the most important methods of the canvas room script (see Listing 10-112). This method is responsible for connecting to the SignalR hub and binding all its events. If the hub connection is successful, this method binds the mouse and touch events for the canvas and synchronizes the canvas to the latest on the server.

Listing 10-112. Connect Method

function connect() {
connectionChange('div#loadingCanvasContent');
var canvasId = getCanvasId();
var connection = $.hubConnection('/signalr', { qs: canvasId });
hubProxy = connection.createHubProxy('CanvasHub');
hubProxy.on('MoveOtherCursor', function (userName, x, y) {
drawOtherBrush(userName, x, y)
});
hubProxy.on('UserChatMessage', function (message) {
$('#chatMessages').append('<li>' + message + '</li>')
});
hubProxy.on('UserConnected', function (userName) {
userConnected(userName);
});
hubProxy.on('UserDisconnected', function (userName) {
userDisconnected(userName);
});
hubProxy.on('DrawCanvasBrushAction', function (canvasBrushAction) {
drawCanvasBrushAction(canvasBrushAction);
});
connection.reconnecting(function () {
connectionChange('div#loadingCanvasContent');
});
connection.reconnected(function () {
syncRoom();
});
connection.disconnected(function () {
connectionChange('div#reloadCanvasContent');
});
connection.start().done(function () {
$('#btnSendMessage').click(function () {
hubProxy.invoke('SendChatMessage', $('#chatInput').val()).done(function(){$('#chatInput').val('');});
});

var canvasTouch = document.getElementById('scratchCanvas');
canvasTouch.addEventListener('touchstart', touchStart, false);
canvasTouch.addEventListener('touchmove', touchMove, false);
canvasTouch.addEventListener('touchend', touchEnd, false);
canvasTouch.addEventListener('touchleave', touchEnd, false);
canvasTouch.addEventListener('touchcancel', touchEnd, false);

if (window.navigator.msPointerEnabled) {
canvasTouch.addEventListener('MSPointerDown', msTouchStart, false);
canvasTouch.addEventListener('MSPointerMove', msTouchMove, false);
canvasTouch.addEventListener('MSPointerUp', msTouchEnd, false);
}
else
{
var canvas = $('#scratchCanvas');
canvas.bind('mousemove', mouseMove);
canvas.bind('mousedown', mouseDown);
canvas.bind('mouseup', mouseUp);
}
syncRoom();
});
}

For our application, there are two types of drawing that occur based on other users’ events: drawing the other users’ cursor and drawing their brush actions.

Drawing the other users’ cursors occurs when the server calls the drawOtherBrush method. This method stores the brush (cursor) position in an array based on username. The event then calls the drawAllBrushes method, which cancels any pending cleanup timers.

The method clears the canvas that the cursors are drawn on to and then loops through the array of users’ cursors, drawing any that been updated within the last second. If any cursors were drawn, this method creates a timer that will fire in one-half second to call the drawAllBrushesmethods to repeat the process.

The second event is to draw the other users’ brush actions (see Listing 10-133). This event is also triggered by the server when it calls the drawCanvasBrushAction method, which stores the latest drawing sequence and draws the passed-in canvasBrushAction. If thecanvasBrushAction type is 1 or 2, it draws or erases, respectively, at the positions defined. If the canvasBrushAction type is 3 or 4, it fills or clears the whole canvas, respectively.

Listing 10-113. Drawing Other Brushes Methods

function drawOtherBrush(userName, x, y) {
otherBrushCursors[userName] = new OtherBrush(userName, x, y);
drawAllBrushes();
}
function drawAllBrushes()
{
if (cleanUpCursorCanvasTimer != null) clearTimeout(cleanUpCursorCanvasTimer);
var dirtyCanvas = false;
var c = document.getElementById("cursorCanvas");
var ctx = c.getContext("2d");
ctx.fillStyle = "#000000";
ctx.clearRect(0, 0, 600, 400);
for (var key in otherBrushCursors) {
var currentBrush = otherBrushCursors[key];
if (Date.now() - currentBrush.updateTime < 1000) {
dirtyCanvas = true;
ctx.fillRect(currentBrush.x - 5, currentBrush.y - 5, 10, 10);
ctx.fillText(currentBrush.UserName, currentBrush.x + 10, currentBrush.y);
}
}
if(dirtyCanvas) cleanUpCursorCanvasTimer = setTimeout(function () { drawAllBrushes(); }, 500);
}
function drawCanvasBrushAction(canvasBrushAction) {
var c = document.getElementById('drawingCanvas');
var ctx = c.getContext("2d");
ctx.beginPath();
lastSequence = canvasBrushAction.Sequence;
if (canvasBrushAction.Type == 1 || canvasBrushAction.Type == 2) {
var brushActionSize = canvasBrushAction.Size;
ctx.strokeStyle = canvasBrushAction.Color;
ctx.lineWidth = brushActionSize;
if (canvasBrushAction.BrushPositions.length > 0) {
ctx.moveTo(canvasBrushAction.BrushPositions[0].X, canvasBrushAction.BrushPositions[0].Y);
}
for (var x = 0; x < canvasBrushAction.BrushPositions.length; x++) {
var position = canvasBrushAction.BrushPositions[x];
ctx.lineTo(position.X, position.Y);
if (canvasBrushAction.Type == 1)
ctx.stroke();
else if(canvasBrushAction.Type == 2)
ctx.clearRect(position.X, position.Y, brushActionSize, brushActionSize)
}
}
else if (canvasBrushAction.Type == 3) {
ctx.fillStyle = canvasBrushAction.Color;
ctx.fillRect(0, 0, 600, 400);
}
else if (canvasBrushAction.Type == 4) {
ctx.clearRect(0, 0, 600, 400);
}
ctx.closePath();
}

The userConnected and userDisconnected methods in Listing 10-114 are called from the SignalR hub whenever a user is connected or disconnected. When this occurs, the user is added or removed from the in-memory list of users. This list of users is then redrawn with thedrawUserList method.

Listing 10-114. User Connection Methods

function userConnected(userName) {
var alreadyExists = false;
for (var x = 0; x < userList.length; x++) {
if (userList[x] == userName) {
alreadyExists = true;
break;
}
}
if(!alreadyExists) userList.push(userName);
drawUserList();
}
function userDisconnected(userName) {
for (var x = userList.length - 1; x > -1; x--) {
if (userList[x] == userName) {
userList.splice(x, 1);
}
}
drawUserList();
}
function drawUserList() {
var userListHTML = [];
for (var x = 0; x < userList.length; x++) {
userListHTML.push('<li>');
userListHTML.push(userList[x]);
userListHTML.push('</li>');
}
$('#userList').html(userListHTML.join(''));
}

For the canvas room, one of the critical functions is to be able to draw. The drawing methods shown in Listing 10-115 handle the generic input, logging of the drawing, and actual drawing to the canvas objects.

Listing 10-115. Drawing Methods

function canvasBrushMove(position) {
if (isBrushDown) {
storeDrawCoordinates(position);
}
if (Date.now() - lastCursorPositionTime > 100) {
lastCursorPositionTime = Date.now()
hubProxy.invoke('MoveCursor', position.x, position.y);
}
}
function canvasBrushDown(position) {
isBrushDown = true;
storeDrawCoordinates(position);
return true;
}
function canvasBrushUp(position) {
isBrushDown = false;
storeDrawCoordinates(position);

sendBrushData(brushPositions);
brushPositions = [];
isDrawingContinued = false;
var c = document.getElementById("scratchCanvas");
var ctx = c.getContext("2d");
ctx.closePath();
ctx.clearRect(0, 0, 600, 400);
}
function sendBrushData(brushPositionsData)
{
var currentBrushData = new CurrentBrushData();
var brushData = {
BrushPositions: brushPositionsData,
ClientSequenceId: Date.now(),
Color: currentBrushData.Color,
Size: currentBrushData.Size,
Type: currentBrushData.Type
}
hubProxy.invoke('SendDrawCommand', brushData).fail(function (error) {
});
}
function storeDrawCoordinates(position) {
var c = document.getElementById("scratchCanvas");
var ctx = c.getContext("2d");
var allowChange = false;
if (brushPositions.length > 0)
{
var lastPosition = brushPositions[brushPositions.length - 1];
if (Math.abs(lastPosition.x - position.x) >= 1 || Math.abs(lastPosition.y - position.y) >= 1) allowChange = true;
}
else {
allowChange = true;
}
if (!isDrawingContinued) {
ctx.beginPath();
isDrawingContinued = true;
ctx.moveTo(position.x, position.y);
}
var currentBrushData = new CurrentBrushData();
ctx.strokeStyle = currentBrushData.Color;
ctx.lineWidth = currentBrushData.Size;
ctx.lineTo(position.x, position.y);
ctx.stroke();
brushPositions.push(position);
if (Date.now() - lastDrawTime > 50) {
lastDrawTime = Date.now();
var tempPositions = brushPositions.splice(0);
brushPositions.push(position);
sendBrushData(tempPositions);
}

For the drawing to occur, we need to handle the input when the brush is brought down, is moved, or is brought up. When the brush is brought down, it triggers the canvasBrushDown method. We set a variable that a drag is occurring and store the position of the brush.

The next input is triggered when the brush is moved, which calls the canvasBrushMove method. If the brush is part of a drag, the coordinates are stored. If the time since the last brush positions sent to the server is more than 50 milliseconds, the unsent brush positions are then sent to the server.

The last input is when the brush is brought up, which triggers the canvasBrushUp method. This method stops the drag, stores the brush’s position, sends the brush data to the server, clears the brush positions array, and clears the canvas surface that was recording the brush’s position.

In the various input methods, the brush position was being stored by the storeDrawCoordinates method. In this method, the position of the brush is drawn to a canvas. The brush positions are then stored in an array. Every so often, these positions are cleared from the array and sent to the server using the sendBrushData method.

The sendBrushData method is the method that gets the current brush size, color, and type. It combines them with the passed–in brush positions and the current time in a numeric format. It then takes this data and sends it to the hub by calling the SendDrawCommand server-side method.

Because the application is critically dependent on the input methods for it to work, we support both mouse and touch movements. As shown previously, there are generic methods for brush down, move, and up. Listing 10-116 shows three methods for mouse, touch for Microsoft-based browsers, and touch for non-Microsoft-based browsers.

Listing 10-116. User Input Capture Methods

function getDrawPosition(e) {
var canvasRect = document.getElementById('scratchCanvas').getBoundingClientRect();
var xPos = e.clientX - canvasRect.left;
var yPos = e.clientY - canvasRect.top;
return new Position(xPos, yPos);
}
function mouseMove(e) {
canvasBrushMove(getDrawPosition(e));
}
function mouseDown(e) {
canvasBrushDown(getDrawPosition(e));
}
function mouseUp(e) {
canvasBrushUp(getDrawPosition(e));
}
function msTouchStart(e) {
e.preventDefault();
if (firstTouch == null && e.buttons == 1) {
firstTouch = e.pointerId;
canvasBrushDown(new Position(e.clientX, e.clientY));
}
}
function msTouchMove(e) {
e.preventDefault();
if (firstTouch == e.pointerId) {
canvasBrushMove(new Position(e.clientX, e.clientY));
}
}
function msTouchEnd(e) {
e.preventDefault();
if(e.buttons == 0 && firstTouch == e.pointerId){
canvasBrushUp(new Position(e.clientX, e.clientY));
firstTouch = null;
}
}
function touchStart(e) {
e.preventDefault();
if (firstTouch == null && e.changedTouches.length > 0) {
var touchData = e.changedTouches[0];
firstTouch = touchData.identifier;
canvasBrushDown(new Position(touchData.pageX, touchData.pageY));
}
}
function touchMove(e) {
e.preventDefault();
for (var t = 0; t < e.changedTouches.length; t++) {
if (e.changedTouches[t].identifier == firstTouch) {
var touchData = e.changedTouches[t];
canvasBrushMove(new Position(touchData.pageX, touchData.pageY));
}
}
}
function touchEnd(e) {
e.preventDefault();
for (var t = 0; t < e.changedTouches.length; t++) {
if (e.changedTouches[t].identifier == firstTouch) {
firstTouch = null;
var touchData = e.changedTouches[t];
canvasBrushUp(new Position(touchData.pageX, touchData.pageY));
}
}
}

The methods are pretty self-explanatory, but note that the mouse methods determine the position in reference to the canvas, and the touch methods are set up to handle the touch pointer one at a time.

Whenever a canvas is first connected or reconnected, the code calls the syncRoom function to synchronize the canvas to what is latest on the server. It can synchronize by calling the SyncToRoom server–side hub method with the last known canvas sequence (see Listing 10-117). If this method returns successfully, it updates the users list and completes any brush actions. If the method fails, it changes the page state to the reload content.

Listing 10-117. Canvas Sync Method

function syncRoom() {
connectionChange('div#syncingCanvasContent');
hubProxy.invoke('SyncToRoom', lastSequence).done(function (canvasSnapShot) {
userList = [];
$.each(canvasSnapShot.Users, function () {
userConnected(this);
});
$.each(canvasSnapShot.Actions, function () {
drawCanvasBrushAction(this);
});
$('h1#CanvasName').html(canvasSnapShot.CanvasName);
$('span#CanvasDescription').html(canvasSnapShot.CanvasDescription);
connectionChange('div#canvasContent');
}).fail(function () {
connectionChange('div#reloadCanvasContent');
});
}

Listing 10-118 gives the complete code for Canvas.js.

Listing 10-118. Complete Canvas.js

var hubProxy = null;
var userList = [];
var lastCursorPositionTime = 0;
var lastDrawTime = 0;
var lastSequence = 0;
var isBrushDown = false;
var firstTouch = null;
var brushPositions = [];
var otherBrushCursors = [];
var isDrawingContinued = false;
var cleanUpCursorCanvasTimer = null;

function Position(x, y) {
this.x = x;
this.y = y;
}
function OtherBrush(userName, x, y) {
this.UserName = userName;
this.x = x;
this.y = y;
this.updateTime = Date.now();
}
function CurrentBrushData() {
this.Color = $('div.color.tool.selected').data('colorvalue') || 1;
this.Size = $('select#sizes').val() || 1;
this.Type = $('div.brush.tool.selected').data('brushtype') || 1;
}

function getCanvasId() {
var canvasId = null;
var queryString = window.location.search.substring(1);
var queryStringArray = queryString.split('&');
for (var x = 0; x < queryStringArray.length; x++) {
var keyValue = queryStringArray[x].split('=');
if (keyValue.length > 1) {
var queryKey = keyValue[0];
var queryValue = keyValue[1];
if (queryKey == 'canvasId') {
canvasId = queryStringArray[x];
}
}
}
if (canvasId == undefined || canvasId.length != 45) {
window.location('/');
}
return canvasId;
}

function connectionChange(contentToShow) {

$('div.connectionContent').hide();
if (contentToShow != undefined)
$(contentToShow).show();
}
$(document).ready(function () {
$('div.color.tool').click(function () {
$('div.color.tool').removeClass('selected');
$(this).addClass('selected');
});
$('div.brush.tool').click(function () {
$('div.brush.tool').removeClass('selected');
$(this).addClass('selected');
});
$('div#reloadCanvasContent button#btnReload').click(function () {
connect();
});
connect();
});

function connect() {
connectionChange('div#loadingCanvasContent');
var canvasId = getCanvasId();
var connection = $.hubConnection('/signalr', { qs: canvasId });
hubProxy = connection.createHubProxy('CanvasHub');
hubProxy.on('MoveOtherCursor', function (userName, x, y) {
drawOtherBrush(userName, x, y)
});
hubProxy.on('UserChatMessage', function (message) {
$('#chatMessages').append('<li>' + message + '</li>')
});
hubProxy.on('UserConnected', function (userName) {
userConnected(userName);
});
hubProxy.on('UserDisconnected', function (userName) {
userDisconnected(userName);
});
hubProxy.on('DrawCanvasBrushAction', function (canvasBrushAction) {
drawCanvasBrushAction(canvasBrushAction);
});
connection.reconnecting(function () {
connectionChange('div#loadingCanvasContent');
});
connection.reconnected(function () {
syncRoom();
});
connection.disconnected(function () {
connectionChange('div#reloadCanvasContent');
});
connection.start().done(function () {
$('#btnSendMessage').click(function () {
hubProxy.invoke('SendChatMessage', $('#chatInput').val()).done(function(){$('#chatInput').val('');});
});

var canvasTouch = document.getElementById('scratchCanvas');
canvasTouch.addEventListener('touchstart', touchStart, false);
canvasTouch.addEventListener('touchmove', touchMove, false);
canvasTouch.addEventListener('touchend', touchEnd, false);
canvasTouch.addEventListener('touchleave', touchEnd, false);
canvasTouch.addEventListener('touchcancel', touchEnd, false);

if (window.navigator.msPointerEnabled) {
canvasTouch.addEventListener('MSPointerDown', msTouchStart, false);
canvasTouch.addEventListener('MSPointerMove', msTouchMove, false);
canvasTouch.addEventListener('MSPointerUp', msTouchEnd, false);
}
else
{
var canvas = $('#scratchCanvas');
canvas.bind('mousemove', mouseMove);
canvas.bind('mousedown', mouseDown);
canvas.bind('mouseup', mouseUp);
}
syncRoom();
});
}

function syncRoom() {
connectionChange('div#syncingCanvasContent');
hubProxy.invoke('SyncToRoom', lastSequence).done(function (canvasSnapShot) {
userList = [];
$.each(canvasSnapShot.Users, function () {
userConnected(this);
});
$.each(canvasSnapShot.Actions, function () {
drawCanvasBrushAction(this);
});
$('h1#CanvasName').html(canvasSnapShot.CanvasName);
$('span#CanvasDescription').html(canvasSnapShot.CanvasDescription);
connectionChange('div#canvasContent');
}).fail(function () {
connectionChange('div#reloadCanvasContent');
});
}

function drawOtherBrush(userName, x, y) {
otherBrushCursors[userName] = new OtherBrush(userName, x, y);
drawAllBrushes();
}
function drawAllBrushes()
{
if (cleanUpCursorCanvasTimer != null) clearTimeout(cleanUpCursorCanvasTimer);
var dirtyCanvas = false;
var c = document.getElementById("cursorCanvas");
var ctx = c.getContext("2d");
ctx.fillStyle = "#000000";
ctx.clearRect(0, 0, 600, 400);
for (var key in otherBrushCursors) {
var currentBrush = otherBrushCursors[key];
if (Date.now() - currentBrush.updateTime < 1000) {
dirtyCanvas = true;
ctx.fillRect(currentBrush.x - 5, currentBrush.y - 5, 10, 10);
ctx.fillText(currentBrush.UserName, currentBrush.x + 10, currentBrush.y);
}
}
if(dirtyCanvas) cleanUpCursorCanvasTimer = setTimeout(function () { drawAllBrushes(); }, 500);
}
function drawCanvasBrushAction(canvasBrushAction) {
var c = document.getElementById('drawingCanvas');
var ctx = c.getContext("2d");
ctx.beginPath();
lastSequence = canvasBrushAction.Sequence;
if (canvasBrushAction.Type == 1 || canvasBrushAction.Type == 2) {
var brushActionSize = canvasBrushAction.Size;
ctx.strokeStyle = canvasBrushAction.Color;
ctx.lineWidth = brushActionSize;
if (canvasBrushAction.BrushPositions.length > 0) {
ctx.moveTo(canvasBrushAction.BrushPositions[0].X, canvasBrushAction.BrushPositions[0].Y);
}
for (var x = 0; x < canvasBrushAction.BrushPositions.length; x++) {
var position = canvasBrushAction.BrushPositions[x];
ctx.lineTo(position.X, position.Y);
if (canvasBrushAction.Type == 1)
ctx.stroke();
else if(canvasBrushAction.Type == 2)
ctx.clearRect(position.X, position.Y, brushActionSize, brushActionSize)
}
}
else if (canvasBrushAction.Type == 3) {
ctx.fillStyle = canvasBrushAction.Color;
ctx.fillRect(0, 0, 600, 400);
}
else if (canvasBrushAction.Type == 4) {
ctx.clearRect(0, 0, 600, 400);
}
ctx.closePath();
}
function getDrawPosition(e) {
var canvasRect = document.getElementById('scratchCanvas').getBoundingClientRect();
var xPos = e.clientX - canvasRect.left;
var yPos = e.clientY - canvasRect.top;
return new Position(xPos, yPos);
}
function mouseMove(e) {
canvasBrushMove(getDrawPosition(e));
}
function mouseDown(e) {
canvasBrushDown(getDrawPosition(e));
}
function mouseUp(e) {
canvasBrushUp(getDrawPosition(e));
}
function msTouchStart(e) {
e.preventDefault();
if (firstTouch == null && e.buttons == 1) {
firstTouch = e.pointerId;
canvasBrushDown(new Position(e.clientX, e.clientY));
}
}
function msTouchMove(e) {
e.preventDefault();
if (firstTouch == e.pointerId) {
canvasBrushMove(new Position(e.clientX, e.clientY));
}
}
function msTouchEnd(e) {
e.preventDefault();
if(e.buttons == 0 && firstTouch == e.pointerId){
canvasBrushUp(new Position(e.clientX, e.clientY));
firstTouch = null;
}
}
function touchStart(e) {
e.preventDefault();
if (firstTouch == null && e.changedTouches.length > 0) {
var touchData = e.changedTouches[0];
firstTouch = touchData.identifier;
canvasBrushDown(new Position(touchData.pageX, touchData.pageY));
}
}
function touchMove(e) {
e.preventDefault();
for (var t = 0; t < e.changedTouches.length; t++) {
if (e.changedTouches[t].identifier == firstTouch) {
var touchData = e.changedTouches[t];
canvasBrushMove(new Position(touchData.pageX, touchData.pageY));
}
}
}
function touchEnd(e) {
e.preventDefault();
for (var t = 0; t < e.changedTouches.length; t++) {
if (e.changedTouches[t].identifier == firstTouch) {
firstTouch = null;
var touchData = e.changedTouches[t];
canvasBrushUp(new Position(touchData.pageX, touchData.pageY));
}
}
}
function canvasBrushMove(position) {
if (isBrushDown) {
storeDrawCoordinates(position);
}
if (Date.now() - lastCursorPositionTime > 100) {
lastCursorPositionTime = Date.now()
hubProxy.invoke('MoveCursor', position.x, position.y);
}
}
function canvasBrushDown(position) {
isBrushDown = true;
storeDrawCoordinates(position);
return true;
}
function canvasBrushUp(position) {
isBrushDown = false;
storeDrawCoordinates(position);

sendBrushData(brushPositions);
brushPositions = [];
isDrawingContinued = false;
var c = document.getElementById("scratchCanvas");
var ctx = c.getContext("2d");
ctx.closePath();
ctx.clearRect(0, 0, 600, 400);
}
function sendBrushData(brushPositionsData)
{
var currentBrushData = new CurrentBrushData();
var brushData = {
BrushPositions: brushPositionsData,
ClientSequenceId: Date.now(),
Color: currentBrushData.Color,
Size: currentBrushData.Size,
Type: currentBrushData.Type
}
hubProxy.invoke('SendDrawCommand', brushData).fail(function (error) {
});
}
function storeDrawCoordinates(position) {
var c = document.getElementById("scratchCanvas");
var ctx = c.getContext("2d");
var allowChange = false;
if (brushPositions.length > 0)
{
var lastPosition = brushPositions[brushPositions.length - 1];
if (Math.abs(lastPosition.x - position.x) >= 1 || Math.abs(lastPosition.y - position.y) >= 1) allowChange = true;
}
else {
allowChange = true;
}
if (!isDrawingContinued) {
ctx.beginPath();
isDrawingContinued = true;
ctx.moveTo(position.x, position.y);
}
var currentBrushData = new CurrentBrushData();
ctx.strokeStyle = currentBrushData.Color;
ctx.lineWidth = currentBrushData.Size;
ctx.lineTo(position.x, position.y);
ctx.stroke();
brushPositions.push(position);
if (Date.now() - lastDrawTime > 50) {
lastDrawTime = Date.now();
var tempPositions = brushPositions.splice(0);
brushPositions.push(position);
sendBrushData(tempPositions);
}

}
function userConnected(userName) {
var alreadyExists = false;
for (var x = 0; x < userList.length; x++) {
if (userList[x] == userName) {
alreadyExists = true;
break;
}
}
if(!alreadyExists) userList.push(userName);
drawUserList();
}
function userDisconnected(userName) {
for (var x = userList.length - 1; x > -1; x--) {
if (userList[x] == userName) {
userList.splice(x, 1);
}
}
drawUserList();
}
function drawUserList() {
var userListHTML = [];
for (var x = 0; x < userList.length; x++) {
userListHTML.push('<li>');
userListHTML.push(userList[x]);
userListHTML.push('</li>');
}
$('#userList').html(userListHTML.join(''));
}

Now that we have gone over all the canvas room logic, let’s add it to the project with the following steps:

1. Add an HTML file called Canvas.html with the contents of Listing 10-107 in the Content folder of the GroupBrush.Web project.

a. Update the version numbers for the scripts to be the same as the version in the Scripts folder.

b. Update the Copy To Output Directory property to Copy Always.

2. Add a .css file called Canvas.css with the contents of Listing 10-108 in the Styles folder of the GroupBrush.Web project.

a. Update the Copy To Output Directory property to Copy Always.

3. Add a JavaScript file called Canvas.js with the contents of Listing 10-118 in the Scripts folder of the GroupBrush.Web project.

a. Update the Copy To Output Directory property to Copy Always.

4. Move all jQuery files in the Scripts directory to the Scripts directory in the Public directory.

a. Update the Copy To Output Directory property to Copy Always.

5. Right-click the GroupBrush.Cloud project and click Publish (see Figure 10-17).

image

Figure 10-17. Cloud Service context menu

6. Validate the settings in the Publish summary dialog box (similar to Figure 10-18).

image

Figure 10-18. Publish summary dialog box

7. Click Publish.

We have now successfully deployed the completed project. Try it out! Figure 10-19 shows an example of the application in action.

image

Figure 10-19. Canvas page

This example shows only one client, the JavaScript client. There are many more clients that are possible to add using the existing server structure that we have already created.

Summary

We have created an application that allows us to draw and message collaboratively in real time. The application is also secured with authentication. The application has the capability to scale as needed without much work after we add the needed code to scale. Finally, we can support a variety of clients with the server we created.

We hope you have enjoyed reading the book and now have a solid understanding of SignalR. We wish you the best of luck in your future SignalR projects.