How to Extend and Customize SignalR Functionality - 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 7. How to Extend and Customize SignalR Functionality

So far, you have seen the basics of SignalR; now it is time to learn the details that will help you customize SignalR to your specific needs. We start the chapter by going over some of the common extensible points. After that, we move on to extending and customizing existing components. If the component cannot be extended to accomplish the task, we discuss replacing the individual components as needed.

Keeping with the theme of expanding SignalR to your needs, we discuss hosting SignalR applications outside of IIS. We also go over how hosting is not limited to just the Windows platform. You’ll learn about the Mono framework and how it can be used to run SignalR applications on Linux and OS X. The last section shows you how to use Xamarin for the Visual Studio add-in, which uses a custom version of the Mono framework that runs SignalR clients on Android and iOS devices.

Extensibility of the SignalR Core

As mentioned in earlier chapters, the developers of SignalR have done a great job of engineering the code to be very flexible and customizable. By using a dependency resolver, you can have complete control over what aspects of SignalR you use in your applications. The dependency resolver also allows you to independently replace major core components of SignalR, depending on your needs. As you’ll see in the following sections, there are various ways to use a dependency resolver, such as replacing UserIdProvider with an implementation that uses cookies to determine the user.

Implementing a Custom Dependency Resolver

Much of the code in SignalR is abstracted to interfaces, which gives you a lot of control over your implementations. To convert these abstracted interfaces into concrete implementations, additional logic is needed. This is where a dependency resolver comes in.

By default, SignalR is configured to use the DefaultDependencyResolver class for all dependency resolutions. DefaultDependencyResolver implements the IDependencyResolver interface and resolves objects out of a simple container.DefaultDependencyResolver also has a default set of services and hub extensions registered in the constructor that is used in most applications. Even as simple as DefaultDependencyResolver is, it works well for most basic applications.

When the application becomes more complex or integrated with an existing application that already has an IoC container, it may be necessary to replace the DefaultDependencyResolver. If you are adding a new IoC container, there are many choices that can be used, including Ninject, Unity, or StructureMap. Regardless of whether you’re using a new or existing IoC container, it must implement the IDependencyResolver interface shown in Listing 7-1 and be configured in the GlobalHost.

Listing 7-1. Interface for IDependencyResolver

public interface IDependencyResolver : IDisposable
{
object GetService(Type serviceType);
IEnumerable<object> GetServices(Type serviceType);
void Register(Type serviceType, Func<object> activator);
void Register(Type serviceType, IEnumerable<Func<object>> activators);
}

As an example, we created an implementation of the IDependencyResolver for Unity (see Listing 7-2). Creating your own dependency resolver takes only a little code and can be a great benefit to your application.

Listing 7-2. Dependency Resolver for Unity

public class UnityDependencyResolver : DefaultDependencyResolver
{
IUnityContainer _container = new UnityContainer();

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 override void Register(Type serviceType, Func<object> activator)
{
_container.RegisterType(serviceType, new InjectionFactory((c) => activator.Invoke()));
base.Register(serviceType, activator);
}
}

This implementation for Unity derives from the DefaultDependencyResolver class, so all registrations that occur in that class are also registered in the Unity container. Now that we have created our own resolver, we register it using the code in Listing 7-3.

Listing 7-3. Code to Register Dependency Resolver

Microsoft.AspNet.SignalR.GlobalHost.DependencyResolver = new UnityDependencyResolver();

The next step is to use the dependency resolver to help customize SignalR applications. The first approach is to extend existing components, but if more customization is needed, components can be completely replaced.

Extending Existing Components

The most common way to extend SignalR features is to extend existing components. You may have already done this by creating a hub or persistent connection because when you create the hub or persistent connection, you extend the Hub or PersistentConnection class. When these hubs or persistent connections are created for you, they are constructed behind the scenes by the dependency resolver.

These extended classes can be made more functional by injecting the dependent classes or a dependency resolver in the constructor. With the dependency resolver, you can resolve any objects that you have registered in the dependency resolver. For example, you might have an extended class that is for a chat client hub. In the chat client, you can resolve a logger to log all the chat that goes through the hub.

But for complete customization, just extending classes may not be enough. So to take this one step farther, we next show you how to replace SignalR components.

Replacing Individual SignalR Components

In the previous section, you saw how to add on to existing components, but with limited customization. To get the customization you want, it is sometimes necessary to replace classes instead of extending them. When using the DefaultDependencyResolver class, there are 13 general classes registered and 10 hub-specific registrations. So for the out-of-the-box server experience with hubs, there are at least 23 components that can be replaced.

The general classes registered provide functions such as message bus communication management, message serialization/minification, transport and communication management, general configuration, performance and tracing, and client/server identification tracking. The hub-specific registrations provide functions that provide available hubs and their methods, request manipulation into hubs, hub management, and hub pipeline stage management. Some of these interfaces have simple implementations; others have very complex implementations with hundreds of lines and complex thread-safe logic.

You might want to replace a class instead of extending it because the current implementation may not provide access to what you need to change. An example of this is to limit the transports in your application to support only Web Sockets transport for paying customers and long polling transport for non-paying customers. By default, the TransportManager class provides Forever Frame, Server Sent Events, long polling, and Web Sockets transports to all customers. Inheriting from the TransportManager class does not provide the functionality for replacing the default transports. So for this replacement implementation, implement the ITransportManager interface and provide the logic for selecting the correct transport based on the customer type.

For a concrete example, we implement the IUserIdProvider interface with a custom class. The default implementation is the PrincipalUserIdProvider class, which provides a user ID from the name property of the user’s identity provided in the request. TheCookieUserIdProvider custom class that is implemented in Listing 7-4 retrieves a value from the request’s cookies. This value is looked up in an in-memory collection of known mappings. If a key is found, the value for that key is returned as the user ID; otherwise, a null value is returned.

Listing 7-4. Example of a Custom Component Implementing the IUserIdProvider

