Xamarin Forms Application For Home Automation
On the plane to the Xamarin Evolve conference last month I decided to get up to speed with the new Xamarin Forms technology. We've been using Xamarin for iOS and Android for some time now but Xamarin Forms is a relatively new addition to Xamarin's offerings that allows UI code to be shared between Android, iOS and Windows Phone and not just the data and service layers that we normally share.
Within the course of a single plane ride I was able to get the basics of my home automation application ported to Xamarin Forms, so now I can use it on Android or iOS. (Previously I had only done the Android version).
You can see the application at the right, with each room or device color-coded to show how recently it was triggered. The colors and timestamps update in real time and it polls the home automation API periodically to update the display too. You can also click on any light, sprinkler or other switched device to turn it on or off.
Xamarin Forms was very easy to use and had most of the capabilities I needed for this application. It's clearly still developing rapidly and the documentation and samples haven't quite caught up yet. It also supports XAML for UI design, but personally I prefer the code-first approach.
There were a couple of areas of the application that took slightly more effort. One was the real-time update of the 'time ago' and color values for each cell in the grid. For that I ended up using Reactive Extensions.
// A Subject\<T\> tracks when a view model is no longer needed private
Subject<bool> finished = new Subject<bool> ();
// A global clock is passed to each view model clock =
Observable.Interval (TimeSpan.FromSeconds (1)) .Where(x =>
this.IsVisible) .Select (x =\> DateTime.UtcNow);
// The view model refreshes the view on each tick until it is told to
finish clock.TakeUntil(finished).ObserveOn
(System.Threading.SynchronizationContext.Current).Subscribe(v => {
Refresh(v); } );
As you can see, Reactive Extensions provides a very nice way to express some fairly complex logic with ticks coming from a clock, being gated by whether the application is visible, and then again by whether the form is still in use, flipping back to the UI thread and then refreshing the cell.
The other slightly tricky piece of code was the method below to fetch Json from my home automation API. Every time you click in the UI it initiates a fetch to get the latest information for the object you selected. Clicking rapidly around the screen led to lengthy delays as each earlier click had to be dealt with before the latest one was handled and ultimately led to random crashes with errors relating to "_wapi_connect: error looking up socket handle".
The code below wraps `HttpClient` and implements a simple timeout, the ability to cancel pending requests and a mechanism to to limit concurrent calls by the mobile application to the web server.
/// <summary>
/// Utility to request Json from the web server
/// </summary>
public class JsonUtility<TRequestType, TResponseType>
{
static MediaTypeWithQualityHeaderValue jsonType =
MediaTypeWithQualityHeaderValue.Parse("application/json");
const string authType = "token";
private static JsonSerializerSettings jsonSerializerSettings = new
JsonSerializerSettings { DateFormatHandling =
DateFormatHandling.IsoDateFormat, DateParseHandling =
DateParseHandling.DateTimeOffset };
public static async Task<TResponseType> HttpGet(string url, string
auth = null) { return await HttpGet (url, CancellationToken.None, auth);
}
// Only allow two simultaneous requests to avoid a wapi_connect:
// error looking up socket handle" problem
private static SemaphoreSlim semaphore = new SemaphoreSlim(2);
public static async Task<TResponseType> HttpGet(string url,
CancellationToken cancellationToken, string auth = null)
{
try
{
cancellationToken.ThrowIfCancellationRequested (); // Check immediately
await semaphore.WaitAsync (cancellationToken);
// Make sure <2 already in progress
cancellationToken.ThrowIfCancellationRequested (); // Check after lock has been obtained
System.Diagnostics.Debug.WriteLine ("Fetching " + url);
string resultString;
using (var webClient = new HttpClient ())
{
webClient.Timeout = TimeSpan.FromSeconds(5);
webClient.DefaultRequestHeaders.Accept.Add (jsonType);
if (auth != null) webClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue (authType, auth);
using (var result = await webClient.GetAsync (url,
HttpCompletionOption.ResponseContentRead, cancellationToken))
{
resultString = await result.Content.ReadAsStringAsync (); } var resp =
JsonConvert.DeserializeObject<TResponseType> (resultString);
System.Diagnostics.Debug.WriteLine ("Fetched " + url);
if (resp == null) return return default(TResponseType);
else return
JsonConvert.DeserializeObject<TResponseType> (resultString,
jsonSerializerSettings); } }
finally { semaphore.Release (); } } }