Freigeben über


Developing a Windows Phone 8.1 client for the sensor data tracker web API via a Portable Class Library


Related Posts:

  1. Writing a simple implementation of dependency injection in MVC 4 Web API with .NET Framework 4.5
  2. Writing BDD style unit tests to verify the MVC based Web API

To recap, so far we put together a MVC based Web API and examined the mechanism for plugging in a very simple dependency injection mechanism without using an established framework. Further on we added BDD style unit tests to verify the API behavior. Since the base framework is established and sensor readings are getting generated as expected, let’s develop a UI client to consume the generated data. Now there are several ways to consume and display the sensor data such as a desktop client, WPF client, browser client, Windows Store client and what not!

How about a mobile client such as one based on Windows Phone 8.1 that also coincides with the mission statement of Microsoft:

“….To create a family of devices and services for individuals and businesses that empower people around the globe at home, at work and on the go, for the activities they value most…." 

Without further ado, let’s dig into this idea! Start by adding a new Windows Phone project to the solution.

Right click on the Solution and choose Add—>New Project… Select Windows Phone as the project type. There are several templates available for creating a Windows Phone project. Since we need two pages, one for generating and storing sensor readings and another for browsing the sensor data, let’s choose Blank App template and add these pivots manually:

image

To adhere to Globalization and Localization best practices for Windows Phone, we’ll store all the resource strings in resource files. Create a new folder called Strings and add another folder under it to store en-US strings. Now add a new Resource file (Resources.resw) under en-US folder as shown below:

image

Once the resource file is created, resource editor window opens, allowing you to enter the resource strings for your app. Copy and paste the following strings:

Name

Value

Comment

PagingControl.Text

Total: {0} - Showing Page {1} of {2}

PivotPostReadings.Header

Record Readings

PivotSensorDataTracker.Title

SENSOR DATA TRACKER

PivotShowReadings.Header

View Readings

SensorReadingTitle.Text

Sensor Readings

SubmitButton.Content

Submit Reading

We’ll create a Portable Class Library (PCL) so that we may share the code that accesses the web API between various clients. Add a new project to the solution called SensorDataTracker.CommonLogic as shown below:

image

When you create a PCL project, Visual Studio asks you which frameworks you wish to target. Make sure that the following options are selected in the dialog:

image

[Note that the last two boxes are visible only if you’ve Xamarin platform installed on your box.]

In Visual Studio, right-click the SensorDataTracker.CommonLogic project, and then select Manage NuGet Packages for solution. In the Manage NuGet Packages dialog, select Online->All in the left pane, make sure that Include Prerelease is selected in the dropdown and then search for “Microsoft.AspNet.WebApi.Client” as follows:

image

Install the selected NuGet package that enables us to use System.Net.Http namespace for formatting and content negotiation.

Create a new folder called Services in the SensorDataTracker.CommonLogic project, and add an IHttpService interface. Copy and paste the following code:

 namespace SensorDataTracker.CommonLogic.Services
{
    using System.Threading;
    using System.Threading.Tasks;
    public interface IHttpService
    {
        Task<string> PostAsync(Uri postUri, string postBody);
        Task<string> GetAsync(Uri requestUri);
        Task<string> PostAsync(Uri postUri, string postBody, CancellationToken cancellationToken);
        Task<string> GetAsync(Uri requestUri, CancellationToken cancellationToken);
    }
}

The interface IHttpService declares four async Task methods, two each for GET and POST API calls with the only difference of cancellation token being passed in the overloaded version.

Let’s go ahead and write a generic implementation of this service interface. Go ahead and create a class called HttpService in the same folder and copy/paste the following code:

 namespace SensorDataTracker.CommonLogic.Services
{
    using System;
    using System.Net.Http;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    public class HttpService : IHttpService
    {
        public async Task<string> PostAsync(Uri postUri, string postBody)
        {
            return await this.PostAsync(postUri, postBody, CancellationToken.None);
        }
        public async Task<string> GetAsync(Uri requestUri)
        {
            return await this.GetAsync(requestUri, CancellationToken.None);
        }
        public async Task<string> PostAsync(Uri postUri, string postBody, CancellationToken cancellationToken)
        {
            using (var httpClient = new HttpClient())
            {
                var postData = new StringContent(postBody, Encoding.UTF8, "application/json");
                var response = await httpClient.PostAsync(postUri, postData, cancellationToken);
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadAsStringAsync();
            }
        }
        public async Task<string> GetAsync(Uri requestUri, CancellationToken cancellationToken)
        {
            using (var httpClient = new HttpClient())
            {
                var response = await httpClient.GetAsync(requestUri, cancellationToken);
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadAsStringAsync();
            }
        }
    }
}