public class CookieUserIdProvider : IUserIdProvider
{
IUserIdStore _memoryUserIdStore;
public CookieUserIdProvider(IDependencyResolver resolver)
{
_memoryUserIdStore = resolver.Resolve<IUserIdStore>();
}
public string GetUserId(IRequest request)
{
string returnValue = null;
Cookie userIdCookie = null;
if (request.Cookies.TryGetValue("userid", out userIdCookie))
{
string strUserId = userIdCookie.Value;
Guid userGuid;
if (Guid.TryParse(strUserId, out userGuid))
{
returnValue = _memoryUserIdStore.GetUserId(userGuid);
}
}
return returnValue;
}
}
public interface IUserIdStore
{
string GetUserId(Guid cookieId);
void AddUserId(Guid cookieId, string userId);
}
public class MemoryUserIdStore : IUserIdStore
{
Dictionary<Guid, string> _knownUsers = new Dictionary<Guid, string>();
public string GetUserId(Guid cookieId)
{
string returnValue = null;
if (_knownUsers.ContainsKey(cookieId))
{
returnValue = _knownUsers[cookieId];
}
return returnValue;
}
public void AddUserId(Guid cookieId, string userId)
{
_knownUsers[cookieId] = userId;
}
}

Once we create the class, we need to register it to replace the default implementation: PrincipalUserIdProvider. As shown in Listing 7-5, we register the in-memory collection implementation MemoryUserIdStore to the IUserIdStore interface and theCookieUserIdProvider to the IUserIdProvider interface. It is critical that we register MemoryUserIdStore so that the dependency can be resolved in the CookieUserIdProvider constructor.

Listing 7-5. Example of Code to Register New Component with the DependencyResolver

GlobalHost.DependencyResolver.Register(typeof(IUserIdStore),new Func<object>(() => new MemoryUserIdStore()));
GlobalHost.DependencyResolver.Register(typeof(IUserIdProvider), new Func<object>(() => new CookieUserIdProvider(GlobalHost.DependencyResolver)));

Now that you have seen how to extend the SignalR application by using the dependency resolver, you have a good base for extending and customizing your applications. But there are many other ways to extend your application that are beyond the scope of modifying the code, including hosting outside of IIS and running on other frameworks. We focus on hosting outside of IIS in the next section.

Self-Hosting SignalR Outside of IIS

Internet Information Services (IIS) has been a great host for C# developers for years, but the footprint to deploy it is large and restrictive. So in recent years, developers have worked on a project called the Katana project, adopted by Microsoft to promote the decoupling of web components. This adoption allows the choice of host, server, and middleware components of an OWIN-based application. (As discussed in Chapter 2, WIN is a standard interface between web servers and applications that is not coupled to a specific software implementation.) SignalR implements the OWIN interface and is a middleware component in the Katana project.

In most cases with the Katana components, the host and server can be interchanged with other hosts and servers without having to recompile the application. The middleware pipeline is configured during application startup. This configuration is minimal to allow you to add only the pieces of the pipeline that you want. In this section, we cover how all these pieces work together to enable you to self-host SignalR outside of IIS. We start with a quick example on how easy it is to set up outside of IIS.

Self-Host Example

1. Create a new console application.

2. Run the Package Manager Console.

3. Type Install-Package Microsoft.AspNet.SignalR.SelfHost.

4. Type Install-Package Microsoft.AspNet.SignalR.JS.

5. Add a new item, Startup.cs, as shown in Figure 7-1.

image

Figure 7-1. OWIN startup class selection

6. Update Startup.cs with the code in Listing 7-6.

Listing 7-6. Self-host Startup Class C# Code

public class Startup
{
public void Configuration(IAppBuilder app)
{
app.MapSignalR<ConsoleApplication2.Program.SamplePersistentConnection>
("/SamplePC");
app.Run((context) =>
{
if (context.Request.Path.Value.Equals("/", StringComparison.
CurrentCultureIgnoreCase))
{
context.Response.ContentType = "text/html";
string result = System.IO.File.ReadAllText(System.Environment.
CurrentDirectory + "\\index.html");
return context.Response.WriteAsync(result);
}
if (context.Request.Path.Value.StartsWith("/scripts/", StringComparison.
CurrentCultureIgnoreCase))
{
context.Response.ContentType = "text/javascript";
//The requested should be verified but adding for simplicity of
example.
string result = System.IO.File.ReadAllText(System.Environment.
CurrentDirectory + context.Request.Path.Value);
return context.Response.WriteAsync(result);
}
return Task.FromResult<object>(null);
});
}
}

7. Add a new HTML page, Index.html, to the root of the project (see Figure 7-2).

image

Figure 7-2. HTML page template selection

8. Update the head section in Index.html to reflect Listing 7-7.

Listing 7-7. JavaScript for Index.html

<script src="/scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
<script src="/scripts/jquery.signalR-2.0.1.min.js" type="text/javascript"></script>
<script>
$(function () {
var connection = $.connection('http://localhost:5045/samplepc');

connection.received(function (data) {
$('#messages').append('<li>' + data + '</li>');
});

connection.start().done(function () {
$("#btnSend").click(function () {
connection.send($('#name').val() + ': ' + $('#message').val());
});
}).fail(function (ex) { alert(ex);})
});
</script>

9. Update the version numbers of the JQuery and JQuery.SignalR scripts to the appropriate version in the Scripts folder.

10.Update the HTML in the Index.html section to reflect Listing 7-8.

Listing 7-8. HTML for Index.html

<ul id="messages" style="border: 1px solid black; height: 250px; width: 450px; overflow:scroll; list-style:none;"></ul>
<label>Name: </label>
<input id="name" value="User A" />
<label>Message: </label>
<input id="message" />
<button id="btnSend">Send</button>

11.For each script and index.html in the properties, set Copy to Output to Copy Always.

12.Create a new SamplePersistentConnection class and add the code in Listing 7-9.

Listing 7-9. Code for SamplePersistentConnection

public class SamplePersistentConnection : PersistentConnection
{
protected override System.Threading.Tasks.Task OnReceived(IRequest request, string connectionId, string data)
{
return Connection.Broadcast(data);
}
}

13.Run the following from the command prompt with correct credentials:

netsh http add urlacl url=http://+:5045/ user=machine\username

