Building a SharePoint Online chat room with SignalR and Azure – Part 1: Create the SharePoint Online app and associated website

Create the SharePoint Online app and associated website

The first step was to determine if SignalR even works in a SharePoint app.  One option was to do a bunch of research to try and discover if it would work.  A more fun option would be to just give it a go.

Step one:  Create an App for SharePoint project.

image_thumb4

Next, select the SharePoint site to use during development and the type of app.  In this case, I’m going to choose a Provider-hosted app, because we want to host it in Azure.

image_thumb61

Next, choose the web project type.  I’m a huge fan of MVC, so I’m going to use that.

image_thumb91

Finally, we’re going to use Windows Azure ACS for the authentication.

image_thumb111[1]

Visual Studio will ask to login to our development tenant so that it can deploy our app automatically.  We’ll do that.

image_thumb13

Now that our SharePoint app has been created, let’s add the necessary SignalR NuGet packages.  Start by right clicking on the Web project and selecting “Manage NuGet Packages…”

image_thumb15

Find the Microsoft ASP.NET SignalR package and install it.  It’s going to install a bunch of other packages as dependencies.  That’s ok.

image_thumb17

Note:  After installing the SignalR NuGet package, for whatever reason, the related .js files weren’t actually included in the project.  However, if I click the “Show All Files” button in the Solution Explorer, they show up and I can then select them, right click and select “Include In Project”:

image_thumb2

Next, we need to create a SignalR HubStart by creating a new Hubs folder in the web project and then adding a class to that named “ChatHub”.

image_thumb11

Our chat room is going to support three operations:  joining the chat room, sending messages, and leaving the chat room.  So, first we’ll have our ChatHub inherit from the Microsoft.AspNet.SignalR.Hub class, and then we’ll add the necessary functions to support our operations.  The context.Clients.All object is dynamic, so the functions can be named whatever we like here without having to actually define them anywhere else.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using
Microsoft.AspNet.SignalR;

namespace
BusinessApps.ChatRoomWeb.Hubs
{
   
public class ChatHub : Hub
    {
       
public void PushMessage(string userName, string photoUrl, string timestamp, string
message)
        {
           
IHubContext context = GlobalHost.ConnectionManager.GetHubContext<ChatHub
>();
            context.Clients.All.pushMessage(userName, photoUrl, timestamp, message);
        }

       
public void JoinRoom(string
userName)
        {
           
IHubContext context = GlobalHost.ConnectionManager.GetHubContext<ChatHub
>();
            context.Clients.All.joinRoom(userName);
        }

       
public void LeaveRoom(string
userName)
        {
           
IHubContext context = GlobalHost.ConnectionManager.GetHubContext<ChatHub>();
            context.Clients.All.leaveRoom(userName);
        }
    }
}

We also need to create a Startup.cs class in the main web project that we can use to tell the app to configure SignalR.

image_thumb3

Then, we’ll add the following code to that Startup.cs class:

using Microsoft.AspNet.SignalR;
using Microsoft.Owin;
using
Owin;

[
assembly: OwinStartupAttribute(typeof(BusinessApps.ChatRoomWeb.Startup))]
namespace
BusinessApps.ChatRoomWeb
{
   
public partial class Startup
    {
       
public void Configuration(IAppBuilder app)
        {
            app.MapSignalR();
        }
    }
}

Since we decided that we want to use SharePoint to gather some information from the user, we’ll make use of the PeopleManager and PersonProperties classes.  To gain access to those classes, we need to add a reference to Microsoft.SharePoint.Client.UserProfiles to our Web project.

image_thumb5

After the Microsoft.SharePoint.Client.UserProfiles library has been added to the project, expand the References of the Web project, right click the Microsoft.SharePoint.Client.UserProfiles library, and select properties:

image_thumb106

In the Properties window, change ‘Copy Local’ to True (this is necessary for the deployment to Azure later):

image_thumb108

We also need to have a class where we can store all of the SharePoint information that we retrieved so we don’t have to look it up in SharePoint all the time.  So, we’ll add a UserInfo.cs class to the Models folder:

