Click here to Skip to main content
15,868,016 members
Articles / Programming Languages / C++

Master the Managed Azure Mobile Services Backend–Part Four

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
17 Sep 2014CPOL14 min read 18.9K   3   1
CodeProject   It’s been a while since the last part, but I have been terribly busy. In the last part you learned about custom controllers, the ApiServices-class and the Authorization level attributes. In this part you will learn how to wire up everything.


 

It’s been a while since the last part, but I have been terribly busy. In the last part you learned about custom controllers, the ApiServices-class and the Authorization level attributes. In this part you will learn how to wire up everything. I will show you how to create a universal app (Windows Phone 8.1 and Windows 8.1, basically WinRT) and how to use also other features like SignalR to enrich your app using Azure Mobile Services.

The Azure SDK has been updated in the meantime to Version 2.4 and the tooling in Visual Studio 2013 has been enhanced as well. You will have to install Visual Studio 2013 Update 3 and Azure SDK 2.4 to make use of all the new features listed in this article.

Extend the generated Help Pages

When you fire-up your service by hitting F5, you will see this page, it’s the landing-page for your service:

startpage.png

 

There’s an entry that is asking you to try out the service. Selecting the small right-arrow opens the API main-page:

Service Homepage

As you can see there is not too much descriptive information about the controllers and the methods available. If you use an excellent naming-scheme everyone understands, it could  be quite enough and the controller-naming as well as the naming of the controller-actions could be just enough. Often it is not. And it could help your fellow developers to find what they are looking for.

Changing the way the documentation is rendered

Because the managed backend is built on top of  OWIN, KATANA and Web API, it supports whatever Web API is offering. And the Help-Page rendering backend is no exception. Here are the main features offered by Web API help pages:

  • Include XML-Comments into the description section for each controller-action
  • Create samples for the responses of each controller-action
  • Exclude specific actions from the documentation

I will show you how to adapt that to the managed backend. If you want to dive deeper into Web API help-pages, I strongly suggest this tutorial series by Yaoh, starting here:

ASP .NET Web API Help Page Part 1: Basic Help Page Customizations

The first thing, that I want to show you, is how to get rid of the error message within the “Request Body Formats” section fore the media-type header-value “application/x-www-form-urlencoded”:

formURLEncodeBefore

 

This can be done using the extension method “SetSampleForType” that belongs to the HttpConfiguration-Class.

This methods takes two parameters:

  • The sample (object)
  • mediaType – the MediaTypeHeaderValue

 

The “sample” can be an object, that end’s up being formatted using a MediaTypeFormatter. The MediaTypeFormatter uses serialization to achieve this goal. If you want to learn about the internals I recommend to

download the ASP .NET Sample Code, and to browse that code. It was open-sourced.

We need to tell the system for what kind of request we want that to happen depending on the media-type (aka Mime Type) header-value (in our case Content-Type header). You can read more about media-types here, at the IANA website:

Media Types (IANA)

For the second parameter we pass “application/x-www-form-urlencoded” which equals newing-up a new instance of the MediaTypeHeaderValue class passing the string “application/x-www-form-urlencoded” to the constructor. With all of that in mind, the short piece of code we need is:

[INSERT SAMPLE CODE FOR SETSAMPLEFORTYPE]

That code is added to the “WebApiConfig.cs” file (the file we use to configure our service), right after we initiate the new HttpConfiguration-object. When you run the service now, you will not see anymore the error-message, but the string “Currently not used” instead:

//Just a sample, on how to set the required options to get rid
            //of some of the sample page error-messages.
            //You can read more here: http://blogs.msdn.com/b/yaohuang1/archive/2012/10/13/asp-net-web-api-help-page-part-2-providing-custom-samples-on-the-help-page.aspx
            config.SetSampleForType(
                "Currently not used.",
                new MediaTypeHeaderValue("application/x-www-form-urlencoded"),
                typeof(OldSchoolArtist));

 

Much better!

Including XML-Comments

Let’s take the documentation to the next level and use the .NET XML-Commenting system to decorate our controller-actions and parameters with some useful comments.

To make this possible, we need to add additional code to the constructor of our “OldSchoolArtistController”. This little piece of code will tell the Web API Help to set the the right documentation-provider to process the XML-Comments that we have added to our controller-actions (the methods). For this purpose we call the “SetDocumentationProvider” extension-method (HttpConfig) and pass it a new instance of the the XMLDocumentationProvider-Class that is located within the “Microsoft.WindowsAzure.Mobile.Service.Description” it is a custom implementation for the managed backend. We need to pass the current ApiService instance to the default-constructor of the XmlDocumentationProvider-class:

//This shows how to set the XmlDocumentationProvider to suck your XML comments on
            //Controller actions into your sample page.
            this.Configuration.SetDocumentationProvider(new XmlDocumentationProvider(this.Services));

 

Before the code can be executed, we need to tell Visual Studio to generate the XML-Documentation files when we build our service. Right-Click the “OldSchoolBeats” project and select “Properties”. Change to the “Build” tab (1) and check the “XML documentation file” option for both configurations “Debug” and “Release”.

Re-build the project and run it. That’s way better. Take a look at our new API home-page:

muchbetter1

And the randomly picked “POST” method:

muchbetter2

Now we are getting closer to what I would call appropriate. There is only one thing missing, the “Response Information”. Why is that? Well, because the “Post” action of our controller returns an IHttpActionResult, the help system does not know, what will be returned. Literally it could be anything. Therefore, we need to set the expected return type. We do that this time (even simpler) using a special attribute, the ResponseType-Attribute. Very simple, pin it to our Post-Action and pass it the expected type. In our case an instance of type “OldSchoolArtist:

/// <summary>
        /// Posts the old school artist.
        /// </summary>
        /// <param name="item">The new artist to add.</param>
        /// <returns></returns>
        [ResponseType(typeof(OldSchoolArtist))]
        public async Task<IHttpActionResult> PostOldSchoolArtist(OldSchoolArtist item) {
            OldSchoolArtist current = await InsertAsync(item);
            return CreatedAtRoute("Tables", new { id = current.Id }, current);
        }

 

Now run the project again and see the difference:

muchbetter3

The final Sample – Changes, additions and final feature-set

I have  implemented several changes since the last post. This includes UI changes (Windows and Windows Phone) as well as numerous changes in the managed backend like the implementation of a SignalR Hub and a diagnostic HubPiplineModule (Part of SignalR, but could not make it work in the managed backend for now, working on it, here is the link to my question on the MSDN forums), a custom controller to manage blob-resources, code to manage SignalR users (mainly taken and brushed up from this sample: “Mapping SignalR Users to Connections”) using table storage as a backend. All of these will be explained in this blog-post. First let’s break down the numerous changes into their main parts.

There are three main feature-areas available in the final sample:

  • Azure Storage – Download, Upload and delete blobs
  • SignalR – Near real-time messaging – sending messages to specific users and broadcast messages
  • Standard CRUD operations on data like creating , reading, updating and deleting data

All of the features are implemented in the Windows app, and two features (SignalR and the Standard CRUD feature) in Windows Phone. The storage feature could be implemented in Windows Phone as well without any problems. It was only a question of time.

UI Implementation/Changes

Let’s take a look now at the Windows and Windows Phone UI’s.

The Windows UI (Universal App, Windows 8.1)

Splashscreen MainPage SignalR Storage

The Windows Phone UI (Universal App, Windows 8.1)

phonemain phonesignalr

 

Final Sample Architecture

Architecture

The Standard CRUD Implementation

If you followed the series so far, this should be nothing new to you. The Web API based Table-Controller (inherits from the generic TableController<T> class) is a standard-controller that was generated by Visual Studio using the Table-Controller template. It is used to manage the data contained within the OldSchoolArtist table:

using System.Linq;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Description;
using System.Web.Http.OData;
using Microsoft.WindowsAzure.Mobile.Service;
using Microsoft.WindowsAzure.Mobile.Service.Description;
using OldSchoolBeats.DataObjects;
using OldSchoolBeats.Models;

namespace OldSchoolBeats.Controllers {
    public class OldSchoolArtistController : TableController<OldSchoolArtist> {
        protected override void Initialize(HttpControllerContext controllerContext) {
            base.Initialize(controllerContext);
            OldSchoolBeatsContext context = new OldSchoolBeatsContext();
            DomainManager = new EntityDomainManager<OldSchoolArtist>(context, Request, Services);

            //This shows how to set the XmlDocumentationProvider to suck your XML comments on
            //Controller actions into your sample page.
            this.Configuration.SetDocumentationProvider(new XmlDocumentationProvider(this.Services));
        }


        /// <summary>
        /// Gets all old school artists.
        /// </summary>
        /// <returns></returns>
        public IQueryable<OldSchoolArtist> GetAllOldSchoolArtist() {
            return Query();
        }


        /// <summary>
        /// Gets the old school artist.
        /// </summary>
        /// <param name="id">The id of the artist we want to retrieve.</param>
        /// <returns></returns>
        public SingleResult<OldSchoolArtist> GetOldSchoolArtist(string id) {
            return Lookup(id);
        }