After implementing the example, you should see results similar to Figure 7-3. Now that you have seen how easy it is to implement, you’ll learn about the components that make it up and how they work.

image

Figure 7-3. Self-host client exampleKatana Project

The Katana project is an open-source project consisting of components that are to be combined to form an OWIN-based application. The project has the components broken down into layers: host, server, middleware, and application. The components need to have the following properties: portable, modular, and scalable.

The layers are divided by functionality and should be exchangeable with other components at the same layer. The following sections discuss the individual layers and the functions they provide with some examples of how they are used. For all layers, there is only one component, but there can be multiple components for the middleware layer.

The properties are provided as guidelines to ensure a consistent experience with components so that any component written should demonstrate these properties.

Host Layer

The host layer is the layer that hosts the application in a process on the operating system. The host is also responsible for setting up a server layer and constructing the OWIN pipeline.

There are currently three supported host scenarios (IIS/ASP.NET, OwinHost.exe, and custom host). With the IIS/ASP.NET host, this configuration runs on the host and server layers because the host also provides the server and cannot be exchanged with other servers. The OwinHost.exe host is a prebuilt host that can be run in a project’s directory, and the host attempts to find the startup class. This host can also be configured to run different servers or have the startup class specified by using command-line parameters when starting the host. The custom host can include a variety of processes such as a Windows service, console application, and so on. The custom host needs to have the functionality to start the OWIN-based server and to set up the OWIN pipeline.

Server Layer

The server layer is the layer that opens a network socket, listens as requests come in, and sends the request through the OWIN pipeline. There are two implementations: SystemWeb and HttpListener. The IIS/ASP.NET host is required to use the SystemWeb server, but the other two hosts use the HttpListener by default. The SystemWeb server works by registering an HttpModule and HttpHandler, and it intercepts the requests as they go through the pipeline. HttpListener is a simple class that opens a socket at the specified address and redirects the requests into the OWIN pipeline.

Middleware Layer

The middleware layer is the layer that has one or more middleware components, which are invoked sequentially in the order that they were added to the OWIN pipeline. The only requirement for a middleware component is that it implements the signature in Listing 7-10.

Listing 7-10. Signature Required for a Middleware Component

Func<IDictionary<string,object>, Task>

There are various middleware components that are very easy for completing frameworks. SignalR is one of those middleware components that is a complete framework. Even though SignalR is a complete framework, there are other popular middleware components that work well in providing additional functionality: WebAPI, Microsoft.Owin.Security.*, and Microsoft.Owin.StaticFiles. These additional components provide the functionality of WebAPI, various forms of security, and hosting of static files, respectively. Regardless of which components are chosen, they are all added and configured in the application layer.

Application Layer

The application layer, which is the actual application that is supported by all the underlying layers, has the logic to configure the middleware. This logic to configure the middleware goes into the startup class. The startup class can be registered multiple ways, but it is generally registered using an assembly tag, as shown in Listing 7-11.

Listing 7-11. Example of Registering a Startup Class with an Assembly Tag

[assembly: OwinStartup(typeof(MyApplication.Startup))]

Adding middleware components is done by calling Use or the respective extension method provided by the middleware, such as UseSignalR on the IAppBuilder object. The order in which the components are registered in the IAppBuilder is the order in which they will be run for every request that comes in. Once all the layers are selected and configured, the application should be ready to run. There are a few common functionalities that are used in Katana applications that can helpful in your application (discussed next).

Adding Windows Authentication and IIS Pipeline Stages to Applications

Even though the goal of the Katana project is to remove all the extra functionality that is not needed, some commonly used functionality is still needed, so it has to be added back into the application pipeline. The two common functionalities covered here are the Windows authentication middleware and the IIS pipeline event integration.

Windows Authentication

Windows authentication is a critical component that a lot of enterprise applications must support. Currently, there are two Katana servers that support it: SystemWeb and HttpListener. The SystemWeb server is configured in the web.config file for IIS and in the project for IIS Express, and the HttpListener is configured in the application startup class.

The SystemWeb server is the server that is part of the IIS pipeline and is supported only in integrated pipeline mode. Depending on whether you host this on IIS or IIS Express, there are two separate unrelated configurations to change. If your host is IIS, you have to update theweb.config file and update the authentication element to have a mode of "Windows" (see Listing 7-12). If this application is hosted in IIS Express, the web site project properties have to be updated. In the properties, Anonymous Authentication needs to be set to Disabled and Windows Authentication needs to be set to Enabled.

Listing 7-12. SystemWeb Server IIS Configuration for Windows Authentication

<authentication mode="Windows" />

The HttpListener server is different from the SystemWeb server to set up Windows authentication. For the HttpListener configuration, you set it in the startup class, as shown in Listing 7-13.

Listing 7-13. HttpListener Server Configuration for Windows Authentication

HttpListener listener = (HttpListener)app.Properties["System.Net.HttpListener"];
listener.AuthenticationSchemes = AuthenticationSchemes.IntegratedWindowsAuthentication;

Those settings are all you need to get your application to support Windows authentication. The operating system you run this on also needs the Windows authentication feature turned on as well.

Interaction with IIS Integrated Pipeline

If your SignalR application needs to interact at certain stages in the IIS pipeline, you can use the UseStageMarker function. To use this extension method, add a using statement to the namespace in Listing 7-14.

Listing 7-14. Using Statement Needed for the UseStageMarker Extension Method

using Microsoft.Owin.Extensions;

You add the UseStageMarker function with the appropriate stage in Listing 7-15 to run the previously registered middleware at that stage in the IIS pipeline. If you are using multiple UseStageMarker method calls, there are several restrictions that must be observed. The first restriction is that a stage can be registered only once. The following is an example of what you should not do. First, register middleware A and B and then call UseStageMarker for the Authorize stage. Next, register middleware C and call UseStageMarker again for theAuthorize stage. In this example, the code would be called only for components A and B, therefore making component C useless. The second restriction is that the stages must be registered for the IIS pipeline stages to occur. If they are called out of order, the later stages are ignored and run at the earliest stage that is registered.

Listing 7-15. Integrated Pipeline Stages