image_thumb7

Inside the UserInfo class, we’ll just create a few properties to store some information about the user:

using System;
using System.Collections.Generic;
using System.Linq;
using
System.Web;

namespace
BusinessApps.ChatRoomWeb.Models
{
   
public class UserInfo
    {
       
public string DisplayName { get; set
; }
       
public string PhotoUrl { get; set
; }
       
public DateTime LastPing { get; set; }
    }
}

Now we’ll make our way to our HomeController.cs.  This is where most of the work happens.  The following functions are contained in the class:

  1. Index():  This is the default Action of the controller.  In this function, we’ll instantiate our new ChatHub, lookup and cache the current user’s details, and finally return the page for our chat room.
  2. SendMessage(string message):  This function is called from our chat room when the user clicks ‘Send’ or hits the Enter key.  The function receives the message, retrieves the sending user’s details, and finally pushes the message out to all the other members of the chat room.
  3. JoinRoom():  This function is automatically called from the chat room when the /Home/Index view is rendered.  The idea here is that if the page renders, then a user has joined the room.  The function gathers the user’s details from the cache and pushes a message out to all of the other members in the chat room.
  4. LeaveRoom():  This function is also automatically called, except this time it’s called when a user leaves leaves the Home/Index page, or when the users cache times out.   The function gathers the user’s details from the cache and pushes a message out to all of the other members in the chat room.
  5. Ping():  This function is called from a JavaScript function on an interval of 20 seconds.  Calling the Ping function serves two purposes:  a)  it tells the system that the user is still in the chat room and b)  it prevents the SharePoint token from expiring.
  6. GetUserDetails():  This is where the interesting bits happen in terms of working with SharePoint.  In this function, we’ll use the PeopleManager and PersonProperties classes to retrieve information about the user, and then use that information to create a URL to retrieve the user’s profile photo.  We’ll store all of that information in a cache that has a sliding expiration window of 3 minutes.  Each time a user sends a Ping(), the sliding cache window is reset.  So, depending on where the window lands exactly, if the user misses 8 or 9 Ping()s in a row the timeout will expire and we’ll assume they’ve left the chat room.
  7. CacheRemovalCallback(string key, object value, CacheItemRemovedReason reason):  This is the function that’s called by the cache when a user’s cache entry expires.  In this function, we just take the user’s details that were stored in the cache to push a message out to the members of the chat room that the user has left the room.

The entire class looks like this:

using BusinessApps.ChatRoomWeb.Hubs;
using BusinessApps.ChatRoomWeb.Models;
using Microsoft.SharePoint.Client;
using Microsoft.SharePoint.Client.UserProfiles;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Caching;
using
System.Web.Mvc;