As you can see, we’re using HttpClient (added via NuGet package reference) class to get an async JSON response from the web service and similarly posting a JSON payload to the post controller method of the API. Pretty straightforward!

Since we’re encapsulating service interaction logic in a PCL, we might need to do some refactoring here in order to make the models definition available to the clients such as Windows Phone. Though in a real world project, you might consider moving the model to another shared library and hooking up with the service API using a repository pattern that allows nice separation of concerns with the ability to write unit tests against mock representation of data. However for this exercise, moving the Models folder (that has SendorReading and ResultPage classes) from web API project to PCL project suffices. After the move, SensorDataTracker.CommonLogic should look like below:

image

Don’t forget that you’ll need to make appropriate namespace corrections and add a reference to PCL project in the web API project (SensorDataTracker) so that models are visible to the latter. Likewise you’ll need to add reference to PCL project in the test project (SensorDataTracker.Test) and fix the namespaces, relatively trivial tasks towards the cost of flexibility!

Since the generic service classes are in place, let’s write another layer that makes use of the generic service methods to retrieve and post sensor data to the web API. Under Services folder, add a new interface ISensorDataTrackerService and copy/paste the following code:

 namespace SensorDataTracker.CommonLogic.Services
{
    using SensorDataTracker.CommonLogic.Models;
    using System.Threading.Tasks;
    interface ISensorDataTrackerService
    {
        Task<ResultPage<SensorReading>> GetSensorReadingsAsync();
        Task<ResultPage<SensorReading>> GetSensorReadingsAsync(int pageNumber);
        Task PostSensorReadingAsync(SensorReading reading);
    }
}

We’ve declared three methods, two for retrieving sensor readings and the third one for posting the sensor reading. Let’s implement them in a class called SensorDataTrackerService under the Services folder as before:

 namespace SensorDataTracker.CommonLogic.Services
{
    using System;
    using System.Globalization;
    using System.Threading.Tasks;
    using Newtonsoft.Json;
    using SensorDataTracker.CommonLogic.Models;
    public class SensorDataTrackerService : ISensorDataTrackerService
    {
        private readonly IHttpService _httpService;
        private const int DefaultPageIndex = 1;
        private const string ServerAddress = "https://sensordatatracker.cloudapp.net";
        private readonly string sensorReadingBaseUrl = string.Format(
            CultureInfo.InvariantCulture,
            "{0}/api/SensorReading",
            ServerAddress);
        private const string PaginationQueryString = "{0}?pageNumber={1}";
        public SensorDataTrackerService(IHttpService httpService)
        {
            this._httpService = httpService;
        }
        
        public async Task<ResultPage<SensorReading>> GetSensorReadingsAsync()
        {
            return await this.GetSensorReadingsAsync(DefaultPageIndex);
        }
        public async Task<ResultPage<SensorReading>> GetSensorReadingsAsync(int pageNumber)
        {
            var requestUri = new Uri(string.Format(PaginationQueryString, this.sensorReadingBaseUrl, pageNumber));
            var responseContent = await this._httpService.GetAsync(requestUri);
            return JsonConvert.DeserializeObject<ResultPage<SensorReading>>(responseContent);            
        }
        public async Task PostSensorReadingAsync(SensorReading reading)
        {
            var postUri = new Uri(sensorReadingBaseUrl);
            await this._httpService.PostAsync(postUri, JsonConvert.SerializeObject(reading));
        }
    }
}

Let’s understand the code above before we inch ahead. SensorReadingTrackerService has a constructor that required a reference to an IHttpService that we defined previously. ServerAddress points to the web API location and sensorReadingBaseUrl contains the additional prefix for the GET and POST methods of the service controller. PaginationQueryString is concatenated to the sensorReadingBaseUrl that allows us to pass in a page number. GetSensorReadingsAsync(int pageNumber) composes a Uri with the given page number and asynchronously calls the GetAsync method of HttpService. It then uses DeserializeObject method of JsonConvert class to cast the response into a ResultPage of SensorReading objects before returning. Similarly PostSensorReadingAsync serializes a SensorReading and posts it to the service.

With PCL in place, it’s time to revisit the Windows Phone 8.1 client. Add a reference to PCL project in Windows Phone 8.1 client project.