Authenticate
PostAuthenticate
Authorize
PostAuthorize
ResolveCache
PostResolveCache
MapHandler
PostMapHandler
AcquireState
PostAcquireState
PreHandlerExecute

There are many other ways to extend the Katana functionality in your application that are beyond the scope of this book. The next section focuses on what is needed to get a SignalR application on Linux or OS X.

Linux and OS X Support Using the Mono Framework

Recently, Microsoft has been pushing open source; it has even created a subsidiary called Microsoft Open Technologies, Inc. The open-source push has helped get more .NET Framework code decoupled from strongly Windows-centric code. Not only has this provided more standardized and generic implementations of the code but it has also influenced a change in licensing. One of the more recent changes Microsoft Open Technologies was part of was to remove the Windows–only licensing restriction from portable class libraries. So as these libraries open up, they can be used on other platforms such as Linux and OS X with the help of other frameworks.

This section goes over the Mono framework, which allows us to run our code on Linux and OS X. We discuss setting up the development environment to compile our code. Next, we introduce MonoDevelop, which is an IDE for the Mono framework, and show you how to set up a web hosting environment using the Mono framework and demonstrating hosting Mono applications. The section concludes by going over related changes from Linux to OS X.

What Is the Mono Framework?

The Mono framework is an open-source project led by Xamarin to allow cross-platform development. It is based on EMCA standards for C# and Common Language Runtime (CLR) support. The framework is a collection of components that include a compiler, runtime, base class libraries, and Mono class libraries.

The Mono version 3.X compiler accepts C# 1.0 - C# 5.0 code, with some limitations. The compiler currently has limited support for the Windows Presentation Foundation (WPF), Windows Workflow Foundation (WF), and Windows Communication Foundation (WCF). The compiler can generate just-in-time (JIT), ahead-of-time (AOT), and full static compilation, depending on the target OS.

The runtime that the Mono framework provides allows the use of JIT and AOT compilation, garbage collection, threading, and other library functions.

The base class libraries are compatible with the .NET Framework classes using EMCA standards. There are a few framework classes that are not supported because of Windows-specific code or other issues. Examples are the Windows-specific code in the System.Managementnamespace or support for WPF. The page at http://www.mono-project.com/Compatibility provides a list of compatibilities with the latest version of Mono.

The Mono class libraries provide support for libraries that may be missing or extend functionality. Examples of this are GTK+ or WinForms, which provide Gnome toolkit and Windows Forms functionality, respectively. So now that you have an idea of what Mono is, we’ll cover how to set it up and use it.

Setting Up the Development Environment

This section discusses how to use SignalR on openSUSE 13.1. The reason for choosing openSUSE 13.1 is that it supports Mono version 3.X that supports C# 5.0, which is required for the server components of SignalR. This example starts with a clean install of the operating system, so if the components exist, you should be able to skip those specific steps.

1. Open a new terminal window.

2. Type sudo zypper install mono-complete and press Return to install Mono. Confirm package download by pressing y and then pressing Return to confirm the install.

3. Type sudo zypper install git-core and press Return to install Git. Confirm package download by pressing y and Return to confirm the install.

4. Type mozroots --import –sync and press Return to sync with the Mozilla certificates.

5. Create a directory to work in and navigate to that directory. Type git clone http://github.com/SignalR/SignalR.git and press Return to pull down the SignalR package. The address is case-sensitive.

6. Navigate to the newly created SignalR directory.

7. Type ./build.sh and press Return to build.

Image Note In Linux, file names and commands are case-sensitive. So if something does not run or if you get a missing file error, check the casing.

After completing the preceding steps, you should now have SignalR libraries in a compatible format. Follow these steps to create a SignalR client application:

1. Create a new directory to work in.

2. Copy the newly created files from .\SignalR\src\Microsoft.AspNet.SignalR.Client\bin\debug to the directory created in step 1.

3. Inside the directory from step 1, type vi MonoClient.cs and press Return.

4. Once vi has started, you’re in Command mode. Press i to enter Insert mode, which is indicated in the bottom left of the screen with the word INSERT.

5. Enter the code in Listing 7-16.

Listing 7-16. MonoClient.cs Code

using System;
public class MonoClient: Form
{
Button btnSend;
TextBox txtName;
TextBox txtMessage;
ListBox lstMessages;
Microsoft.AspNet.SignalR.Client.Connection myConnection = new Microsoft.AspNet.SignalR. Client.Connection("http://localhost:####/SignalR/");

static public void Main ()
{
Application.Run (new MonoClient());
}

public MonoClient()
{
btnSend = new Button() { Text = "Send", Width = 75, Top = 5, Left = 175 };
txtName = new TextBox() { Width = 75, Top = 5, Left =5 };
txtMessage = new TextBox() { Width = 75, Top = 5, Left = 90 };
lstMessages = new ListBox() { Width = 245, Top = 30, Left = 5 };
btnSend.Click += btnSend_Click;
myConnection.Received += myConnection_Received;
this.Controls.Add(btnSend);
this.Controls.Add(txtName);
this.Controls.Add(txtMessage);
this.Controls.Add(lstMessages);
StartConnection();
}

async void StartConnection()
{
await myConnection.Start();
}

private void btnSend_Click(object sender, EventArgs e)
{
myConnection.Send(txtName.Text + ":" + txtMessage.Text);
}

void myConnection_Received(string obj)
{
lstMessages.Invoke(new Action(() => lstMessages.Items.Add(obj)));
}
}

6. Press Esc to exit Insert mode to go back into Command mode.

7. Type :wq and press Return to save the file and exit vi.

8. Type mcs MonoClient.cs -pkg:dotnet -reference:Microsoft.AspNet.SignalR.Client.dll and press Return to build the Mono client.

9. To launch the client, type mono MonoClient.exe and press Return.

Image Note In step 8, we tell the compiler to include the dotnet package and to reference the DLL that we created when we compiled the SignalR solution.

Now that there is a client (see Figure 7-4), we need to create a server to connect to. The next section creates a web site that will be hosted via Apache. This example is a bit more complicated than the previous one, so you can take advantage of the MonoDevelop IDE, which can help you manage the numerous files and dependencies for a build rather than trying to manage it manually.