namespace
BusinessApps.ChatRoomWeb.Controllers
{
   
public class HomeController : Controller
    {
       
private static ChatHub
_chatHub;

        [
SharePointContextFilter
]
       
public ActionResult
Index()
        {
           
if (_chatHub == null
)
                _chatHub =
new ChatHub
();

            GetUserDetails();

           
return
View();
        }

        [
SharePointContextFilter
]
       
public void SendMessage(string
message)
        {
           
if (Session["UserAccountName"] == null || HttpRuntime.Cache[Session["UserAccountName"].ToString()] == null
)
                GetUserDetails();

           
UserInfo userInfo = (UserInfo)HttpRuntime.Cache[Session["UserAccountName"
].ToString()];

            _chatHub.PushMessage(userInfo.DisplayName, userInfo.PhotoUrl,
DateTime
.UtcNow.ToString(), message);
        }

        [
SharePointContextFilter
]
       
public void
JoinRoom()
        {
           
if (Session["UserAccountName"] != null
)
            {
               
UserInfo userInfo = (UserInfo)HttpRuntime.Cache[Session["UserAccountName"
].ToString()];
                _chatHub.JoinRoom(userInfo.DisplayName);
            }
        }

        [
SharePointContextFilter
]
       
public void
LeaveRoom()
        {
           
if (Session["UserAccountName"] != null
)
            {
               
UserInfo userInfo = (UserInfo)HttpRuntime.Cache[Session["UserAccountName"
].ToString()];
                _chatHub.LeaveRoom(userInfo.DisplayName);
            }
        }

        [
SharePointContextFilter
]
       
public void
Ping()
        {
           
if (Session["UserAccountName"] != null && HttpRuntime.Cache[Session["UserAccountName"].ToString()] != null
)
            {
                ((
UserInfo)HttpRuntime.Cache[Session["UserAccountName"].ToString()]).LastPing = DateTime
.Now;
            }
        }

       
private void
GetUserDetails()
        {
           
var spContext = SharePointContextProvider
.Current.GetSharePointContext(HttpContext);

           
using (var
clientContext = spContext.CreateUserClientContextForSPHost())
            {
               
if (clientContext != null
)
                {
                   
PeopleManager peopleManager = new PeopleManager
(clientContext);
                   
PersonProperties
properties = peopleManager.GetMyProperties();
                    clientContext.Load(properties);
                    clientContext.ExecuteQuery();

                   
UserInfo userInfo = new UserInfo
()
                    {
                        DisplayName = properties.DisplayName,
                        PhotoUrl = spContext.SPHostUrl +
"/_layouts/userphoto.aspx?accountname="
+ properties.Email,
                        LastPing =
DateTime
.Now
                    };

                   
HttpRuntime.Cache.Add(properties.AccountName, userInfo, null, Cache.NoAbsoluteExpiration, new TimeSpan(0, 3, 0), CacheItemPriority.Normal, new CacheItemRemovedCallback
(CacheRemovalCallback));

                    Session[
"UserAccountName"
] = properties.AccountName;
                }
            }
        }

       
private void CacheRemovalCallback(string key, object value, CacheItemRemovedReason
reason)
        {
            _chatHub.LeaveRoom(((
UserInfo)value).DisplayName);
        }
    }
}

Ok, now we’re done with the server side stuff.  Sweet!  Next we need to create the client side View and JavaScript and we’ll be all done building the chat room website!

We’ll make our way to the _Layout.cshtml file under /Views/Shared.  Since this is going to be a SharePoint app, we don’t want all of the extra chrome that default ASP.NET MVC apps provide.  We’ll remove all of that and just leave the bare bones markup behind, like so:

<!DOCTYPE html>
<
html
>
<
head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>
    @Styles.Render("~/Content/css"
)
    @
Scripts.Render("~/bundles/modernizr")
</head
>
<
body>
    <div class="container body-content">
        @RenderBody()
   
</div>

    @
Scripts.Render("~/bundles/jquery"
)
    @
Scripts.Render("~/bundles/bootstrap"
)
    @
Scripts.Render("~/bundles/spcontext"
)
    @RenderSection(
"scripts", required: false)
</body
>
</
html
>

Next, find the Index.cshtml file under /Views/Home.  This is where we’ll create the actual chat room itself and wire it up to the necessary JavaScript bits.  The markup looks like this:

@{
    ViewBag.Title =
"Home Page"
;
}

<div id="chat-room">
    <div id="message-window">
    </div>

   
<div id="message-box">
        <textarea onkeyup="KeyUp(event)"></textarea>
        <input type="button" onclick="SendMessage()" value="Send" />
    </div
>
</
div>

@section scripts {
   
<script src="~/Scripts/jquery.signalR-2.2.0.js"></script>
    <script src="~/signalr/hubs"></script>
    <script src="~/Scripts/ChatRoom.js"></script>
}

The most confusing bit of this code for me had to do with the /signalr/hubs script reference.  What in the world is that?  It turns out that SignalR will create a client proxy for us on the fly which simplifies our client side code.  This is that proxy.  You can find more information on the proxy at http://www.asp.net/signalr/overview/guide-to-the-api/hubs-api-guide-javascript-client#clientsetup.

