Mobile Corner

Add Real-Time Diagnostics to Your Windows Phone 8.1 App with SignalR

Adding real-time diagnostics and communication to a Windows Phone 8.1 application is no challenge for SignalR.

If an application malfunctions or crashes, often users don't provide feedback, or what feedback they provide offers little value to the application developer. So it's up to developers to add analytics throughout an application, and they're often forced to rely on what little information is available in a dump or stack trace available through the appropriate marketplace.

But there are some applications that make easy work of diagnostics, some even offering real-time chat capabilities within the application so as to try to retain more users in the case that the application fails to operate correctly. One such app is SignalR, a library developed by Microsoft Open Technologies that's available for ASP.NET Web developers. I've used it, and I can show how you can add it to your apps for instrumenting and providing real-time communication with your application users.

In this article, I'll add SignalR to a Web application, but I'm then going to leverage the SignalR client libraries to allow both a Windows Phone and Windows Presentation Foundation (WPF) application to communicate through the SignalR hub. For more information on SignalR and how it operates, I suggest reading the documentation available via the GitHub WiKi.

I'll start by creating the SignalR hub, which will be hosted in an ASP.NET Web application. While I'll be creating a new Web application, shown in Figure 1, SignalR can easily be added to an existing application.

[Click on image for larger view.] Figure 1. Creating the ASP.NET Web Application

After selecting the ASP.NET Web Application template from the New Project dialog, I then select the Web API template and click OK to create the application.

At this point I'll also create both a WPF application (DiagnosticOutput) and a Windows Phone 8.1 application (TraceApplication). The latter will be the application to which I'll add diagnostic tracing, while the former will be used to display the diagnostic log information.

So that I have something to trace in the TraceApplication, I'll add a another page, SecondPage.xaml, to the Windows Phone application, based on the BasicPage item template. This allows Visual Studio to add in the common files on which the template depends. On the MainPage of the application I'll add a button, and in the event handler trigger a navigation to the SecondPage:

private void NavigateClick(object sender, RoutedEventArgs e)
{
  Frame.Navigate(typeof (SecondPage));
}

To use SignalR, the NuGet packages listed in Table 1 need to be added to the appropriate projects.

Table 1: NuGet Packages to Add to Application Projects
NuGet Package Project
SignalRHost (ASP.NET Web Application) Microsoft.AspNet.SignalR
TraceApplication (Windows Phone 8.1) Microsoft.AspNet.SignalR.Client
DiagnosticOutput (WPF) Microsoft.AspNet.SignalR.Client

Next, I need to initialize SignalR in the Web application by calling app.MapSignalR in the Startup class of the SignalRHost application:

public partial class Startup
{
  ...
  public void ConfigureAuth(IAppBuilder app)
  {
    ...
    app.MapSignalR();
  }
}

While this initializes SignalR, it won't work without defining a hub through which the clients will communicate. The SignalR Hub Class item template in Figure 2 can be used to add a hub -- DiagnosticHub -- to the Web application.

[Click on image for larger view.] Figure 2. Adding a SignalR Hub to the Web Application

As shown in Listing 1, the DiagnosticHub exposes a number of methods that can be invoked in order to broadcast appropriate messages via the hub to all, or a specific client.

Listing 1: DiagnosticHub Exposing Methods To Be Invoked
public class DiagnosticHub : Hub
{
  public void RequestAssistance(string connectionId)
  {
    Clients.All.assistance(connectionId);
  }

  public void OfferAssistance(string connectionIdOfRequestee, string connectionIdOfOfferer)
  {
    Clients.Client(connectionIdOfRequestee).offer(connectionIdOfOfferer);
  }

  public void Log(string connectionId, string message)
  {
    Clients.Client(connectionId).log(message);
  }

  public void Question(string connectionId, string question)
  {
    Clients.Client(connectionId).question(question);
  }
}

Rather than broadcasting all messages from the TraceApplication onto the hub, which would be very chaotic with even a low number of clients, a simple exchange of connection IDs is required so that a one-to-one communication channel can be set up via the hub. Initially the TraceApplication will use the RequestAssistance method, supplying its ConnectionId, to broadcast the "assistance" message to all clients. The DiagnosticOutput application will respond to this message by calling the OfferAssistance method supplying both the TraceApplication ConnectionId and the ConnectionId of the DiagnosticOutput application. The former will be used to identify which client to route the message to, which will contain the DiagnosticOutput ConnectionId (that is,so that the TraceApplication knows which client to send messages to).