image

Figure 7-4. Example of Mono SignalR clientUsing MonoDevelop for More Complex Projects

As projects grow more complex, it is a good idea to manage them in IDE rather than manually (as in the last section):

1. Open a new terminal window.

2. Type sudo zypper install monodevelop and press Return to install MonoDevelop. Confirm the package download by pressing y and pressing Return.

3. Type sudo zypper install libgnomeui and press Return to install the gnome UI. Confirm the package download by pressing y and pressing Return.

4. Press Alt+F2 for the runner, type monodev, and press Return to run the MonoDevelop IDE, as shown in Figure 7-5.

image

Figure 7-5. Shortcut to run the MonoDevelop IDE

Now that the MonoDevelop IDE is installed, it is ready to use. The MonoDevelop IDE is used in the next section to create a SignalR Mono Server.

Setting Up the Hosting Environment

The following steps set up Apache and XSP for running Mono web sites (if the package is already installed, skip that step):

1. Open a new terminal window.

2. Type sudo zypper install apache2 and press Return to install the Apache web server. Confirm the package download by pressing y and pressing Return.

3. Type sudo zypper install xsp and press Return to install the XSP server. Confirm the package download by pressing y and pressing Return.

4. Type sudo zypper install apache2-mod_mono and press Return. to install the Mono module for Apache. Confirm the package download by pressing y and pressing Return.

5. Type su, press Return, enter your password, and press Return. This process escalates your command-line privileges.

6. Type a2enmod mod_mono_auto and press Return to autoconfigure the Mono module.

7. Navigate to /etc/apache2.

8. Type vi default-server.conf and press Return to edit the configuration file.

9. Once vi has started, you are in Command mode, so press i to enter Insert mode, which is indicated in the bottom left of the screen with the word INSERT.

10.Update the two lines that say /srv/www/htdocs to the path where your project will be.

11.Press Esc to exit Insert mode back into Command mode.

12.Type :wq and press Return to save the file and exit vi.

Creating a Mono SignalR Server

To create a server, we incorporate the libraries that we compiled when setting up the development section. We will host it in a web application to be run under Apache. Follow these steps:

1. Launch MonoDevelop.

2. Create a new MVC project (see Figure 7-6).

image

Figure 7-6. Web project selection in MonoDevelop

3. Add a new class called Startup with the code in Listing 7-17.

Listing 7-17. Startup Class for Mono Server Example

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

4. Add a new class called Auction with the code in Listing 7-18.

Listing 7-18. Server Code for Mono Server Example

public class AuctionHub : Microsoft.AspNet.SignalR.Hub
{
public AuctionHub()
{
BidManager.Start();
}
public override System.Threading.Tasks.Task OnConnected()
{
Clients.Caller.CloseBid();
Clients.All.UpdateBid(BidManager.CurrentBid);
return base.OnConnected();
}
public void MakeCurrentBid()
{
BidManager.CurrentBid.BidPrice += 1;
BidManager.CurrentBid.ConnectionId = this.Context.ConnectionId;
Clients.All.UpdateBid(BidManager.CurrentBid);
}
public void MakeBid(double bid)
{
if (bid < BidManager.CurrentBid.BidPrice)
{
return;
}
BidManager.CurrentBid.BidPrice = bid;
BidManager.CurrentBid.ConnectionId = this.Context.ConnectionId;
Clients.All.UpdateBid(BidManager.CurrentBid);
}
}

public static class BidManager
{
static System.Threading.Timer _timer = new System.Threading.Timer(BidInterval,
null, 0, 2000);
public static Bid CurrentBid { get; set; }
public static void Start()
{
//Empty class to make sure Static class is started
}
static void BidInterval(object o)
{
var clients = Microsoft.AspNet.SignalR.GlobalHost.ConnectionManager.
GetHubContext<AuctionHub>().Clients;
if (BidManager.CurrentBid == null || BidManager.CurrentBid.TimeLeft <= 0)
{
BidManager.SetBid();
}
BidManager.CurrentBid.TimeLeft -= 2;
if (BidManager.CurrentBid.TimeLeft <= 0)
{
clients.AllExcept(CurrentBid.ConnectionId).CloseBid();
if (!string.IsNullOrWhiteSpace(CurrentBid.ConnectionId))
clients.Client(CurrentBid.ConnectionId).CloseBidWin(CurrentBid);
}
clients.All.UpdateBid(BidManager.CurrentBid);
}
static List<Bid> _items = new List<Bid>(){
new Bid(){Name="Bike", Description="10 Speed", TimeLeft = 30, BidPrice = 120.0},
new Bid(){Name="Car", Description="Sports Car", TimeLeft = 30, BidPrice = 1500.0},
new Bid(){Name="TV", Description="Big screen TV", TimeLeft = 30, BidPrice = 330.0},
new Bid(){Name="Boat", Description="Party Boat", TimeLeft = 30, BidPrice = 1200.0}
};
public static void SetBid()
{
Random rnd = new Random();
CurrentBid = (Bid)_items[rnd.Next(0, _items.Count - 1)].Clone();
}
}

public class Bid
{
public Bid Clone()
{
return (Bid)MemberwiseClone();
}
public string Name { get; set; }
public string Description { get; set; }
public double BidPrice { get; set; }
public int TimeLeft { get; set; }
public string ConnectionId { get; set; }
}

5. Add a reference to the Microsoft.AspNet.SignalR.Core.dll, Microsoft.Owin.dll, Newtonsoft.Json.dll, and Owin.dll.

6. Compile your project.

The application that was created earlier in this chapter can be used to point to http://localhost/SignalR and test the Mono server deployment. To test the server from other machines, make sure that you enable the firewall to allow port 80 to be accessed.

Mono Framework on OS X

Because the OS X operating system supports Linux and Mono version 3.X, you can run the same applications as Linux in this section. The only difference is that the commands to install the libraries on OS X are different from Linux, as you’ll see in the next section.

Using the Xamarin Add-in for Visual Studio to Create iOS and Android SignalR Clients