Double click MainPage.xaml to open the designer and paste the following code, overwriting the existing:

 <Page
    x:Class="SensorDataTracker.WindowsPhone.Client.MainPage"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"></RowDefinition>
            <RowDefinition Height="30"></RowDefinition>
        </Grid.RowDefinitions>
        <Pivot Title="" x:Uid="PivotSensorDataTracker" Grid.Row="0"
               x:Name="PivotContainer"
               SelectionChanged="PivotContainer_OnSelectionChanged">
            <PivotItem Header="PivotItem 0" x:Uid="PivotPostReadings">
                <Grid Margin="12,0,12,0">
                    <StackPanel Orientation="Vertical">
                        <TextBlock Text="" x:Uid="SensorReadingTitle"
                                   Margin="10,0,0,0"
                                   FontFamily="Verdana"
                                   FontSize="20"/>
                        <TextBlock 
                                   x:Name="TextSensorReadingId"
                                   FontFamily="Verdana"
                                   FontSize="80"
                                   FontStretch="UltraExpanded"
                                   FontWeight="ExtraBlack"
                                   HorizontalAlignment="Center"
                                   Margin="10"
                                   Foreground="Khaki" />
                        <Button Content="" x:Uid="SubmitButton"
                                Click="SubmitButton_OnClick" />
                    </StackPanel>
                </Grid>
            </PivotItem>
            <PivotItem Header="PivotItem 1" x:Uid="PivotShowReadings">
                <Grid Margin="12,0,12,0">
                    <StackPanel Orientation="Vertical">
                        <Grid>
                            <TextBlock Text="" x:Uid="SensorReadingTitle"
                                   Margin="10,0,0,0"
                                   FontFamily="Verdana"
                                   FontSize="20"/>
                        </Grid>
                        <ItemsControl Margin="10"
                                      Name="SensorReadingList">
                            <ItemsControl.Template>
                                <ControlTemplate TargetType="ItemsControl">
                                    <Border BorderBrush="Aqua"
                                            BorderThickness="1"
                                            CornerRadius="10">
                                        <ItemsPresenter />
                                    </Border>
                                </ControlTemplate>
                            </ItemsControl.Template>
                            <ItemsControl.ItemTemplate>
                                <DataTemplate>
                                    <Grid>
                                        <Ellipse Fill="Silver"
                                                 Opacity="0.50"
                                                 Stroke="DarkSlateBlue"
                                                 StrokeThickness="2" />
                                        <StackPanel>
                                            <TextBlock Margin="3,3,3,0"
                                                       FontSize="20"
                                                       HorizontalAlignment="Center"
                                                       Text="{Binding Path=Id}" />
                                            <TextBlock Margin="3,0,3,7"
                                                       FontSize="20"
                                                       HorizontalAlignment="Center"
                                                       Text="{Binding Path=CreateDate}" />
                                        </StackPanel>
                                    </Grid>
                                </DataTemplate>
                            </ItemsControl.ItemTemplate>
                        </ItemsControl>
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="30"></RowDefinition>
                                <RowDefinition Height="50"></RowDefinition>
                            </Grid.RowDefinitions>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="120"></ColumnDefinition>
                                <ColumnDefinition Width="*"></ColumnDefinition>
                                <ColumnDefinition Width="120"></ColumnDefinition>
                            </Grid.ColumnDefinitions>
                            <TextBlock x:Name="PageInfo"
                                       x:Uid="PagingControl"
                                       Grid.Column="0"
                                       Grid.Row="0"
                                       Grid.ColumnSpan="3"
                                       HorizontalAlignment="Center"
                                       VerticalAlignment="Center"
                                       FontSize="16" />
                            <Button x:Name="PrevPage"
                                    Grid.Column="0"
                                    Grid.Row="1"
                                    IsEnabled="False"
                                    Content="&#xE100;"
                                    FontFamily="Segoe UI Symbol"
                                    FontSize="20"
                                    Click="BrowsePage_OnClick" />
                            <Button x:Name="NextPage"
                                    Grid.Column="2"
                                    Grid.Row="1"
                                    Content="&#xE101;"
                                    FontFamily="Segoe UI Symbol"
                                    FontSize="20"
                                    Click="BrowsePage_OnClick" />
                        </Grid>
                    </StackPanel>
                </Grid>
            </PivotItem>
        </Pivot>
        <StackPanel VerticalAlignment="Top"
                    Grid.Row="1">
            <ProgressBar Margin="10"
                         x:Name="NetworkActivity"
                         IsIndeterminate="False"
                         Height="15" />
        </StackPanel>
    </Grid>
</Page>

In the XAML markup, we’re creating a Pivot control with two pivot items, first for posting the sensor readings and second for viewing the pages of sensor readings.