Also notice the reference to ChatRoom.js.  This is the JavaScript file that I created to support SignalR and the UI.  Let’s create the ChatRoom.js file under the Scripts folder:

image_thumb9

That file contains the following functions.

  1. StartHub():  This function is called as soon as the page has rendered.  It binds the functions that we created in our ChatHub.cs file to the JavaScript functions that will be executed.  We also startup the SignalR hub.
  2. SendMessage():  This is the function that’s called when the user clicks the Send button in the UI.  It makes an AJAX call to /Home/SendMessage, passing the necessary SharePoint URLs and the user’s message.
  3. ReceiveMessage(userName, photoUrl, timeStamp, message):  This function is called when the ChatHub.cs/PushMessage is called.  This function receives the information that was sent as part of the message, injects it into some HTML, and finally adds that HTML to the chat window.
  4. ReceiveJoinRoom(userName):  This function is similar to ReceiveMessage, except that it receives the JoinRoom message and adds it to the chat window.
  5. ReceiveLeaveRoom(userName):  This function is exactly like ReceiveJoinRoom, except that it receives the LeaveRoom message.
  6. JoinRoom():  This function is called automatically when the /Home/Index page is loaded to tell other members of the chat room that a user has joined the room.   The AJAX call in this function calls into HomeController.cs/JoinRoom().
  7. LeaveRoom():  This function is called when the user leaves the /Home/Index page to indicate that they have left the chat room.  The AJAX call in this function calls into HomeController.cs/LeaveRoomRoom().
  8. AddMessage(html):  This is a helper function that adds HTML to the chat window. It also auto scrolls the chat window so the most recent messages are always displayed at the bottom of the window.
  9. KeyUp(e):  This is the function that’s called when KeyUp event is fired in the text area where the user types their message and allows the user to hit the Enter key rather than having to click ‘Send’.
  10. Ping():  This is the function that’s fired every 20 seconds to indicate that the user is still in the chat room.  The AJAX call in this function calls into HomeController.cs/Ping().

The entire ChatRoom.js file looks like this:

function StartHub() {
   
var
chatHub = $.connection.chatHub;

    chatHub.client.pushMessage =
function
(userName, photoUrl, timeStamp, message) {
        ReceiveMessage(userName, photoUrl, timeStamp, message);
    };

    chatHub.client.joinRoom =
function
(userName) {
        ReceiveJoinRoom(userName);
    };

    chatHub.client.leaveRoom =
function
(userName) {
        ReceiveLeaveRoom(userName);
    };

    $.connection.hub.start();
}

function
SendMessage() {
   
var textBox = $("#message-box textarea"
);

   
if (textBox.val() != ""
) {
        $.ajax({
            type:
"GET"
,
            url:
"/Home/SendMessage" + location.search + "&message="
+ textBox.val(),
            cache:
false
        });

        textBox.val(
""
);
    }

    textBox.focus();
}

function
ReceiveMessage(userName, photoUrl, timeStamp, message) {
   
var localTimeStamp = new Date(timeStamp + " UTC"
);

    AddMessage(
       
'<div class=\'message\'>'
+
           
'<img class=\'message-sender-image\' src=\'' + photoUrl + '\' alt=\'Photo of ' + userName + '\'/>'
+
           
'<div class=\'message-right\'>'
+
               
'<div class=\'message-sender\'>' + userName + '</div>'
+
               
'<div class=\'message-timestamp\'>' + localTimeStamp.toLocaleTimeString() + '</div>'
+
               
'<div class=\'message-content\'>' + message + '</div>'
+
           
'</div>'
+
       
'</div>'
        );
}

function
ReceiveJoinRoom(userName) {
    AddMessage(
           
'<div class=\'system-message\'>'
+
                userName +
" has joined the room."
+
           
'</div>'
            );
}

function
ReceiveLeaveRoom(userName) {
    AddMessage(
           
'<div class=\'system-message\'>'
+
                userName +
" has left the room."
+
           
'</div>'
            );
}