Once this exchange has taken place, the TraceApplication will call the Log method with both the ConnectionId of the DiagnosticOutput client and the message to be logged. The DiagnosticOutput can call the Question method to send through a question to the user of the TraceApplication -- this, of course, could be extended to allow for a full-fledged chat session between the two applications.

In the TraceApplication, I'll add a new helper class, DiagnosticSupport, which will encapsulate the SignalR connection. After establishing the connection, it requests assistance by invoking the RequestAssistance. It also has handlers for the "offer" message in order to complete the setting up of the connection to the DiagnosticOutput client, as well as the "question" message, which will prompt the user for some yes/no feedback.

The Log method the DiagnosticSupport singleton exposes can be called from anywhere in the application to invoke the Log method on the hub, passing diagnostic information back to the waiting DiagnosticOutput client, as shown in Listing 2.

Listing 2: The Log Method Exposed by DiagnosticSupport
public class DiagnosticSupport
{

  private static readonly EasClientDeviceInformation deviceInfo = 
    new EasClientDeviceInformation();

  public static bool IsRunningOnEmulator
  {
    get
    {
      return (deviceInfo.SystemProductName == "Virtual");
    }
  }

  private static readonly DiagnosticSupport DefaultInstance = new DiagnosticSupport();
  public static DiagnosticSupport Default
  {
    get { return DefaultInstance; }
  }


  public string ConnectionIdOfRemoteUser { get; set; }

  public HubConnection Connection { get; set; }
  public IHubProxy Proxy { get; set; }

  private CoreDispatcher Dispatcher
  {
    get { return CoreApplication.MainView.CoreWindow.Dispatcher; }
  }

  public async Task Connect()
  {
    Connection = new HubConnection("http://localhost:55077/");

    Proxy = Connection.CreateHubProxy("DiagnosticHub");

    if (IsRunningOnEmulator)
    {
      await Connection.Start(new LongPollingTransport());
    }
    else
    {
      await Connection.Start();
    }



    Proxy.On<string>("offer",
      msg =>
      {
        ConnectionIdOfRemoteUser = msg;
        Proxy.Invoke("Log", ConnectionIdOfRemoteUser, "Thanks for the assistance");
      }
      );
    Proxy.On<string>("question",
      async msg =>
      {
        await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () =>
        {
          var dialog = new MessageDialog(msg);
          dialog.Commands.Add(new UICommand("Yes"));
          dialog.Commands.Add(new UICommand("No"));
          var result = await dialog.ShowAsync();

          await Proxy.Invoke("Log", ConnectionIdOfRemoteUser, msg + " " + result.Label);
        });
      }
      );
    await Proxy.Invoke("RequestAssistance", Connection.ConnectionId);
  }

  public async void Log(string message)
  {
    Debug.WriteLine("Log: " + message);
    if (Proxy != null && !string.IsNullOrWhiteSpace(ConnectionIdOfRemoteUser))
    {
      await Proxy.Invoke("Log", ConnectionIdOfRemoteUser, message);
    }
  }
}

Establishing the connection to the SignalR hub can be done at any stage of the application lifecycle. In this case the Connect method will be added at the beginning of the OnLaunched method in the App.xaml.cs. However, it should be recognized that having an active connection can be a drain on the network and bandwidth, so it should only be established if the end user requests diagnostic information or assistance with the application:

DiagnosticSupport.Default.Connect();

In order to capture where the user navigates in the application, an event handler can be attached to the Frame, which is created in the App.xaml.cs file. In this case it just logs the type of page to which the user has navigated:

rootFrame = new Frame();
rootFrame.Navigated += 
  (navs, nave) => DiagnosticSupport.Default.Log(
  "Navigation: " + nave.SourcePageType.Name);

Listing 3 shows the XAML and corresponding view model for the MainWindow of the DiagnosticOutput application. There's a TextBox into which the support person can enter a question and click the Button to send the question to the active TraceApplication. There's also a ListBox that will be used to display the Log messages coming from the TraceApplication.