In the first pivot item, a grid control lays out a couple of text blocks to display a title and a large random sensor reading, finishing with a button to submit the reading. In the second pivot item, I tried to be a little bit artistic (and evidently I’m not good at it but at least I tried Nerd smile) by creating a rounded rectangle with an ellipse inside it enclosing the sensor reading. A pager control follows the visually appealing rendition that displays information about the number of records out of total and the number of page we’re on, succeeded by a row containing previous and next buttons. A progress bar is included that is outside the pivot control (to track network activity) and thus is available on both pages.

Okay…let’s now wire up the event handlers to enliven the dead UI elements! Open MainPage.xaml.cs and copy/paste the following code:

 namespace SensorDataTracker.WindowsPhone.Client
{
    using SensorDataTracker.CommonLogic.Models;
    using SensorDataTracker.CommonLogic.Services;
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using Windows.UI.Xaml;
    using Windows.UI.Xaml.Controls;
    using Windows.UI.Xaml.Navigation;
    public sealed partial class MainPage : Page
    {
        private readonly Random _readingGenerator;
        private const int MaxNum = 10000;
        private int _currentPage = 1;
        private const double PageSize = 5;
        private readonly IDictionary<string, int> _buttonPageActions = new Dictionary<string, int>
            {
                {"PrevPage",-1},
                {"NextPage",1},
            };
        private readonly SensorDataTrackerService _sensorService = new SensorDataTrackerService(new HttpService());
        private string _pagerFormatString;
        public MainPage()
        {
            this.InitializeComponent();
            this.NavigationCacheMode = NavigationCacheMode.Required;
            _readingGenerator = new Random();
        }
        private void PivotContainer_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (PivotContainer.SelectedIndex == 1)
            {
                this.LoadSensorReadings();
            }
            else
            {
                this.ShowNextReading();
            }
        }
        private async void LoadSensorReadings()
        {
            NetworkActivity.IsIndeterminate = true;
            var sensorReadings = await _sensorService.GetSensorReadingsAsync(_currentPage);
            this.UpdateControls(sensorReadings);
            NetworkActivity.IsIndeterminate = false;
        }
        private void ShowNextReading()
        {
            TextSensorReadingId.Text = string.Format(CultureInfo.CurrentCulture, "{0}", _readingGenerator.Next(0, MaxNum));
        }
        private async void SubmitButton_OnClick(object sender, RoutedEventArgs e)
        {
            var sensorReading = new SensorReading()
                                {
                                    Id = Convert.ToInt32(TextSensorReadingId.Text),
                                    CreateDate = DateTime.Now
                                };
            NetworkActivity.IsIndeterminate = true;
            await _sensorService.PostSensorReadingAsync(sensorReading);
            this.ShowNextReading();
            NetworkActivity.IsIndeterminate = false;
        }
        private void BrowsePage_OnClick(object sender, RoutedEventArgs e)
        {
            var button = (Button)sender;
            var key = button.Name;
            var increment = this._buttonPageActions.ContainsKey(key) ? this._buttonPageActions[key] : 0;
            _currentPage += increment;
            LoadSensorReadings();
        }
        private void UpdateControls(ResultPage<SensorReading> resultPage )
        {
            var totalCount = Convert.ToInt32(resultPage.TotalCount);
            var totalNumberOfPages = (totalCount <= PageSize)
                ? 1
                : Math.Ceiling(totalCount / PageSize);
            if (string.IsNullOrWhiteSpace(_pagerFormatString)) _pagerFormatString = PageInfo.Text;
            PageInfo.Text = string.Format(CultureInfo.CurrentCulture,
                _pagerFormatString,
                resultPage.TotalCount,
                _currentPage,
                totalNumberOfPages);
            PrevPage.IsEnabled = _currentPage > 1;
            NextPage.IsEnabled = _currentPage < totalNumberOfPages;
            SensorReadingList.ItemsSource = resultPage.Results;
        }
    }
}

Let’s start dissecting the code from beginning. Fields include:

  1. A random reading generator that generates a random number under a specified MaxNum.
  2. Current page number tracker.
  3. Number of records to fetch per page indicated by PageSize.
  4. A dictionary to keep track of browse button clicks, –1 for previous and 1 for next.
  5. SensorDataTrackerService instance to invoke the service methods.

Event handler PivotContainer_OnSelectionChanged, handles the cycling between pivot items and based on the index either readings are fetched or a new reading is generated for submission.

LoadSensorReadings fetches the readings from the web API asynchronously and updates the controls including the progress indicator.