        /// <summary>
        /// Patches the old school artist.
        /// </summary>
        /// <param name="id">The identifier of the artist to change.</param>
        /// <param name="patch">The patch. This is the object that contains the changes we want to apply.</param>
        /// <returns></returns>
        public Task<OldSchoolArtist> PatchOldSchoolArtist(string id, Delta<OldSchoolArtist> patch) {
            return UpdateAsync(id, patch);
        }


        /// <summary>
        /// Posts the old school artist.
        /// </summary>
        /// <param name="item">The new artist to add.</param>
        /// <returns></returns>
        [ResponseType(typeof(OldSchoolArtist))]
        public async Task<IHttpActionResult> PostOldSchoolArtist(OldSchoolArtist item) {
            OldSchoolArtist current = await InsertAsync(item);
            return CreatedAtRoute("Tables", new { id = current.Id }, current);
        }


        /// <summary>
        /// Deletes the old school artist.
        /// </summary>
        /// <param name="id">The id of the artist to delete.</param>
        /// <returns></returns>
        public Task DeleteOldSchoolArtist(string id) {
            return DeleteAsync(id);
        }

    }
}

 

Data Access Implementation Client-Side

It offers all the methods to read, insert, update and delete the records of the OldSchoolArtists table. To use the different CRUD methods, I have implemented a separate data-access service OldSchoolArtistDataService based on the interface IDataService:

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
using OldSchoolBeats.ClientModel;
using Microsoft.WindowsAzure.MobileServices;
using OldSchoolBeats.Universal.ViewModel;

namespace OldSchoolBeats.Universal.Services {
    public interface IDataService<T> {

        MobileServiceCollection<T, T> Items {
            get;
            set;
        }

        T SelectedItem {
            get;
            set;
        }

        BindableOldSchoolArtist DataContext {
            get;
            set;
        }

        void SearchItems(Expression<Func<T, bool>> predicate);

        Task FillItems();

        ICollection<T> SearchAndReturnItems(Expression<Func<T, bool>> predicate);

        Task DeleteItem(T item);

        Task AddItem(T item);
        Task UpdateItem(BindableOldSchoolArtist item, T delta);

    }
}

 

This interface has 6 methods, that need to be implemented by any “DataService” class:

  • SerachItems, used to search for table-entries
  • FillItems, used to add items to the Items collection of type MobileServiesCollection<T,T> that “hosts” an observable collection of T which is used to bind the views to
  • SearchAndReturnItems, used to search items and return an ICollection<T>
  • DeleteItem, used to delete a record
  • UpdateItem, used to update items

Here is the implementation of the IDataService interface, the OldSchoolArtistsDataService.cs file:

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Microsoft.WindowsAzure.MobileServices;
using OldSchoolBeats.ClientModel;
using System.Net.Http;
using GalaSoft.MvvmLight;
using OldSchoolBeats.Universal.ViewModel;
using System.Linq;

namespace OldSchoolBeats.Universal.Services {
    public class OldSchoolArtistsDataService:ObservableObject,IDataService<OldSchoolArtist> {

        MobileServiceClient _client;

        MobileServiceCollection<OldSchoolArtist, OldSchoolArtist> items;

        public MobileServiceCollection<OldSchoolArtist,OldSchoolArtist> Items {
            get {
                return items;
            }

            set {
                items = value;
                RaisePropertyChanged("Items");
            }
        }

        public OldSchoolArtist SelectedItem {
            get;
            set;
        }

        private BindableOldSchoolArtist dataContext;

        public BindableOldSchoolArtist DataContext {
            get {
                return dataContext;
            }

            set {
                dataContext = value;
                RaisePropertyChanged("DataContext");
            }
        }

        public OldSchoolArtistsDataService(MobileServiceClient client) {
            this._client = client;
            this.DataContext = new BindableOldSchoolArtist();
        }

        public async Task FillItems() {

            var query = _client.GetTable<OldSchoolArtist>().Take(10);

            this.Items = await query.ToCollectionAsync<OldSchoolArtist>();
        }

        public void SearchItems(System.Linq.Expressions.Expression<Func<OldSchoolArtist, bool>> predicate) {

            var query = _client.GetTable<OldSchoolArtist>().Where(predicate);
            this.Items = new MobileServiceCollection<OldSchoolArtist>(query);
        }

        public async Task DeleteItem(OldSchoolArtist item) {

            await _client.GetTable<OldSchoolArtist>().DeleteAsync(item);

            var query = _client.GetTable<OldSchoolArtist>().Take(10);


            this.Items = await query.ToCollectionAsync<OldSchoolArtist>();

        }

        public async Task AddItem(OldSchoolArtist item) {


            var url = await _client.InvokeApiAsync<string>("LastFM", HttpMethod.Post, new Dictionary<string, string>() {
                { "artistName", item.ImageUrl
                }
            });


            if(string.IsNullOrEmpty(url)) {
                url = "http://lorempixel.com/g/150/150/";
            }

            item.ImageUrl = url;

            await _client.GetTable<OldSchoolArtist>().InsertAsync(item);
            var query = _client.GetTable<OldSchoolArtist>().Take(10);


            this.Items = await query.ToCollectionAsync<OldSchoolArtist>();

        }

        public async Task UpdateItem(BindableOldSchoolArtist delta, OldSchoolArtist item) {

            var dbItems = await _client.GetTable<OldSchoolArtist>().Where(i => i.ImageUrl == item.ImageUrl).ToListAsync();

            var dbItem = dbItems[0];

            dbItem.Artist = delta.Artist;
            dbItem.RelatedStyles = delta.RelatedStyles;
            dbItem.YearsArchive = delta.YearsArchive;


            await _client.GetTable<OldSchoolArtist>().UpdateAsync(dbItem);

            var query = _client.GetTable<OldSchoolArtist>().Take(10);


            this.Items = await query.ToCollectionAsync<OldSchoolArtist>();
        }



        public ICollection<OldSchoolArtist> SearchAndReturnItems(System.Linq.Expressions.Expression<Func<OldSchoolArtist, bool>> predicate) {

            var query = _client.GetTable<OldSchoolArtist>().Where(predicate);
            var items =  new MobileServiceCollection<OldSchoolArtist>(query);

            return items;
        }
    }
}

Within this specific implementation of our data-service, I have added an additional property called DataContext, that will hold later on the current values of a changed record (leaving the original record untouched). Another interesting method is the AddItem method. This method not only adds a record, but it tries to find the accurate image using the LastFM API, or if no image could be found, it adds a lorempixel.com link with an arbitrary placeholder image. To use this feature, you have to sign-up for an LastFM API account, or just comment out the API call to the LastFM API. Here is the implementation of the LastFM custom API-Controller:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using Microsoft.WindowsAzure.Mobile.Service;
using Lastfm;
using Lastfm.Services;

namespace OldSchoolBeats.Controllers {
    /// <summary>
    ///  Utilize lastfm to
    ///  get images from artists.
    /// </summary>
    public class LastFmController : ApiController {
        public ApiServices Services {
            get;
            set;
        }

        // POST api/LastFm
        /// <summary>
        /// Posts the specified artist name.
        /// </summary>
        /// <param name="artistName">Name of the artist.</param>
        /// <returns></returns>
        public string Post(string artistName) {

            var apiKey = Services.Settings["LastFMApiKey"];
            var apiSecret = Services.Settings["LastFMSecret"];
            var userName = Services.Settings["LastFMUserName"];
            var password = Services.Settings["LastFMPassword"];

            //Create new lastfm session
            var session = new Session(apiKey,apiSecret);
            //authenticate
            session.Authenticate(userName,Lastfm.Utilities.md5(password));

            //Get album art
            var artist = new Artist(artistName,session);

            var imageUrl = artist.GetImageURL(ImageSize.Large);

            return imageUrl;

        }

    }
}

 

 

The implementation is very simple and based on the lastfm-sharp project, which is very easy to use, as you see. You need only to pass the four required values  (just replace the values in Web.config with your values) password,username, api-key and secret and you are good to go.

The Azure Storage Implementation – Blob Management

All the storage-magic is happening within the BlobResourceHelperController.cs file. This is a custom-api controller (a regular Web API controller) that manages all the blob-operations on a specific container. It is a sample that shows you, how to handle blob-access within your mobile applications. The nice thing about the controller is, that the returned values contain all the information’s about the blobs you will ever need in an exchangeable format: JSON. The methods return JTokens that can be filtered using LINQ. This allows you to use them in an Universal App Project in Windows and Windows Phone or within any other project that can use Json .NET. At the time of this writing the Azure Storage libraries could not be used in Windows Phone 8.1 (WinRT) because of authentication issues. UPDATE: This seems to be fixed now. Please see this GItHub issue for more informations: NuGet Package does not install assemblies within portable class library projects  (read it whilst writing this post).

The storage part is the only part that is using a code-behind implementation and not a view-mode approachl, based on the prior mentioned issue about the Storage libraries for Windows Phone 8.1. Therefore you will find all the UI/Storage based implementation’s within the Storage.xaml.cs file in the Windows app project. It should not be too hard to refactor that to use a view-model and a storage-data-service.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using Microsoft.WindowsAzure.Mobile.Service;
using Microsoft.WindowsAzure.Mobile.Service.Security;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Auth;
using Microsoft.WindowsAzure.Storage.Blob;
using OldSchoolBeats.Models;