Today, everything is transitioning to mobile, and currently we have shown you only how to support Windows Phone devices. This section changes that by showing how to support SignalR clients with iOS and Android devices. Currently, the easiest way to get SignalR support in these devices is to use a commercial offering from Xamarin. This section is based on the business edition, which is currently priced at $999 per platform, per developer, so that the Xamarin add-in for Visual Studio can be used. The Xamarin product setup for iOS and Android is the same for both platforms, but the iOS platform requires additional steps and an Apple operating system to test devices as well as an Apple Developer account.

Setting Up the Xamarin Add-in for Visual Studio

To get the Xamarin add-in for Visual Studio, you can download it from http://xamarin.com. When you install the software, it checks to see whether Java, Xamarin Studio, Android SDKs, and other software packages are installed. If they are not, it downloads and installs necessary software packages. Once the software is installed, you have to update the NuGet package manager so that the SignalR NuGet packages are compatible with portable class libraries. (The NuGet update can be found under Menu Tools image Extensions and Updates and then inside the dialog box under Updates image Visual Studio Gallery.) With these steps done, you should be able to start creating Android applications, but the iOS applications require a little more setup, as described later in the section.

Creating Android Applications

Creating the sample application should be very straightforward. If there are any issues, make sure that all software is up to date, including USB drivers for Android devices, Visual Studio add-ins, SDKs, and so on. The updates may be needed because the speed of development for mobile devices is very fast, and things are always changing. To complete the Android application, complete the following steps:

1. Create a new Android application, as shown in Figure 7-7.

image

Figure 7-7. Android application project selection

2. Type Install-Package Microsoft.AspNet.SignalR.Client and press Return in the Package Manager Console.

3. Remove references to System.Threading.Tasks and System.RunTime.

4. Open the properties for the Android application project.

5. Go to the Android Manifest tab and select the Internet permission (see Figure 7-8).

image

Figure 7-8. Android application manifest properties

6. Replace the code for the Activity1 class with the code in Listing 7-19. Add any missing using statements.

Listing 7-19. C# Code for Android Example

public class Activity1 : Activity
{
Button btnCurrentBid;
Button btnMakeBid;
txtBid;
TextView lblName;
TextView lblDescr;
TextView lblBid;
TextView lblTime;
TextView lblWins;
Microsoft.AspNet.SignalR.Client.HubConnection _hubConnection;
Microsoft.AspNet.SignalR.Client.IHubProxy _auctionProxy;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView(Resource.Layout.Main);
btnCurrentBid = FindViewById<Button>(Resource.Id.btnCurrentBid);
btnCurrentBid.Click += delegate { _auctionProxy.Invoke("MakeCurrentBid"); };
btnMakeBid = FindViewById<Button>(Resource.Id.btnMakeBid);
btnMakeBid.Click += delegate { _auctionProxy.Invoke<string>("MakeBid",
txtBid.Text); };
txtBid = FindViewById<EditText>(Resource.Id.txtBid);
lblName = FindViewById<TextView>(Resource.Id.lblName);
lblDescr = FindViewById<TextView>(Resource.Id.lblDescr);
lblBid = FindViewById<TextView>(Resource.Id.lblBid);
lblTime = FindViewById<TextView>(Resource.Id.lblTime);
lblWins = FindViewById<TextView>(Resource.Id.lblWins);
StartHub();
}
async void StartHub()
{
_hubConnection = new Microsoft.AspNet.SignalR.Client.HubConnection
("http://localhost/signalr");
_auctionProxy = _hubConnection.CreateHubProxy("AuctionHub");
_auctionProxy.Subscribe("UpdateBid").Received += UpdateBid_auctionProxy;
_auctionProxy.Subscribe("CloseBid").Received += CloseBid_auctionProxy;
_auctionProxy.Subscribe("CloseBidWin").Received += CloseBidWin_auctionProxy;
await _hubConnection.Start();
}
void UpdateBidMethod(Newtonsoft.Json.Linq.JToken bid, int formObject)
{
if (bid != null && bid.HasValues)
{
lblName.Text = (string)bid["Name"];
lblDescr.Text = (string)bid["Description"];
lblBid.Text = (string)bid["BidPrice"];
lblTime.Text = "Time Left: " + (string)bid["TimeLeft"];
if (formObject > 0)
{
string win = bid["Name"] + " at " + bid["BidPrice"] + "\r\n";
lblWins.Text += win;
}
}
}
void UpdateButtonsMethod(bool enabled)
{
this.RunOnUiThread(delegate
{
btnCurrentBid.Enabled = enabled;
btnMakeBid.Enabled = enabled;
});
}
void UpdateBid_auctionProxy(IList<Newtonsoft.Json.Linq.JToken> obj)
{
this.RunOnUiThread(delegate
{
UpdateBidMethod(obj[0], 0);
UpdateButtonsMethod(true);
});
}
void CloseBid_auctionProxy(IList<Newtonsoft.Json.Linq.JToken> obj)
{
this.RunOnUiThread(delegate
{
UpdateButtonsMethod(false);
});
}
void CloseBidWin_auctionProxy(IList<Newtonsoft.Json.Linq.JToken> obj)
{
this.RunOnUiThread(delegate
{
UpdateButtonsMethod(false);
UpdateBidMethod(obj[0], 1);
});
}
}

7. Replace the XML in Resources image Layout image Main.Xaml with the content in Listing 7-20. It may require right-clicking the file and using Open With XML Editor.

Listing 7-20. XAML for Android example

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="50px">
<TextView
android:id="@+id/lblName"
android:layout_width="150px"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/lblBid"
android:layout_width="150px"
android:layout_height="wrap_content"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="50px">
<TextView
android:id="@+id/lblDescr"
android:layout_width="150px"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/lblTime"
android:layout_width="150px"
android:layout_height="wrap_content"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="50px">
<Button
android:id="@+id/btnCurrentBid"
android:layout_width="150px"
android:layout_height="wrap_content"
android:text="Current Bid"/>
<Button
android:id="@+id/btnMakeBid"
android:layout_width="150px"
android:layout_height="wrap_content"
android:text="Make Bid"/>
<EditText
android:id="@+id/txtBid"
android:layout_width="150px"
android:layout_height="wrap_content"
android:textSize ="22px"/>
</LinearLayout>
<TextView
android:id="@+id/lblWins"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
</LinearLayout>

