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. 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:
There’s an entry that is asking you to try out the service. Selecting the small right-arrow opens the API main-page:
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”:
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:
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.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:
And the randomly picked “POST” method:
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:
[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:
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)
The Windows Phone UI (Universal App, Windows 8.1)
Final Sample 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.Configuration.SetDocumentationProvider(new XmlDocumentationProvider(this.Services));
}
public IQueryable<OldSchoolArtist> GetAllOldSchoolArtist() {
return Query();
}
public SingleResult<OldSchoolArtist> GetOldSchoolArtist(string id) {
return Lookup(id);
}
public Task<OldSchoolArtist> PatchOldSchoolArtist(string id, Delta<OldSchoolArtist> patch) {
return UpdateAsync(id, patch);
}
[ResponseType(typeof(OldSchoolArtist))]
public async Task<IHttpActionResult> PostOldSchoolArtist(OldSchoolArtist item) {
OldSchoolArtist current = await InsertAsync(item);
return CreatedAtRoute("Tables", new { id = current.Id }, current);
}
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 {
public class LastFmController : ApiController {
public ApiServices Services {
get;
set;
}
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"];
var session = new Session(apiKey,apiSecret);
session.Authenticate(userName,Lastfm.Utilities.md5(password));
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 {
[AuthorizeLevel(AuthorizationLevel.User)]
public class BlobResourceHelperController : ApiController {
public ApiServices Services {
get;
set;
}
private CloudStorageAccount storageAccount {
get;
set;
}
private CloudBlobClient blobClient {
get;
set;
}
private CloudBlobContainer container {
get;
set;
}
[HttpPost]
[Route("api/getallblobs")]
public async Task<IEnumerable<CloudBlockBlob>> GetAllBlobsInContainer(BlobManipulationData data) {
await InitBlobContainer(data);
var blobs = container.ListBlobs().OfType<CloudBlockBlob>();
return blobs;
}
[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;
}
[HttpPost]
[Route("api/getblobcount")]
public async Task<int> GetBlobCount(BlobManipulationData data) {
return await Task.Run<int>(async () => {
await InitBlobContainer(data);
return container.ListBlobs().ToList().Count;
});
}
[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);
}
[HttpPost]
[Route("api/deleteblob")]
public async Task DeleteBlob(BlobManipulationData data) {
await InitBlobContainer(data);
var blob = container.GetBlockBlobReference(data.BlobName);
await blob.DeleteIfExistsAsync();
}
[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);
await renameTo.StartCopyFromBlobAsync(blob);
await blob.DeleteAsync();
}
}
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();
}
[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;
namespace OldSchoolBeats.Universal {
public sealed partial class Storage : Page {
private const int PAGING_SKIP = 0;
private const int PAGING_TAKE = 10;
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”.
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)
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.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() {
SignalRExtensionConfig.Initialize();
ConfigOptions options = new ConfigOptions();
options.SetRealtimeAuthorization(AuthorizationLevel.User);
ConfigBuilder builder = new ConfigBuilder(options, (httpConfig, autofac) => {
autofac.RegisterInstance(new TableLoggingService()).As<ILoggingService>();
});
HttpConfiguration config = ServiceConfig.Initialize(builder);
config.SetSampleForType(
"Currently not used.",
new MediaTypeHeaderValue("application/x-www-form-urlencoded"),
typeof(OldSchoolArtist));
Database.SetInitializer(new OldSchoolBeatsInitializer());
}
}
public class OldSchoolBeatsInitializer : DropCreateDatabaseIfModelChanges<OldSchoolBeatsContext> {
protected override void Seed(OldSchoolBeatsContext context) {
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 {
[HubName("MessagingHub")]
public class MessagingHub:Hub {
public ApiServices Services {
get;
set;
}
[AuthorizeLevel(AuthorizationLevel.User)]
public void Broadcast(string broadcastMesssage) {
Clients.All.broadcastMessage(broadcastMesssage);
}
[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;
serializedMessage = await Task.Factory.StartNew<string>(()=> {
return JsonConvert.SerializeObject(user2UserMessage);
});
Clients.Client(queryResult.RowKey).receiveMessageFromUser(serializedMessage);
}
}
[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();
var user2UserMessage = new SignalRMessage(message,null,toUserId);
string serializedMessage = string.Empty;
serializedMessage = await Task.Factory.StartNew<string>(() => {
return JsonConvert.SerializeObject(user2UserMessage);
});
Clients.Client(queryResult.RowKey).receiveMessageForUser(serializedMessage);
}
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();
}
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) {
}
return base.OnDisconnected(stopCalled);
}
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();
}
private CloudTable GetConnectionTable() {
var storageAccount =
CloudStorageAccount.Parse(
Services.Settings["StorageConnectionString"]);
var tableClient = storageAccount.CreateCloudTableClient();
return tableClient.GetTableReference("signalrconnections");
}
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 {
public class SignalRUserStore:TableEntity {
public SignalRUserStore() {
}
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 {
public class SignalRMessage {
[JsonProperty]
private string FromUser {
get;
set;
}
[JsonProperty]
private string ToUser {
get;
set;
}
[JsonProperty]
private string Message {
get;
set;
}
[JsonProperty]
public MessageType MessageTypeToSend {
get;
private set;
}
public SignalRMessage (string message,string fromUser=null,string toUser=null) {
if((string.IsNullOrEmpty(toUser) || string.IsNullOrWhiteSpace(toUser)) &&
(string.IsNullOrEmpty(fromUser) || string.IsNullOrWhiteSpace(fromUser))) {
this.MessageTypeToSend = MessageType.BroadcastMessage;
}
if (!(string.IsNullOrEmpty(toUser) || !string.IsNullOrWhiteSpace(toUser)) &&
(string.IsNullOrEmpty(fromUser) || string.IsNullOrWhiteSpace(fromUser))) {
this.MessageTypeToSend = MessageType.SingleUserMessage;
}
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");
}
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 {
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 {
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;
}
}
public const string EditAreaVisiblemPropertyName = "EditAreaVisible";
private Visibility _editAreaVisible;
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;
}
public MainViewModel() {
if (ViewModelBase.IsInDesignModeStatic) {
InitDesignMode();
}
else {
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) {
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;
}
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) {
}
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() {
var provider = "MicrosoftAccount";
PasswordVault vault = new PasswordVault();
PasswordCredential credential = null;
var dialogMessage = new ShowDialogMessage() { };
while (credential == null) {
try {
credential = vault.FindAllByResource(provider).FirstOrDefault();
}
catch (Exception) {
}
if (credential != null) {
MobUser = new MobileServiceUser(credential.UserName);
credential.RetrievePassword();
MobUser.MobileServiceAuthenticationToken = credential.Password;
App.MobileService.CurrentUser = MobUser;
}
else {
try {
MobUser = await App.MobileService
.LoginAsync(MobileServiceAuthenticationProvider.MicrosoftAccount);
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 {
var credential = vault.FindAllByResource(provider).FirstOrDefault();
if(credential != null) {
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 {
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 {
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