namespace OldSchoolBeats.Controllers {

    /// <summary>
    /// Helps us to manage blob-resources.
    /// </summary>
    [AuthorizeLevel(AuthorizationLevel.User)]
    public class BlobResourceHelperController : ApiController {

        /// <summary>
        /// Gets or sets the services.
        /// </summary>
        /// <value>
        /// The services.
        /// </value>
        public ApiServices Services {
            get;
            set;
        }


        /// <summary>
        /// Gets or sets the storage account.
        /// </summary>
        /// <value>
        /// The storage account.
        /// </value>
        private CloudStorageAccount storageAccount {
            get;
            set;
        }


        /// <summary>
        /// Gets or sets the BLOB client.
        /// </summary>
        /// <value>
        /// The BLOB client.
        /// </value>
        private CloudBlobClient blobClient {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the container.
        /// </summary>
        /// <value>
        /// The container.
        /// </value>
        private CloudBlobContainer container {
            get;
            set;
        }

        /// <summary>
        /// Gets all blobs in container.
        /// </summary>
        /// <param name="data">The data.</param>
        /// <returns></returns>
        [HttpPost]
        [Route("api/getallblobs")]
        public async Task<IEnumerable<CloudBlockBlob>> GetAllBlobsInContainer(BlobManipulationData data) {

            await InitBlobContainer(data);

            var blobs = container.ListBlobs().OfType<CloudBlockBlob>();

            return blobs;

        }


        /// <summary>
        /// Pages the blobs in container.
        /// </summary>
        /// <param name="data">The data.</param>
        /// <returns></returns>
        [HttpPost]
        [Route("api/pageblobs")]
        public async Task<IEnumerable<CloudBlockBlob>> PageBlobsInContainer(BlobManipulationData data) {

            await InitBlobContainer(data);

            var blobs = container.ListBlobs().OfType<CloudBlockBlob>().Skip(data.Skip).Take(data.Take);

            return blobs;
        }

        /// <summary>
        /// Gets the BLOB count.
        /// </summary>
        /// <param name="data">The data.</param>
        /// <returns></returns>
        [HttpPost]
        [Route("api/getblobcount")]
        public async Task<int> GetBlobCount(BlobManipulationData data) {

            return await Task.Run<int>(async () => {

                await InitBlobContainer(data);

                //Lazy. Therefore convert that thing to a list.
                return container.ListBlobs().ToList().Count;

            });

        }


        /// <summary>
        /// Uploads the BLOB.
        /// </summary>
        /// <param name="data">The data.</param>
        /// <returns></returns>
        [HttpPost]
        [Route("api/uploadblob")]
        public async Task UploadBlob(BlobManipulationData data) {

            await InitBlobContainer(data);

            var blob = container.GetBlockBlobReference(data.BlobName);

            await blob.UploadFromByteArrayAsync(data.BlobData, 0, data.BlobData.Length);


        }

        /// <summary>
        /// Deletes the BLOB.
        /// </summary>
        /// <param name="data">The data.</param>
        /// <returns></returns>
        [HttpPost]
        [Route("api/deleteblob")]
        public async Task DeleteBlob(BlobManipulationData data) {

            await InitBlobContainer(data);

            var blob = container.GetBlockBlobReference(data.BlobName);

            await blob.DeleteIfExistsAsync();

        }


        /// <summary>
        /// Renames the BLOB.
        /// </summary>
        /// <param name="data">The data.</param>
        /// <returns></returns>
        [HttpPost]
        [Route("api/renameblob")]
        public async Task RenameBlob(BlobManipulationData data) {

            await InitBlobContainer(data);

            var blob = container.GetBlockBlobReference(data.BlobName);

            var exists = await blob.ExistsAsync();


            if (exists) {

                var renameTo = container.GetBlockBlobReference(data.NewBlobName);

                //copy the blob to rename to the new one
                await renameTo.StartCopyFromBlobAsync(blob);

                //delete the existing blob
                await blob.DeleteAsync();

            }
        }

        /// <summary>
        /// Initializes the BLOB container.
        /// </summary>
        /// <param name="data">The data.</param>
        /// <returns></returns>
        private async Task InitBlobContainer(BlobManipulationData data) {

            var storageConnection = Services.Settings["StorageConnectionString"];

            storageAccount = CloudStorageAccount.Parse(storageConnection);

            blobClient = storageAccount.CreateCloudBlobClient();

            container = blobClient.GetContainerReference(data.ContainerName);

            await container.CreateIfNotExistsAsync();
        }


        /// <summary>
        /// Downloads the BLOB using sas resource URL.
        /// </summary>
        /// <param name="sasUrl">The sas URL.</param>
        /// <returns></returns>
        [HttpPost]
        [Route("api/dowloadblob")]
        public async Task<byte[]> DownloadBlob(BlobManipulationData data) {

            return await Task.Run<byte[]>(() => {

                var storageConnection = Services.Settings["StorageConnectionString"];

                storageAccount = CloudStorageAccount.Parse(storageConnection);

                blobClient = storageAccount.CreateCloudBlobClient();

                container = blobClient.GetContainerReference(data.ContainerName);

                var blob = container.GetBlockBlobReference(data.BlobName);

                blob.FetchAttributes();

                var blobData = new byte[blob.Properties.Length];

                blob.DownloadToByteArray(blobData, 0);

                return blobData;
            });
        }

    }
}

 

Based on the implementations of the BlobResourceHelperController, the code-behind file Storage.xaml.cs uses all the standard-components of MVVM like commands to implement the blob operations and to invoke the custom API:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using GalaSoft.MvvmLight.Command;
using Microsoft.WindowsAzure.Storage.Blob;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
using OldSchoolBeats.ClientModel;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;

// The Blank Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=234238

namespace OldSchoolBeats.Universal {

    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class Storage : Page {

        //Where do we start to page
        private const int PAGING_SKIP = 0;
        //How many items do we take at a time
        private const int PAGING_TAKE = 10;

        //Which page are we on?
        private static int currentPage;




        private RelayCommand pageNext;

        public RelayCommand PageNext {
            get {
                return pageNext;
            }

            set {
                pageNext = value;
            }
        }

        private RelayCommand pagePrevious;

        public RelayCommand PagePrevious {
            get {
                return pagePrevious;
            }

            set {
                pagePrevious = value;
            }
        }


        private RelayCommand<string> download;

        public RelayCommand<string> Download {
            get {
                return download;
            }

            set {
                download = value;
            }
        }

        private RelayCommand upload;

        public RelayCommand Upload {
            get {
                return upload;
            }

            set {
                upload = value;
            }
        }

        private RelayCommand<string> delete;

        public RelayCommand<string> Delete {
            get {
                return delete;
            }

            set {
                delete = value;
            }
        }


        public Storage() {
            this.InitializeComponent();

            this.PagePrevious = new RelayCommand(PagePrev);
            this.PageNext = new RelayCommand(PageNxt);
            this.Upload = new RelayCommand(Upld);
            this.Download = new RelayCommand<string>(Dwnld);
            this.Delete = new RelayCommand<string>(Del);
            this.DataContext = this;
            this.Loaded += Storage_Loaded;
        }

        private async void Del(string blobName) {

            var queryObject = new BlobManipulationData();

            queryObject.BlobName = queryObject.BlobName = (string)lstBlobs.SelectedValue;
            queryObject.ContainerName = "testcontainer";

            var dataToken = JToken.FromObject(queryObject);
            var blobsJToken = await App.MobileService.InvokeApiAsync("deleteblob", dataToken);


        }

        private async void Dwnld(string blobName) {


            var queryObject = new BlobManipulationData();

            queryObject.BlobName = (string) lstBlobs.SelectedValue;
            queryObject.ContainerName = "testcontainer";


            var dataToken = JToken.FromObject(queryObject);

            var blobsJToken = await App.MobileService.InvokeApiAsync("dowloadblob", dataToken);

            var blobData = blobsJToken.ToObject<byte[]>();

            StorageFolder folder = Windows.Storage.ApplicationData.Current.LocalFolder;

            StorageFile storageFile = await folder.CreateFileAsync(queryObject.BlobName, CreationCollisionOption.ReplaceExisting);

            var stream = await storageFile.OpenStreamForWriteAsync();

            await stream.WriteAsync(blobData,0,blobData.Length);

            stream.Dispose();

        }

        private async void Upld() {


            var filePicker = new FileOpenPicker();

            filePicker.SuggestedStartLocation = PickerLocationId.ComputerFolder;

            filePicker.FileTypeFilter.Clear();

            filePicker.FileTypeFilter.Add("*");

            var selectedFile = await filePicker.PickSingleFileAsync();

            if(selectedFile != null) {


                var stream = await selectedFile.OpenStreamForReadAsync();

                stream.Position = 0;

                var fileData = new byte[stream.Length];

                await stream.ReadAsync(fileData,0, (int) stream.Length);

                var queryObject = new BlobManipulationData();

                queryObject.BlobName = selectedFile.Name;
                queryObject.BlobData = fileData;
                queryObject.ContainerName = "testcontainer";

                var dataToken = JToken.FromObject(queryObject);

                await App.MobileService.InvokeApiAsync("uploadblob",dataToken);
            }


        }

        private async void PageNxt() {

            var queryObject = new BlobManipulationData();

            queryObject.Skip = 10 * currentPage++;
            queryObject.Take = PAGING_TAKE;
            queryObject.ContainerName = "testcontainer";

            var dataToken = JToken.FromObject(queryObject);
            var blobsJToken = await App.MobileService.InvokeApiAsync("pageblobs", dataToken);


            var blobs = JsonConvert.DeserializeObject(blobsJToken.ToString());

            var source = blobsJToken.Values<string>("name").ToList<string>();

            this.lstBlobs.ItemsSource = source;
        }

        private async void PagePrev() {

            var queryObject = new BlobManipulationData();

            if(currentPage > 0) {
                queryObject.Skip = 10 * --currentPage;
            }

            else {
                queryObject.Skip = 0;
            }

            queryObject.Take = PAGING_TAKE;
            queryObject.ContainerName = "testcontainer";

            var dataToken = JToken.FromObject(queryObject);
            var blobsJToken = await App.MobileService.InvokeApiAsync("pageblobs", dataToken);



            var blobs = JsonConvert.DeserializeObject(blobsJToken.ToString());

            var source = blobsJToken.Values<string>("name").ToList<string>();

            this.lstBlobs.ItemsSource = source;
        }

        async void Storage_Loaded(object sender, RoutedEventArgs e) {

            await this.LoadFirstTenBlobEntries();

        }

        private async Task LoadFirstTenBlobEntries() {

            var queryObject = new BlobManipulationData();

            queryObject.Skip = PAGING_SKIP;
            queryObject.Take = PAGING_TAKE;
            queryObject.ContainerName = "testcontainer";

            var dataToken = JToken.FromObject(queryObject);
            var blobsJToken = await App.MobileService.InvokeApiAsync("pageblobs",dataToken);

            var blobs = JsonConvert.DeserializeObject(blobsJToken.ToString());

            var source = blobsJToken.Values<string>("name").ToList<string>();

            this.lstBlobs.ItemsSource = source;

        }

    }
}

 

Five methods have been implemented:

  • Del, to delete a blob
  • Dwnld, to download a blob
  • Upld, to upload a blob
  • PageNxt, next page of blobs
  • PagePrev, previous page of blobs
  • LoadFirstTenBlobEntries, to fill the list with ten blob entries after the page has been loaded

To understand the code better, I suggest to open the Storage.xml.cs and the BlobResourceHelperController.cs file side by side. That way you can see much better how the API invocation calls are done. The rest (if you are used to MVVM) is pretty self-explanatory.

The SignalR Implementation – Near Real-Time messaging

Many Windows and Windows Phone developers are looking for a quick way to integrate near-real-time-messaging or even chat functionalities into their apps. This is where SignalR comes into play. Due to the simplicity it can be added using the managed backend I see a ton of new chat applications coming for WP and WIN.

I will not give you an introduction into SignalR here. There is tons of great content out there on how SignalR works and what it is. Here is a great starting point: Introduction to SignalR . I will show you how to wire up the components in the managed backend and how to use them within WP and WIN apps.

The NuGet package that you need for your mobile service (the backend implementation) is the “Windows Azure Mobile Service .NET Backend SignalR Extension”.

backendsignalrnuget

On the client side you will need the SignalR client libraries for Windows Phone and Windows (in your Universal app project or any other .NET based client)

clientsidenugetsignalr

After you have installed the required client and backend SignalR packages, you need to register the SignalR extension in WebApiConfig.cs within your mobile services project:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Web.Http;
using OldSchoolBeats.DataObjects;
//using OldSchoolBeats.Migrations;
using OldSchoolBeats.Models;
using Microsoft.WindowsAzure.Mobile.Service;
using System.Net.Http.Headers;
using Microsoft.WindowsAzure.Mobile.Service.Config;
using Microsoft.WindowsAzure.Mobile.Service.Security;
using OldSchoolBeats.Services;
using Autofac;

namespace OldSchoolBeats {
    public static class WebApiConfig {
        public static void Register() {

            //Fire-up SignalR
            SignalRExtensionConfig.Initialize();



            // Use this class to set configuration options for your mobile service
            ConfigOptions options = new ConfigOptions();



            //Allow only users to use our SignalR-Hub
            options.SetRealtimeAuthorization(AuthorizationLevel.User);


            ConfigBuilder builder = new ConfigBuilder(options, (httpConfig, autofac) => {
                autofac.RegisterInstance(new TableLoggingService()).As<ILoggingService>();

            });


            // Use this class to set WebAPI configuration options
            HttpConfiguration config = ServiceConfig.Initialize(builder);

            //Just a sample, on how to set the required options to get rid
            //of some of the sample page error-messages.
            //You can read more here: http://blogs.msdn.com/b/yaohuang1/archive/2012/10/13/asp-net-web-api-help-page-part-2-providing-custom-samples-on-the-help-page.aspx
            config.SetSampleForType(
                "Currently not used.",
                new MediaTypeHeaderValue("application/x-www-form-urlencoded"),
                typeof(OldSchoolArtist));

            //YOU CAN USE THE RESOURCE BROKER AFTER THE ASSEMBLIES FOR
            //THE MANAGED BACKEND USE THE LATEST WEB-API ASSEMBLIES

            //This is directly taken from the documenation about resource
            //Controllers on GitHub (created by the Azure Mobile Services Team)
            //https://github.com/Azure/azure-mobile-services-resourcebroker
            // Create a custom route mapping the resource type into the URI.
            //var resourcesRoute = config.Routes.CreateRoute(
            //                         routeTemplate: "api/resources/{type}",
            //                         defaults: new { controller = "resources" },
            //                         constraints: null);

            // Insert the ResourcesController route at the top of the collection to avoid conflicting with predefined routes.
            //config.Routes.Insert(0, "Resources", resourcesRoute);


            // To display errors in the browser during development, uncomment the following
            // line. Comment it out again when you deploy your service for production use.
            // config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;

            Database.SetInitializer(new OldSchoolBeatsInitializer());

            //trigger migrations manually
            //var migrator = new DbMigrator(new Configuration());
            //migrator.Update();
        }
    }

    public class OldSchoolBeatsInitializer : DropCreateDatabaseIfModelChanges<OldSchoolBeatsContext> {
        protected override void Seed(OldSchoolBeatsContext context) {
            //List<TodoItem> todoItems = new List<TodoItem>
            //{
            //    new TodoItem { Id = "1", Text = "First item", Complete = false },
            //    new TodoItem { Id = "2", Text = "Second item", Complete = false },
            //};

            //foreach (TodoItem todoItem in todoItems)
            //{
            //    context.Set<TodoItem>().Add(todoItem);
            //}

            base.Seed(context);
        }
    }
}

 

By adding the single line SignalRExtensionConfig.Initialize() to the Register-Method, all the required SignalR configuration wiring is done for you and you are ready to add your SignalR-Hub (the messaging component) to your mobile service. Here is the sample implementation:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using Microsoft.AspNet.SignalR;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Table;
using Newtonsoft.Json;
using Microsoft.AspNet.SignalR.Hubs;
using System.Diagnostics;
using Microsoft.WindowsAzure.Mobile.Service.Security;
using Microsoft.WindowsAzure.Mobile.Service;

namespace OldSchoolBeats.SignalR {
    /// <summary>
    /// A basic messaging hub.
    /// See it as a base implementation
    /// to send messages to all users,
    /// or to specific users. This can
    /// be uses in many scenarios.
    /// </summary>

    [HubName("MessagingHub")]

    public class MessagingHub:Hub {


        /// <summary>
        /// Gets or sets the services.
        /// </summary>
        /// <value>
        /// The services.
        /// </value>
        public ApiServices Services {
            get;
            set;
        }

        /// <summary>
        /// Broadcasts the specified broadcast messsage.
        /// </summary>
        /// <param name="broadcastMesssage">The broadcast messsage.</param>
        [AuthorizeLevel(AuthorizationLevel.User)]
        public void Broadcast(string broadcastMesssage) {


            Clients.All.broadcastMessage(broadcastMesssage);

        }



        /// <summary>
        /// Sends to specific user from specific user.
        /// </summary>
        /// <param name="fromUserId">From user identifier.</param>
        /// <param name="toUserId">To user identifier.</param>
        /// <param name="message">The message.</param>
        /// <returns></returns>
        [AuthorizeLevel(AuthorizationLevel.User)]
        public async Task SendToSpecificUserFromSpecificUser(string toUserId, string message) {


            var currentUser = ((ServiceUser)Context.User).Id;

            var storageTable = this.GetConnectionTable();

            await storageTable.CreateIfNotExistsAsync();

            var userOnline = this.IsUserOnline(toUserId);

            if(userOnline) {



                var query = new TableQuery<SignalRUserStore>()
                .Where(TableQuery.GenerateFilterCondition(
                           "PartitionKey",
                           QueryComparisons.Equal,
                           toUserId));

                var queryResult = storageTable.ExecuteQuery(query).FirstOrDefault();

                var user2UserMessage = new SignalRMessage(message,currentUser,toUserId);

                string serializedMessage = string.Empty;

                //This is the recommendation directly coming from GitHub
                //if this has been fixed, please use the async serialization method
                serializedMessage =  await Task.Factory.StartNew<string>(()=> {
                    return JsonConvert.SerializeObject(user2UserMessage);
                });

                Clients.Client(queryResult.RowKey).receiveMessageFromUser(serializedMessage);

            }


        }