Image Note The emulators for Android devices are very slow. The program includes a warning (see Figure 7-9) that holds true even on high-end systems. So when you are debugging on the emulator, you might have to wait a couple of minutes before your application is deployed and running on the device.

image

Figure 7-9. Emulator slowness warning

Now that you have created an Android client application (see Figure 7-10), you can also create a similar application on the iOS devices. You need additional setup steps before you can do that, however.

image

Figure 7-10. Example of Android hub applicationAdditional Steps for iOS Applications

For iOS applications, you must have an active Apple operating system on a device such as an iMac, Mac Pro, MacBook, or Mac Mini. This Apple device must be connected and accessible on the same network as your development machine. The first step is to install the Xamarin Studio package on your Apple OS, which is also available from http://xamarin.com.

The next step is to join the Apple Developer program, which allows you to deploy software to your provisioned devices. After it is set up, you have to set up the link between Visual Studio and the Apple OS with the Xamarin.iOS Build Host.

Installing Xamarin Studio on Apple OS

The install on your Apple operating system should be very similar to the install for the Windows OS. It checks what components are installed and then installs any missing components. The Apple install has an extra installed program called the Xamarin.iOS Build Host, which is needed to communicate between the Apple OS and the Visual Studio deployment.

Joining the Apple Developer Program

Joining the Apple iOS Developer Program costs $99 at the time of writing. After you register, it might take up to 24 hours to receive the activation e-mail if the registration is successful. Once your account is active, you have to get a developer certificate so that you can provision your devices.

Provisioning Your Devices

There are several ways to provision your devices. (A quick search on the Internet provides many guides that may be even more useful, depending on the number of devices and type of provisioning.) Follow these steps:

1. Connect your device to a machine running OS X and launch Xcode.

2. Inside Xcode, click the Use For Development button in Organizer - Devices.

3. You are asked to associate the device with the Apple Developer account you created. Select the development team you want to provision the device to and click Choose.

4. The software scans for a certificate on the portal (it may not exist if this is the first time).

5. If it does not exist, you can click Request to have one generated for you.

a. It may take a while to refresh; to speed up the operation, go to https://developer.apple.com/account/ios/certificate/ and download the Development iOS Team Provisioning Profile to your desktop.

b. The download link can be made visible by clicking the profile in the list.

c. Once you have downloaded the provisioning profile, return to Xcode.

d. Go to Organizer - Devices.

e. Select Provisioning Profiles and click Add.

f. Find the file that you downloaded and click Open.

6. Load Xamarin Studio on the OS X machine used in step 1 and go to Preferences on the Xamarin Studio menu.

7. In Preferences, go to the Developer Accounts section.

8. Click the plus sign to add your Apple ID that is associated with the Apple Developer Program.

9. Click OK.

Now that Xamarin is set up, the final step is to set up the Build Host, which is a service that allows Visual Studio to build iOS applications. Next, we show you how to set up the host on the OS X and Windows side.

Setting up the Xamarin.iOS Build Host

1. To set up the link between the Visual Studio and the OS X machine to launch the Xamarin.iOS Build Host, you can locate it by searching for it in Finder (see Figure 7-11).

image

Figure 7-11. Locating the Xamarin.iOS Build Host on OS X

2. Once it is running, click the Pair button (see Figure 7-12).

image

Figure 7-12. Xamarin.iOS Build Host dialog box

3. It starts listening for a connection for Visual Studio and displays a PIN (see Figure 7-13) that you need to enter from the Windows machine.

image

Figure 7-13. Displaying a PIN in the Xamarin.iOS Build Host dialog box

4. Return to Windows and launch Visual Studio.

5. Click the Tools menu and click Options.

6. Select the Xamarin tab and then select iOS settings.

7. On this tab, click the Find Mac Build Host button.

8. It displays a message about the Xamarin.iOS Build Host that was launched in an earlier step. Read it; then click Continue.

9. The configuration window attempts to find the host automatically.

10.If the Build Host is not found, you can try to manually enter the information to locate it.

11.If the Build Host is found, click the machine and then click Connect (see Figure 7-14.)

image

Figure 7-14. Build Host selection dialog box

12.The configuration pops up a dialog box for the PIN (see Figure 7-15) that was displayed earlier on the Apple machine.

image

Figure 7-15. Visual Studio pairing PIN dialog box

13.Enter the PIN and click Pair. If it paired successfully, click Finish and then OK to exit the Options menu.

Creating a Sample iOS Application

Now that everything is set up, you can create a sample iPad application using the auction client example from Chapter 6:

1. Create a new project of type Visual C# image iOS image iPad and choose the Hello World template, as shown in Figure 7-16.

image

Figure 7-16. iOS Universal project with HelloWorld template selection

2. Right-click the project and go to Properties.

3. Click the iOS Application tab.

4. Add an application name, identifier, and version to the respective fields.

5. Choose the appropriate deployment target.

6. Save and close the properties window.

7. Run the Package Manager Console.

8. Type Install-Package Microsoft.AspNet.SignalR.Client.

9. Remove System.Runtime, System.Threading.Tasks, and Newtonsoft.Json from the References.

10.Add a reference to Newtonsoft.Json.dll, which can be found under the project root directory\packages\Newtonsoft.Json.X.X.X\lib\portable-net40+sl4+wp7+win8 where the X is the current version of Newtonsoft.Json that exists in your project.

11.Replace the class in MyViewController.cs with the class in Listing 7-21, which will create the UI components of the example.

Listing 7-21. MyViewController C# Code