Let’s take a closer look at UpdateControls method:

         private void UpdateControls(ResultPage<SensorReading> resultPage )
        {
            var totalCount = Convert.ToInt32(resultPage.TotalCount);
            var totalNumberOfPages = (totalCount <= PageSize)
                ? 1
                : Math.Ceiling(totalCount / PageSize);
            if (string.IsNullOrWhiteSpace(_pagerFormatString)) _pagerFormatString = PageInfo.Text;
            PageInfo.Text = string.Format(CultureInfo.CurrentCulture,
                _pagerFormatString,
                resultPage.TotalCount,
                _currentPage,
                totalNumberOfPages);
            PrevPage.IsEnabled = _currentPage > 1;
            NextPage.IsEnabled = _currentPage < totalNumberOfPages;
            SensorReadingList.ItemsSource = resultPage.Results;
        }

UpdateControls gets a JSON deserialized ResultPage that holds SensorReading objects in the form of Results collection and a TotalCount of records, the latter being required to calculate the number of pages. TotalCount is divided by PageSize to obtain the number of pages if it’s greater than the latter else one page is assumed. Pager format string is initialized with the resource string PageInfo.Text and respective placeholders are replaced with total count, current page and total number of pages, e.g. Total: 12 – Showing page 1 of 3. Based on the current page and total number of pages, prev and next buttons are enabled or disabled using a simple calculation. Finally the Results collection is assigned as the source of items for ItemsControl declared in XAML.

Event handler SubmitButton_OnClick creates a new SensorReading with the random number as Id and current date as CreateDate that is posted asynchronously to the web API.

Event handler BrowsePage_OnClick handles the paging between sensor readings pages. First it determines which button was clicked based on the sender’s Name property, looks it up as a key in the dictionary and then uses the corresponding value to advance the current page forward or backward.

That pretty much sums up the Windows Phone 8.1 client functionality! There is an inherent issue with Windows Phone emulator. Since Windows Phone 8.1 emulator employs a virtual machine under the covers, it has the same IP address as the host machine - so all the references that use the localhost IP address end up in loopback. To get around this issue, you need to configure the IIS express or IIS server so that it may distinguish between the requests coming from the emulator and local machine. See this article on curah.microsoft.com for some possible workarounds. However on a corporate domain, the emulator appears as a separate network device that is not joined to the domain. As a result, you may also have to get an exception from your IT department before the emulator can connect to services that are running on the domain-joined development computer. Due to the aforementioned nuisance Angry smile I prefer to deploy my web API to Microsoft Azure Cloud which is trivial comparatively!

Let’s convert the web API that we created previously to an Azure Cloud project. Right click on SensorDataTracker project and choose Convert—>Convert to Windows Azure Cloud Service Project option:

image

A new cloud wrapper project gets created as shown below:

image

Right click on SensorDataTracker.Azure project and choose Publish option. The following dialog appears:

image

Choose your Azure subscription and a new or existing Cloud service to deploy to (in the next dialog shown below) and click on the Publish button. You may optionally save the target profile giving it a name of your choice or leave the defaults.

image

As you may’ve already noted, I used the service URL of the Azure deployed web API service in SensorDataTrackerService class. Set Windows Phone 8.1 client as the Startup project and click on emulator 8.1 option to deploy the package to Windows Phone 8.1 emulator and start the app:

image

When the emulator starts it appears as below:

image

Click on Submit Reading button. Once the reading is posted, a new random reading appears. Play around by posting several readings and then scroll to View Readings pivot item that should appear somewhat like below:

image

It has been a pretty long post but if you’ve carefully followed all the steps up till now, you should’ve a full fledged Windows Phone 8.1 client for your web API and well on your way towards developing the next one that is, well, not a rocket science to guess! Since we already have a nice separation of functionality in a PCL, we’ll look into extending our client base in the next post via a Windows Store client! As always your feedback is highly appreciated! Please don’t hesitate to point out any errors or omissions.


Related Posts:

  1. Writing a simple implementation of dependency injection in MVC 4 Web API with .NET Framework 4.5
  2. Writing BDD style unit tests to verify the MVC based Web API

Comments

  • Anonymous
    June 20, 2014
    Nice www.orangemediastudio.com

  • Anonymous
    June 20, 2014
    What this great post, very clear explanation as well acara the idea that I'm having, but not using a PCL, it will be difficult I can port my code to other platforms?

  • Anonymous
    July 27, 2014
    How to create post request and pass post data from windows phone i have used PostAsync but not working.

  • Anonymous
    July 28, 2014
    Rasik, Can you provide more details as to what you were trying to accomplish, the piece of code you used and the exception details?