        /// <summary>
        /// Sends to specific user.
        /// Not used in the sample.
        /// </summary>
        /// <param name="toUserId">To user identifier.</param>
        /// <param name="message">The message.</param>
        [AuthorizeLevel(AuthorizationLevel.User)]
        public async Task SendToSpecificUser(string toUserId, string message) {

            var currentUser = ((ServiceUser)Context.User).Id.Split(':')[1];

            var storageTable = this.GetConnectionTable();

            await storageTable.CreateIfNotExistsAsync();




            var query = new TableQuery<SignalRUserStore>()
            .Where(TableQuery.GenerateFilterCondition(
                       "PartitionKey",
                       QueryComparisons.Equal,
                       toUserId));

            var queryResult = storageTable.ExecuteQuery(query).FirstOrDefault();

            //Send to specific user
            var user2UserMessage = new SignalRMessage(message,null,toUserId);

            string serializedMessage = string.Empty;

            //This is the recommendation directly coming from GitHub
            //if this has been fixed, please use the async serialization method
            serializedMessage = await Task.Factory.StartNew<string>(() => {
                return JsonConvert.SerializeObject(user2UserMessage);
            });

            Clients.Client(queryResult.RowKey).receiveMessageForUser(serializedMessage);


        }

        /// <summary>
        /// Called when the connection connects to this hub instance.
        /// Code taken from:
        /// http://www.asp.net/signalr/overview/signalr-20/hubs-api/mapping-users-to-connections
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Threading.Tasks.Task" />
        /// </returns>

        public override Task OnConnected() {

            try {
                var currentUser = ((ServiceUser)Context.User).Id;
                var id = Context.ConnectionId;

                var table = GetConnectionTable();
                table.CreateIfNotExists();

                var entity = new SignalRUserStore(
                    currentUser.Split(':')[1],
                    Context.ConnectionId);
                var insertOperation = TableOperation.InsertOrReplace(entity);
                table.Execute(insertOperation);
            }

            catch (Exception ex) {

                Debug.WriteLine(ex.ToString());

            }


            return base.OnConnected();
        }

        /// <summary>
        /// Called when a connection has disconnected gracefully from this hub instance,
        /// i.e. stop was called on the client.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Threading.Tasks.Task" />
        /// </returns>

        public override Task OnDisconnected(bool stopCalled) {

            var name = ((ServiceUser)Context.User).Id.Split(':')[1];
            var table = GetConnectionTable();

            try {
                if (!string.IsNullOrEmpty(name)) {
                    var deleteOperation = TableOperation.Delete(
                    new SignalRUserStore(name, Context.ConnectionId) {
                        ETag = "*"
                    });
                    TableOperation retrieveOperation = TableOperation.Retrieve<SignalRUserStore>(name, Context.ConnectionId);
                    TableResult retrievedResult = table.Execute(retrieveOperation);
                    SignalRUserStore checkEntity = retrievedResult.Result as SignalRUserStore;

                    if (checkEntity != null) {
                        table.Execute(deleteOperation);
                    }
                }
            }

            catch (Exception ex) {

                //throw;
            }

            return base.OnDisconnected(stopCalled);


        }

        /// <summary>
        /// Called when the user is re-connected to this hub.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Threading.Tasks.Task" />
        /// </returns>
        public override Task OnReconnected() {

            try {
                var currentUser = ((ServiceUser)Context.User).Id.Split(':')[1];
                var id = Context.ConnectionId;

                var table = GetConnectionTable();
                table.CreateIfNotExists();

                var entity = new SignalRUserStore(
                    currentUser,
                    Context.ConnectionId);
                var insertOperation = TableOperation.InsertOrReplace(entity);
                table.Execute(insertOperation);
            }

            catch (Exception ex) {

                Debug.WriteLine(ex.ToString());

            }




            return base.OnReconnected();


        }


        /// <summary>
        /// Gets the table to save user data to
        /// code taken from sample:
        /// http://www.asp.net/signalr/overview/signalr-20/hubs-api/mapping-users-to-connections
        /// Very good sample, check it out!
        /// </summary>
        /// <returns></returns>
        private CloudTable GetConnectionTable() {
            var storageAccount =
                CloudStorageAccount.Parse(
                    Services.Settings["StorageConnectionString"]);
            var tableClient = storageAccount.CreateCloudTableClient();
            return tableClient.GetTableReference("signalrconnections");
        }

        /// <summary>
        /// Determines whether [is user online] [the specified user name].
        /// code taken from sample:
        /// http://www.asp.net/signalr/overview/signalr-20/hubs-api/mapping-users-to-connections
        /// </summary>
        /// <param name="userName">Name of the user.</param>
        /// <returns></returns>
        /// <exception cref="System.ArgumentException">Parameter cannot be null, empty or whitespace.</exception>
        private bool IsUserOnline(string userName) {


            if(string.IsNullOrWhiteSpace(userName) || string.IsNullOrEmpty(userName)) {

                throw new ArgumentException("Parameter cannot be null, empty or whitespace.",userName);


            }

            var table = GetConnectionTable();

            var partitionKey = userName.Split(':')[1];

            string pkFilter = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, partitionKey);

            var query = new TableQuery<SignalRUserStore>().Where(pkFilter);

            var queryResult = table.ExecuteQuery(query);


            if (queryResult.Count() == 0) {
                return false;
            }

            return true;


        }

    }
}

 

You can find the MessagingHub.cs file within the folder “SignalR” in the managed-backend project. The MessagingHub class derives from the abstract class Hub that is contained within the SignalR assemblies. The hub is secured by only allowing authorized users to execute the Broadcast,SendToSpecificUserFromSpecificUser and SendToSpecificUser methods (indicated by the AuthorizationLevel-Attribute).

All this methods are called by the client-applications to send broadcast-messages (to all connected users) or messages meant to be sent to a specific user. The rest of the methods are simply method-overrides of the HubClass to add users to table storage (users are online) or remove them from table storage (users are offline). Because executing queries against Azure tables that contain a “:” within the partition-key is not working very well, I made the decision to simply split the mobile services username into two parts and to use the second part after the “:” as username and partition-key. This is why you see all of this split-statements everywhere.

The SignalRUserStore.cs file contains the implementation of the table-entity that is used to save hub-based user data to the to Azure table storage.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.WindowsAzure.Storage.Table;

namespace OldSchoolBeats.SignalR {
    /// <summary>
    /// This is where we save our users permanently
    /// during SignalR sessions.
    /// </summary>
    public class SignalRUserStore:TableEntity {

        /// <summary>
        /// Initializes a new instance of the <see cref="SignalRUserStore"/> class.
        /// </summary>
        public SignalRUserStore() {

        }

        /// <summary>
        /// Initializes a new instance of the <see cref="SignalRUserStore"/> class.
        /// </summary>
        /// <param name="mobileServiceUserId">The mobile service user identifier.</param>
        /// <param name="userAlias">The user alias.</param>
        /// <param name="signalRConnectionId">The signal r connection identifier.</param>
        public SignalRUserStore(string mobileServiceUserId,string signalRConnectionId) {
            this.PartitionKey = mobileServiceUserId;
            this.RowKey = signalRConnectionId;
        }

    }
}

 

Messages that are sent over the wire are created using the SignalRMessage class:

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

namespace OldSchoolBeats.SignalR {
    /// <summary>
    /// The message to be serializd
    /// and be sent out via SignalR.
    /// </summary>
    public class SignalRMessage {