function
JoinRoom() {
    $.ajax({
        type:
"GET"
,
        url:
"/Home/JoinRoom"
+ location.search,
        cache:
false
    });
}

function
LeaveRoom() {
    $.ajax({
        type:
"GET"
,
        url:
"/Home/LeaveRoom"
+ location.search,
        cache:
false
    });
}

function
AddMessage(html) {
    $(
'#message-window'
).append(html);

    $(
"#message-window"
).animate({
        scrollTop: $(
"#message-window"
)[0].scrollHeight
    });
}

function
KeyUp(e) {
   
if
(e.keyCode === 13 || e.which === 13) {
        SendMessage();
    }
}

function
Ping() {
    $.ajax({
        type:
"GET"
,
        url:
"/Home/Ping"
+ location.search,
        cache:
false
    });
}

StartHub();

setInterval(
function
() { Ping() }, 20000);

window.onload =
function
() { JoinRoom() }
window.onunload =
function () { LeaveRoom() }

The last thing we need to do in terms of UI is update the /Content/Site.css file so that our controls will be rendered appropriately.  The entire CSS file now looks like this:

body {
   
padding-top: 50px
;
   
padding-bottom: 20px
;
   
height: 800px
;
   
font-family: "Trebuchet MS",Arial,Helvetica,sans-serif
;
}

/* Set padding to keep content from hitting the edges */
.body-content
{
   
padding-left: 15px
;
   
padding-right: 15px
;
   
height: 100%
;
}

/* 
Override the default bootstrap behavior where horizontal description lists
   will truncate terms that are too long to fit in the left column
 
*/
.dl-horizontal dt
{
   
white-space: normal
;
}

#chat-room
{
   
width: 100%
;
   
height: 100%
;
}

#message-window
{
   
width: 100%
;
   
height: 80%
;
   
overflow-y: scroll
;
   
border: solid 1px black
;
}

#message-box
{
   
height: 15%
;
   
border: solid 1px black
;
}

#message-box textarea
{
   
width: 90%
;
   
height: 100%
;
   
overflow-y: scroll
;
}

#message-box input[type=button]
{
   
width: 10%
;
   
height: 100%
;
   
background-color: darkblue
;
   
color: white
;
   
float: right
;
}

.message
{
   
width: 100%
;
   
padding-bottom: 2%
;
   
min-height: 13%
;
   
clear: both
;
}


.message-sender-image
{
   
height: 8%
;
   
width: 8%
;
   
float: left
;
   
margin-left: 1%
;
   
margin-top: 1%
;
}

.message-right
{
   
float: right
;
   
width: 91%
}

.message-sender
{
   
color: blue
;
   
float: left
;
   
padding-left: 1%
;
   
padding-top: 1%
;   
}

.message-timestamp
{
   
float: right
;
   
color: blue
;
   
padding-right: 1%
;
   
padding-top: 1%
;
}

.message-content
{
   
clear: both
;
   
padding-left: 1%
;
   
padding-top: 1%
;
   
-ms-word-wrap: break-word
;
   
word-wrap: break-word
;
}

.system-message
{
   
width: 100%
;
   
padding-right: 1%
;
   
padding-top: 1%
;
   
padding-left: 1%
;
   
padding-bottom: 1%
;
   
min-height: 1%
;
   
clear: both
;
   
color: red
;
   
font-style: italic;
}

That’s it!  We’re done with our coding.  Now, one last step before we give our flashy new chat room a test drive.  We need to give the app permissions to Write (even though we’ll only be reading data) to the User Profiles in SharePoint.  To do that, we’ll open the AppManifest.xml file, navigate to the Permissions tab, select User Profiles (Social), and set the Permission to Write:

image_thumb111

 

Part 1: Create the SharePoint Online app and associated website

Part 2: Test the app in Visual Studio

Part 3: Create the SharePoint Online web part

Part 4: Deploy the site to Azure

Part 5: Deploy the app in SharePoint Online

Part 6: Gotchas and Thoughts

Leave a Reply

Your email address will not be published.