public class MyViewController : UIViewController
{
UIButton btnCurrentBid;
UIButton btnMakeBid;
UITextField txtBid;
UILabel lblName;
UILabel lblDescr;
UILabel lblBid;
UILabel lblTime;
UILabel lblWins;
public override void ViewDidLoad()
{
base.ViewDidLoad();

View.Frame = UIScreen.MainScreen.Bounds;
View.BackgroundColor = UIColor.LightGray;
View.AutoresizingMask = UIViewAutoresizing.FlexibleWidth |
UIViewAutoresizing. FlexibleHeight;

txtBid = new UITextField(new RectangleF(260, 120, 140, 30)) { BackgroundColor
= UIColor.White };
lblName = new UILabel(new RectangleF(20, 20, 200, 30));
lblBid = new UILabel(new RectangleF(260, 20, 200, 30));
lblDescr = new UILabel(new RectangleF(20, 60, 200, 30));
lblTime = new UILabel(new RectangleF(260, 60, 200, 30));
lblWins = new UILabel(new RectangleF(20, 170, 380, 150)) { BackgroundColor =
UIColor. White, LineBreakMode = UILineBreakMode.WordWrap, Lines = 0 };
btnCurrentBid = UIButton.FromType(UIButtonType.System);
btnCurrentBid.Frame = new RectangleF(20, 120, 100, 30);
btnCurrentBid.SetTitle("Current Bid", UIControlState.Normal);
btnCurrentBid.BackgroundColor = UIColor.Gray;
btnMakeBid = UIButton.FromType(UIButtonType.RoundedRect);
btnMakeBid.SetTitle("Make Bid", UIControlState.Normal);
btnMakeBid.Frame = new RectangleF(140, 120, 100, 30);
btnMakeBid.BackgroundColor = UIColor.Gray;

View.AddSubview(btnCurrentBid);
View.AddSubview(btnMakeBid);
View.AddSubview(txtBid);
View.AddSubview(lblName);
View.AddSubview(lblDescr);
View.AddSubview(lblBid);
View.AddSubview(lblTime);
View.AddSubview(lblWins);
}
}

12.Add the SignalR Hub client code, which adds what is in Listing 7-22 to the MyViewController class.

Listing 7-22. iOS Code to Wire Up SignalR Hub

Microsoft.AspNet.SignalR.Client.HubConnection _hubConnection;
Microsoft.AspNet.SignalR.Client.IHubProxy _auctionProxy;
List<string> winningBids = new List<string>();
async void StartHub()
{
_hubConnection = new Microsoft.AspNet.SignalR.Client.HubConnection
("http://localhost/signalr");
_auctionProxy = _hubConnection.CreateHubProxy("AuctionHub");
_auctionProxy.Subscribe("UpdateBid").Received += UpdateBid_auctionProxy;
_auctionProxy.Subscribe("CloseBid").Received += CloseBid_auctionProxy;
_auctionProxy.Subscribe("CloseBidWin").Received += CloseBidWin_auctionProxy;
await _hubConnection.Start();
}

void UpdateBidMethod(Newtonsoft.Json.Linq.JToken bid, int formObject)
{
if (bid != null && bid.HasValues)
{
lblName.Text = (string)bid["Name"];
lblDescr.Text = (string)bid["Description"];
lblBid.Text = (string)bid["BidPrice"];
lblTime.Text = "Time Left: " + (string)bid["TimeLeft"];
if (formObject > 0)
{
string win = bid["Name"] + " at " + bid["BidPrice"] + "\r\n";
lblWins.Text += win;
}
}
}
void UpdateButtonsMethod(bool enabled)
{
this.InvokeOnMainThread(delegate
{
btnCurrentBid.Enabled = enabled;
btnMakeBid.Enabled = enabled;
});
}
void UpdateBid_auctionProxy(IList<Newtonsoft.Json.Linq.JToken> obj)
{
this.InvokeOnMainThread(delegate
{
UpdateBidMethod(obj[0], 0);
UpdateButtonsMethod(true);
});
}
void CloseBid_auctionProxy(IList<Newtonsoft.Json.Linq.JToken> obj)
{
this.InvokeOnMainThread(delegate
{
UpdateButtonsMethod(false);
});
}
void CloseBidWin_auctionProxy(IList<Newtonsoft.Json.Linq.JToken> obj)
{
this.InvokeOnMainThread(delegate
{
UpdateButtonsMethod(false);
UpdateBidMethod(obj[0], 1);
});
}

13.After you have the hub proxy in the class, add the code in Listing 7-23 to the bottom of the ViewDidLoad method before the View.AddSubview methods. This code associates a UI touch event with a corresponding proxy command.

Listing 7-23. Logic to Associate UI Touch Event to Hub Command

btnCurrentBid.TouchUpInside += (object sender, EventArgs e) =>
{
_auctionProxy.Invoke("MakeCurrentBid");
};

btnMakeBid.TouchUpInside += (object sender, EventArgs e) =>
{
_auctionProxy.Invoke<string>("MakeBid", txtBid.Text);
};

14.The last piece to put in place is the method that connects the hub proxy. The method call in Listing 7-24 needs to be added to the top of the ViewDidLoad method.

Listing 7-24. Logic to Call Hub Start when the UI Loads

StartHub();

Once this is complete, your application should be ready to run. If your application builds successfully but does not run, and you see No Devices Attached as shown in Figure 7-17, go to the Manager and change the active solution platform to the simulator.

image

Figure 7-17. Example of No Devices Attached in device selection

If the active solution platform is the iPhone (see Figure 7-18), it tries to deploy to the actual device attached if the active solution platform is the iPhone simulator (see Figure 7-19).

image

Figure 7-18. Configuration Manager with actual device selected

image

Figure 7-19. Configuration Manager with simulator selected

Once the application is deployed, you should see something similar to Figure 7-20. The same code should work for iPhone (just change the deployment target).

image

Figure 7-20. Example of iOS hub applicationSummary

This chapter showed how to extend and customize SignalR applications so that the applications can fit project requirements. The main component that helps with customizing and extending your application is the dependency resolver.

SignalR, which is considered a middleware component in the Katana project, implements the OWIN interface. The Katana host and server components can easily be changed and can be configured to work with other middleware configured in the pipeline.

You learned that you can use the Mono 3.X Framework to run SignalR libraries on operating systems such as Linux and OS X. Finally, we went over the customized version of the Mono Framework that allows SignalR to run on Android and iOS client devices.