        /// <summary>
        /// From user.
        /// </summary>
        /// <value>
        /// The user the message comes from.
        /// </value>
        [JsonProperty]
        private string FromUser {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets to user.
        /// </summary>
        /// <value>
        /// To which user to send the message
        /// </value>
        [JsonProperty]
        private string ToUser {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the message.
        /// </summary>
        /// <value>
        /// The message text.
        /// </value>
        [JsonProperty]
        private string Message {
            get;
            set;
        }


        /// <summary>
        /// Gets the message type to send.
        /// </summary>
        /// <value>
        /// The message type to send.
        /// </value>
        [JsonProperty]
        public MessageType MessageTypeToSend {
            get;
            private set;
        }


        /// <summary>
        /// Initializes a new instance of the <see cref="SignalRMessage"/> class.
        /// To send a broadcast message, just omit the fromUser and toUser
        /// parameters.
        /// To send a message to a specific user, set toUser and omit fromUser.
        /// To send a message from on user, to another user set fromUser and toUser.
        /// Message cannot be omitted and is obligatory.
        /// </summary>
        /// <param name="fromUser">From user.</param>
        /// <param name="toUser">To user.</param>
        /// <param name="message">The message.</param>
        public SignalRMessage (string message,string fromUser=null,string toUser=null) {

            //We have a broadcast message here
            if((string.IsNullOrEmpty(toUser) || string.IsNullOrWhiteSpace(toUser)) &&
                    (string.IsNullOrEmpty(fromUser) || string.IsNullOrWhiteSpace(fromUser))) {

                this.MessageTypeToSend = MessageType.BroadcastMessage;
            }

            //We have a single user message here
            if (!(string.IsNullOrEmpty(toUser) || !string.IsNullOrWhiteSpace(toUser)) &&
                    (string.IsNullOrEmpty(fromUser) || string.IsNullOrWhiteSpace(fromUser))) {

                this.MessageTypeToSend = MessageType.SingleUserMessage;
            }

            //We have a user to user message
            if (!(string.IsNullOrEmpty(toUser) || !string.IsNullOrWhiteSpace(toUser)) &&
                    (!string.IsNullOrEmpty(fromUser) || !string.IsNullOrWhiteSpace(fromUser))) {

                this.MessageTypeToSend = MessageType.UserToUserMessage;
            }


            if (string.IsNullOrEmpty(message) || string.IsNullOrWhiteSpace(message)) {
                throw new ArgumentException("Parameter cannot be null, empty or whitespace", "message");
            }

            //Let's keep up the performance
            if(message.Length > 200) {
                message = message.Substring(0,199);
            }



            this.FromUser = fromUser;
            this.ToUser = toUser;
            this.Message = message;
        }

    }
}

 

Depending on the the two optional parameters  fromUser and toUser in the constructor, the type of message to be sent-out is defined. If both parameters are omitted, it is a broadcast message. If fromUser and toUser are set, it is a user to user message and in the last case, when from user is null and toUser is set it is a single user message. The message-type is defined within the MessageType enumeration:

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

namespace OldSchoolBeats.SignalR {

    /// <summary>
    /// What kind of message do
    /// we have?
    /// </summary>
    public enum MessageType {
        BroadcastMessage,
        SingleUserMessage,
        UserToUserMessage
    }
}

 

The rest of the classes is related to SignalR HubPipeline-Modules that are used to log hub-actions to a log or to save the messages to a database. A HubPiplineModule is like an interceptor that listens to all the actions going on on the hub. But like mentioned before, I could not get it to work with the mobile-backend. Just check them out to see how a HubPiplineModule can be implemented for diagnostic purposes.

Client side MVVM-Based Implementation

Both platforms Windows Phone and Windows use the same ViewModel, commands, behaviors, etc. to implement the UI functionalities. Everything is done within the MainViewModel.cs class (usually you would have more view-models, but to keep it simple, I used just one):

 

using System.Threading.Tasks;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using GalaSoft.MvvmLight.Ioc;
using GalaSoft.MvvmLight.Messaging;
using Microsoft.Practices.ServiceLocation;
using Microsoft.WindowsAzure.MobileServices;
using OldSchoolBeats.ClientModel;
using OldSchoolBeats.Universal.Services;
using Windows.UI.Xaml;
using Microsoft.AspNet.SignalR.Client;
using System.Collections.ObjectModel;
using Newtonsoft.Json;
using OldSchoolBeats.Universal.Messaging;
using Windows.UI.Popups;
using GalaSoft.MvvmLight.Threading;
using System;

namespace OldSchoolBeats.Universal.ViewModel {
    /// <summary>
    /// This class contains properties that the main View can data bind to.
    /// <para>
    /// Use the <strong>mvvminpc</strong> snippet to add bindable properties to this ViewModel.
    /// </para>
    /// <para>
    /// You can also use Blend to data bind with the tool's support.
    /// </para>
    /// <para>
    /// See http://www.galasoft.ch/mvvm
    /// </para>
    /// </summary>
    public class MainViewModel : ViewModelBase {

        public IDataService<OldSchoolArtist> DataService {
            get;
            set;
        }


        public INavigationService NavService {
            get;
            set;
        }

        public ILoginService LoginService {
            get;
            set;
        }


        private BindableOldSchoolArtist newArtist;

        public BindableOldSchoolArtist NewArtist {
            get {
                return newArtist;
            }

            set {
                newArtist = value;
            }
        }


        public SignalRMessage SignalRUserMessage {
            get;
            set;
        }


        private ObservableCollection<SignalRMessage> signalrMessages;

        public ObservableCollection<SignalRMessage> SignalrMessages {
            get {
                return signalrMessages;
            }

            set {
                signalrMessages = value;
            }
        }

        private ObservableCollection<string> signalrBroadcastMessages;

        public ObservableCollection<string> SignalrBroadcastMessages {
            get {
                return signalrBroadcastMessages;
            }

            set {
                signalrBroadcastMessages = value;

            }
        }



        /// <summary>
        /// The <see cref="EditAreaVisible" /> property's name.
        /// </summary>
        public const string EditAreaVisiblemPropertyName = "EditAreaVisible";

        private Visibility _editAreaVisible;

        /// <summary>
        /// Sets and gets the EditAreaVisible property.
        /// Changes to that property's value raise the PropertyChanged event.
        /// </summary>
        public Visibility EditAreaVisible {
            get {
                return _editAreaVisible;
            }

            set {
                if (_editAreaVisible == value) {
                    return;
                }

                _editAreaVisible = value;
                RaisePropertyChanged(EditAreaVisiblemPropertyName);
            }
        }


        private Visibility _addAreaVisible;

        public Visibility AddAreaVisible {
            get {
                return _addAreaVisible;
            }

            set {
                _addAreaVisible = value;
                RaisePropertyChanged("AddAreaVisible");
            }
        }

        private RelayCommand<BindableOldSchoolArtist> addNewArtistCommand;

        public RelayCommand<BindableOldSchoolArtist> AddNewArtistCommand {

            get {
                return this.addNewArtistCommand;
            }

            set {
                this.addNewArtistCommand = value;
            }

        }

        private RelayCommand<OldSchoolArtist> deleteArtistCommand;

        public RelayCommand<OldSchoolArtist> DeleteArtistCommand {

            get {
                return this.deleteArtistCommand;
            }

            set {
                this.deleteArtistCommand = value;
            }

        }


        private RelayCommand editArtistCommand;

        public RelayCommand EditArtistCommand {

            get {
                return this.editArtistCommand;
            }

            set {
                this.editArtistCommand = value;
            }

        }

        private RelayCommand<string> lookupArtistImageCommand;

        public RelayCommand<string> LookupArtistImageCommand {

            get {
                return this.lookupArtistImageCommand;
            }

            set {
                this.lookupArtistImageCommand = value;
            }

        }



        private RelayCommand crudActionCommand;

        public RelayCommand CrudActionCommand {

            get {
                return this.crudActionCommand;
            }

            set {
                this.crudActionCommand = value;
            }

        }


        private RelayCommand cancelCommand;

        public RelayCommand CancelCommand {

            get {
                return this.cancelCommand;
            }

            set {
                this.cancelCommand = value;
            }

        }


        private RelayCommand logoutCommand;

        public RelayCommand LogoutCommand {

            get {
                return this.logoutCommand;
            }

            set {
                this.logoutCommand = value;
            }

        }


        private RelayCommand<string> signalRBroadcastCommand;

        public RelayCommand<string> SignalRBroadcastCommand {

            get {
                return this.signalRBroadcastCommand;
            }

            set {
                this.signalRBroadcastCommand = value;
            }

        }

        private RelayCommand<SignalRMessage> signalRToSpecificUserCommand;

        public RelayCommand<SignalRMessage> SignalRToSpecificUserCommand {

            get {
                return this.signalRToSpecificUserCommand;
            }

            set {
                this.signalRToSpecificUserCommand = value;
            }

        }

        private RelayCommand<SignalRMessage> signalRFromUserToUserCommand;

        public RelayCommand<SignalRMessage> SignalRFromUserToUserCommand {

            get {
                return this.signalRFromUserToUserCommand;
            }

            set {
                this.signalRFromUserToUserCommand = value;
            }

        }

        private RelayCommand<string> navigate;

        public RelayCommand<string> Navigate {
            get {
                return navigate;
            }

            set {
                navigate = value;
            }
        }

        private RelayCommand executeDelteCommand;

        public RelayCommand ExecuteDelteCommand {
            get {
                return executeDelteCommand;
            }

            set {
                executeDelteCommand = value;
            }
        }

        private RelayCommand loginCommand;

        public RelayCommand LoginCommand {
            get {
                return loginCommand;
            }

            set {
                loginCommand = value;
            }
        }


        private RelayCommand<string> toggleEdit;

        public RelayCommand<string> ToggleEdit {
            get {
                return toggleEdit;
            }

            set {
                toggleEdit = value;
            }
        }

        private OldSchoolArtist currentArtist {
            get;
            set;
        }