Listing 3: XAML and View Model for the DiagnosticOutput MainWindow
XAML
<Window x:Class="DiagnosticOutput.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition
        Height="Auto" />
      <RowDefinition
        Height="Auto" />
      <RowDefinition />
    </Grid.RowDefinitions>
    <TextBox
      Text="{Binding Question, Mode=TwoWay}" />
    <Button
      Grid.Row="1"
      Content="Ask Question"
      Click="AskQuestionClick"></Button>
    <ListBox
      Grid.Row="2"
      ItemsSource="{Binding Messages}">
      <ListBox.ItemTemplate>
        <DataTemplate>
          <TextBlock
            Text="{Binding}" />
        </DataTemplate>
      </ListBox.ItemTemplate>
    </ListBox>
  </Grid>
</Window>

View Model
public class MainViewModel:INotifyPropertyChanged
{
  private string question;

  public string Question
  {
    get { return question; }
    set
    {
      if (Question == value) return;
      question = value;
      OnPropertyChanged();
    }
  }
        
  private ObservableCollection<string> messages = new ObservableCollection<string>();

  public ObservableCollection<string> Messages
  {
    get { return messages; }
  }


  public event PropertyChangedEventHandler PropertyChanged;

  protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
  {
    PropertyChangedEventHandler handler = PropertyChanged;
    if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
  }
}

In the code behind the MainWindow an instance of the MainViewModel is created and set up as the DataContext for the page. Then in the Loaded event handler a new HubConnection is established and handlers added for both the "assistance" and "log" messages. The former auto-responds with a call to the OfferAssistance method on the hub, supplying the ConnectionId of the current connection, while the latter simply adds the log message to the list of messages being displayed on the screen (via data binding). The event handler for the Button Click event is used to call the Question method on the hub, sending the question entered by the support person, as shown in Listing 4.

Listing 4: Button Click Event Handler Calls Question Method
public partial class MainWindow
{
  public MainWindow()
  {
    InitializeComponent();
            
    ViewModel = new MainViewModel();
    DataContext = ViewModel;

    Loaded += MainWindow_Loaded;
  }

  private MainViewModel ViewModel { get; set; }

  public string ConnectionIdOfRemoteUser { get; set; }

  public HubConnection Connection { get; set; }
  public IHubProxy Proxy { get; set; }

  async void MainWindow_Loaded(object sender, RoutedEventArgs e)
  {

    Connection = new HubConnection("http://localhost:55077/");

    Proxy = Connection.CreateHubProxy("DiagnosticHub");
    await Connection.Start();

    Proxy.On<string>("assistance",
      msg =>
      {
        ConnectionIdOfRemoteUser = msg;
        Proxy.Invoke("OfferAssistance", ConnectionIdOfRemoteUser, Connection.ConnectionId);
      }
      );
    Proxy.On<string>("log",
      msg => this.Dispatcher.BeginInvoke(new Action(() => ViewModel.Messages.Add(msg))));
  }

  private void AskQuestionClick(object sender, RoutedEventArgs e)
  {
    if (Proxy != null && !string.IsNullOrWhiteSpace(ConnectionIdOfRemoteUser))
    {
      Proxy.Invoke("Question", ConnectionIdOfRemoteUser, ViewModel.Question);
    }
  }
}

Running the Web application and the two client applications can be done by setting all three projects as startup projects (right-click on the Solution node in Solution Explorer and select Set Startup Project). By placing breakpoints on the methods in the DiagnosticHub you can see if messages are being sent through the hub. If you don't see any messages, first check that the Windows Phone emulator has Internet access by attempting to browse to a site in Internet Explorer. If you're trying to run on a real device you'll need to change the hosting address to something other than localhost, as this can't be resolved by a device.

You'll find that adding real-time communication using SignalR to your application can be useful for diagnostic purposes, such as in assisting or diagnosing issues with test builds, and allowing chat between your application users. Unlike other mechanisms, such as push notifications, the advantage of SignalR is that it can be used across different native clients -- WPF, Windows, Windows Phone, iOS, Android -- as well as the Web (because it originated from the need to have real-time updating in Web sites).

comments powered by Disqus
Upcoming Events

.NET Insight

Sign up for our newsletter.

I agree to this site's Privacy Policy.