        /// <summary>
        /// Initializes a new instance of the MainViewModel class.
        /// </summary>
        public MainViewModel() {



            if (ViewModelBase.IsInDesignModeStatic) {
                // Code runs in Blend --> create design time data.
                //Let us feed the model with some sample data
                InitDesignMode();
            }

            else {
                // Code runs "for real"
                //Get the real thing
                InitRuntimeMode();
            }
        }

        private void InitRuntimeMode() {
            try {

                #if WINDOWS_APP
                this.EditAreaVisible = Visibility.Collapsed;
                this.AddAreaVisible = Visibility.Collapsed;
                #endif

                this.DataService = SimpleIoc.Default.GetInstance<IDataService<OldSchoolArtist>>();
                this.NavService = SimpleIoc.Default.GetInstance<INavigationService>();
                this.LoginService = SimpleIoc.Default.GetInstance<ILoginService>();
                this.SignalrBroadcastMessages = new ObservableCollection<string>();
                this.SignalrMessages = new ObservableCollection<SignalRMessage>();

                this.NewArtist = new BindableOldSchoolArtist();
                this.SignalRUserMessage = new SignalRMessage();

                this.AddNewArtistCommand = new RelayCommand<BindableOldSchoolArtist>(AddNewArtist);
                this.DeleteArtistCommand = new RelayCommand<OldSchoolArtist>(DeleteArtist);
                this.EditArtistCommand = new RelayCommand(EditArtist);
                this.LookupArtistImageCommand = new RelayCommand<string>(LookupArtist);
                this.CrudActionCommand = new RelayCommand(ExecuteUpdate);
                this.CancelCommand = new RelayCommand(Cancel);
                this.SignalRBroadcastCommand = new RelayCommand<string>(SendBroadCast, CanExecuteSignalRMessageSend);
                this.SignalRFromUserToUserCommand = new RelayCommand<SignalRMessage>(SendFromToUser, CanExecuteSignalRMessageSendFromToUser);
                this.SignalRToSpecificUserCommand = new RelayCommand<SignalRMessage>(SendToSpecificUser, CanExecuteSignalRMessageSendToSpecificUser);
                this.Navigate = new RelayCommand<string>(NavigateAction);
                this.LogoutCommand = new RelayCommand(Logout, CanExecuteLogout);
                this.ExecuteDelteCommand = new RelayCommand(ExecuteDelete);
                this.LoginCommand = new RelayCommand(Login);
                this.ToggleEdit = new RelayCommand<string>(ToggleEditAction);



            }

            catch (System.Exception ex) {

                throw;
            }

        }



        private void ToggleEditAction(string action) {



            if(action.Equals("add")) {
                this.AddAreaVisible = Visibility.Visible;
            }

            if(action.Equals("edit")) {
                this.EditAreaVisible = Visibility.Visible;
                this.EditArtist();
            }
        }

        private async void ExecuteDelete() {
            await this.DataService.DeleteItem(this.DataService.SelectedItem);
        }

        private void NavigateAction(string pageName) {
            this.NavService.NavigateTo(pageName);
        }

        private bool CanExecuteSignalRMessageSendToSpecificUser(SignalRMessage arg) {
            return App.HubConnection.State == ConnectionState.Connected;
        }

        private bool CanExecuteSignalRMessageSendFromToUser(SignalRMessage arg) {
            return App.HubConnection.State == ConnectionState.Connected;
        }

        private bool CanExecuteSignalRMessageSend(string arg) {
            return App.HubConnection.State == ConnectionState.Connected;
        }

        private async void SendToSpecificUser(SignalRMessage signalrData) {

            await App.HubProxy.Invoke("SendToSpecificUser", new object[] { signalrData.ToUser, signalrData.Message });
        }

        private async void SendFromToUser(SignalRMessage signalrData) {
            await App.HubProxy.Invoke("SendToSpecificUserFromSpecificUser", new object[] { signalrData.ToUser, signalrData.Message });
        }

        private async void SendBroadCast(string message) {
            await App.HubProxy.Invoke("Broadcast", new object[] { message });
        }



        private bool CanExecuteLogout() {

            bool canLogout = false;

            Task.Run(async ()=> {
                canLogout = await this.LoginService.UserLoggedIn();
            }).Wait();

            return canLogout;
        }

        private async void Logout() {
            await this.LoginService.LogOut();
        }

        private async void Login() {


            await DispatcherHelper.RunAsync(async () => {
                if (!await this.LoginService.UserLoggedIn()) {
                    var success = await this.LoginService.Login();

                    if (success) {
                        App.User = this.LoginService.MobUser;

                        await this.ConnectToSignalR();

                        await this.DataService.FillItems();
                    }

                    else {
                        App.User = null;
                    }
                }

                else {
                    App.User = this.LoginService.MobUser;
                    await this.ConnectToSignalR();
                    await  this.DataService.FillItems();
                }
            });

        }

        private void Cancel() {

            this.DataService.SelectedItem = null;
            #if WINDOWS_APP
            this.EditAreaVisible = Visibility.Collapsed;
            this.AddAreaVisible = Visibility.Collapsed;
            #endif
            #if WINDOWS_PHONE_APP
            this.Navigate.Execute("MainPage");
            #endif
        }

        private async void ExecuteUpdate() {


            await DataService.UpdateItem(this.DataService.DataContext ,this.DataService.SelectedItem);

            #if WINDOWS_PHONE_APP
            this.Navigate.Execute("MainPage");
            #endif
            #if WINDOWS_APP
            this.EditAreaVisible = Visibility.Collapsed;
            #endif
        }

        private  void LookupArtist(string artistName) {

            //Items are directly filled and bound
            DataService.SearchItems(a=>a.Artist.Equals(artistName));

        }

        private void EditArtist() {


            this.DataService.DataContext = new BindableOldSchoolArtist() {
                YearsArchive = this.DataService.SelectedItem.YearsArchive,
                Artist = this.DataService.SelectedItem.Artist,
                ImageUrl = this.DataService.SelectedItem.ImageUrl,
                RelatedStyles = this.DataService.SelectedItem.RelatedStyles
            };
            #if WINDOWS_PHONE_APP
            this.Navigate.Execute("EditArtist");
            #endif

        }

        private void DeleteArtist(OldSchoolArtist artist) {

            var dialogMessage = new ShowDialogMessage();

            dialogMessage.Yes = this.ExecuteDelteCommand;
            dialogMessage.No = this.CancelCommand;


            Messenger.Default.Send<ShowDialogMessage>(dialogMessage, MessagingIdentifiers.DELETE_CONFIRM_MESSAGE);



        }

        private async void AddNewArtist(BindableOldSchoolArtist artist) {

            var oldArtist = new OldSchoolArtist() {
                Artist = artist.Artist, ImageUrl = artist.ImageUrl, RelatedStyles = artist.RelatedStyles, YearsArchive = artist.YearsArchive
            };

            await this.DataService.AddItem(oldArtist);

            this.NewArtist = new BindableOldSchoolArtist();

            #if WINDOWS_APP
            this.EditAreaVisible = Visibility.Collapsed;
            #endif

            #if WINDOWS_PHONE_APP
            this.Navigate.Execute("MainPage");
            #endif

            #if WINDOWS_APP
            this.AddAreaVisible = Visibility.Collapsed;
            #endif
        }

        private void InitDesignMode() {

            this.EditAreaVisible = Visibility.Visible;
            this.DataService = SimpleIoc.Default.GetInstance<DesignTimeDataService>();
        }

        private async Task ConnectToSignalR() {

            App.HubConnection = new HubConnection(App.MobileService.ApplicationUri.AbsoluteUri);



            if (App.User != null) {

                App.HubConnection.Headers["x-zumo-auth"] = App.User.MobileServiceAuthenticationToken;

            }

            else {
                return;
            }




            //Creating the hub proxy. That allows us to send and receive
            //Messages,sexy.
            App.HubProxy = App.HubConnection.CreateHubProxy("MessagingHub");

            try {

                if (App.HubConnection.State == ConnectionState.Disconnected) {
                    App.HubConnection.StateChanged += HubConnection_StateChanged;
                    await App.HubConnection.Start();
                }
            }

            catch (Microsoft.AspNet.SignalR.Client.Infrastructure.StartException ex) {

                throw;
            }

            App.HubProxy.On<string>("receiveMessageFromUser", async (msg) => {


                var message = await this.DesirializeSignalRMEssage(msg);
                DispatcherHelper.CheckBeginInvokeOnUI(() => {
                    this.SignalrMessages.Add(message);
                });
            });



            App.HubProxy.On<string>("broadcastMessage", (msg) => {


                DispatcherHelper.CheckBeginInvokeOnUI(() => {
                    this.SignalrBroadcastMessages.Add(msg);
                });

            });

            App.HubProxy.On<string>("receiveMessageForUser", async (msg) => {

                var message = await this.DesirializeSignalRMEssage(msg);
                DispatcherHelper.CheckBeginInvokeOnUI(() => {
                    this.SignalrMessages.Add(message);
                });
            });





        }

        void HubConnection_StateChanged(StateChange obj) {

            //You can check here the state of the signalr connection.
        }


        private async Task<SignalRMessage> DesirializeSignalRMEssage(string message) {

            return await Task.Run<SignalRMessage>(() => {

                return JsonConvert.DeserializeObject<SignalRMessage>(message);

            });

        }


    }
}

 

The navigation-service is implemented using a simple interface (INavigationService) and a bit of reflection to navigate to the bound view-name:

using System;
using System.Collections.Generic;
using System.Text;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace OldSchoolBeats.Universal.Services {
    class NavigationService:INavigationService {
        public void NavigateTo(string frameType) {


            Type t = Type.GetType("OldSchoolBeats.Universal."+frameType);

            Frame rootFrame = Window.Current.Content as Frame;

            rootFrame.Navigate(t);

        }
    }
}

 

Authentication and login are managed by the WamsLoginService class that implements the ILoginService interface:

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Microsoft.WindowsAzure.MobileServices;
using Windows.Security.Credentials;
using System.Linq;
using Windows.UI.Popups;
using GalaSoft.MvvmLight.Messaging;
using OldSchoolBeats.Universal.Messaging;


namespace OldSchoolBeats.Universal.Services {
    public class WamsLoginService:ILoginService {
        public MobileServiceUser MobUser {
            get;
            set;
        }


        public WamsLoginService() {

        }

        public async Task<bool> Login() {

            // This sample uses the Facebook provider.
            var provider = "MicrosoftAccount";

            // Use the PasswordVault to securely store and access credentials.
            PasswordVault vault = new PasswordVault();
            PasswordCredential credential = null;

            var dialogMessage = new ShowDialogMessage() { };


            while (credential == null) {
                try {
                    // Try to get an existing credential from the vault.
                    credential = vault.FindAllByResource(provider).FirstOrDefault();
                }

                catch (Exception) {
                    // When there is no matching resource an error occurs, which we ignore.
                }

                if (credential != null) {
                    // Create a user from the stored credentials.
                    MobUser = new MobileServiceUser(credential.UserName);
                    credential.RetrievePassword();
                    MobUser.MobileServiceAuthenticationToken = credential.Password;

                    // Set the user from the stored credentials.
                    App.MobileService.CurrentUser = MobUser;


                }

                else {
                    try {
                        // Login with the identity provider.
                        MobUser = await App.MobileService
                                  .LoginAsync(MobileServiceAuthenticationProvider.MicrosoftAccount);

                        // Create and store the user credentials.
                        credential = new PasswordCredential(provider,
                                                            MobUser.UserId, MobUser.MobileServiceAuthenticationToken);
                        vault.Add(credential);
                    }

                    catch (MobileServiceInvalidOperationException ex) {

                        Messenger.Default.Send<ShowDialogMessage>(dialogMessage,MessagingIdentifiers.LOGIN_ERROR_MESSAGE);
                    }
                }

                this.MobUser = App.MobileService.CurrentUser;

                Messenger.Default.Send<ShowDialogMessage>(dialogMessage, MessagingIdentifiers.LOGIN_SUCCESS_MESSAGE);





            }

            return true;
        }

        public async Task<bool> LogOut() {

            return await Task.Run<bool>( ()=> {
                var provider = "MicrosoftAccount";

                PasswordVault vault = new PasswordVault();

                try {
                    // Try to get an existing credential from the vault.
                    var credential = vault.FindAllByResource(provider).FirstOrDefault();

                    if(credential != null) {
                        //We remove the credential here
                        vault.Remove(credential);

                        return true;
                    }

                    return false;
                }

                catch (Exception) {
                    return false;
                }
            });
        }


        public async Task<bool> UserLoggedIn() {
            return await Task.Run<bool>(() => {
                var provider = "MicrosoftAccount";

                PasswordVault vault = new PasswordVault();

                try {
                    // Try to get an existing credential from the vault.
                    var credential = vault.FindAllByResource(provider).FirstOrDefault();

                    if (credential != null) {

                        credential.RetrievePassword();

                        App.MobileService.CurrentUser = new MobileServiceUser(credential.UserName) {
                            MobileServiceAuthenticationToken = credential.Password
                        };

                        this.MobUser = App.MobileService.CurrentUser;

                        return true;
                    }

                    return false;
                }

                catch (Exception) {
                    return false;
                }
            });
        }
    }
}

 

A very interesting approach to show dialog-messages in a MVVM-based scenario was implemented by the MVP Jason Roberts. It’s using the MVVM-Light Messenger and behaviors, coupled with a specific  identifier for a message like an error or delete message (in this sample I use GUIDS) to show a dialog in Universal apps after putting a specific message (containing the identifier) on the wire using the MVVM-Light Messenger.

Here is the implementation of the custom dialog-behavior:

using System;
using Windows.UI.Popups;
using Windows.UI.Xaml;
using GalaSoft.MvvmLight.Messaging;
using Microsoft.Xaml.Interactivity;
using OldSchoolBeats.Universal.Messaging;

namespace OldSchoolBeats.Universal.Behaviours {

    //This code is taken from
    //http://dontcodetired.com/blog/post/Telling-a-View-to-display-a-Message-Dialog-from-the-ViewModel-With-MVVMLight-in-Windows-81-Store-Apps.aspx#comment
    //Which is an excellent solution by Jason Roberts
    internal class DialogBehavior : DependencyObject, IBehavior {
        public DialogBehavior() {
            MessageText = "Are you sure?";
            YesText = "Yes";
            NoText = "No";
            CancelText = "Cancel";
        }


        public string Identifier {
            get;
            set;
        }
        public string MessageText {
            get;
            set;
        }

        public string TitleText {
            get;
            set;
        }

        public string YesText {
            get;
            set;
        }
        public string NoText {
            get;
            set;
        }
        public string CancelText {
            get;
            set;
        }


        public void Attach(DependencyObject associatedObject) {
            AssociatedObject = associatedObject;

            Messenger.Default.Register<ShowDialogMessage>(this, Identifier, ShowDialog);
        }

        public void Detach() {
            Messenger.Default.Unregister<ShowDialogMessage>(this, Identifier);
            AssociatedObject = null;
        }

        public DependencyObject AssociatedObject {
            get;
            private set;
        }

        private async void ShowDialog(ShowDialogMessage m) {
            var d = new MessageDialog(MessageText,TitleText);

            if (m.Yes != null) {
                d.Commands.Add(new UICommand(YesText, command => m.Yes.Execute(null)));
            }

            if (m.No != null) {
                d.Commands.Add(new UICommand(NoText, command => m.No.Execute(null)));
            }

            if (m.Cancel != null) {
                d.Commands.Add(new UICommand(CancelText, command => m.Cancel.Execute(null)));
            }

            if (m.Cancel != null) {
                d.CancelCommandIndex = (uint)d.Commands.Count - 1;
            }

            await d.ShowAsync();
        }
    }
}

 

 How to make the sample work:

First of all you need an Azure Account. You can sign-up for a free trail here: Windows Azure . Then you need to replace [YOUR MOBILE SERVICES KEY HERE] with your mobile-service application-key and [YOUR MOBILE SERVICES-URL HERE] with the URL of your mobile-service.

To be able to work with the storage part you have to replace [YOUR STORAGE KEY HERE] with your Azure storage access key and [YOUR AZURE STORAGE ACCOUNT NAME] with your Azure Storage account-name in the Web.config file of the managed-backend project.

If you want to use the LastFM-API, you have to replace the following keys in Web.config with your values:

  • LastFMApiKey => Your LastFM API-Key
  • LastFMSecret => Your LastFM API-Secret
  • LastFMUserName => Your LastFM username
  • LastFMPassword => Your LastFM password

Then you have to associate both apps with the store. That’s it!

 

Conclusion

This blog-post concludes the series and I hope you enjoyed it. Thanks for reading and happy coding!

SOURCE-DOWNLOAD ON GITHUB

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer ExGrip LCC
Germany Germany
Working as professional freelancer for the last 5 years. Specialized on and addicted to .NET and a huge fan of Windows Azure from the beginning. Socializing people of all areas, from CEO's to co-workers. Consider myself as a social architect.

Now the proud owner of ExGrip LLC - building mobile cloud experiences. Our latest product "Tap-O-Mizer" is shortly in Beta2. It enables you to see what really needs to be changed in your Windows Phone 8 or Windows 8 app (in real-time, if needed), to ensure customer satisfaction.

Started authorship for "Pluralsight - Hardcore Developer Training" and going to publish the first course on Windows Azure soon.

A few years ago I made a major shift from developer to "devsigner".Focusing my creativity also on great user experiences on Windows, Windows 8 and Windows Phone. Utilizing Expression Design, Expression Blend, Photoshop and Illustrator.

I started developing my first programs on a Commodore C64 (basic and assembly) at the age of fourteen years. Later on an Amiga 500 (C++). After that I made the shift to DOS and Windows from version 3.11 and up.

To me the most important part of developing new experiences is to work with creative and outstanding people and to bring new, exciting ideas to life.

I strongly believe that everyone can be a hero if he/she get's pushed and motivated and valued. Therefore, and that under any circumstances: "People first!"

Specialties:Extremely motivated and pushing people to create results.

Comments and Discussions

 
QuestionBlob upload example not working Pin
Madhusudhan05713-Apr-16 17:55
Madhusudhan05713-Apr-16 17:55 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.