Migrating the remaining blog posts
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 43 KiB |
@@ -0,0 +1,30 @@
|
||||
---
|
||||
author: "Jim Bennett"
|
||||
date: 2015-06-12T18:47:03Z
|
||||
description: ""
|
||||
draft: false
|
||||
slug: "a-little-less-conversation-a-little-more-ibeacons"
|
||||
title: "A little less conversation, a little more iBeacons!"
|
||||
|
||||
images:
|
||||
- /blogs/a-little-less-conversation-a-little-more-ibeacons/banner.png
|
||||
featured_image: banner.png
|
||||
---
|
||||
|
||||
|
||||
iBeacons are a cool technology that allows you to add location awareness to you apps, not based on your location on a map, but based on your proximity to iBeacons, be they at a fixed or moving point. They allow your apps to have an accurate understanding of where you are both inside and out, from sending you offers when you are in a shop to knowing where you are in a sports stadium so you can be directed to your seat. iBeacons are a big up and coming thing - even Facebook are now sending them out to businesses so their app can become location aware. Companies like Estimote are creating and extending iBeacons to bring even more amazing features to the developer.
|
||||
|
||||
<p align="center">
|
||||

|
||||
</p>
|
||||
|
||||
On the 8th July 2015 I'll be hosting an iBeacons mini-hack using Xamarin and Estimote beacons, at the Birmingham Xamarin Mobile Cross Platform User Group in Birmingham, UK. In this mini-hack I’ll be talking briefly about this technology and a little about it’s uses, then we’ll hit the code. Bring a Mac laptop, iPhone and lightning cable, and team up with others in building a treasure hunt app using the Estimote iOS Xamarin component and Estimote iBeacons. There will be a prize of an Estimote developer kit or two (containing 3 iBeacons, worth $100 each) for the team that can complete the treasure hunt the fastest.
|
||||
|
||||
<p align="center">
|
||||

|
||||
</p>
|
||||
|
||||
For more details and to join in visit the [Meetup page](http://www.meetup.com/Birmingham-Xamarin-Mobile-Cross-Platform-User-Group/events/223173916/).
|
||||
|
||||
Note - iBeacons are originally an Apple technology. Although Estimote do have limited support for Android we will be focusing purely on their iOS Xamarin component. The iPhone simulator also does not support iBeacons, so you will need an actual device to code against. I will however be splitting the attendees into teams so even if you don't have an iPhone developer setup come along anyway and I'll make sure you are teamed up with someone who does.
|
||||
|
||||
|
After Width: | Height: | Size: 186 KiB |
@@ -0,0 +1,37 @@
|
||||
---
|
||||
author: "Jim Bennett"
|
||||
categories: ["Technology", "try .net", "dotnet new", "vscode"]
|
||||
date: 2019-06-11T10:31:32Z
|
||||
description: ""
|
||||
draft: false
|
||||
slug: "adding-try-net-to-vscode-launch-json"
|
||||
summary: "Learn how to launch Try .NET from VS Code using the debug menu instead of the terminal."
|
||||
tags: ["Technology", "try .net", "dotnet new", "vscode"]
|
||||
title: "Adding Try .NET to VSCode launch.json"
|
||||
|
||||
images:
|
||||
- /blogs/adding-try-net-to-vscode-launch-json/banner.png
|
||||
featured_image: banner.png
|
||||
---
|
||||
|
||||
|
||||
I've been playing a lot with [Try .NET](https://github.com/dotnet/try?WT.mc_id=trydotnet-blog-jabenn). I even blogged about it recently - [[jimbobbennett.io/trying-out-try-net](/blogs/trying-out-try-net/)](/blogs/trying-out-try-net/).
|
||||
|
||||
One thing that was beginning to annoy me slightly was having to constantly launch the terminal and type `dotnet try` to test out what I was working in. My life would be infinitely improved (not really), if I could run it via **F5** or the debug menu/tab instead of the terminal.
|
||||
|
||||
Turns out its pretty easy to do - just add a new entry to your `launch.json` file either directly from the file in the `.vscode` folder, or adding a configuration using the debug menu.
|
||||
|
||||
Add this to it:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Try .NET",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args":"try"
|
||||
}
|
||||
```
|
||||
|
||||
That's all you need. Now you can run `dotnet try` just by pressing **F5**.
|
||||
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 186 KiB |
@@ -0,0 +1,88 @@
|
||||
---
|
||||
author: "Jim Bennett"
|
||||
categories: ["xamarin", "xamarin.forms", "Technology", "UI", "animation"]
|
||||
date: 2017-01-05T08:43:11Z
|
||||
description: ""
|
||||
draft: false
|
||||
slug: "animating-xamarin-forms-progress-bars"
|
||||
tags: ["xamarin", "xamarin.forms", "Technology", "UI", "animation"]
|
||||
title: "Animating Xamarin Forms progress bars"
|
||||
|
||||
images:
|
||||
- /blogs/animating-xamarin-forms-progress-bars/banner.png
|
||||
featured_image: banner.png
|
||||
---
|
||||
|
||||
|
||||
Don't you just hate boring UIs? With no animations to bring them to life?
|
||||
|
||||
Me too! Especially progress bars - they increment or decrement instantly giving a dull UI.
|
||||
|
||||
<div class="image-div" style="max-width: 300px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
As you can see from the image above, as the progress bar changes value, the UI updates instantly and it looks a bit dull. What would be nicer is if the progress bar smoothly animated its progress.
|
||||
|
||||
Out of the box Xamarin.Forms provides a method to do this - an extension method called `ProgressTo`. You can call this code from inside your UI to smoothly animate the progress value to whatever value you like. This is great, but it's not ideal as it relies on the animation happening in the view, so the view needs to know what the value is to animate to. I'm a big fan of MVVM (true - I even [written a book on how to use MVVM to build Xamarin apps](http://xam.jbb.io)). The progress value should be in a view model, but then we'd have to wire up a number of UI components to watch view models for changes then start the animation. Ideally it would be better to have a bindable property that we could use to trigger this animation.
|
||||
|
||||
We could get this bindable property by creating our own control derived from ProgressBar that has a property for the animation, but that involves a custom control. What would be even better is if we could use the existing progress bar and somehow add a property to it that we can bind to and when the value changes the progress animates to the new value. Happily for us, Xamarin Forms supports this in the form of [Attached Properties](https://docs.microsoft.com/en-gb/xamarin/xamarin-forms/xaml/attached-properties/?WT.mc_id=formsanimations-blog-jabenn).
|
||||
|
||||
Attached properties are bindable properties, just like the ones you would create in your own controls, except that they can be 'attached' to any other control. You define them on one class, and attach them to another. Using these we can create a property on a utility class that is attached to `ProgressBar`. These properties don't directly change anything on the class they are attached to, instead you can hook into the property change mechanism to do whatever you need to do.
|
||||
|
||||
For example, if we wanted an animated progress bar we could create a new attached property called `AnimatedProgress` attached to `ProgressBar`, and every time the value changes we could tell the progress bar to change it's progress value via the `ProgressTo` extension method:
|
||||
|
||||
```
|
||||
public static class AttachedProperties
|
||||
{
|
||||
public static BindableProperty AnimatedProgressProperty =
|
||||
BindableProperty.CreateAttached("AnimatedProgress",
|
||||
typeof(double),
|
||||
typeof(ProgressBar),
|
||||
0.0d,
|
||||
BindingMode.OneWay,
|
||||
propertyChanged: (b, o, n) =>
|
||||
ProgressBarProgressChanged((ProgressBar)b, (double)n));
|
||||
|
||||
private static void ProgressBarProgressChanged(ProgressBar progressBar, double progress)
|
||||
{
|
||||
ViewExtensions.CancelAnimations(progressBar);
|
||||
progressBar.ProgressTo((double)progress, 800, Easing.SinOut);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this code we are defining a static helper class with the static attached property. This property is called `"AnimatedProgress"`, has a `double` value, is attached to `ProgressBar`, has a default value of `0.0`. It defaults to bind one way, and this is fine as users cannot interact with a progress bar to change this value. The cool part is the `propertyChanged` action, this calls a method that animates the progress bar to the new value every time it changes.
|
||||
|
||||
Once we've defined this property we can attach it to any progress bar we want, using XAML or code behind. The XAML way to set it is:
|
||||
|
||||
```
|
||||
?xml version="1.0" encoding="utf-8"?>
|
||||
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:local="clr-namespace:AnimatedProgress"
|
||||
x:Class="AnimatedProgress.AnimatedProgressPage">
|
||||
<StackLayout Padding="20,100" Spacing="40">
|
||||
<ProgressBar local:AttachedProperties.AnimatedProgress="{Binding Progress}"/>
|
||||
<Entry Text="{Binding ProgressPercent}"/>
|
||||
</StackLayout>
|
||||
</ContentPage>
|
||||
```
|
||||
|
||||
In this XAML we define the `local` XML namespace pointing to our local C# namespace, then bind the property using `local:AttachedProperties.AnimatedProgress="{Binding Progress}"`. This code assumes you have a view model for the page with a property called `Progress`.
|
||||
|
||||
You can also set this in code:
|
||||
|
||||
```
|
||||
MyProgressBar.SetBinding(AttachedProperties.AnimatedProgressProperty,
|
||||
"Progress");
|
||||
```
|
||||
|
||||
Once this is wired up we get a lovely animation when we change our progress!
|
||||
|
||||

|
||||
|
||||
You can find the code for this post on my [GitHub Repo](https://github.com/jimbobbennett/AnimatedProgress).
|
||||
|
||||
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 186 KiB |
@@ -0,0 +1,163 @@
|
||||
---
|
||||
author: "Jim Bennett"
|
||||
categories: ["technology", "Spatial", "Maps", "azure", "REST"]
|
||||
date: 2019-09-10T00:08:58Z
|
||||
description: ""
|
||||
draft: false
|
||||
images:
|
||||
- /blogs/are-you-where-you-should-be-checking-geofences-using-azure-maps/banner.png
|
||||
featured_image: banner.png
|
||||
slug: "are-you-where-you-should-be-checking-geofences-using-azure-maps"
|
||||
summary: "A Geofence is a virtual boundary defined using an area on a map. Azure Maps has tools for checking if a coordinate is inside that Geofence. This post looks at how to do these checks."
|
||||
tags: ["technology", "Spatial", "Maps", "azure", "REST"]
|
||||
title: "Are you where you should be? Checking Geofences using Azure Maps"
|
||||
|
||||
images:
|
||||
- /blogs/are-you-where-you-should-be-checking-geofences-using-azure-maps/banner.png
|
||||
featured_image: banner.png
|
||||
---
|
||||
|
||||
|
||||
**A Geofence is a virtual boundary defined using an area on a map. Azure Maps has tools for checking if a coordinate is inside that Geofence. This post looks at how to do these checks.**
|
||||
|
||||
{{< figure src="stephen-monroe-yg8Cz-i5U30-unsplash.jpg" >}}
|
||||
|
||||
There are many use cases for tracking where an item is, and if it moves into or out of a defined location. One great example is legal compliance - for example has an asset crossed an international or state boundary, is a vehicle on roads that require road tax to be paid (for example [New Zealand's RUC](https://www.nzta.govt.nz/vehicles/licensing-rego/road-user-charges/)) or is a ship near a port.
|
||||
|
||||
One use case I've been thinking about recently is wildlife tracking. I'm building a sample app that can be used to track animal sightings including a GPS device that could be attached to an animal to provide 24/7 tracking (I'm not really going to put it on a real animal, this is just for demo purposes, so will be using a plushie bear). As well as knowing where an animal is, it would also be good to have an alert if the animal enters certain areas, for example if a bear goes near a center of population. This post shows how to create and check geofences using Azure Maps.
|
||||
|
||||
## Getting started
|
||||
|
||||
* Start by signing up for Azure if you don't have an account:If you are a student, sign up at [azure.microsoft.com/free/students](https://azure.microsoft.com/free/students/?WT.mc_id=azuremaps-blog-jabenn) to get US$100 of free credit and free services for a year.Otherwise sign up at [azure.microsoft.com/free](https://azure.microsoft.com/free/?WT.mc_id=azuremaps-blog-jabenn) to get US$200 of credit for 30 days and a year of free services.
|
||||
* Sign into Azure and create an Azure Maps resource by following [this link](https://ms.portal.azure.com/?WT.mc_id=azuremaps-blog-jabenn%2F#create/Microsoft.Maps).
|
||||
|
||||
{{< figure src="2019-09-06_16-51-33.png" >}}
|
||||
|
||||
## Defining a Geofence
|
||||
|
||||
Once you have an Azure Maps resource you need to define a geofence. These are defined using GeoJSON - a JSON document designed for geographic information. TO create a simple geofence for the Seattle/Redmond area, you would define it like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"geometryId": "1"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[
|
||||
-122.41864, 47.54548
|
||||
],
|
||||
[
|
||||
-122.41864, 47.70502
|
||||
],
|
||||
[
|
||||
-122.00867, 47.70502
|
||||
],
|
||||
[
|
||||
-122.00867, 47.54548
|
||||
],
|
||||
[
|
||||
-122.41864, 47.54548
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This defines a geometric feature, that is a polygon using the coordinates given, with the last coordinate being the same as the first to close the shape - in this case a rectangle.
|
||||
|
||||
{{< figure src="response.png" >}}
|
||||
|
||||
You can do a lot with GeoJson, and you can read more in the [GeoJSON docs](https://docs.microsoft.com/azure/azure-maps/geofence-geojson/?WT.mc_id=azuremaps-blog-jabenn).
|
||||
|
||||
To set the geofence, you need to send this JSON to a call to the Azure Maps REST API. There aren't any SDKs available for this service yet, but hopefully should be some soon.
|
||||
|
||||
Uploading a geofence needs a couple of calls - you make one call to upload which returns an endpoint that stores the data, then you call that endpoint to get back an Id you use to access the geofence from later calls.
|
||||
|
||||
The first REST call is:
|
||||
|
||||
```sh
|
||||
https://atlas.microsoft.com/mapData/upload?
|
||||
subscription-key={subscription-key}
|
||||
&api-version=1.0
|
||||
&dataFormat=geojson
|
||||
```
|
||||
|
||||
You will need to replace `{subscription-key}` with your primary or secondary key from the _Shared Key Authentication_ section of the _Settings -> Authentication_ tab in the map blade in the Azure portal.
|
||||
|
||||
{{< figure src="2019-09-09_16-13-17.png" >}}
|
||||
|
||||
The body of the request will need to be set to `application/json` and contain the GeoJSON for your geofence.
|
||||
|
||||
When you call this end point you should get back a status of **202 - Accepted**. In the headers will be a `Location` containing an end point to call to get the Id of the geofence.
|
||||
|
||||
{{< figure src="2019-09-09_16-23-18.png" >}}
|
||||
|
||||
To get the Id, call the `Location` endpoint, adding your subscription key as an additional parameter:
|
||||
|
||||
```sh
|
||||
https://atlas.microsoft.com/mapData/{location}/status?api-version=1.0
|
||||
&subscription-key={subscription-key}
|
||||
```
|
||||
|
||||
This will return a status of **201 - Created**, with a body containing a JSON document with a single field - `"udId"`, the unique Id of the geofence.
|
||||
|
||||
## Testing if a coordinate is in the geofence
|
||||
|
||||
The purpose of a geofence is to know when something goes in or out of it. This is done by making a call to another REST API, giving it the udId of the geofence, and the latitude and longitude of the coordinate you want to check.
|
||||
|
||||
```sh
|
||||
https://atlas.microsoft.com/spatial/geofence/json
|
||||
?subscription-key={subscription-key}
|
||||
&api-version=1.0
|
||||
&udId={udId}
|
||||
&lat={latitude}
|
||||
&lon=-{longitude}
|
||||
&deviceId={device-id}
|
||||
```
|
||||
|
||||
In the above call replace `{subscription-key}` with your Azure Maps shared access key, `{udId}` with the udId from the second REST call, `{latitude}` and `{longitude}` with the latitude and longitude of the coordinate you want to check. The final parameter you need to set is `{device-id}`, and this needs to be set to an Id for the device that the coordinates come from. This device id doesn't seem to be used for anything, so can be set to whatever you want, but it must be set otherwise you get an error.
|
||||
|
||||
The result of this call is a JSON document containing details about the location of the coordinates relative to the geofence.
|
||||
|
||||
```json
|
||||
{
|
||||
"geometries": [
|
||||
{
|
||||
"deviceId": "device",
|
||||
"udId": "xxxxxxxxx",
|
||||
"geometryId": "1",
|
||||
"distance": -999.0,
|
||||
"nearestLat": 47.54548,
|
||||
"nearestLon": -122.2
|
||||
}
|
||||
],
|
||||
"expiredGeofenceGeometryId": [],
|
||||
"invalidPeriodGeofenceGeometryId": []
|
||||
}
|
||||
```
|
||||
|
||||
This JSON document returns the device and udId values passed in, useful if you want to pass this on to some form of notification system. It also gives a distance and the nearest latitude and longitude.
|
||||
|
||||
One thing to be aware of is GPS is not always exact - although calculations can be exact, coordinates are not always totally accurate. GPS sensors are at best accurate to a few meters, so a device could be inside the geofence but detected outside. The `distance` value takes this into consideration:
|
||||
|
||||
* Positive distance values are outside the geofence, negative are inside.
|
||||
* If the location is within a short distance (default of 50m and referred to as a _search buffer)_ of the edge of the geofence, the `distance` will be the distance to the geofence in meters.
|
||||
* If the device is further away from the edge, the value will be `999` if outside, `-999` inside.
|
||||
* The search buffer can be configured in the REST call by setting the `searchBuffer` parameter to a value in meters from `0` to `500`. If this is not set, the default of 50m is used.
|
||||
|
||||
When triggering alerts based off being close to the edge of a geofence you should take other information into consideration - for example if the GSP sensor is on a road going device, is the nearest road inside the geofence?
|
||||
|
||||
The `nearestLat` and `nearestLon` values give the point on the geofence that is nearest to the device - useful for example if you are tracking animals in an enclosed space, this might be where there is hole in the fence!
|
||||
|
||||
# Learn more
|
||||
|
||||
If you want to learn more, check out these links:
|
||||
|
||||
* [Azure maps docs](https://docs.microsoft.com/azure/azure-maps/?WT.mc_id=azuremaps-blog-jabenn)
|
||||
* [Azure Maps REST API docs](https://docs.microsoft.com/rest/api/maps/?WT.mc_id=azuremaps-blog-jabenn)
|
||||
|
||||
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 186 KiB |
@@ -0,0 +1,190 @@
|
||||
---
|
||||
author: "Jim Bennett"
|
||||
categories: ["xamarin", "xamarin.forms", "technology", "azure", "facebook", "authentication", "functions"]
|
||||
date: 2017-11-17T23:54:08Z
|
||||
description: ""
|
||||
draft: false
|
||||
images:
|
||||
- /blogs/authenticating-your-xamarin-app-with-azure-and-facebook/banner.jpg
|
||||
featured_image: banner.jpg
|
||||
slug: "authenticating-your-xamarin-app-with-azure-and-facebook"
|
||||
tags: ["xamarin", "xamarin.forms", "technology", "azure", "facebook", "authentication", "functions"]
|
||||
title: "Getting a users Facebook profile after Authenticating your Xamarin app with Azure"
|
||||
|
||||
images:
|
||||
- /blogs/authenticating-your-xamarin-app-with-azure-and-facebook/banner.png
|
||||
featured_image: banner.png
|
||||
---
|
||||
|
||||
|
||||
I've recently been looking at the authentication offered by Azure for use in a mobile app, specifically using social auth providers like Facebook to allow a user to sign up for my app. The auth setup is incredibly easy, with Azure taking care of a lot of the hard stuff. There are also loads of great docs on how to get it set up - including the Microsoft docs available here - [https://docs.microsoft.com/en-us/azure/app-service-mobile/app-service-mobile-xamarin-forms-get-started-users](https://docs.microsoft.com/en-us/azure/app-service-mobile/app-service-mobile-xamarin-forms-get-started-users?wt.mc_id=toyidentifier-blog-jabenn).
|
||||
|
||||
The whole login flow is very well documented, so I won't repeat what's in the previous link. Just follow the instructions in that link and you can use your Xamarin app to authenticate with Facebook and get back a logged in user.
|
||||
|
||||
What does seem to be missing from the docs though, is what happens next. When you log in using the `MobileServiceClient.LoginAsync` method you get back an access token, and that's it. So what is this access token, and how can it be used to access the users personal information, such as Facebook photo or friends? That's what we are going to look at in this post.
|
||||
|
||||
When you make a call to `LoginAsync`, the SDK opens a web view pointing to your app service, which in turn redirects to the Facebook login. When you log in, the web view redirects to your app service, which redirects back to your app. Along the way, the Azure app service picks up an access token from Facebook, which it keeps hold of, and returns you an access token from Azure.
|
||||
|
||||
<div class="image-div" style="max-width: 600px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
<br>
|
||||
|
||||
Your app doesn't get the Facebook access token, just an Azure one. Instead your app service keeps hold of the Facebook access token. This I imagine is intentional - your app can authenticate with multiple providers at the same time, for example linking to both a Facebook and Twitter account. Instead of returning access tokens to your mobile app for all of these providers, it returns just one, and this can be used to extract all the information you require via an auth service built into the Azure app service.
|
||||
|
||||
This auth service is available from the `https://<my azure website>/.auth/me` end point on your app service, passing the access token as a `X-ZUMO-AUTH` header. This returns a JSON document containing information from all social auth providers that the user has signed in with, including the access tokens, as well as some basic personal information such as first and last name. The Facebook access token that comes back can then be used with the Facebook Graph API to download whatever details we've granted to the app, such as our profile pic or friends list.
|
||||
|
||||
From my mobile app I want to download the users Facebook profile, but I don't want to make multiple server calls to do it. From my app I could call the `/.auth/me` endpoint, get the access token, then make another call to the Facebook graph, but it might be better to do it in one call, handling all the steps server side (including the logic around which social provider to use if more than one was provided). By doing this server side I can also cache information in my app service, maybe for later processing. The easiest way to do this is via an Azure Function.
|
||||
|
||||
<div class="image-div" style="max-width: 300px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
If you've been living under a rock for the past few years you may have missed out on the rise of serverless, but I imagine most of you are familiar with the concept. Azure functions are simple functions that are run in response to a trigger, such as a web request, a timer or a message on a queue. These run on someone else's hardware (hence the same serverless - you don't have to provision a server to run them on, someone else does it for you), and you only pay for the CPU/RAM usage, not a monthly cost for hardware. This means they are cheap - potentially millions of calls for less than a cup of coffee. They are also infinitely scalable, if your function gets hit a bazillion times it will scale up automatically. If you want to learn more, check out [this Channel9 video from Jeremy Likness, a Cloud Developer Advocate at Microsoft.](https://channel9.msdn.com/events/Connect/2017/E102?term=azure%20functions)
|
||||
|
||||
|
||||
We can use a simple function to do all our server side processing for us. Log into the Azure portal and create a new Function App. Fill in all the normal details, making the name something simple to remember such as `<my app service name>_functions`. The hosting plan is a new option, specific to functions. You can choose to link it to an existing app service so that it will only use those resources (and not end up costing any more than the app service rate), or a consumption plan where you pay per usage - with a _very_ generous free tier including being able to run a million function calls before you start paying.
|
||||
|
||||
<div class="image-div" style="max-width: 300px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
<br>
|
||||
|
||||
Fill in all the details and click **Create**. Wait a couple of minutes for your function to be created then head to it. Select the functions node in the tree under your functions app, then select **+ New Function**. From here you can choose from a set of function templates with different triggers and languages. We want a function that uses an Http trigger so that we can call it from our mobile app, and I'm using C# but you can use F# or Javascript if you prefer. Select **HttpTrigger - C#**, give your function a name such as `GetUserDetails`, set the authorization level to anonymous (I'll look at security more in a later blog post), then click **Create**.
|
||||
|
||||
When created you will see your new function, which is essentially a C# script file that you can edit in your browser, with one method in it - `public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)`. This method is passed the Http request used to make the call to your function, and returns an Http response with all the usual response stuff, like status code and any content. The function has a sample implementation that looks for a `name` field in a JSON body and returns a hello message to that name - the canonical Azure functions 'Hello World'. If you click the **Run** button, a pane will slide out from the right with a 'Test' tab that can be used to test the function. You'll see the response from the call in the 'Output' box, so run this function to see it in action, changing the name in the request body to see how it all works. The browser experience is limited though, so you can't debug your functions, just run them (but you can write to the `log` parameter to see output in the log box on the bottom).
|
||||
|
||||
The first thing our function needs to do is to call the auth endpoint to download the user details including access tokens for our social providers. To do this, it needs the access token from our mobile app, so we'll assume this will be passed as an Http header called `ACCESS_TOKEN`. Delete the code inside the function, and start by adding the line below to read the access token from the headers:
|
||||
|
||||
```
|
||||
var accessToken = req.Headers.GetValues("ACCESS_TOKEN").FirstOrDefault();
|
||||
```
|
||||
|
||||
Now we have the token, we need to pass it to our auth end point using the standard C# HttpClient. Add the code below, changing `<my site>` to the address of your Azure app service (this is your app service used for authentication, **NOT** your Azure function service):
|
||||
|
||||
```
|
||||
var client = new HttpClient();
|
||||
client.DefaultRequestHeaders.Add("X-ZUMO-AUTH", accessToken);
|
||||
var meResponse = await client.GetAsync("https://<my site>/.auth/me");
|
||||
var content = await meResponse.Content.ReadAsStringAsync();
|
||||
```
|
||||
|
||||
This code creates a new HttpClient, adds a `X-ZUMO-AUTH` header using the access token from the headers, and downloads the content. To see this in action, run your Xamarin mobile app through a debugger, log in using facebook, then copy the value of the `MobileServiceAuthenticationToken` property on the `MobileServiceUser` returned by the call to `LoginAsync`. From the Azure function, go to the test tab, set the Http method to 'Get', and add a new `ACCESS_TOKEN` header with the value set to the `MobileServiceAuthenticationToken` property. Finally add some code to the end of the function to return the auth response:
|
||||
|
||||
```
|
||||
var response = req.CreateResponse(HttpStatusCode.OK);
|
||||
response.Content = new StringContent(content, System.Text.Encoding.UTF8, "application/json");
|
||||
return response;
|
||||
```
|
||||
|
||||
If you now run your function, you will see the response from the auth endpoint in the output window as JSON. If you tidy this up, such as using the Prettify JSON extension in VS Code you will see it is an array of objects that contain information for each social auth provider the user is authenticated against, with fields such as the `provider_name`, a list of `user_claims`, which is assorted data about the user relevant to the provider, such as name, gender and location. It also contains a field called `access_token`, which is the provider specific access token that we can use to access the Facebook graph API.
|
||||
|
||||
We can get this field from the JSON using our old friend Json.Net. Azure functions have a few NuGet packages that are always available, and one of these is Json.Net. To use it we have to start by referencing the assembly, then add a `using` directive to the top of the function:
|
||||
|
||||
```csharp
|
||||
# r "Newtonsoft.Json"
|
||||
using Newtonsoft.Json.Linq;
|
||||
```
|
||||
|
||||
We can then use it to parse out the Facebook access token:
|
||||
|
||||
```
|
||||
var fbAccessToken = JArray.Parse(content)[0]["access_token"].ToString();
|
||||
```
|
||||
|
||||
If you want to test this out, change the content of the response to show the `fbAccessToken` instead of `content` and run the function. You'll then see a nice long access token string in the output.
|
||||
|
||||
Once we have this access token it can be used to query the Facebook Graph API. You can read the [docs on the Graph API here](https://developers.facebook.com/docs/graph-api) and try it out using their [Graph API explorer](https://developers.facebook.com/tools/explorer).
|
||||
|
||||
Essentially it is an API that you make GET requests to, passing a query string defining the fields you are interested in, and using the access token for bearer authorization. Create a new `HttpClient` and set the bearer authorization using the `Authorization` header and a value of `Bearer <facebook_access_token>` as shown below:
|
||||
|
||||
```
|
||||
var graphClient = new HttpClient();
|
||||
graphClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {fbAccessToken}");
|
||||
```
|
||||
|
||||
Then we can make a call to the Graph API, in this case loading the users first name, last name and profile picture. The fields we want are passed to the graph API call as a comma-separate list passed as a query parameter:
|
||||
|
||||
```
|
||||
var graphResponse = await graphClient.GetAsync("https://graph.facebook.com/v2.11/me?fields=first_name,last_name,picture");
|
||||
var graphContent = await graphResponse.Content.ReadAsStringAsync();
|
||||
```
|
||||
|
||||
Finally change the response to return the graph content:
|
||||
|
||||
```
|
||||
response.Content = new StringContent(graphContent, System.Text.Encoding.UTF8, "application/json");
|
||||
```
|
||||
|
||||
Now if you run this, you will see the output from the Facebook Graph API, providing the URL of the users picture, their first name, last name and unique Facebook Id. A sanitized version is shown below:
|
||||
|
||||
```json
|
||||
{
|
||||
"picture": {
|
||||
"data": {
|
||||
"height": 50,
|
||||
"is_silhouette": false,
|
||||
"url": "<url of my facebook profile pic>",
|
||||
"width": 50
|
||||
}
|
||||
},
|
||||
"first_name": "Jim",
|
||||
"last_name": "Bennett",
|
||||
"id": "<my id>"
|
||||
}
|
||||
```
|
||||
|
||||
The basic flow is:
|
||||
<div class="image-div" style="max-width: 600px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
<br>
|
||||
|
||||
Now we have our function, we can call this from our Xamarin mobile app as soon as the user is logged in. Once we have the user object, we can call our Azure function as if it was any other REST API using the code below:
|
||||
|
||||
```
|
||||
var client = new HttpClient();
|
||||
client.DefaultRequestHeaders.Add("ACCESS_TOKEN", user.MobileServiceAuthenticationToken);
|
||||
var response = await client.GetAsync("https://<my function app>/api/GetUserDetails");
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
```
|
||||
|
||||
We pass the `MobileServiceAuthenticationToken` property from the user object as an Http header - this is the access token from our Azure app service. We then call the new function which is at `https://<my function app>/api/GetUserDetails`. The URL of your function app is `<the name you gave it>.azurewebsites.net`, unless you have decided to use a custom domain, and you can find this value by clicking on your function app in the tree in the Azure portal and looking at the details on the right.
|
||||
|
||||
The content that comes back is the JSON from Facebook, and you can convert this into useful data by building a simple class to represent the data and deserializing the JSON into it using Json.Net. The classes to use to deserialize are:
|
||||
|
||||
```
|
||||
public class FacebookPictureData
|
||||
{
|
||||
public string Url { get; set; }
|
||||
}
|
||||
public class FacebookPicture
|
||||
{
|
||||
public FacebookPictureData Data { get; set; }
|
||||
}
|
||||
public class FacebookDetails
|
||||
{
|
||||
[JsonProperty("first_name")]
|
||||
public string FirstName { get; set; }
|
||||
[JsonProperty("last_name")]
|
||||
public string LastName { get; set; }
|
||||
public FacebookPicture Picture { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
You can then deserialize the content from the function call using:
|
||||
|
||||
```
|
||||
var facebookData = JsonConvert.DeserializeObject<FacebookDetails>(content);
|
||||
```
|
||||
|
||||
Done - you now have the users first name, last name and a URL of their public Facebook picture that you can use as an image source for a Xamarin.Forms Image control. If you want more data from Facebook just add more fields to the Graph API call an the details class on your mobile app.
|
||||
|
||||
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
BIN
blog/content/blogs/azure-makers-series/2018-11-14_17-08-53.png
Normal file
|
After Width: | Height: | Size: 136 KiB |
BIN
blog/content/blogs/azure-makers-series/banner.png
Normal file
|
After Width: | Height: | Size: 186 KiB |
27
blog/content/blogs/azure-makers-series/index.md
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
author: "Jim Bennett"
|
||||
categories: ["Technology", "xamarin", "azure", "AI", "cognitive services", "azure free"]
|
||||
date: 2018-11-14T17:09:50Z
|
||||
description: ""
|
||||
draft: false
|
||||
images:
|
||||
- /blogs/azure-makers-series/banner.png
|
||||
featured_image: banner.png
|
||||
slug: "azure-makers-series"
|
||||
tags: ["Technology", "xamarin", "azure", "AI", "cognitive services", "azure free"]
|
||||
title: "Azure makers series"
|
||||
|
||||
images:
|
||||
- /blogs/azure-makers-series/banner.png
|
||||
featured_image: banner.png
|
||||
---
|
||||
|
||||
|
||||
I recently recorded a quick video to talk about an app I was building using the [Azure free account](https://azure.microsoft.com/free/?WT.mc_id=azurefree-blog-jabenn). You can check it out here:
|
||||
|
||||
{{< youtube ejVtdb57Y5Y >}}
|
||||
|
||||
<br>
|
||||
|
||||
If you want to see the code, it's in my GitHub repo here - https://github.com/jimbobbennett/AzurePhotoSharer
|
||||
|
||||
|
After Width: | Height: | Size: 523 KiB |
|
After Width: | Height: | Size: 186 KiB |
@@ -0,0 +1,60 @@
|
||||
---
|
||||
author: "Jim Bennett"
|
||||
categories: ["Technology", "xamarin", "xamarin.ios", "xamarin.android", "xamarin in action", "mvvmcross", "mvvm"]
|
||||
date: 2017-02-24T17:55:54Z
|
||||
description: ""
|
||||
draft: false
|
||||
images:
|
||||
- /blogs/be-quick-50-off-xamarin-in-action-for-one-week/banner.png
|
||||
featured_image: banner.png
|
||||
slug: "be-quick-50-off-xamarin-in-action-for-one-week"
|
||||
tags: ["Technology", "xamarin", "xamarin.ios", "xamarin.android", "xamarin in action", "mvvmcross", "mvvm"]
|
||||
title: "Be quick - 50% off Xamarin In Action for one week"
|
||||
|
||||
images:
|
||||
- /blogs/be-quick-50-off-xamarin-in-action-for-one-week/banner.png
|
||||
featured_image: banner.png
|
||||
---
|
||||
|
||||
|
||||
The MEAP of my book, Xamarin In Action, has just been updated to include a new chapter covering building cross-platform view models. This chapter talks about how you can use view models to increase the amount of cross-platform, unit testable code in your Xamarin app by moving UI logic away from the platform specific layers down into your cross-platform code.
|
||||
|
||||
To celebrate you can get 50% off my book for one week only. Head to http://xam.jbb.io and use the code *mlbennett*.
|
||||
|
||||
Not sure if this is the book for you? Well if you are building or plan to build Xamarin apps then yes, this is the book for you. Rather than being a dry reference book that replicates what is in the on-line docs, this instead teaches you how to build and ship production quality Xamarin apps from idea to the store. I teaches MVVM as a way to ramp up the amount of cross-platform code in your app - after all large amounts of code sharing is the killer feature of Xamarin!
|
||||
|
||||
This book is still being written, so if you buy it now you get the first 8 chapters, with new chapters as they are being written, and you can get involved with making the book better by giving feedback and suggesting improvements.
|
||||
|
||||
You can download the first chapter for free from [here](https://manning-content.s3.amazonaws.com/download/8/a45b766-0a46-417f-8afa-724107f1c415/Bennett_Xamarin_MEAP_V05_ch1.pdf). You can also read an excerpt all about MVVM on [DZONE](https://dzone.com/articles/mvvm-the-design-pattern-for-xamarin-apps), [Medium](https://medium.com/@jimbobbennett/mvvm-the-design-pattern-for-xamarin-apps-9781e60ef587#.ibzk4lehv) or [my own blog](/blogs/mvvm-the-design-pattern-for-xamarin-apps/) (whichever is your preference).
|
||||
|
||||
The current table of contents is:
|
||||
|
||||
###### Part 1 - Getting started with native cross-platform apps
|
||||
|
||||
1 [Introducing native cross-platform applications with Xamarin](https://manning-content.s3.amazonaws.com/download/8/a45b766-0a46-417f-8afa-724107f1c415/Bennett_Xamarin_MEAP_V05_ch1.pdf)
|
||||
2 Hello MVVM
|
||||
3 MVVM – a deeper dive
|
||||
4 Hello MVVM – a deeper dive
|
||||
5 What are we (a)waiting for? - An introduction to multithreaded code
|
||||
|
||||
###### Part 2 - From blank solution to an empty, working app
|
||||
|
||||
6 Designing MVVM cross-platform apps
|
||||
7 Building cross-platform models
|
||||
8 Building cross-platform view models
|
||||
9 Building platform specific views - Android
|
||||
10 Building platform specific views - iOS
|
||||
11 Running the app
|
||||
|
||||
###### Part 3 - From a working app to a production ready app
|
||||
|
||||
12 Testing the app using UITest
|
||||
13 Instrumentation and monitoring
|
||||
14 Deploying the app
|
||||
|
||||
###### Appendices
|
||||
|
||||
A1 What are design patterns anyway?
|
||||
A2 Using someone else's code
|
||||
A3 UI flows and threads for SquareRt and Countr
|
||||
|
||||
|
After Width: | Height: | Size: 150 KiB |
BIN
blog/content/blogs/binding-ios-libraries-in-xamarin/banner.png
Normal file
|
After Width: | Height: | Size: 186 KiB |
208
blog/content/blogs/binding-ios-libraries-in-xamarin/index.md
Normal file
@@ -0,0 +1,208 @@
|
||||
---
|
||||
author: "Jim Bennett"
|
||||
categories: ["iOS", "xamarin", "xamarin.ios", "xamarin.forms", "estimote", "ibeacon", "binding"]
|
||||
date: 2015-02-19T15:25:15Z
|
||||
description: ""
|
||||
draft: false
|
||||
slug: "binding-ios-libraries-in-xamarin"
|
||||
tags: ["iOS", "xamarin", "xamarin.ios", "xamarin.forms", "estimote", "ibeacon", "binding"]
|
||||
title: "Binding iOS libraries in Xamarin"
|
||||
|
||||
images:
|
||||
- /blogs/binding-ios-libraries-in-xamarin/banner.png
|
||||
featured_image: banner.png
|
||||
---
|
||||
|
||||
|
||||
Update: Since writing this, James Montemagno from Xamarin contacted me to say they are working on an Estimote component. This is now available on the Xamarin component store. Check out [his blog post here](http://blog.xamarin.com/adding-real-world-context-with-estimote-beacons-and-stickers/).
|
||||
|
||||
<hr/>
|
||||
|
||||
I've been playing around with iBeacons a lot recently. They seem a really cool technology with a load of interesting potential use cases. My iBeacon of choice is from [Estimote](http://estimote.com), I've got some of their beacons already and have a pack of stickers on pre-order. As well as providing the basic iBeacon functionality, they also provide a whole host of other features including accelerometers and temperature sensors.
|
||||
|
||||
To access all their beacon functionality, above and beyond the basics that are part of the Apple iBeacon standard, you need to use their SDK. The Apple CoreLocation API will do the basics - ranging, notifications when going in and out of range etc., but for full access you need their SDK. They helpfully provide it for iOS and Android, but not for Xamarin.
|
||||
|
||||
This is not a big deal though, as Xamarin provide an easy enough way to wrap the native SDK's. This post will focus on how to do it for iOS. I'll try to do it for Android later and blog about that separately.
|
||||
|
||||
##### Step 1 - Grab the code
|
||||
The SDK code is on the [Estimote GitHub site](https://github.com/Estimote/iOS-SDK). Clone this locally.
|
||||
|
||||
#### Step 2 - Create a binding project
|
||||
Xamarin studio has project types for binding APIs. Start by selecting File -> New Solution, and choosing 'iOS Binding Project' from the Unified API section of the iOS entry in the tree. Apple requires all new apps that are submitted to support 64-bit, so to support this in our apps we need to use the new Unified APIs.
|
||||
|
||||

|
||||
|
||||
This will create a project with a couple of files in it.
|
||||
|
||||
* `ApiDefinition.cs` - This file will end up containing the classes created by wrapping the API.
|
||||
* `StructsAndEnums.cs` - This file will contain any structus or enums that are created from the API.
|
||||
|
||||
We now need to add the actual library to the project, so do this by dragging it from Finder/Explorer into the project in Xamarin Studio. Drag the `iOS-SDK\EstimoteSDK\libEstimoteSDK.a` file into your project.
|
||||
One this is in the project you'll see it in the tree with an expander by it. Expanding this will show a new file `libEstimoteSDK.linkwith.cs`. This file contains the instructions on how to link the library, and I'll cover this in more detail later.
|
||||
|
||||
#### Step 3 - Generate the API
|
||||
Once we have the library in place, we need to generate the API code to call it. We could do it by hand, but that's far too much trouble. Instead we'll use [Objective Sharpie](http://developer.xamarin.com/guides/ios/advanced_topics/binding_objective-c/objective_sharpie/), a tool from Xamarin to create the code for you. It's currently a beta, but works fine for this SDK. I'm hoping one day it will be built into Xamarin Studio so creating binding projects is a one step thing.
|
||||
Objective Sharpie is a command line tool, you give it the header files that come with the library, tell it what SDK to use, which namespace etc. and it will do the magic.
|
||||
|
||||
In my case, I run the `sharpie` command it from the EstimoteSDK folder in the SDK github repository, using the following options:
|
||||
|
||||
```sh
|
||||
sharpie bind --namespace=Estimote --sdk=iphoneos8.1 headers/*.h -unified
|
||||
|
||||
```
|
||||
|
||||
The options are:
|
||||
|
||||
* `bind` - this tells it to bind the header files
|
||||
|
||||
* `--namespace=Estimote` - this is the defult namespace used in the created files.
|
||||
|
||||
* `--sdk=iphoneos8.1` - this tells it to use the iOS 8.1 SDK for code generation. You should always use the latest SDK.
|
||||
|
||||
* `headers/*.h` - this is the path to the header files you want to generate code for.
|
||||
|
||||
* `--unified` - this tells it to use the new Unified API for 64-bit support.
|
||||
|
||||
The code generated is in two files - `output.cs` and `output.enums.cs`. For some reason it doesn't output the files using the same naming convention as the binding projects (`ApiDefinition.cs` and `StructsAndEnums.cs`). It also doesn't add using directives to the files.
|
||||
|
||||
Once the code has been generated, simply copy the contents of `output.cs` into `ApiDefinition.cs` (keeping the namespaces) and `output.enums.cs` into `StructsAndEnums.cs`.
|
||||
|
||||
#### Step 4 - Building the code
|
||||
Not as easy as you may think! If you do a build, there will be errors. This is because the API uses names for parameters etc. that are reserved words in C#.
|
||||
|
||||
For example check out the ESTBeaconManager.Constructor method:
|
||||
|
||||
```cs
|
||||
IntPtr Constructor (ESTBeaconManagerDelegate delegate);
|
||||
```
|
||||
|
||||
This has a parameter `delegate`, which is a reserved word in C#. Luckily, the parameters can be renamed without any issues, so just rename this to `del` or another name of your chosing. You'll also have to do the same in `ESTBeaconUpdateInfo`.
|
||||
|
||||
After changing these, build again and you'll get more errors. These next errors are due to the interfaces in the iOS SDK deriving from `NSObject<NSCoding>` or `NSObject <NSCopying, NSCoding>`. The resulting interfaces created by Objective Sharpie derive from `NSCoding` or `NSCopying`, and an interface cannot derive from a concrete class. Luckily, we can just remove the all base classes, so `interface ESTBeaconVO : NSCoding` becomes `interface ESTBeaconVO`. Do this for all other interfaces that the compiler complains about.
|
||||
|
||||
Again, we build, and again, more errors. The next set are due to name casing being changed between iOS and Xamarin - namely `NSUUID` is `NSUuid`. A quick Replace all and these are fixed.
|
||||
|
||||
The next set of errors are due to missing usings. Objective Sharpie doesn't add any using directives to the generated code which is a shame. A right click -> Resolve -> using CoreLocation; on `CLProximity` and Resolve -> using CoreBluetooth; on `CBPeripheral` and the `ApiDefinition.cs` file will now build. You'll need to so Resolve -> using ObjCRuntime; on the `[Native]` attribute in `StructsAndEnums.cs` as well.
|
||||
|
||||
The code should now build.
|
||||
|
||||
#### Step 5 - Events instead of delegates.
|
||||
Objective-C has an eventing mechanism based on delegates - you create a class that implements a specific delegate protocol and pass it to another class, which then calls back into the methods of your delegate class when things happen. The code generated by Objective Sharpie follows this pattern as it is a one-to-one mapping to the Objective-C code. Luckily, binding projects are smart enough to be able to translate this into events, with the help of a few attributes to help guide it when it generates these events.
|
||||
|
||||
Working down through the code, the first one we come to is `ESTBeaconDelegate`, used by the `ESTBeacon` class. The delegate is used to allow the beacon to call back when the connection state changes, or if the accelerometer detects movement.
|
||||
|
||||
```cs
|
||||
[Protocol, Model]
|
||||
[BaseType (typeof (NSObject))]
|
||||
interface ESTBeaconDelegate {
|
||||
// ...
|
||||
}
|
||||
|
||||
[BaseType (typeof (NSObject))]
|
||||
interface ESTBeacon {
|
||||
[Export ("delegate", ArgumentSemantic.Weak)]
|
||||
[NullAllowed]
|
||||
NSObject WeakDelegate { get; set; }
|
||||
|
||||
[Wrap ("WeakDelegate")]
|
||||
ESTBeaconDelegate Delegate { get; set; }
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The code above has been trimmed down to just show the important stuff. The `ESTBeacon` class has properties to store the delegate - the two different properties and the attributes allow it to strongly type the delegate class that is passed in (Objective-C uses weakly typed delegates).
|
||||
|
||||
Rather than use a delegate, we want to use events. This is actually really easy, we mark the `ESTBeacon` class with an attribute to tell it which properties are used for delegates, and to tell it which delegate types to generate events for.
|
||||
|
||||
```cs
|
||||
[BaseType (typeof (NSObject),
|
||||
Delegates=new string [] { "WeakDelegate" },
|
||||
Events=new Type [] {typeof(ESTBeaconDelegate)})]
|
||||
interface ESTBeacon {
|
||||
// ...
|
||||
```
|
||||
|
||||
This code tells the compiler to generate events for the `ESTBeacon` class using the `ESTBeaconDelegate` to define the events, and the delegate is set in the `WeakDelegate` property. One of these days I'll decompile the code to see how it does all this, but I imagine it will create a private class the exposes the delegate and calls back into the `ESTBeacon` class to raise the events.
|
||||
|
||||
We also need to tell the compiler how to generate the event signatures for the delegate. This is also done using an attribute, one that provides the name of the event args class.
|
||||
|
||||
```cs
|
||||
interface ESTBeaconDelegate {
|
||||
|
||||
// @optional -(void)beaconConnectionDidSucceeded:(ESTBeacon *)beacon;
|
||||
[Export ("beaconConnectionDidSucceeded:")]
|
||||
void BeaconConnectionDidSucceeded (ESTBeacon beacon);
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
becomes:
|
||||
|
||||
```cs
|
||||
interface ESTBeaconDelegate {
|
||||
|
||||
// @optional -(void)beaconConnectionDidSucceeded:(ESTBeacon *)beacon;
|
||||
[Export ("beaconConnectionDidSucceeded:")]
|
||||
[EventArgs("BeaconConnectionDidSucceeded")]
|
||||
void BeaconConnectionDidSucceeded (ESTBeacon beacon);
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
When the event is created it has the signature:
|
||||
|
||||
```cs
|
||||
public event EventHandler<BeaconConnectionDidSucceededEventArgs> BeaconConnectionDidSucceeded;
|
||||
```
|
||||
|
||||
The class `BeaconConnectionDidSucceededEventArgs` is created and wraps all the parameters in the delegate call as properties - so it will have public property of type `ESTBeacon` named `Beacon`. Note that the attribute doesn't have `EventArgs` on the end of the name - this is added automatically by the compiler. If you do add it, you get an error:
|
||||
|
||||
```
|
||||
BTOUCHTASK: error BI1005: btouch: EventArgs in
|
||||
EstimoteBinding.ESTBeaconDelegate.BeaconConnectionDidSucceeded attribute should not include
|
||||
the text 'EventArgs' at the end
|
||||
```
|
||||
|
||||
This process needs to be repeated for all delegate methods and all classes that use delegates - or as much as you think you will need for internal projects. On my long list of things to do is to look at generating these using VS2015 and see if I can automate this step using Roslyn.
|
||||
|
||||
#### Step 6 - Getting it ready to use with another project
|
||||
The code now compiles, but as always compilable code isn't shippable code. There's a few more things left to do.
|
||||
|
||||
First, we need to ensure it supports the 64-bit processors. In the `libEstimoteSDK.linkwith.cs` you will see it is set to link with v7, v7s and the simulator:
|
||||
|
||||
```
|
||||
LinkTarget.ArmV7 | LinkTarget.ArmV7s | LinkTarget.Simulator
|
||||
```
|
||||
|
||||
We need to add the 64-bit processors to this list, so change it to:
|
||||
|
||||
```
|
||||
LinkTarget.ArmV7 | LinkTarget.ArmV7s | LinkTarget.Arm64 | LinkTarget.Simulator | LinkTarget.Simulator64
|
||||
```
|
||||
|
||||
<br/>
|
||||
|
||||
Secondly we should review naming. Objective Sharpie tries, but it does get things wrong. For example, the `beaconConnectionDidFail` method call is named wrong, instead being named by it's parameter:
|
||||
|
||||
```cs
|
||||
// @optional -(void)beaconConnectionDidFail:(ESTBeacon *)beacon withError:(NSError *)error;
|
||||
[Export ("beaconConnectionDidFail:withError:")]
|
||||
void WithError (ESTBeacon beacon, NSError error);
|
||||
```
|
||||
|
||||
This should be changed to be:
|
||||
|
||||
```cs
|
||||
// @optional -(void)beaconConnectionDidFail:(ESTBeacon *)beacon withError:(NSError *)error;
|
||||
[Export ("beaconConnectionDidFail:withError:")]
|
||||
void BeaconConnectionDidFail (ESTBeacon beacon, NSError error);
|
||||
```
|
||||
|
||||
I haven't yet worked out a pattern as to when these odd namings happen - if you work it out please let me know!.
|
||||
|
||||
#### Step 7 - ???
|
||||
#### Step 8 - Profit!
|
||||
Not really, but you now have a nice binding for the Estimote SDK. Hopefully these instructions make sense, tweet me/mail me with any issues you may have or any questions. Xamarin has a load of [really helpfull docs](http://developer.xamarin.com/guides/ios/advanced_topics/binding_objective-c/) covering pretty much everything you would need to know about binding.
|
||||
|
||||
And if you can't be bothered to do all this your self, you can just download my bindings from [my GitHub page](https://github.com/jimbobbennett/EstimoteBinding) which I am trying to keep pretty much up to date and includes a Xamarin.Forms based test app to show it all working. I hope to have a NuGet package or a Xamarin component available soon.
|
||||
|
||||
|
After Width: | Height: | Size: 186 KiB |
@@ -0,0 +1,173 @@
|
||||
---
|
||||
author: "Jim Bennett"
|
||||
categories: ["Technology", "xamarin", "xamarin.android", "java", "binding", "aar", "jar"]
|
||||
date: 2018-09-10T11:22:44Z
|
||||
description: ""
|
||||
draft: false
|
||||
slug: "binding-the-cognitive-services-android-speech-sdk-part-2-making-the-code-more-c-like"
|
||||
tags: ["Technology", "xamarin", "xamarin.android", "java", "binding", "aar", "jar"]
|
||||
title: "Binding the Cognitive Services Android Speech SDK - Part 2, making the code more C#-like"
|
||||
|
||||
images:
|
||||
- /blogs/binding-the-cognitive-services-android-speech-sdk-part-2-making-the-code-more-c-like/banner.png
|
||||
featured_image: banner.png
|
||||
---
|
||||
|
||||
|
||||
In the [first part](/blogs/binding-the-cognitive-services-android-speech-sdk) of this post, I showed how to get started binding the [Microsoft Cognitive Services speech API](https://docs.microsoft.com/azure/cognitive-services/speech-service/?WT.mc_id=speech-blog-jabenn). In this part I show how to make the code look more C#-like. In the [third part](/blogs/binding-the-cognitive-services-android-speech-sdk-part-3-java-8-fun) I'll show how to use it and fix up a nasty issue with the Android compiler and using jars created with the latest versions of Java.
|
||||
|
||||
## Making the namespaces more C#-like
|
||||
|
||||
The namespaces that come from Java are different from traditional C# style namespaces. Java namespaces are reverse-URL format, so the default one created for this project is `Com.Microsoft.Cognitiveservices.Speech.Internal`. A more-C# like one wouldn't have the `Com` at the start. The default namespace also capitalizes the first letter of each part, but obviously doesn't know what other letters should be capitalized - in this case the `S` in `CognitiveServices`.
|
||||
|
||||
The namespaces can be fixed by another `attr` entry in the `Metadata.xml` file - this time with the `name` set to `managedName` and the path set to the namespace. You will need one `attr` node for every namespace inside the Java library - so for `com.microsoft.cognitiveservices.speech` as well as the `internal`, `util`, `intent` and `translation` sub-namespaces. Change the namespaces to be more C# like, for example to `Microsoft.Azure.CognitiveServices.Speech.*`.
|
||||
|
||||
```xml
|
||||
<attr path="/api/package[@name='com.microsoft.cognitiveservices.speech']" name="managedName">Microsoft.Azure.CognitiveServices.Speech</attr>
|
||||
<attr path="/api/package[@name='com.microsoft.cognitiveservices.speech.internal']" name="managedName">Microsoft.Azure.CognitiveServices.Speech.Internal</attr>
|
||||
<attr path="/api/package[@name='com.microsoft.cognitiveservices.speech.util']" name="managedName">Microsoft.Azure.CognitiveServices.Speech.Util</attr>
|
||||
<attr path="/api/package[@name='com.microsoft.cognitiveservices.speech.intent']" name="managedName">Microsoft.Azure.CognitiveServices.Speech.Intent</attr>
|
||||
<attr path="/api/package[@name='com.microsoft.cognitiveservices.speech.translation']" name="managedName">Microsoft.Azure.CognitiveServices.Speech.Translation</attr>
|
||||
```
|
||||
|
||||
Once these namespaces are changed, your code will no longer build as the `StdMapWStringWStringMapIterator` class you added to the `Additions` folder will be using the old namespace, so fix this one up manually. Your code should then now build.
|
||||
|
||||
## Handling code with callbacks
|
||||
|
||||
The Speech SDK is actually implemented as cross-platform C++ code, with some platform-specific C++ code added to support each platform. This code is than wrapped using [SWIG](http://www.swig.org) to make it available as Java code for Android, C# code for Windows etc.
|
||||
|
||||
This model has the problem that events are not implemented as standard Java listeners. If they were, then the binding library would automatically convert them to C# events. Seeing as they are not, you will need to convert them to events manually.
|
||||
|
||||
> This is a very specific example for this one library, so it is unlikely that other libraries will need exactly the same code - instead I thought I'd write about it as an example of the kind of thing you may have to do to make the code more C# like.
|
||||
|
||||
### How events are implemented in this SDK
|
||||
|
||||
In this SDK, events are implemented by passing an object that implements the `IEventHandler` interface to the `AddEventListener` method on a property of type `EventHandlerImpl`. When the event is raised, the `OnEvent` method on the `IEventHandler` interface is called.
|
||||
|
||||
For example, in the `SpeechRecognizer` class there is a property called `FinalResultReceived` of type `EventHandlerImpl`, and you subscribe to this 'event' by passing an instance of `IEventHandler` to the `AddEventListener` on this property.
|
||||
|
||||
This pattern is not idiomatic C#, and is annoying to use as you will need to declare a class that implements the `IEventHandler` interface just to handle the event.
|
||||
|
||||
### Making this code more C#-like
|
||||
|
||||
To make this code more C# like, what you can do is:
|
||||
|
||||
* Create a generic implementation of `IEventHandler` that raises an event
|
||||
* Add a C# event to the class has a property of type `EventHandlerImpl`
|
||||
* Add an instance of the `IEventHandler` implementation to this class, and add this as a listener to the `EventHandlerImpl` property
|
||||
* In the C# event, explicitly implement the `add` and `remove` methods. In these methods, add or remove the event from the event on the `IEventHandler` implementation
|
||||
* Hide the `EventHandlerImpl` property from client code by marking it `internal`
|
||||
|
||||
### Creating the generic event handler
|
||||
|
||||
To create the event handler, add a new class to the `Additions` folder called `EventMapper`. The code for this is:
|
||||
|
||||
```cs
|
||||
class EventMapper<T, T1> : EventMapper, IEventHandler
|
||||
where T : class
|
||||
where T1 : class
|
||||
{
|
||||
readonly object sender;
|
||||
readonly Func<T1, T> argExtractor;
|
||||
public EventMapper(object sender, Func<T1, T> argExtractor)
|
||||
{
|
||||
this.sender = sender;
|
||||
this.argExtractor = argExtractor;
|
||||
}
|
||||
|
||||
public event EventHandler<EventArgs<T>> EventRaised;
|
||||
public void OnEvent(Java.Lang.Object p0, Java.Lang.Object p1)
|
||||
{
|
||||
EventRaised?.Invoke(sender, new EventArgs<T>(argExtractor(p1 as T1)));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is an internal class, so is only available to the binding library.
|
||||
|
||||
This class has two parts - an event and an `argExtractor`.
|
||||
|
||||
The event is a standard C# event using the `EventHander<>` delegate type - so when called it passes the sender as an object and some event arguments that derive from `EventArgs`. The `EventArgs<T>` type is not one that exists in .NET Standard (which I am very surprised about as I've created this so many times, as have others). You will need to implement this yourself, so add a class called `EventArgs` with the code below. This event args class is a simple wrapper for a value that needs to be passed to the event and saves you creating a load of custom event arg classes for each different value type that you want to pass.
|
||||
|
||||
```cs
|
||||
public class EventArgs<T> : EventArgs
|
||||
{
|
||||
public T Value { get; }
|
||||
public EventArgs(T value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When events are handled by the SDK, it passes it's own event args implementation containing a value for those args. For example, the `FinalRecognitionResult` event handler on the `SpeechRecogniser` is passed an instance of `SpeechRecognitionResultEventArgs`, containing a `Result` property of type `SpeechRecognitionResult`. These event args don't derive from the standard .NET `EventArgs` class, so you need a way to extract the relevant value and populate that into an `EventArgs` class, and this is what the `argExtractor` does - it takes the SDK args and pulls out the value needed. This is then wrapped in an `EventArgs<T>` and passed to the event invocation.
|
||||
|
||||
### Handling an event
|
||||
|
||||
In the `SpeechRecogniser` class there is a `FinalRecognitionResult` handler that raises an event passing an instance of `SpeechRecognitionResultEventArgs`, containing a `Result` property of type `SpeechRecognitionResult`. To map this to C#, you would add another part to the `SpeechRecogniser` class:
|
||||
|
||||
```cs
|
||||
public partial class SpeechRecognizer
|
||||
{
|
||||
}
|
||||
```
|
||||
|
||||
You would then add a field for the event mapper:
|
||||
|
||||
```cs
|
||||
EventMapper<SpeechRecognitionResult, SpeechRecognitionResultEventArgs> finalResultMapper;
|
||||
```
|
||||
|
||||
Then you add a C# event for the final result:
|
||||
|
||||
```cs
|
||||
public event EventHandler<EventArgs<SpeechRecognitionResult>> FinalResult
|
||||
{
|
||||
add {}
|
||||
remove {}
|
||||
}
|
||||
```
|
||||
|
||||
In the `add` method, if the `EventMapper` hasn't been created yet, you create it and pass it to the `AddEventListener` of the bound handler. When it is created you will need to pass in the `sender` which is passed to the events when invoked, and this is always `this`. You also need to pass in a mapper function to extract the `SpeechRecognitionResult` from the `SpeechRecognitionResultEventArgs`, which can be a simple lambda function to return the `Result` property. Then you subscribe the passed in `value` to the event on the mapper.
|
||||
|
||||
```cs
|
||||
add
|
||||
{
|
||||
if (finalResultMapper == null)
|
||||
{
|
||||
finalResultMapper = new EventMapper<SpeechRecognitionResult, SpeechRecognitionResultEventArgs>(sender, e => e.Result);
|
||||
finalResultMapper.AddEventListener(handler);
|
||||
}
|
||||
finalResultMapper.EventRaised += value;
|
||||
}
|
||||
```
|
||||
|
||||
For the remove function, if the mapper has been created you can unsubscribe the value from the event:
|
||||
|
||||
```cs
|
||||
remove
|
||||
{
|
||||
if (finalResultMapper != null)
|
||||
finalResultMapper.EventRaised -= value;
|
||||
}
|
||||
```
|
||||
|
||||
This is a lot of boilerplate code, so in my version I refactored this into some static methods. You can see these in my [GitHub repo](https://github.com/jimbobbennett/SpeechSdkXamarinSample/blob/master/Microsoft.Azure.CognitiveServices.Speech.Client/Additions/EventMapper.cs).
|
||||
|
||||
### Hiding the original event handler
|
||||
|
||||
Now that you have C# style events, it is cleaner to hide the old event handler implementation to stop client code from calling instead of your nice, shiny, C# events. To do this, you can use an `attr` in the `Metadata.xml` file to change the visibility of the properties for the old event handlers to `internal`. Seeing as these are the only places that `IEventHandler` and `EventHandlerImpl` are used, you can also mark these as internal. That way if you leave any `EventHandlerImpl` properties as public, the compiler will give you an error - a great way to ensure you have mapped all the events.
|
||||
|
||||
To mark these as internal, grab the paths from the source files in the `obj` folder, and add an `attr` node with the `name` set to `visibility`, and the content of the node set to `internal`. The code below shows this for the `FinalResultReceived` property on the `SpeechRecognizer`, as well as the `IEventHandler` and `EventHandlerImpl` classes. Repeat this for all the event handler properties across all classes.
|
||||
|
||||
```xml
|
||||
<attr path="/api/package[@name='com.microsoft.cognitiveservices.speech']/class[@name='SpeechRecognizer']/field[@name='FinalResultReceived']" name="visibility">internal</attr>
|
||||
|
||||
<attr path="/api/package[@name='com.microsoft.cognitiveservices.speech.util']/class[@name='EventHandlerImpl']" name="visibility">internal</attr>
|
||||
<attr path="/api/package[@name='com.microsoft.cognitiveservices.speech.util']/class[@name='IEventHandler']" name="visibility">internal</attr>
|
||||
```
|
||||
|
||||
<hr/>
|
||||
|
||||
In the [final part](/blogs/binding-the-cognitive-services-android-speech-sdk-part-3-java-8-fun), I'll show how you can call this code from a client app, as well as fixing up a nasty issue with the Android compiler and using jars created with the latest versions of Java. You can find the code for this [in my GitHub](https://github.com/jimbobbennett/SpeechSdkXamarinSample), and you can read more on [docs.microsoft.com](https://docs.microsoft.com/xamarin/android/platform/binding-java-library/?WT.mc_id=speech-blog-jabenn)
|
||||
|
||||
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 186 KiB |
@@ -0,0 +1,73 @@
|
||||
---
|
||||
author: "Jim Bennett"
|
||||
categories: ["xamarin", "Technology", "xamarin.android", "java", "binding", "aar", "jar", "COMPILETODALVIK", "invalid opcode ba", "invokedynamic"]
|
||||
date: 2018-09-10T11:22:55Z
|
||||
description: ""
|
||||
draft: false
|
||||
slug: "binding-the-cognitive-services-android-speech-sdk-part-3-java-8-fun"
|
||||
tags: ["xamarin", "Technology", "xamarin.android", "java", "binding", "aar", "jar", "COMPILETODALVIK", "invalid opcode ba", "invokedynamic"]
|
||||
title: "Binding the Cognitive Services Android Speech SDK - Part 3 - Java 8 fun"
|
||||
|
||||
images:
|
||||
- /blogs/binding-the-cognitive-services-android-speech-sdk-part-3-java-8-fun/banner.png
|
||||
featured_image: banner.png
|
||||
---
|
||||
|
||||
|
||||
In the [first part](/blogs/binding-the-cognitive-services-android-speech-sdk) of this post, I showed how to get started binding the [Microsoft Cognitive Services speech API](https://docs.microsoft.com/azure/cognitive-services/speech-service/?WT.mc_id=speech-blog-jabenn). In the [second part](/blogs/binding-the-cognitive-services-android-speech-sdk-part-2-making-the-code-more-c-like) I showed how to make the code look more C#-like. In this part, I'll show how to use it and fix up a nasty issue with the Android compiler and using jars created with the latest versions of Java.
|
||||
|
||||
## Using the SDK
|
||||
|
||||
To use the SDK, you will need an Android app. Create a new single-view Android app, and reference the SDK binding project. Then build the app and try to run it.
|
||||
|
||||
Then marvel, as your app spectacularly fails to compile with a really weird error message.
|
||||
|
||||
```sh
|
||||
COMPILETODALVIK : Uncaught translation error : com.android.dx.cf.code.SimException: invalid opcode ba (invokedynamic requires --min-sdk-version >= 26)
|
||||
```
|
||||
|
||||
WooHoo, invalid opcode ba. Ba indeed! What is this gibberish?
|
||||
|
||||
Well the issue comes down to Java versions. Android in the past only supported Java code up to version 7. They are now adding support for later versions but Xamarin doesn't have this yet, and this is only available on newer versions of Android (>= 26). To make your code work on earlier versions and with Xamarin you have to do a thing called desugaring (yes, really), and this alters the Java bytecode to convert Java 8 bytecode to a version that is supported by Java 7.
|
||||
|
||||
At the moment there isn't a nice IDE way to turn on desugaring, instead it has to be set inside the `.csproj` file of the client application. Open up the `.csproj` file for your newly created Android app inside [VSCode](https://code.visualstudio.com/?WT.mc_id=speech-blog-jabenn) (other editors are available, but hey - why would you), or by editing the file inside Visual Studio, and add the following to the default `PropertyGroup`:
|
||||
|
||||
```xml
|
||||
<AndroidEnableDesugar>true</AndroidEnableDesugar>
|
||||
```
|
||||
|
||||
Your app should now build without errors!
|
||||
|
||||
> I have this working and compiling in the preview versions of Visual Studio on Windows at the time of writing cos that's how I roll. If you are on stable and get weird errors then try with preview as I know support for this is being actively worked on.
|
||||
|
||||
> If you do this on VS for Mac then you will get a crash at run-time. The workaround is documented here: https://github.com/xamarin/xamarin-android/pull/1973
|
||||
|
||||
## Buiding an app using the SDK
|
||||
|
||||
To use the SDK you do need to sign up for the Speech service in Azure. Head to [portal.azure.com](https://portal.azure.com/?WT.mc_id=speech-blog-jabenn) and add a new Speech resource (at the time of writing this is in preview).
|
||||
|
||||
<div class="image-div" style="max-width:600px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Once you have this, note down the endpoint from the __Overview__ page. It will be a URL, and you will need the bit before `.api.cognitive.microsoft.com`. For example, if your endpoint is `https://northeurope.api.cognitive.microsoft.com/sts/v1.0`, then you will need `northeurope`. You will also need one of the two keys from the __Keys__ page.
|
||||
|
||||
You can then create a `SpeechFactory` using these values:
|
||||
|
||||
```cs
|
||||
var factory = SpeechFactory.FromSubscription(<SpeechApiKey>, <endpoint>);
|
||||
```
|
||||
|
||||
Once you have a speech factory, you can create different recognizers - simple speech, a translator, or an intent recognizer using [LUIS](https://www.luis.ai/?WT.mc_id=speech-blog-jabenn). To detect speech, handle the relevant events. You can see an example of using the `TranslationRecognizer` to convert English to spoken German in an example project in my [GitHub repo](https://github.com/jimbobbennett/SpeechSdkXamarinSample/blob/master/SpeechQuickStart/MainActivity.cs).
|
||||
|
||||
<blockquote class="twitter-tweet" data-lang="en"><p lang="en" dir="ltr">Had a successful day. Created a <a href="https://twitter.com/hashtag/Xamarin?src=hash&ref_src=twsrc%5Etfw">#Xamarin</a> binding for the <a href="https://twitter.com/Azure?ref_src=twsrc%5Etfw">@Azure</a> <a href="https://twitter.com/hashtag/CognitiveServices?src=hash&ref_src=twsrc%5Etfw">#CognitiveServices</a> Android speech SDK, and built a sample app that translates me voice into spoken German. <a href="https://t.co/Bg4XDvhBjv">pic.twitter.com/Bg4XDvhBjv</a></p>— Jim Bennett ☁️ (@jimbobbennett) <a href="https://twitter.com/jimbobbennett/status/1035559022743760896?ref_src=twsrc%5Etfw">August 31, 2018</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
|
||||
|
||||
|
||||
<hr/>
|
||||
|
||||
In these three posts you have seen how to [create a binding library for the Speech SDK `aar`](/blogs/binding-the-cognitive-services-android-speech-sdk), make the code [more C#-like](/blogs/binding-the-cognitive-services-android-speech-sdk-part-2-making-the-code-more-c-like), then finally use it from a client app, working around a Java bytecode issue. You can check out my implementation and a sample at on [GitHub](https://github.com/jimbobbennett/SpeechSdkXamarinSample). As always, the best source of information with much more depth is the [java binding dos on docs.microsoft.com](https://docs.microsoft.com/xamarin/android/platform/binding-java-library/?WT.mc_id=speech-blog-jabenn).
|
||||
|
||||
Let me know what you build with this SDK - my DMs are always open on [Twitter](https://twitter.com/jimbobbennett).
|
||||
|
||||
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 186 KiB |
@@ -0,0 +1,176 @@
|
||||
---
|
||||
author: "Jim Bennett"
|
||||
categories: ["Technology", "xamarin", "xamarin.android", "java", "binding", "aar", "jar"]
|
||||
date: 2018-09-09T14:55:00Z
|
||||
description: ""
|
||||
draft: false
|
||||
slug: "binding-the-cognitive-services-android-speech-sdk"
|
||||
tags: ["Technology", "xamarin", "xamarin.android", "java", "binding", "aar", "jar"]
|
||||
title: "Binding the Cognitive Services Android Speech SDK - Part 1, binding the library"
|
||||
|
||||
images:
|
||||
- /blogs/binding-the-cognitive-services-android-speech-sdk/banner.png
|
||||
featured_image: banner.png
|
||||
---
|
||||
|
||||
|
||||
As part of the [Microsoft Cognitive Services speech API](https://docs.microsoft.com/azure/cognitive-services/speech-service/?WT.mc_id=speech-blog-jabenn), there is a native Java Android SDK available as an `.aar` file. I wanted to use this in a Xamarin app, so I created a binding project for it.
|
||||
|
||||
The code for this is available [in my GitHub](https://github.com/jimbobbennett/SpeechSdkXamarinSample).
|
||||
|
||||
Binding an SDK is a four step process:
|
||||
|
||||
* Create the binding project with the relevant `jar` or `aar` file
|
||||
* Make any necessary tweaks to the code or project to make it compile
|
||||
* Make any required amendments to the code to make it into idiomatic C#
|
||||
* Test it all out and fix up any issues
|
||||
|
||||
There are some great docs available on the basics for doing this at [docs.microsoft.com](https://docs.microsoft.com/xamarin/android/platform/binding-java-library/?WT.mc_id=speech-blog-jabenn), but each library is different and can have it's own unique challenges so I thought I'd write a few posts to highlight the steps I needed to take to bind the speech SDK.
|
||||
|
||||
In this first part, I'll show how to create a binding project, add the speech SDK `aar` file, and make everything compile. In the [second part](/blogs/binding-the-cognitive-services-android-speech-sdk-part-2-making-the-code-more-c-like) I'll show how to make the code more idiomatic C#, then in the [third part](/blogs/binding-the-cognitive-services-android-speech-sdk-part-3-java-8-fun) I'll show how to use it and fix up a nasty issue with the Android compiler and using jars created with the latest versions of Java.
|
||||
|
||||
## Binding the SDK
|
||||
|
||||
The first step was to create a binding project and add the `.aar` file. I followed the instructions in the [Xamarin docs](https://docs.microsoft.com/xamarin/android/platform/binding-java-library/?WT.mc_id=speech-blog-jabenn), creating a new Android binding project and adding the `client-sdk-0.6.0.aar` file to the `Jars` folder.
|
||||
|
||||
<div class="image-div" style="max-width:400px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
When you compile this project, the compile step will generate code to bind every Java class it finds. Each class in the generated code is a wrapper for a Java class - it doesn't re-implement the Java code, it instead creates the same kind of thin wrapper that is used by the Xamarin Android SDK bindings. If you want to see this generated code, you can find it in the `obj/${Congfiguration}/generated/src` folder.
|
||||
|
||||
## Making it work
|
||||
|
||||
After doing this, I compiled the library and hit a couple of compiler errors that I needed to fix up:
|
||||
|
||||
```sh
|
||||
/.../Com.Microsoft.Cognitiveservices.Speech.Internal.StdMapWStringWString.cs(72,72): Error CS0738: 'StdMapWStringWString' does not implement interface member 'IIterable.Iterator()'. 'StdMapWStringWString.Iterator()' cannot implement 'IIterable.Iterator()' because it does not have the matching return type of 'IIterator'. (CS0738) (Speech)
|
||||
```
|
||||
|
||||
```sh
|
||||
/.../Com.Microsoft.Cognitiveservices.Speech.Internal.StdMapWStringWStringMapIterator.cs(83,83): Error CS0738: 'StdMapWStringWStringMapIterator' does not implement interface member 'IIterator.Next()'. 'StdMapWStringWStringMapIterator.Next()' cannot implement 'IIterator.Next()' because it does not have the matching return type of 'Object'. (CS0738) (Speech)
|
||||
```
|
||||
|
||||
The reason for this is that the Xamarin Java SDK doesn't contain the generic versions of `IIterator` and `IIterable` which are used by this library, instead it will default to using the non-generic versions, and the implementation of the generic interfaces doesn't match the signature of the non-generic version. So - how can it be fixed?
|
||||
|
||||
### Metadata.xml and Additions
|
||||
|
||||
Inside the binding project you can both alter the generated code and add new code.
|
||||
|
||||
* `Transforms/Metadata.xml` - in this XML file you can alter the code that is generated. You can add entries to this file to remove some of the autogenerated code, either at class, method or property level. You can also change the generated code, for example changing the namespace - something especially useful to change from Java style namespaces to C# style.
|
||||
|
||||
* `Additions` - In this folder you can add code that is compiled into the final dll. Each autogenerated class is declared as `partial`, so not only can you add new classes and code, you can also add new parts to a generated class.
|
||||
|
||||
### Fixing the `StdMapWStringWString` code
|
||||
|
||||
The `StdMapWStringWString` class implements a generic version of the `IIterable` interface - `IIterable<StdMapWStringWStringMapIterator>`. The Xamarin Java SDK doesn't contain the generic base interface, so the bound library defaults to implementing `IIterable`. The problem is this interface contains a method `Iterator` that returns a different type in the generic version to the non-generic version. The generated code implements this method returning a `StdMapWStringWStringMapIterator`, but the non-generic version expects a method returning `IIterator`, so you get a compiler error.
|
||||
|
||||
This is simple enough to fix - you just need to change the return type of the binding to be `IIterator`, and this can be done in the `Metadata.xml` file.
|
||||
|
||||
The `Metadata.xml` file is a file containing transforms that you want to make to the generated code - and can be adding new items, removing items or change the attributes of items such as the name or the return type.
|
||||
|
||||
Open the generated code from the `obj/${Congfiguration}/generated/src/` folder - the file will be called `Microsoft.Azure.CognitiveServices.Speech.Internal.StdMapWStringWString.cs`. If you look at all the public items in this file (the class, public methods and public properties), you will see each one has a comment describing the `Metadata.xml path`:
|
||||
|
||||
```cs
|
||||
// Metadata.xml XPath class reference: path="/api/package[@name='com.microsoft.cognitiveservices.speech.internal']/class[@name='StdMapWStringWStringMapIterator']"
|
||||
public partial class StdMapWStringWStringMapIterator
|
||||
...
|
||||
```
|
||||
|
||||
This path is used to identify each item to the `Metadata.xml` file, so locate the `Iterator()` method and note the `path`.
|
||||
|
||||
Open the `Metadata.xml` file and add a new `attr` node inside the `metadata` node. Set the `path` attribute of this node to match the path in the comment for the `Iterator()` method. Then add an attribute called `name` with the value `managedReturn` to tell the transformations that this is a changed to the managed return type - so the type for the binding library only. This will treat the underlying return value as the original type which is what you want. The value for this attribute is set inside the node, and should be `Java.Util.IIterator`.
|
||||
|
||||
The full node is shown below:
|
||||
|
||||
```xml
|
||||
<metadata>
|
||||
<attr path="/api/package[@name='com.microsoft.cognitiveservices.speech.internal']/class[@name='StdMapWStringWString']/method[@name='iterator' and count(parameter)=0]" name="managedReturn">Java.Util.IIterator</attr>
|
||||
</metadata>
|
||||
```
|
||||
|
||||
Now if you compile the project, one error will be gone. If you re-open the generated file you will see the new return type.
|
||||
|
||||
You can read more on the capabilities if this file in the [docs](https://docs.microsoft.com/xamarin/android/platform/binding-java-library/customizing-bindings/java-bindings-metadata/?WT.mc_id=speech-blog-jabenn).
|
||||
|
||||
### Fixing the `StdMapWStringWStringMapIterator` code
|
||||
|
||||
The Xamarin Java SDK doesn't contain the generic version of `IIterator`, so the bound code uses the non-generic version. This interface has a `Next()` method that returns the next item from the collection being iterated. In the non-generic version of this interface, `Next()` returns a `Java.Lang.Object`, whereas in the generic version it returns the generic arg, in this case a `Java.Lang.String`. This means the generated code uses the non-generic interface, but the implementation uses the generic method, causing a compiler error.
|
||||
|
||||
The fix for this is a little bit more work - you can't just change the return type as it is used in a private method created by the binding. Instead, the fix for this is to remove the generic `Next()` method and replace it with a non-generic version. Re-writing binding methods is not easy as there is a lot of code in the binding, so for cases like this the best way is to copy the generated code, adjusting it to suit. If you open the `com.microsoft.cognitiveservices.speech.internal.StdMapWStringWStringMapIterator.cs` file from the `obj` directory, and look at the `Next()` method you will see the implementation consists of not just the `Next()` method, but also an `n_next()` method, a `GetNextHandler()` method and a `cb_next` `Delegate` field. This is all the plumbing needed to create the binding method and call through to the underlying Java method.
|
||||
|
||||
#### Adding the new method
|
||||
|
||||
Lets's start by adding the new method as this will mostly be a copy of the existing code. Add a new class to the `Additions` folder called `StdMapWStringWStringMapIterator`, mark the class as `partial` and change the namespace to match the generated file (these will be fixed up later to be more C#-like).
|
||||
|
||||
```cs
|
||||
namespace Com.Microsoft.Cognitiveservices.Speech.Internal
|
||||
{
|
||||
public partial class StdMapWStringWStringMapIterator
|
||||
{
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Copy the `Next()`, `GetNextHandler()`, `n_next()` methods and the `cb_next` field from the generated code and paste them into the new class part. Strip out any unnecessary namespaces and use `var` everywhere - this will make it easier when you change the namespaces to be more C#-like in the next post.
|
||||
|
||||
Change the return type of `Next` to be `Java.Lang.Object` instead of `Java.Lang.String`. Leave the types in the `Register` attribute and `__id` fields as they are, as the underlying method that is called returns a `Java.Lang.String`, and you only need to change the return type for the binding wrapper.
|
||||
|
||||
In the `n_next()` method, change the `return` call to call `ToString()` on the result of the call to `Next()` to use the correct type. The final code will look like this:
|
||||
|
||||
```cs
|
||||
namespace Com.Microsoft.Cognitiveservices.Speech.Internal
|
||||
{
|
||||
public partial class StdMapWStringWStringMapIterator
|
||||
{
|
||||
static Delegate cb_next;
|
||||
# pragma warning disable 0169
|
||||
static Delegate GetNextHandler()
|
||||
{
|
||||
if (cb_next == null)
|
||||
cb_next = JNINativeWrapper.CreateDelegate((Func<IntPtr, IntPtr, IntPtr>)n_Next);
|
||||
return cb_next;
|
||||
}
|
||||
|
||||
static IntPtr n_Next(IntPtr jnienv, IntPtr native__this)
|
||||
{
|
||||
var __this = Object.GetObject<StdMapWStringWStringMapIterator>(jnienv, native__this, JniHandleOwnership.DoNotTransfer);
|
||||
return JNIEnv.NewString(__this.Next()?.ToString()); // ToString called on the object.
|
||||
}
|
||||
# pragma warning restore 0169
|
||||
|
||||
[Register("next", "()Ljava/lang/String;", "GetNextHandler")]
|
||||
public virtual unsafe Java.Lang.Object Next() // Return type changed from string to object
|
||||
{
|
||||
const string __id = "next.()Ljava/lang/String;";
|
||||
try
|
||||
{
|
||||
var __rm = _members.InstanceMethods.InvokeVirtualObjectMethod(__id, this, null);
|
||||
return JNIEnv.GetString(__rm.Handle, JniHandleOwnership.TransferLocalRef);
|
||||
}
|
||||
finally
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Removing the `Next` method
|
||||
|
||||
To remove a method from the autogenerated file, you add a `remove-node` entry to the `Metadata.xml` file with the path of the `Next()` method node you want to remove. To remove the `Next()` method, add this following to this file:
|
||||
|
||||
```xml
|
||||
<remove-node path="/api/package[@name='com.microsoft.cognitiveservices.speech.internal']/class[@name='StdMapWStringWStringMapIterator']/method[@name='next' and count(parameter)=0]"/>
|
||||
```
|
||||
|
||||
The syntax is `<remove-node path="..."/>` where the `path` comes from the comment in the generated code. Once this line has been added, compile the code and check the generated file in the `obj` directory. The compiler error about the missing `Next()` method will have gone, and the `Next()` method will be removed from the autogenerated file - when the library is built it will use the version in the file in the `Additions` folder.
|
||||
|
||||
You should now be able to compile this library successfully.
|
||||
|
||||
<hr/>
|
||||
|
||||
In the [second part](/blogs/binding-the-cognitive-services-android-speech-sdk-part-2-making-the-code-more-c-like/), I'll show how you can make the code more C#-like. You can find the code for this [in my GitHub](https://github.com/jimbobbennett/SpeechSdkXamarinSample), and you can read more on [docs.microsoft.com](https://docs.microsoft.com/xamarin/android/platform/binding-java-library/?WT.mc_id=speech-blog-jabenn)
|
||||
|
||||
BIN
blog/content/blogs/blind-technology/banner.png
Normal file
|
After Width: | Height: | Size: 186 KiB |
37
blog/content/blogs/blind-technology/index.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
author: "Jim Bennett"
|
||||
categories: ["Technology", "technology", "binaural", "microsoft", "blind", "navigation"]
|
||||
date: 2014-11-18T21:45:01Z
|
||||
description: ""
|
||||
draft: false
|
||||
slug: "blind-technology"
|
||||
tags: ["Technology", "technology", "binaural", "microsoft", "blind", "navigation"]
|
||||
title: "Blind technology"
|
||||
|
||||
images:
|
||||
- /blogs/blind-technology/banner.png
|
||||
featured_image: banner.png
|
||||
---
|
||||
|
||||
|
||||
Busy, busy, busy. In the last month I've relocated form Thailand to the UK with my family, got my UK company up and running (more coming on this in a later post), lined up one client, fought with the Apple app store to get my developer account set up right so I can sell apps (still not done), found a new place to live and been fighting with overbearing bureaucracy. I've also found loads of time to spend playing with my daughter before work becomes too overbearing, and the upshot of this is little time for technology. My [Udemy course](/blogs/time-to-teach/) is still nowhere near finished, my apps are not on the Apple app store yet and I don't even have a working Windows 10 VM (mind you, from what I hear not many others do either).
|
||||
|
||||
Hey ho, back to the grindstone from now on.
|
||||
|
||||
One cool thing I did have time to do is head to Microsoft's Future Decoded conference in London. It was a great day, some fantastic speakers (including that [drummer guy from D'Ream](http://bit.ly/14GyJJz)) and the chance to hear about some awesome technology.
|
||||
|
||||
The one piece that really caught my eye surprisingly wasn't the announcement that Microsoft have [open sourced .Net](http://blogs.msdn.com/b/dotnet/archive/2014/11/12/net-core-is-open-source.aspx) and released [new Visual Studio goodies](http://blogs.msdn.com/b/visualstudio/archive/2014/11/12/visual-studio-2015-preview-visual-studio-community-2013-visual-studio-2013-update-4-and-more.aspx). It was in fact a software/hardware solution from [Microsoft Services work with Future Cities](https://futurecities.catapult.org.uk/project-full-view/-/asset_publisher/oDS9tiXrD0wi/content/project-cities-unlocked/) in conjunction with [Guide Dogs for the Blind](http://www.guidedogs.org.uk) to help blind people navigate. A very good friend of mine is almost totally blind so I'm very interested in technology that helps people in his situation. The history of the project is that they were asked to help people navigate around a new city, and seeing as one of their team is blind they decided it would be a good place to start - the needs of the sighted are a lot less than those of people who are visually impaired in a new city so solving for the harder case will also help ensure you cover as much as possible for the easy case.
|
||||
|
||||
The basic idea behind the project is to provide visually impaired people with the ability to navigate around a city. For us sighted people it's easy - we whip out our smartphone and load up google maps. If you're blind, this is impossible. The Microsoft guys have come up with what is almost like turn by turn navigation using audio clues to guide you around. Dogs are great for stopping you walking out in front of cars but can't distinguish between Starbucks and a dry cleaners.
|
||||
|
||||
The first part of the technology uses [binaural](http://en.wikipedia.org/wiki/Binaural_recording) sound processing - the ability to make a sound seem like it is coming from somewhere in 3D space using normal headphones. Unlike normal stereo, this allows sounds to be behind, in front, above, or anywhere instead of simply left/right positioned. Combine this with a smartphone with a built in compass and you have the ability to put sound in a particular location regardless of which way the user is facing (assuming the smartphone is facing the same way relative to the user of course).
|
||||
|
||||
The downsides for this as a navigation tool are that, as mentioned, the smartphone has to always be facing in the same direction as the user, as well as the user having to wear headphones. A blind person would never want this as their ears are vital to navigating around. If you can't hear traffic for example then you couldn't safely walk the streets.
|
||||
The solution to both of these is a special headset. It's based around bone conductivity headphones (these transmit sound by vibrating the bones in your skull allowing your ears to be free from blockages) with a box of electronics on the back that contains a compass, accelerometer and a bluetooth connection to the phone. This means the headset can always be in the same relative direction to the user to allow the sound to be positioned accurately.
|
||||
|
||||
So now we have a tool that can track which way you are facing an provide audio clues at a given compass point. So the next step is to combine this with GPS, map data and a speach interface. For basic city walking it is able to name shops that you are interested in with the name being in the right direction to where you are facing. It can also navigate you to a given destination using a simple sound in the direction you need to turn - so if the place you want is 100 yards ahead on the current street, then right for 50 yards then right again for 20 it will sound in front of you whilst you walk, then turn right when you should turn, then right again when you should turn then announce when you are at your destination. All without being intrusive.
|
||||
|
||||
It sounds like such a simple solution and having tried it out I can say it is really well done. I'm hoping they are going to open source the binaural technology so I can have a play at creating something similar.
|
||||
|
||||
Good work Microsoft!
|
||||
|
||||
BIN
blog/content/blogs/bubble-tea/banner.png
Normal file
|
After Width: | Height: | Size: 186 KiB |
BIN
blog/content/blogs/bubble-tea/bubbletea--1--1.jpg
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
blog/content/blogs/bubble-tea/bubbletea--2-.jpg
Normal file
|
After Width: | Height: | Size: 85 KiB |
29
blog/content/blogs/bubble-tea/index.md
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
author: "Jim Bennett"
|
||||
categories: ["food", "bubble boy", "bubble tea", "chocolate"]
|
||||
date: 2014-08-10T02:20:45Z
|
||||
description: ""
|
||||
draft: false
|
||||
slug: "bubble-tea"
|
||||
tags: ["food", "bubble boy", "bubble tea", "chocolate"]
|
||||
title: "Bubble Tea"
|
||||
|
||||
images:
|
||||
- /blogs/bubble-tea/banner.png
|
||||
featured_image: banner.png
|
||||
---
|
||||
|
||||
|
||||
After a post on FaceBook about enjoying some Bubble Tea, a friend of mine suggested that I should blog about it.
|
||||
Seeing as I am currently in Thailand, home of the best food in the world, I guess it makes sense to extend this to all food. Most of my meals are incredible food cooked fresh and purchased from markets for usually 40p-80p a dish - so why not share them with the world.
|
||||
|
||||
I'll start today with bubble tea - read about it on [Wikipedia](http://en.wikipedia.org/wiki/Bubble_tea) if you've never heard of it. Essentially it's a Taiwanese drink made of milky tea with tapioca pearls in the bottom, but there are many variants made with green tea, fruit teas, juices and jellies instead of bubbles.
|
||||
|
||||
This morning I stopped at the Bubble Boy (a chain of Bubble Tea stands) at the [Ekkami BTS station](https://goo.gl/maps/rZQtC).
|
||||
|
||||

|
||||
|
||||
I grabbed a Chocolate milk tea with chocolate pudding at the bargain price of 45 baht (83p/$1.40). It's really, really nice - you can't taste the tea that much, just a milky chocolate drink with undertones of bitter dark chocolate. The chocolate pudding is dark chocolate flavour pearls that are nice and chewy without a too intense taste. Definitely one I'll have again.
|
||||
|
||||

|
||||
|
||||
BIN
blog/content/blogs/bubbles-bubbles-bubbles-my-bubbles/banner.png
Normal file
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,20 @@
|
||||
---
|
||||
author: "Jim Bennett"
|
||||
categories: ["food", "bubble boy", "bubble tea"]
|
||||
date: 2014-08-11T02:25:02Z
|
||||
description: ""
|
||||
draft: false
|
||||
slug: "bubbles-bubbles-bubbles-my-bubbles"
|
||||
tags: ["food", "bubble boy", "bubble tea"]
|
||||
title: "Bubbles, bubbles, bubbles. My bubbles!"
|
||||
|
||||
images:
|
||||
- /blogs/bubbles-bubbles-bubbles-my-bubbles/banner.png
|
||||
featured_image: banner.png
|
||||
---
|
||||
|
||||
|
||||
More bubble tea today. Coffee milk tea with bubbles. Tastes of sweet coffee with an aftertaste of tea - weird but not unpleasant. Nice chewy bubbles.
|
||||
|
||||

|
||||
|
||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
BIN
blog/content/blogs/building-a-live-caption-tool/banner.png
Normal file
|
After Width: | Height: | Size: 186 KiB |
118
blog/content/blogs/building-a-live-caption-tool/index.md
Normal file
@@ -0,0 +1,118 @@
|
||||
---
|
||||
author: "Jim Bennett"
|
||||
categories: ["technology", "Python", "azure", "cognitive services", "speech"]
|
||||
date: 2019-07-02T11:39:24Z
|
||||
description: ""
|
||||
draft: false
|
||||
images:
|
||||
- /blogs/building-a-live-caption-tool/banner.png
|
||||
featured_image: banner.png
|
||||
slug: "building-a-live-caption-tool"
|
||||
summary: "Learn how to build a live captioner using Python and the Azure Cognitive Services"
|
||||
tags: ["technology", "Python", "azure", "cognitive services", "speech"]
|
||||
title: "Building a live caption tool - part 1"
|
||||
|
||||
images:
|
||||
- /blogs/building-a-live-caption-tool/banner.png
|
||||
featured_image: banner.png
|
||||
---
|
||||
|
||||
|
||||
I've started a [Twitch stream where I'm learning Python](https://twitch.tv/jimbobbennett) every Wednesday at 12pm UK time. One way I'd like to make my stream more accessible is by having live captions whilst I'm speaking.
|
||||
|
||||
What I need is a tool that will stream captions to something I can add to my OBS scenes, but also be customizable. A lot of off the shelf speech to text models are great, but I need something I can tune to my voice and accent, as well as any special words I am using such as technical tools and terms.
|
||||
|
||||
The [Azure Cognitive Services](https://azure.microsoft.com/services/cognitive-services/directory/speech/?WT.mc_id=livecaption-blog-jabenn) have such a tool - as well as using a standard speech to text model, you can customize the model for your voice, accent, background noise and special words.
|
||||
|
||||
In this part, I'll show how to get started building a live captioner in Python. In the next part, I'll show how to customize the output.
|
||||
|
||||
## Create the speech resource
|
||||
|
||||
To get started, you first need to create a Speech resource in Azure. You can do it from the Azure Portal by following [this link](https://portal.azure.com/?WT.mc_id=twitchcaptions-blog-jabenn#create/Microsoft.CognitiveServicesSpeechServices). There is a free tier which I'm using - after all we all love free stuff!
|
||||
|
||||
> If you don't have an Azure account you can create a free account at [azure.microsoft.com/free](?WT.mc_id=twitchcaptions-blog-jabenn) and get $200 of free credit for the first 30 days and a host of services free for a year. Students and academic faculty can sign up at [azure.microsoft.com/free/students](https://azure.microsoft.com/free/students/?WT.mc_id=livecaption-blog-jabenn) and get $100 that lasts a year as well as 12 months of free services, and this can be renewed every year that you are a student.
|
||||
|
||||
{{< figure src="2019-07-02_11-21-40.png" caption="" >}}
|
||||
|
||||
When the resource is created, note down the first part of the endpoint from the **Overview** tab. The endpoint will be something like `https://uksouth.api.cognitive.microsoft.com/sts/v1.0/issuetoken`, and the bit you want is the part before `api.microsoft.com`, so in my case `uksouth`. This will be the name of the region you created your resource in. You all also need to grab a key from the **Keys** tab.
|
||||
|
||||
Once you have your Speech resource the next step is to use it to create captions.
|
||||
|
||||
## Create a captioner
|
||||
|
||||
Seeing as my stream is all about learning Python, I thought it would be fun to build the captioner in Python. All the Microsoft Cognitive Services have [Python APIs](https://azure.microsoft.com/resources/samples/cognitive-services-python-sdk-samples/?WT.mc_id=livecaption-blog-jabenn) which makes them easy to use.
|
||||
|
||||
I launched VS Code (which has excellent Python support thanks to the [Python extension](https://code.visualstudio.com/docs/languages/python/?WT.mc_id=livecaption-blog-jabenn)), and created a new Python project. The Speech SDK is available via `pip`, so I installed via the Terminal it using:
|
||||
|
||||
```sh
|
||||
pip install azure-cognitiveservices-speech
|
||||
```
|
||||
|
||||
To recognize speech you need to create a `speechRecognizer`, telling it the details of your resource via a `speechConfig`.
|
||||
|
||||
```python
|
||||
import azure.cognitiveservices.speech as speechsdk
|
||||
|
||||
speech_config = speechsdk.SpeechConfig(subscription=speech_key, region=service_region)
|
||||
speech_recognizer = speechsdk.SpeechRecognizer(speech_config=speech_config)
|
||||
```
|
||||
|
||||
In the code above, replace `speech_key` with the key from the Speech resource, and replace `service_region` with the region name.
|
||||
|
||||
> This will create a speech recognizer using the default microphone. If you want to change the microphone you will need to know the device id and use this to create an `AudioConfig` object which is used to create the recognizer. You can read more about this in [the docs](https://docs.microsoft.com/azure/cognitive-services/speech-service/how-to-select-audio-input-devices/?WT.mc_id=twitchcaptions-blog-jabenn).
|
||||
|
||||
The speech recognizer can be run as a one off and listen for a single block of speech until a break is found, or it can run continuously providing a constant stream of text via events. To detect continuously, an event needs to be wired up to collect the text.
|
||||
|
||||
```python
|
||||
def recognizing(args):
|
||||
# Do something
|
||||
|
||||
speech_recognizer.recognizing.connect(recognizing)
|
||||
speech_recognizer.start_continuous_recognition()
|
||||
```
|
||||
|
||||
In the above code, the `recognizing` event is fired every time some text is recognized. This event is fired multiple times for the same set of words, building up the text over time as the model refines the output. After a break it will reset and send new text.
|
||||
|
||||
The `args` parameter is a `SpeechRecognitionEventArgs` instance with a property called `result` that contains the result of the recognition. This result has a property called `text` with the recognized text.
|
||||
|
||||
For example, if you run this and say "Hello and welcome to the speech captioner", this event will be called probably 7 times:
|
||||
|
||||
```
|
||||
hello
|
||||
hello and
|
||||
hello and welcome
|
||||
hello and welcome to
|
||||
hello and welcome to the
|
||||
hello and welcome to the speech
|
||||
hello and welcome to the speech captioner
|
||||
```
|
||||
|
||||
If you then pause and say "This works" it will be called 2 more times, with just the new words.
|
||||
|
||||
```
|
||||
this
|
||||
this works
|
||||
```
|
||||
|
||||
The text is refined as the words are analyzed, so the text can change over time. For example if you say "This is a live caption test", you may get back:
|
||||
|
||||
```
|
||||
this
|
||||
this is
|
||||
this is alive
|
||||
this is a live caption
|
||||
this is a live caption text
|
||||
```
|
||||
|
||||
Notice in the third result there is the word "alive", which gets split into "a live" as more context is understood by the model.
|
||||
|
||||
The model doesn't understand sentences, and in reality humans rarely speak in coherent sentences with a structure that is easy for the model to break up, hence why you won't see full stops or capital letters.
|
||||
|
||||
The `start_continuous_recognition` call will run the recognition in the background, so the app will need a way to keep running, such as a looping sleep or an app loop using a GUI framework like Tkinter.
|
||||
|
||||
I've created a GUI app using Tkinter using this code. My app will put a semi-opaque window at the bottom of the screen that has a live stream of the captions in a label. The label is updated with the text from the `recognizing` event, so will be updated as I speak, then cleared down after each block of text ends and a new one begins.
|
||||
|
||||
You can find it on [GitHub](https://github.com/jimbobbennett/TwitchCaptioner), to use it add your key and region to the _config.py_ file, install the `pip` packages from the _requirements.txt_ file and run _captioner.py_ through Python.
|
||||
|
||||
In the next part, I'll show how to customize the model to my voice and terms I use.
|
||||
|
||||
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 208 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 186 KiB |
@@ -0,0 +1,428 @@
|
||||
---
|
||||
author: "Jim Bennett"
|
||||
categories: ["xamarin", "xamarin.android", "Technology", "mvvmlight", "mvvm", "tutorial"]
|
||||
date: 2016-01-30T17:51:36Z
|
||||
description: ""
|
||||
draft: false
|
||||
slug: "building-a-xamarin-android-app-part-3"
|
||||
tags: ["xamarin", "xamarin.android", "Technology", "mvvmlight", "mvvm", "tutorial"]
|
||||
title: "Building a Xamarin Android app - part 3"
|
||||
|
||||
images:
|
||||
- /blogs/building-a-xamarin-android-app-part-3/banner.png
|
||||
featured_image: banner.png
|
||||
---
|
||||
|
||||
|
||||
This is the third part in the my series about building an Android app using Xamarin.Android. You can find the first part [here](/blogs/building-an-android-app-part-1/) and the second part [here](/blogs/building-an-android-app-part-2/), and I highly recommend reading these first.
|
||||
|
||||
#### Data binding
|
||||
In the first 2 parts of this we created our basic app shell and built a core library with models that used SQLite to store and retrieve our data. We're now going to expand on this by adding the layer in-between to bind the data to our UI.
|
||||
|
||||
Having been a WPF developer in the past I'm a big fan of the [MVVM pattern](https://en.wikipedia.org/wiki/Model–view–viewmodel), so I'm planning on using the same pattern here. I won't go into the details of this pattern, so if you've never come across it before it's worth reading up as it's a useful pattern supported natively in Xamarin.Forms and on iOS and Android using tools like MVVMLight or [MVVMCross](https://github.com/MvvmCross/MvvmCross). To help with this I'm going to use [MVVMLight](http://www.mvvmlight.net) by [Laurent Bugnion](http://www.galasoft.ch). As the name suggests this is a light-weight MVVM library which provides the basic toolbox to allow you to use the pattern without constraining you to a particular architecture. MVVMLight has two different nuget packages to install - one for the Android app and one for our core project.
|
||||
|
||||
For StupendousCounter.Droid install MVVMLight. This has all the platform specific code in (so for an Android project you get a library with Android specific code in) that allows for things like UI binding - this needs to know about the controls so needs to be platform-specific. When installing this you may get an error about not being able to find App.Xaml - just ignore this as it isn't relevant for Android projects. Once this is installed you will see a new folder created called `ViewModel` that contains a dummy view model and an example ViewModelLocator. We'll come back to these later on.
|
||||
|
||||
<div class="image-div" style="width: 700px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
<br/>
|
||||
<div class="image-div" style="width: 300px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
For StupendousCounter.Core install the libs only one - MVVMLightLibs. This has things like the base view model but nothing platform specific.
|
||||
|
||||
<div class="image-div" style="width: 700px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
###### Showing some data
|
||||
Lets start by showing some data on screen. To make it easier we'll just mock some up on startup if we don't have an existing counters. In our `MainActivity.cs` we can add some dummy data if we are in debug (to save accidentally leaving this in in release code) just after we've setup the `DatabaseHelper`:
|
||||
|
||||
```
|
||||
private static async Task AddDummyData()
|
||||
{
|
||||
var dbHelper = new DatabaseHelper();
|
||||
if (!(await dbHelper.GetAllCountersAsync()).Any())
|
||||
{
|
||||
var counter1 = new Counter
|
||||
{
|
||||
Name = "Monkey Count",
|
||||
Description = "The number of monkeys",
|
||||
Value = 10
|
||||
};
|
||||
|
||||
var counter2 = new Counter
|
||||
{
|
||||
Name = "Playtpus Count",
|
||||
Description = "The number of duck-billed platypuses",
|
||||
Value = 4
|
||||
};
|
||||
|
||||
await dbHelper.AddOrUpdateCounterAsync(counter1);
|
||||
await dbHelper.AddOrUpdateCounterAsync(counter2);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And call this if we're in debug in our 'OnCreate' method:
|
||||
|
||||
```
|
||||
protected override async void OnCreate(Bundle savedInstanceState)
|
||||
{
|
||||
base.OnCreate(savedInstanceState);
|
||||
|
||||
var path = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal);
|
||||
var dbPath = Path.Combine(path, "counters.db3");
|
||||
DatabaseHelper.CreateDatabase(dbPath);
|
||||
|
||||
# if DEBUG
|
||||
await AddDummyData();
|
||||
# endif
|
||||
```
|
||||
|
||||
Notice how we're awaiting the call to `AddDummyData`? Yup - this is an async method we're in. In the default `OnCreate` it's not async - but we can make it so by just adding the `async` keyword and it just works. Which is really nice.
|
||||
|
||||
<div class="image-div" style="width: 300px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
We have some data, so lets create our first view model to display it.
|
||||
When we added MVVMLight to the StupendousCounter.Droid project it created a dummy `MainViewModel` and 'ViewModelLocator'. These are useful starting points for our code but they are not in the right place - one of the big upsides of MVVM is the separation of concerns that is gives us, so the UI is completely independent of the Model (which contains our data) and the view model (which is a converter that converts from the UI view of our data to the Models view). Despite the view model converting from model to UI it should never contain any UI specific code - it converts the Models into a set of properties and commands that are bound to the UI in the View and allows the UI to get or set data or perform actions. Properties are simple data types that the view can show, update and detect updates to based off the [INotifyPropertyChanged](https://msdn.microsoft.com/en-us/library/system.componentmodel.inotifypropertychanged%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396) interface. Commands are implementations of [ICommand](https://msdn.microsoft.com/en-us/library/system.windows.input.icommand(v=vs.110).aspx) that allow an event raised on the UI to run an action including checking if that action is allowed based on some criteria. These are all non-UI specific pieces and therefore do not need to be in the UI layer, nor do they need to be platform specific. So it's better to put them in the Core project so that if we want to port this app to iOS we don't have to re-implement the view models, just the Views. We can also then write unit tests against them simulating the View's interaction by getting or setting properties, detecting the `PropertyChanged` event on the `INotifyPropertyChanged` interface and executing commands.
|
||||
|
||||
To do this just drag the folder to the StupendousCounter.Core project and delete it from StupendousCounter.Droid. Then just update the namespaces in the 2 files to match the new location - using Resharper of course!
|
||||
|
||||
We need a couple of view models - one for the individual counters and one for the overall collection of counters.
|
||||
|
||||
###### CounterViewModel
|
||||
|
||||
Lets start with the `CounterViewModel`, a view model that wraps a `Counter`:
|
||||
|
||||
```
|
||||
using System.Threading.Tasks;
|
||||
using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight.Command;
|
||||
|
||||
namespace StupendousCounter.Core.ViewModel
|
||||
{
|
||||
public class CounterViewModel : ViewModelBase
|
||||
{
|
||||
private readonly Counter _counter;
|
||||
private readonly IDatabaseHelper _databaseHelper;
|
||||
|
||||
public CounterViewModel(Counter counter, IDatabaseHelper databaseHelper)
|
||||
{
|
||||
_counter = counter;
|
||||
_databaseHelper = databaseHelper;
|
||||
}
|
||||
|
||||
public string Name => _counter.Name;
|
||||
public string Description => _counter.Description;
|
||||
public string Value => _counter.Value.ToString("N0");
|
||||
|
||||
private RelayCommand _incrementCommand;
|
||||
public RelayCommand IncrementCommand => _incrementCommand ?? (_incrementCommand = new RelayCommand(async () => await IncrementAsync()));
|
||||
|
||||
private async Task IncrementAsync()
|
||||
{
|
||||
await _databaseHelper.IncrementCounterAsync(_counter);
|
||||
RaisePropertyChanged(() => Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This view model derives from `GalaSoft.MvvmLightViewModelBase`. This base class provides a few useful bits of functionality including an implementation of `INotifyPropertyChanged` to make it easy to raise events when properties change. It has other cool stuff as well, but that's outside the scope of this but it's worth reading up on it.
|
||||
|
||||
The constructor takes a `Counter` that will provide all our data, and an `IDatabaseHelper` that we can use to update the counters value. Note that its the interface, not the concrete `DatabaseHelper` that we use so that we can mock it later for unit tests.
|
||||
|
||||
The `Name` and `Description` properties are simple read-only pass throughs to the properties on the underlying `Counter`. The `Value` property is slightly more complicated - I'm converting it to a string with no decimal places so it can be shown in a text view. I'm using the new, cleaner [C# 6 expression bodied property](https://visualstudiomagazine.com/articles/2015/06/03/c-sharp-6-expression-bodied-properties-dictionary-initializer.aspx) syntax for these which is so much cleaner than writing the properties out in full. These are read only as there is no need to update them directly - the only property that we can update is the value when the counter is incremented and we don't want to do this with a direct property update, instead we want to do this in the `IncrementCommand`. This command uses a MVVMLight `RelayCommand` to wrap an async call to our `Increment` method. This will increment the counter in the SQLite db using our database helper, then call `RaisePropertyChanged` to indicate back to the view that the value has changed so the UI should update what it shows on screen. One thing I still think is odd is that there isn't an implementation of `ICommand` in the .Net framework - you either have to write your own or use one from a library like MVVMLight. Seems like a serious omission to me.
|
||||
|
||||
###### Unit testing the CounterViewModel
|
||||
|
||||
We've now got the view model for our counter, so before we use it we should test it (I know some people will say we should have written our tests before writing the view model but hey ho). For this we need a basic class library called `StupendousCounter.Core.Tests`.
|
||||
|
||||
<div class="image-div" style="width: 700px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
We can add the same `NUnit` and `FluentAssertions` nuget packages as we did for the Android bases tests, and `MVVMLightLibs`. We also want to install [Moq](https://www.nuget.org/packages/Moq/), an enjoyable mocking library that we can use to mock out the database layer. The `CommonServiceLocator` shown in the picture below is a dependency of `MVVMLightLibs` and is installed automatically.
|
||||
|
||||
<div class="image-div" style="width: 700px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
Now we have our packages installed, lets delete the auto created `Class1.cs` and add our `CounterViewModelTests` class. I like to keep the structure of the tests project the same as the project it's testing so I've created it in the `ViewModel` folder.
|
||||
|
||||
<div class="image-div" style="width: 300px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
```
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using StupendousCounter.Core.ViewModel;
|
||||
|
||||
namespace StupendousCounter.Core.Tests.ViewModel
|
||||
{
|
||||
[TestFixture]
|
||||
public class CounterViewModelTests
|
||||
{
|
||||
private Mock<IDatabaseHelper> _mockDatabaseHelper;
|
||||
|
||||
private CounterViewModel CreateCounterViewModel(int value = 10, string description = "Bar", string name = "Foo")
|
||||
{
|
||||
var counter = new Counter { Name = name, Description = description, Value = value };
|
||||
return new CounterViewModel(counter, _mockDatabaseHelper.Object);
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_mockDatabaseHelper = new Mock<IDatabaseHelper>();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NameShouldComeFromTheCounter()
|
||||
{
|
||||
CreateCounterViewModel().Name.Should().Be("Foo");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DescriptionShouldComeFromTheCounter()
|
||||
{
|
||||
CreateCounterViewModel().Description.Should().Be("Bar");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ValueShouldComeFromTheCounter()
|
||||
{
|
||||
CreateCounterViewModel().Value.Should().Be("10");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExecutingTheIncrementCommandShouldIncrementTheValueInTheDatabaseHelper()
|
||||
{
|
||||
_mockDatabaseHelper.Setup(d => d.IncrementCounterAsync(It.IsAny<Counter>())).Callback<Counter>(c => ++c.Value);
|
||||
var counter = new Counter {Value = 10};
|
||||
var vm = new CounterViewModel(counter, _mockDatabaseHelper.Object);
|
||||
vm.IncrementCommand.Execute(null);
|
||||
_mockDatabaseHelper.Verify(d => d.IncrementCounterAsync(counter));
|
||||
vm.Value.Should().Be("11");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExecutingTheIncrementCommandShouldRaiseAPropertyChangeForValue()
|
||||
{
|
||||
var vm = CreateCounterViewModel();
|
||||
vm.MonitorEvents();
|
||||
vm.IncrementCommand.Execute(null);
|
||||
vm.ShouldRaisePropertyChangeFor(v => vm.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Some of the tests should be self-explanatory. The `xxxShouldComeFromTheCounter` tests verify that the values on the view model actually come from the counter. These are great sanity tests as it's easy to copy/past properties when creating lots of passthroughs in bigger view models and forget to update the underlying property name. For the value test we are expecting a string value, so we're comparing against a string representation of the set value.
|
||||
|
||||
`ExecutingTheIncrementCommandShouldIncrementTheValueInTheDatabaseHelper` is a bit more interesting. This tests 2 things - firstly that the database helper is used to increment the counter (remember our architecture is such that the incrementation is done in the database helper to ensure we store the history, and secondly that the value is updated in the view model. It's in this test the `Moq` is used. In the `SetUp` method a `Mock<IDatabaseHelper>` is created - this is a dummy object that we can use to provide our own implementation of the interface. It exposes an `Object` property that is the interface we're implementing and allows us to setup methods and properties to do what we want, and verify calls against them. In this case in the test were setting up the `IncrementCounterAsync` method to increment the counters value. When we execute the `IncrementCommand` it calls this method on the database helper stored in the view model, which is our mock, so our method gets called. After the command is executed we verify that the method on the Mock was actually called, and validate that the value was incremented. Mocks are really handy as we've managed to test code that calls the database helper without actually needing to set up a SQLite db.
|
||||
|
||||
The final test `ExecutingTheIncrementCommandShouldRaiseAPropertyChangeForValue` is interesting as well. This is to validate that our UI is going to be updated. We can take it for granted that if a `PropertyChanged` event is fired then the UI will be updated as we will be binding this property using MVVMLight later, so all we need to do is ensure that when we execute the `IncrementCommand` the correct property change event is fired. FluentAssertions has some nice extension methods to ensure property change events are fired and these take expressions allowing the property in question to be defined in code instead of using a magic string - less fragile if the property name changes.
|
||||
|
||||
If you run these tests they should all pass allowing us to have some confidence in this view model.
|
||||
|
||||
<div class="image-div" style="width: 400px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
###### CountersVieModel
|
||||
|
||||
Once we have a view model for the individual counters we can create one for the overall list of counters. For this we will create a new class, `CountersViewModel`. Like the `CounterViewModel` above this will also derive from `ViewModelBase`.
|
||||
|
||||
```
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Threading.Tasks;
|
||||
using GalaSoft.MvvmLight;
|
||||
|
||||
namespace StupendousCounter.Core.ViewModel
|
||||
{
|
||||
public class CountersViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IDatabaseHelper _databaseHelper;
|
||||
|
||||
private readonly ObservableCollection<CounterViewModel> _counters = new ObservableCollection<CounterViewModel>();
|
||||
public ReadOnlyObservableCollection<CounterViewModel> Counters { get; private set; }
|
||||
|
||||
public CountersViewModel(IDatabaseHelper databaseHelper)
|
||||
{
|
||||
_databaseHelper = databaseHelper;
|
||||
Counters = new ReadOnlyObservableCollection<CounterViewModel>(_counters);
|
||||
}
|
||||
|
||||
public async Task LoadCountersAsync()
|
||||
{
|
||||
var counters = await _databaseHelper.GetAllCountersAsync();
|
||||
foreach (var counter in counters)
|
||||
{
|
||||
_counters.Add(new CounterViewModel(counter, _databaseHelper));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is quite a simple view model - it takes the database helper and exposes a method to load the counters from the database and wrap them all in the appropriate view model. These counters are exposed as a `ReadonlyObservableCollection<CounterViewModel>`, as opposed to a standard `ObservableCollection<CounterViewModel>`. Most people just use observable collections to expose collections of objects in the view model, but I prefer to expose them as a read only collection. The reason for this is the public properties and methods expose the intent and capabilities of a class, and to me exposng an observable collection that can be modified suggests that it is perfectly acceptable to modify the collection externally. In this case we don't want it to be modified externally, and when we implement an add button we will do it via a command that adds the new counter instead of by modifiying the collection directly. Therefore by exposing it as a read only collection we are saying that we explicitly don't want anyone else to modify it.
|
||||
|
||||
###### Unit testing the CountersViewModel
|
||||
|
||||
Despite being a simple view model we still shoudl write tests - always good to get as much converage as possible to catch any silly errors. At the moment all we need to test is that when we load the conters they are loaded correctly so this is a simple unit test class:
|
||||
|
||||
```
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using StupendousCounter.Core.ViewModel;
|
||||
|
||||
namespace StupendousCounter.Core.Tests.ViewModel
|
||||
{
|
||||
[TestFixture]
|
||||
public class CountersViewModelTests
|
||||
{
|
||||
private Mock<IDatabaseHelper> _mockDatabaseHelper;
|
||||
|
||||
private readonly Counter _monkeyCounter = new Counter
|
||||
{
|
||||
Name = "Monkey Count",
|
||||
Description = "The number of monkeys",
|
||||
Value = 10
|
||||
};
|
||||
|
||||
private readonly Counter _platypusCounter = new Counter
|
||||
{
|
||||
Name = "Playtpus Count",
|
||||
Description = "The number of duck-billed platypuses",
|
||||
Value = 4
|
||||
};
|
||||
|
||||
private static bool Matches(CounterViewModel cvm, Counter counter)
|
||||
{
|
||||
return cvm.Name == counter.Name &&
|
||||
cvm.Description == counter.Description &&
|
||||
cvm.Value == counter.Value.ToString("N0");
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_mockDatabaseHelper = new Mock<IDatabaseHelper>();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task LoadCountersAsyncShouldLoadTheCountersFromTheDatabase()
|
||||
{
|
||||
_mockDatabaseHelper.Setup(d => d.GetAllCountersAsync()).ReturnsAsync(new List<Counter> {_monkeyCounter, _platypusCounter});
|
||||
var vm = new CountersViewModel(_mockDatabaseHelper.Object);
|
||||
await vm.LoadCountersAsync();
|
||||
vm.Counters.Should().HaveCount(2);
|
||||
vm.Counters.Should().Contain(c => Matches(c, _monkeyCounter));
|
||||
vm.Counters.Should().Contain(c => Matches(c, _platypusCounter));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The single test here will load the counters and ensure they match the ones we've mocked the database helper to return. The only lightly new thing here is this test is async - note the `async Task` return type. This tells NUnit to run these using async/await so inside the tests we can `await` any async calls, like the `LoadCountersAsync` method on the database helper.
|
||||
|
||||
###### Exposing our view models
|
||||
|
||||
The autogenerated classes provided by MVVMLight expose a main view model and a view model locator. The `MainViewModel.cs` file isn't really of any use to us, so it can be deleted.
|
||||
The `ViewModelLocator` however is useful. It's an implementation of the service locator pattern using SimpleIoC internally. Once tidied up and changed to suit our needs it looks like this:
|
||||
|
||||
```
|
||||
using GalaSoft.MvvmLight.Ioc;
|
||||
using Microsoft.Practices.ServiceLocation;
|
||||
|
||||
namespace StupendousCounter.Core.ViewModel
|
||||
{
|
||||
public class ViewModelLocator
|
||||
{
|
||||
static ViewModelLocator()
|
||||
{
|
||||
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
|
||||
|
||||
SimpleIoc.Default.Register<IDatabaseHelper>(() => new DatabaseHelper());
|
||||
SimpleIoc.Default.Register<CountersViewModel>();
|
||||
}
|
||||
|
||||
public static CountersViewModel Counters => ServiceLocator.Current.GetInstance<CountersViewModel>();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The hordes of comments have been removed as we don't really need them. The clean method is also gone as again we don't need it. The important change though is in the static constructor where we register a new `DatabaseHelper` instance as an `IDatabaseHelper`, as well as registering the `CountersViewModel` type. These are registered against the `SimpleIoc.Default` instance, which is set up as the locator provider for the `ServiceLocator`. `SimpleIoc.Default` is a default instance of the `SimpleIoc` container, and anything registered in a container instance is effectively a singleton inside that instance. Having this default instance set up as the service locators provider means that whenever we access the `ServiceLocator.Current` singleton and get an instance we will get the single instance that we registered.
|
||||
For example out `Counters` property gets the instance of `CountersViewModel` from the service locator, which in turn gets it from the default SimpleIoc instance, which checks if it has an instance of the type and if so returns it, if not it creates a new one injecting any constructor parameters using types also registered in the SimpleIoc instance.
|
||||
If none of this makes sense then I suggest you stop reading here and read up on the [dependency injection pattern](https://en.wikipedia.org/wiki/Dependency_injection) and [service locator patterns](https://en.wikipedia.org/wiki/Service_locator_pattern). These are pretty standard, allow for nicely decoupled applications, and are pretty fundamental to a good MVVM implementation.
|
||||
|
||||
The code for this can be found in GitHub on the Part3 branch at https://github.com/jimbobbennett/StupendousCounter/tree/Part3
|
||||
|
||||
In the [next part](/blogs/building-a-xamarin-android-app-part-4/) we'll work on the actual UI and start wiring everything up.
|
||||
|
||||
|
||||
<hr/>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<p>
|
||||
<table cellspacing="0" cellpadding="0" style='border: none;border-collapse: collapse;'>
|
||||
<tr style='padding: 0;'>
|
||||
<td style='padding: 0;vertical-align: top;'>
|
||||
<iframe style="width:120px;height:240px;" marginwidth="0" marginheight="0" scrolling="no" frameborder="0" src="//ws-eu.amazon-adsystem.com/widgets/q?ServiceVersion=20070822&OneJS=1&Operation=GetAdHtml&MarketPlace=GB&source=ss&ref=ss_til&ad_type=product_link&tracking_id=expecti-21&marketplace=amazon®ion=GB&placement=B0000250CZ&asins=B0000250CZ&linkId=&show_border=false&link_opens_in_new_window=true">
|
||||
</iframe>
|
||||
</td>
|
||||
<td style='padding: 0px 30px;vertical-align: top;'>
|
||||
<iframe style="width:120px;height:240px;" marginwidth="0" marginheight="0" scrolling="no" frameborder="0" src="//ws-eu.amazon-adsystem.com/widgets/q?ServiceVersion=20070822&OneJS=1&Operation=GetAdHtml&MarketPlace=GB&source=ss&ref=ss_til&ad_type=product_link&tracking_id=expecti-21&marketplace=amazon®ion=GB&placement=B00W3G9LQI&asins=B00W3G9LQI&linkId=&show_border=false&link_opens_in_new_window=true">
|
||||
</iframe>
|
||||
</td>
|
||||
<td style='padding: 0px 30px;vertical-align: top;'>
|
||||
<iframe style="width:120px;height:240px;" marginwidth="0" marginheight="0" scrolling="no" frameborder="0" src="//ws-eu.amazon-adsystem.com/widgets/q?ServiceVersion=20070822&OneJS=1&Operation=GetAdHtml&MarketPlace=GB&source=ss&ref=ss_til&ad_type=product_link&tracking_id=expecti-21&marketplace=amazon®ion=GB&placement=B00533Y2KI&asins=B00533Y2KI&linkId=&show_border=false&link_opens_in_new_window=true">
|
||||
</iframe>
|
||||
</td>
|
||||
<td style='padding: 0px 30px;'>
|
||||
<p style='color:rgb(104, 104, 104);!important;'>Continuing on from the last post where I mentioned what I was listening to whilst developing, today I'm listening to <a href='http://www.heathernova.com'>Heather Nova</a></p>
|
||||
<p style='color:rgb(104, 104, 104);!important;'>Note - these are an affiliate links - if you click them and buy I get a small cut.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 186 KiB |
@@ -0,0 +1,386 @@
|
||||
---
|
||||
author: "Jim Bennett"
|
||||
categories: ["xamarin", "xamarin.android", "tutorial", "mvvmlight", "Technology", "recycler view", "UI"]
|
||||
date: 2016-02-01T07:36:22Z
|
||||
description: ""
|
||||
draft: false
|
||||
images:
|
||||
- /blogs/building-a-xamarin-android-app-part-4/banner.png
|
||||
featured_image: banner.png
|
||||
slug: "building-a-xamarin-android-app-part-4"
|
||||
tags: ["xamarin", "xamarin.android", "tutorial", "mvvmlight", "Technology", "recycler view", "UI"]
|
||||
title: "Building a Xamarin Android app - part 4"
|
||||
|
||||
images:
|
||||
- /blogs/building-a-xamarin-android-app-part-4/banner.png
|
||||
featured_image: banner.png
|
||||
---
|
||||
|
||||
|
||||
This is the fourth part in the my series about building an Android app using Xamarin.Android. You can find the first part [here](/blogs/building-an-android-app-part-1/), the second part [here](/blogs/building-an-android-app-part-2/) and the third part [here](/blogs/building-a-xamarin-android-app-part-3/), and I highly recommend reading these first.
|
||||
|
||||
|
||||
#### Binding our view models to the UI
|
||||
|
||||
We have our models, we have our view models, now to work on the views!
|
||||
First thing we need to do is a bit of a tidy up - the default UI code we've picked up from our templates doesn't match what we want to show in screen, so lets start by clearing everything up a bit.
|
||||
|
||||
The UI we have has a navigation drawer with 2 screens you can select, as well as some sub menu options. The screen selection options load one of two possible fragments into our UI. We can repurpose these - one fragment to show our counters and one to show an about screen so we can tell the world who created such a stupendous counter app!
|
||||
|
||||
Stating with the first fragment we can rename the class from `Fragment1` to `CountersFragment`, as well as renaming the associated layout from `fragment1.axml` to `counters_fragment.axml`. After renaming the layout file we also need to change the id that is used in the `CountersFragment.OnCreateView` method to reflect the new name:
|
||||
|
||||
```
|
||||
return inflater.Inflate(Resource.Layout.counters_fragment, null);
|
||||
```
|
||||
|
||||
For the second one we can rename it from `Fragment2` to `AboutFragment` and `fragment2.axml` to `about_fragment.axml`, and again updating the id:
|
||||
|
||||
```
|
||||
return inflater.Inflate(Resource.Layout.about_fragment, null);
|
||||
```
|
||||
|
||||
<div class="image-div" style="width: 300px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
Now is also a good time to update the menu in `menu\nav_menu.xml` to remove the unwanted sub items and rename the main items:
|
||||
|
||||
```
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<group android:checkableBehavior="single">
|
||||
<item
|
||||
android:id="@+id/nav_counters"
|
||||
android:icon="@drawable/ic_add_circle_black_48dp"
|
||||
android:title="Counters" />
|
||||
<item
|
||||
android:id="@+id/nav_about"
|
||||
android:icon="@drawable/ic_settings_black_48dp"
|
||||
android:title="About" />
|
||||
</group>
|
||||
|
||||
</menu>
|
||||
```
|
||||
|
||||
You'll notice the icons have changed to items not in our drawables folder, so we'll need to add these. The icons we're using are from the Google material icons - you can download them from https://design.google.com/icons/. You'll need to download the 'add circle' and 'settings' icons as pngs and copy them from the drawables folders of the downloads to the same named drawables folders locally, then add them to the project.
|
||||
|
||||
<div class="image-div" style="width: 300px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
Changing the menu ids will break our `MainActivity` so we need to fix the `OnCreate` method by changing the subscription to the `NavigationView.NavigationItemSelected` event. At the same time we can remove the snack bar call as we don't want one popping up when we change the fragment.
|
||||
|
||||
```
|
||||
navigationView.NavigationItemSelected += (sender, e) =>
|
||||
{
|
||||
e.MenuItem.SetChecked(true);
|
||||
|
||||
switch (e.MenuItem.ItemId)
|
||||
{
|
||||
case Resource.Id.nav_counters:
|
||||
ListItemClicked(0);
|
||||
break;
|
||||
case Resource.Id.nav_about:
|
||||
ListItemClicked(1);
|
||||
break;
|
||||
}
|
||||
|
||||
drawerLayout.CloseDrawers();
|
||||
};
|
||||
```
|
||||
|
||||
###### CountersFragment
|
||||
|
||||
In this fragment we want to display a list of all the counters that we have stored. The latest and greates way to do this is with a `RecyclerView` which is documented [here on the Xamarin docs](https://developer.xamarin.com/guides/android/user_interface/recyclerview/). This is like a list view but enforces good design and ensures the views created are always re-used when they go off screen reducing the memory footprint. It also enforces the use of the [view holder pattern](https://blog.xamarin.com/creating-highly-performant-smooth-scrolling-android-listviews/) to futher improve performance.
|
||||
|
||||
To use the recycler view we need to add a nuget package to our `StupendoudCounter.Droid` project - `Xamarin.Android.Support.v7.RecyclerView`. This provides the recycler view for all versions of Android from API level 7 and above.
|
||||
|
||||
|
||||
<div class="image-div" style="width: 600px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
Once we have our nuget package installed we can add the recycler view to our UI and create it's backing field. In `counters_fragment.axml` add the recycler view:
|
||||
|
||||
```
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent">
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/countersRecyclerView"
|
||||
android:scrollbars="vertical"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent" />
|
||||
</LinearLayout>
|
||||
```
|
||||
|
||||
In `CountersFragment` add a field for the recycler view and find it from the view in the `OnCreateView` method:
|
||||
|
||||
```
|
||||
using Android.Support.V7.Widget;
|
||||
|
||||
...
|
||||
|
||||
private RecyclerView _recyclerView;
|
||||
|
||||
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
|
||||
{
|
||||
var ignored = base.OnCreateView(inflater, container, savedInstanceState);
|
||||
var view = inflater.Inflate(Resource.Layout.counters_fragment, null);
|
||||
_recyclerView = view.FindViewById<RecyclerView>(Resource.Id.countersRecyclerView);
|
||||
return view;
|
||||
}
|
||||
```
|
||||
|
||||
To use this recycler view we need to use or implement a few things:
|
||||
|
||||
* A layout manager
|
||||
* An adapter
|
||||
* A view holder
|
||||
|
||||
###### Layout manager
|
||||
Each instance of a recycler view has a layout manager - this determines how the items in the view are laid out. Android provides 3 basic ones which are good enough for most cases, but you can implement your own if you wish. There's `LinearLayoutManager` which displays the items in a horizontal or vertical list, `GridLayoutManager` that displayes the items in a grid and `StaggeredGridLayoutManager` which displays them in a grid with uneven rows or columns.
|
||||
For what we need the `LinearLayoutManager` is good enough, so we can create one and set it on our recycler view:
|
||||
|
||||
```
|
||||
_recyclerView.SetLayoutManager(new LinearLayoutManager(Context, LinearLayoutManager.Vertical, false));
|
||||
```
|
||||
|
||||
The three parameters for the constructor are the current context which we can get from the `Context` property, the orientation for which we are using the `Vertical` constant defined on `LinearLayoutManager`, and a boolean to say if the items should be reveresed or not when we show them - so should we show the items in our list from top to bottom, or bottom to top (reverse is useful when adding new items to the end of a list but showing them in latest-first order, such as an email client order by date).
|
||||
|
||||
###### Adapter
|
||||
The adapter's job is to act like a view model for the recycler view - it needs to know about the collection of items we are showing in the list and be able to tell the recycler view how many there are and needs to be able to create the views for items in the collection where necessary or recycle them to be used by other items in the collection.
|
||||
All adapters need three things - a class derived from `RecyclerView.Adapter`, a view to create to show the item and a view holder that maps the items in the collection to the view. To create the adapter create a class called `CountersAdapter`:
|
||||
|
||||
```
|
||||
using Android.Support.V7.Widget;
|
||||
using Android.Views;
|
||||
using StupendousCounter.Core.ViewModel;
|
||||
|
||||
namespace StupendousCounter.Droid.Fragments
|
||||
{
|
||||
public class CountersAdapter : RecyclerView.Adapter
|
||||
{
|
||||
public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int position)
|
||||
{
|
||||
var item = ViewModelLocator.Counters.Counters[position];
|
||||
((CounterViewHolder) holder).BindCounterViewModel(item);
|
||||
}
|
||||
|
||||
public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int viewType)
|
||||
{
|
||||
var itemView = LayoutInflater.From(parent.Context).Inflate(Resource.Layout.counter_view, parent, false);
|
||||
return new CounterViewHolder(itemView);
|
||||
}
|
||||
|
||||
public override int ItemCount => ViewModelLocator.Counters.Counters.Count;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When we override `RecyclerView.Adapter` we have to implement three things - `ItemCount`, `OnCreateViewHolder` and `OnBindVewHolder`.
|
||||
|
||||
`ItemCount` just needs to return the number of items in the collection. This just returns the count from the `CountersViewModel` instance from the static `ViewModelLocator`.
|
||||
|
||||
`OnCreateViewHolder` is called whenever an item in the recycler view is created for the first time. This needs to create a view and wrap it in a class derived from `RecyclerView.ViewHolder`.
|
||||
|
||||
`OnBindViewHolder` is responsible for updating the view holder to reflect the relevant item in the collection. The item is given by the `position` parameter - this indicates the position in the collection of the item we need to show in the view. In our code we are using this to get the item from our view model which we access using the static `ViewModelLocator`, and this is passed to a method on the view holder to populate it. In a lot of code you will see the view holder updated directly here with the controls in the view holder exposed as public properties, but I prefer to encapsulate the controls inside the view holder and have a single method to call to update the view. This means if the view changes the adapter doesn't need to change.
|
||||
|
||||
These last two are the basis of how the recycler view works - it calls `OnCreateViewHolder` to create just enough views to fill the screen, then calls `OnBingViewHolder` to show the data. As the collection is scrolled instead of creating new views, the views that are no longer visible are re-used. So if you scroll down a a view disappears off the top it is moved to the bottom to remove the overhead of creating a new view. To make sure it shows the right data `OnBindViewHolder` is called to update the view to show the correct data.
|
||||
|
||||
To create the view add a new layout called `counter_view.xml`:
|
||||
|
||||
```
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:card_view="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
card_view:cardElevation="8dp"
|
||||
card_view:cardCornerRadius="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginTop="8dp">
|
||||
<GridLayout
|
||||
android:minWidth="25px"
|
||||
android:minHeight="25px"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:columnCount="3"
|
||||
android:rowCount="1">
|
||||
<TextView
|
||||
android:id="@+id/counter_value"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="10"
|
||||
android:layout_row="0"
|
||||
android:layout_column="1"
|
||||
android:textSize="48sp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginRight="16sp"
|
||||
android:textColor="@color/primaryDark" />
|
||||
<ImageButton
|
||||
android:id="@+id/counter_increment"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_row="0"
|
||||
android:layout_column="2"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="16sp"
|
||||
android:src="@drawable/ic_add_circle_black_48dp"
|
||||
android:background="#00000000"/>
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_gravity="fill"
|
||||
android:layout_row="0"
|
||||
android:layout_column="0"
|
||||
android:padding="16sp">
|
||||
<TextView
|
||||
android:id="@+id/counter_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Name"
|
||||
android:padding="4sp"
|
||||
android:textSize="24sp"
|
||||
android:textColor="@color/primaryText" />
|
||||
<TextView
|
||||
android:id="@+id/counter_description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="The counters description"
|
||||
android:padding="4sp"
|
||||
android:textSize="16sp" />
|
||||
</LinearLayout>
|
||||
</GridLayout>
|
||||
</android.support.v7.widget.CardView>
|
||||
```
|
||||
|
||||
This view uses a `CardView` which is documented [here on the Xamarin docs](https://developer.xamarin.com/guides/android/user_interface/cardview/). Inside the `CardView` there is a `GridLayout` and `LinearLayout` to layout the various widgets, three `TextViews` to show the counter details and value, and an `ImageButton` to allow the counter to be incremented. The view looks like this:
|
||||
|
||||
<div class="image-div" style="width: 400px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
The `GridLayout` is used to layout 3 columns - one for the counter details, one for the count and one for the increment button. Inside the first column is the `LinearLayout` that shows the counters name and description as a vertical layout.
|
||||
|
||||
###### ViewHolder
|
||||
The view holder's job is to create backing fields for the controls in the view to improve performance by only having to find the controls by id once per instance of the view. To create the view holder create a class called `CounterViewHolder`:
|
||||
|
||||
```
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using Android.App;
|
||||
using Android.Graphics;
|
||||
using Android.Support.V4.Content;
|
||||
using Android.Support.V7.Widget;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
using StupendousCounter.Core.ViewModel;
|
||||
|
||||
namespace StupendousCounter.Droid.Fragments
|
||||
{
|
||||
public class CounterViewHolder : RecyclerView.ViewHolder
|
||||
{
|
||||
private readonly TextView _name;
|
||||
private readonly TextView _description;
|
||||
private readonly TextView _value;
|
||||
|
||||
private CounterViewModel _counterViewModel;
|
||||
|
||||
public CounterViewHolder(View itemView) : base(itemView)
|
||||
{
|
||||
_name = itemView.FindViewById<TextView>(Resource.Id.counter_name);
|
||||
_description = itemView.FindViewById<TextView>(Resource.Id.counter_description);
|
||||
_value = itemView.FindViewById<TextView>(Resource.Id.counter_value);
|
||||
|
||||
var increment = itemView.FindViewById<ImageButton>(Resource.Id.counter_increment);
|
||||
increment.SetColorFilter(new Color(ContextCompat.GetColor(Application.Context, Resource.Color.primaryDark)));
|
||||
increment.Click += IncrementOnClick;
|
||||
}
|
||||
|
||||
private void IncrementOnClick(object sender, EventArgs eventArgs)
|
||||
{
|
||||
_counterViewModel.IncrementCommand.Execute(null);
|
||||
}
|
||||
|
||||
public void BindCounterViewModel(CounterViewModel counterViewModel)
|
||||
{
|
||||
if (_counterViewModel != null)
|
||||
_counterViewModel.PropertyChanged -= CounterViewModelOnPropertyChanged;
|
||||
|
||||
_counterViewModel = counterViewModel;
|
||||
_counterViewModel.PropertyChanged += CounterViewModelOnPropertyChanged;
|
||||
|
||||
_name.Text = counterViewModel.Name;
|
||||
_description.Text = counterViewModel.Description;
|
||||
_value.Text = counterViewModel.Value;
|
||||
}
|
||||
|
||||
private void CounterViewModelOnPropertyChanged(object sender, PropertyChangedEventArgs args)
|
||||
{
|
||||
if (args.PropertyName == nameof(CounterViewModel.Value))
|
||||
_value.Text = _counterViewModel.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This class derives from `RecyclerView.ViewHolder`. In the constructor a view is passed in - and this is the view created by our adapter. In here we are manually going to wire up the values for the counter view model to the view. Although we are using MVVMLight for our view models which exposes a binding mechanism this currently doesn't work with view holders (although Laurent tells me it should do in a couple of weeks time so stay tuned for an update), so we have to do it all manually.
|
||||
|
||||
The first thing we do is grab the name, description and value text edit fields and store these. `FindById` is slow, hence why we only want to do this once per view instance and store the found controls.
|
||||
For the image button we don't need to store it in our class, we just need to wire up the `Click` event so we can respond to it. We also call `SetColorFilter` on the button - this is because the icon that was downloaded from Google material design images is a black button and we want it to match our theme. `SetColorFilter` will change the colour of the button to the given colour, giving a nice purple button.
|
||||
|
||||
In the adapter in the `OnBindViewHolder` method we delegated the updating of the UI to a method on the view holder, and this is implemented here in the `BindCounterViewModel` method. This method takes a `CounterViewModel` that refers to the item in the relevant position in the collection, and this is stored in a field. The name, description and value controls are updated to match the view model. We also subscribe to the `PropertyChanged` event so that the value can be updated when it changes on the view model - such as when the increment button is pressed. To avoid the wrong counters being incremented we also unsubscribe from this event from the view model stored in our field if it is set before we update it to store the one passed in.
|
||||
|
||||
Once the view model is stored, we can increment it when the increment button is clicked. In the click event handler (`IncrementOnClick`) we execute the `IncrementCommand` which will cause the value to increment and be updated in SQLite as shown in the previous post. This will also cause the `PropertyChange` event to be fired for the `Value` property, which we handle and update the UI to reflect the new value.
|
||||
|
||||
#### Lets try it out
|
||||
That should be everything we need to do to show some dummy data and increment the counters, so lets build it, run it and try it out. Click on the plus button to increment each counter, then try closing and re-opening the app - you'll notice the values are persisted thanks to our SQLite db.
|
||||
|
||||
<div class="image-div" style="width: 400px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
The code for this can be found in GitHub on the Part4 branch at https://github.com/jimbobbennett/StupendousCounter/tree/Part4
|
||||
|
||||
In the next part we'll work on adding an Add button to add a new counter.
|
||||
|
||||
<hr/>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<p>
|
||||
<table cellspacing="0" cellpadding="0" style='border: none;border-collapse: collapse;'>
|
||||
<tr style='padding: 0;'>
|
||||
<td style='padding: 0;vertical-align: top;'>
|
||||
<iframe style="width:120px;height:240px;" marginwidth="0" marginheight="0" scrolling="no" frameborder="0" src="//ws-eu.amazon-adsystem.com/widgets/q?ServiceVersion=20070822&OneJS=1&Operation=GetAdHtml&MarketPlace=GB&source=ss&ref=ss_til&ad_type=product_link&tracking_id=expecti-21&marketplace=amazon®ion=GB&placement=B00L1WB9H4&asins=B00L1WB9H4&linkId=&show_border=false&link_opens_in_new_window=true">
|
||||
</iframe>
|
||||
</td>
|
||||
<td style='padding: 0px 30px;'>
|
||||
<p style='color:rgb(104, 104, 104);!important;'>Continuing on from the last post where I mentioned what I was listening to whilst developing, today I'm listening to <a href='http://sleepingatlast.com'>Sleeping at Last</a></p>
|
||||
<p style='color:rgb(104, 104, 104);!important;'>Note - these are an affiliate links - if you click them and buy I get a small cut.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</p>
|
||||
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 84 KiB |
@@ -0,0 +1,616 @@
|
||||
---
|
||||
author: "Jim Bennett"
|
||||
categories: ["xamarin", "xamarin.android", "mvvmlight", "binding", "Technology", "UI", "tutorial"]
|
||||
date: 2016-02-18T08:59:14Z
|
||||
description: ""
|
||||
draft: false
|
||||
images:
|
||||
- /blogs/building-a-xamarin-android-app-part-5/banner.png
|
||||
featured_image: banner.png
|
||||
slug: "building-a-xamarin-android-app-part-5"
|
||||
tags: ["xamarin", "xamarin.android", "mvvmlight", "binding", "Technology", "UI", "tutorial"]
|
||||
title: "Building a Xamarin Android app - part 5"
|
||||
|
||||
images:
|
||||
- /blogs/building-a-xamarin-android-app-part-5/banner.png
|
||||
featured_image: banner.png
|
||||
---
|
||||
|
||||
|
||||
This is the fifth part in the my series about building an Android app using Xamarin.Android. I highly recommend reading these first.
|
||||
The previous parts are:
|
||||
|
||||
* [Creating the basic app](/blogs/building-an-android-app-part-1/)
|
||||
* [Defining our data](/blogs/building-an-android-app-part-2/)
|
||||
* [Building view models](/blogs/building-a-xamarin-android-app-part-3/)
|
||||
* [Binding the view models to the UI](/blogs/building-a-xamarin-android-app-part-4/)
|
||||
|
||||
#### Adding the Add button
|
||||
Currently we have a nice recycler view showing our dummy counters in card views. So the next step is to allow new counters to be added. The current way to add new items is using a floating action button - a button at the bottom of the screen that when pressed will add the new counter.
|
||||
|
||||
<div class="image-div" style="width: 300px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
First thing to do is add it to the layout in `counters_fragment.axml`. At the moment this contains a `LinearLayout` but we'll need to change that to a `FrameLayout` so that the add button is correctly placed on top. We're not using a normal button here, but a `FloatingActionButton`, a button designed to float above other controls in the view. This means we can add a load of counters and scroll the recycler view up and down without the add button moving from it's floating location in the bottom right.
|
||||
|
||||
```
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent">
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/countersRecyclerView"
|
||||
android:scrollbars="vertical"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:background="@color/divider" />
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
android:id="@+id/floatingAddNewCounterButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|right"
|
||||
android:layout_margin="16dp"
|
||||
android:elevation="4dp"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_add_white_24dp"
|
||||
app:borderWidth="0dp" />
|
||||
</FrameLayout>
|
||||
```
|
||||
|
||||
The icon being used here, `ic_add_white_24dp`, also comes from Googles material icons at https://design.google.com/icons/. Its a 24dp icon, the recommended size for floating action buttons. We don't need to set the colour of the action button, this automatically comes from the colours we added from Material Palette in [part 1](/blogs/building-an-android-app-part-1/).
|
||||
|
||||
#### Adding navigation
|
||||
Now we have the add button, we need to think about what it needs to do. We need this button to navigate to another activity where the user can enter details about the new counter and save it. MVVMLight has a nice navigation system but we are limited in how we can use it because we are using AppCompat. Luckily I've already created a workaround which is documented [on another blog post here](/blogs/mvvmlight-navigation-and-appcompatactivity/). To add this working navigation we just need to add [the MVVMLight.AppCompat nuget package](https://www.nuget.org/packages/JimBobBennett.MvvmLight.AppCompat/), and set it up. We start with the set up by changing our `BaseActivity` to derive from `AppCompatActivityBase`.
|
||||
```
|
||||
public abstract class BaseActivity : AppCompatActivityBase
|
||||
```
|
||||
Then we need to register the navigation and dialog services with the IoC container. The concrete implementations of the services are platform specific, so we can't simply register them in the view model locator. Instead we have to expose methods to allow the registration to happen externally so that we can call it from our Android project. The following methods to register need to be added to the `ViewModelLoator`:
|
||||
```
|
||||
public static void RegisterNavigationService(INavigationService navigationService)
|
||||
{
|
||||
SimpleIoc.Default.Register(() => navigationService);
|
||||
}
|
||||
|
||||
public static void RegisterDialogService(IDialogService dialogService)
|
||||
{
|
||||
SimpleIoc.Default.Register(() => dialogService);
|
||||
}
|
||||
```
|
||||
These are then called from our `MainActivity` in a new constructor:
|
||||
```
|
||||
public MainActivity()
|
||||
{
|
||||
var navigationService = new AppCompatNavigationService();
|
||||
ViewModelLocator.RegisterNavigationService(navigationService);
|
||||
ViewModelLocator.RegisterDialogService(new AppCompatDialogService());
|
||||
}
|
||||
```
|
||||
Whilst we're editing the `MainActivity` it makes sense to delete the dummy counter code as well - seeing as we are adding the ability to add new counters we don't need to pre-populate the app with fake counters. Just delete the `AddDummyData` method and the call to it.
|
||||
|
||||
#### Creating the new counter
|
||||
To create the new counter we need to define a new activity to allow the user to enter details about the counter. This activity needs a layout, a view model and to be wired into the navigation so that we can navigate to it from our new add button.
|
||||
|
||||
###### Layout
|
||||
Lets start with the layout. This needs to have text boxes so the user can enter the name and description for the counter, along with a button to create the new counter. We're not going to bother with a cancel option - the UI will require the create button to be clicked to create the counter with back acting as a cancel, either the back button on the tool bar or the hardware back button.
|
||||
|
||||
```
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<android.support.design.widget.AppBarLayout
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:id="@+id/toolbar_layout">
|
||||
<include
|
||||
android:id="@+id/toolbar"
|
||||
layout="@layout/toolbar"
|
||||
app:layout_scrollFlags="scroll|enterAlways" />
|
||||
</android.support.design.widget.AppBarLayout>
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="24dp">
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp">
|
||||
<EditText
|
||||
android:id="@+id/new_counter_name"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="Name" />
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp">
|
||||
<EditText
|
||||
android:id="@+id/new_counter_description"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="Description" />
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
<Button
|
||||
android:text="Create Counter"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/new_counter_create" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
```
|
||||
This is a standard app bar layout showing the toolbar, same as the main layout, with text boxes for the name and description, and a 'Create Counter' button. The cool thing we're doing here is the edit boxes. We're not just using a boring label with a text box below, instead we're using a `TextInputLayout`. This wraps the `EditText` so you see a hint inside the text box, then when you touch inside to enter the text the hint moves to above the box.
|
||||
|
||||
<div class="image-div" style="width: 300px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
###### View model
|
||||
Before we create an activity to show this layout, lets create the view model. We start by creating the `NewCounterViewModel` in our `ViewModel` folder. The first part of this is to create the properties to hold the name and description fields.
|
||||
|
||||
```
|
||||
public class NewCounterViewModel : ViewModelBase
|
||||
{
|
||||
private string _name;
|
||||
|
||||
public string Name
|
||||
{
|
||||
get { return _name; }
|
||||
set { Set(() => Name, ref _name, value); }
|
||||
}
|
||||
|
||||
private string _description;
|
||||
|
||||
public string Description
|
||||
{
|
||||
get { return _description; }
|
||||
set { Set(() => Description, ref _description, value); }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This view model is derived from `ViewModelBase`, and the property setters use the base class `Set` method to not only set the value but to raise the relevant property change event.
|
||||
|
||||
Now we have the data part sorted, it's time to add some commands.
|
||||
|
||||
```
|
||||
public class NewCounterViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IDatabaseHelper _databaseHelper;
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly INavigationService _navigationService;
|
||||
|
||||
public NewCounterViewModel(IDatabaseHelper databaseHelper, IDialogService dialogService, INavigationService navigationService)
|
||||
{
|
||||
_databaseHelper = databaseHelper;
|
||||
_dialogService = dialogService;
|
||||
_navigationService = navigationService;
|
||||
}
|
||||
|
||||
private RelayCommand _goBackCommand;
|
||||
public RelayCommand GoBackCommand => _goBackCommand ?? (_goBackCommand = new RelayCommand(() => _navigationService.GoBack()));
|
||||
|
||||
private RelayCommand _addCounterCommand;
|
||||
public RelayCommand AddCounterCommand => _addCounterCommand ?? (_addCounterCommand = new RelayCommand(async () => await AddCounter()));
|
||||
|
||||
private async Task AddCounter()
|
||||
{
|
||||
if (string.IsNullOrEmpty(Name))
|
||||
{
|
||||
await _dialogService.ShowError("The name must be set", "No name", "OK", null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(Description))
|
||||
{
|
||||
await _dialogService.ShowError("The description must be set", "No description", "OK", null);
|
||||
return;
|
||||
}
|
||||
|
||||
await _databaseHelper.AddOrUpdateCounterAsync(new Counter {Name = Name, Description = Description});
|
||||
_navigationService.GoBack();
|
||||
}
|
||||
}
|
||||
```
|
||||
This adds two commands.
|
||||
The first is the `GoBackCommand`, which will be wired up to the toolbars back button. This uses the navigation service that is injected into the constructor to navigate back - under the hood this pops the current activity off the stack and returns to the previous one, the same as the hardware back button.
|
||||
The second command, `AddCounterCommand` will create and add a new command. It starts with some simple validation to ensure the values are set, and if not raises an alert using the MVVMLight dialog service injected into the constructor. If this validation is passed the new counter is created, added to the database using the database helper injected into the constructor, and the activity is popped off the stack using the navigation service.
|
||||
|
||||
Like all good coders we should be testing our view model, so lets add `NewCounterViewModelTests` to the test project.
|
||||
```
|
||||
[TestFixture]
|
||||
public class NewCounterViewModelTests
|
||||
{
|
||||
private Mock<IDatabaseHelper> _mockDatabaseHelper;
|
||||
private Mock<IDialogService> _mockDialogService;
|
||||
private Mock<INavigationService> _mockNavigationService;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_mockDatabaseHelper = new Mock<IDatabaseHelper>();
|
||||
_mockDialogService = new Mock<IDialogService>();
|
||||
_mockNavigationService = new Mock<INavigationService>();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SettingTheNameRaisesAPropertyChangedEvent()
|
||||
{
|
||||
var vm = new NewCounterViewModel(_mockDatabaseHelper.Object, _mockDialogService.Object, _mockNavigationService.Object);
|
||||
vm.MonitorEvents();
|
||||
vm.Name = "Foo";
|
||||
vm.ShouldRaisePropertyChangeFor(v => v.Name);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SettingTheDescriptionRaisesAPropertyChangedEvent()
|
||||
{
|
||||
var vm = new NewCounterViewModel(_mockDatabaseHelper.Object, _mockDialogService.Object, _mockNavigationService.Object);
|
||||
vm.MonitorEvents();
|
||||
vm.Description = "Foo";
|
||||
vm.ShouldRaisePropertyChangeFor(v => v.Description);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GoBackCommandNavigatesBackwards()
|
||||
{
|
||||
var vm = new NewCounterViewModel(_mockDatabaseHelper.Object, _mockDialogService.Object, _mockNavigationService.Object);
|
||||
vm.GoBackCommand.Execute(null);
|
||||
_mockNavigationService.Verify(n => n.GoBack(), Times.Once);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddCommandRaisesAnErrorIfTheNameIsNotSet()
|
||||
{
|
||||
var vm = new NewCounterViewModel(_mockDatabaseHelper.Object, _mockDialogService.Object, _mockNavigationService.Object);
|
||||
vm.Description = "Bar";
|
||||
vm.AddCounterCommand.Execute(null);
|
||||
_mockDialogService.Verify(d => d.ShowError(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), null), Times.Once);
|
||||
_mockDatabaseHelper.Verify(d => d.AddOrUpdateCounterAsync(It.IsAny<Counter>()), Times.Never);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddCommandRaisesAnErrorIfTheDescriptionIsNotSet()
|
||||
{
|
||||
var vm = new NewCounterViewModel(_mockDatabaseHelper.Object, _mockDialogService.Object, _mockNavigationService.Object);
|
||||
vm.Name = "Foo";
|
||||
vm.AddCounterCommand.Execute(null);
|
||||
_mockDialogService.Verify(d => d.ShowError(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), null), Times.Once);
|
||||
_mockDatabaseHelper.Verify(d => d.AddOrUpdateCounterAsync(It.IsAny<Counter>()), Times.Never);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddComandAddsTheCounterAndNavigatesBack()
|
||||
{
|
||||
var vm = new NewCounterViewModel(_mockDatabaseHelper.Object, _mockDialogService.Object, _mockNavigationService.Object);
|
||||
vm.Name = "Foo";
|
||||
vm.Description = "Bar";
|
||||
vm.AddCounterCommand.Execute(null);
|
||||
_mockDialogService.Verify(d => d.ShowError(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), null), Times.Never);
|
||||
_mockDatabaseHelper.Verify(d => d.AddOrUpdateCounterAsync(It.IsAny<Counter>()), Times.Once);
|
||||
_mockNavigationService.Verify(n => n.GoBack(), Times.Once);
|
||||
}
|
||||
}
|
||||
```
|
||||
Because we're using dependency injection we can mock all the interfaces that are injected into the view model. This means we can test the behaviour of the commands ensuring the `GoBackCommand` calls the relevant `GoBack` method on the navigation service, that the navigation doesn't happen if we add a counter with data missing and that if all the information is there the counter gets added to the database. We can also check that setting the properties raises the relevant property change events. We can get pretty awesome coverage here to ensure out view model works.
|
||||
|
||||
The last thing to do with the view model is to add it to our locator. We need this so that the IoC container can resolve the constructor injection.
|
||||
|
||||
```
|
||||
static ViewModelLocator()
|
||||
{
|
||||
...
|
||||
SimpleIoc.Default.Register<NewCounterViewModel>();
|
||||
}
|
||||
|
||||
public const string NewCounterPageKey = "NewCounterPage";
|
||||
|
||||
public static NewCounterViewModel NewCounter => ServiceLocator.Current.GetInstance<NewCounterViewModel>();
|
||||
```
|
||||
|
||||
Here we register it with the IoC container in the constructor. Unfortunately SimpleIoC only supports singletons, so we have to always deal with a single instance - something we will have to consider later on.
|
||||
Once registered we can expose a static property to return the instance, and a constant that defines a key for it. This key will be registered with the navigation service once we have defined the activity.
|
||||
|
||||
###### Activity
|
||||
We have our layout and we have our view model, so now we can create the Activity that brings it all together.
|
||||
|
||||
Lets start with the basics:
|
||||
```
|
||||
[Activity(Label = "New Counter")]
|
||||
public class NewCounterActivity : BaseActivity
|
||||
{
|
||||
protected override int LayoutResource => Resource.Layout.new_counter;
|
||||
|
||||
public override bool OnOptionsItemSelected(IMenuItem item)
|
||||
{
|
||||
if (item.ItemId == Android.Resource.Id.Home)
|
||||
{
|
||||
ViewModel.GoBackCommand.Execute(null);
|
||||
return true;
|
||||
}
|
||||
return base.OnOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here we're creating the activity, derived from our `BaseActivity`. We override the `LayoutResource` to point to our new layout. We also override 'OnOptionsItemSelected' to detect if the `Home` button is pressed, executing the command on our view model to navigate backwards.
|
||||
|
||||
Next we need to wire up the `NewCounterViewModel`. We do this in the `OnCreate` method and store it in a public property (more on this later). As mentioned earlier the IoC container only stores singletons, so we need to clear the data before we can use it to stop the view showing the name and description of the previous counter that was added.
|
||||
|
||||
```
|
||||
public NewCounterViewModel ViewModel { get; private set; }
|
||||
|
||||
protected override void OnCreate(Bundle savedInstanceState)
|
||||
{
|
||||
base.OnCreate(savedInstanceState);
|
||||
ViewModel = ViewModelLocator.NewCounter;
|
||||
ViewModel.Name = string.Empty;
|
||||
ViewModel.Description = string.Empty;
|
||||
}
|
||||
```
|
||||
|
||||
Then we need to add public properties for the controls on our view.
|
||||
|
||||
```
|
||||
private EditText _name;
|
||||
public EditText Name => _name ?? (_name = FindViewById<EditText>(Resource.Id.new_counter_name));
|
||||
|
||||
private EditText _description;
|
||||
public EditText Description => _description ?? (_description = FindViewById<EditText>(Resource.Id.new_counter_description));
|
||||
|
||||
private Button _createCounter;
|
||||
public Button CreateCounter => _createCounter ?? (_createCounter = FindViewById<Button>(Resource.Id.new_counter_create));
|
||||
```
|
||||
|
||||
These properties will resolve the widgets by looking for them in the layout, and once found stored in a field so we only need to do one lookup.
|
||||
|
||||
Lastly we need to bind the controls to the properties on the view model using the MVVMLight binding mechanism. And this is where the public visibility of the properties comes into play.
|
||||
|
||||
```
|
||||
private readonly List<Binding> _bindings = new List<Binding>();
|
||||
|
||||
protected override void OnCreate(Bundle savedInstanceState)
|
||||
{
|
||||
...
|
||||
Bind();
|
||||
}
|
||||
|
||||
private void Bind()
|
||||
{
|
||||
_bindings.Add(this.SetBinding(() => ViewModel.Name, () => Name.Text, BindingMode.TwoWay));
|
||||
_bindings.Add(this.SetBinding(() => ViewModel.Description, () => Description.Text, BindingMode.TwoWay));
|
||||
CreateCounter.SetCommand(nameof(Button.Click), ViewModel.AddCounterCommand);
|
||||
}
|
||||
```
|
||||
|
||||
In out `OnCreate` method we call a new method, `Bind`, that uses MVVMLight to bind up the properties. MVVMLight has extension methods for binding properties and commands. We call `SetBinding` passing in an expression that points to the source for the binding, an expression that points to the target, and the binding mode.
|
||||
|
||||
Behind the scenes the `SetBinding` method will resolve the expressions to the public properties on the object that the extension method is called on. So for example in the first binding it looks at the source expression and finds the `ViewModel` property (remember from before that we made this property public), and on that view model finds the `Name` property. It then looks at the target expression and resolves the public `Name` property on the Activity (the EditText public property we added earlier). It will start by copying the value from the source by evaluating the `ViewModel.Name` property and setting it on the `Name.Text` property. Then based on the binding mode it will wire up listeners for value changes. We're using `BindingMode.TwoWay` so we need a two-way binding - changes in the source update the target and changes in the target update the source. This means the binding will listen for property change events on the `ViewModel` and if one is raised for the `Name` property it will update the control, and it will listen for changes to the text of the `Name` edit text control, and if the test changes it will update the view model.
|
||||
The `SetBinding` creates a weak binding, so we need to keep a reference to it to stop the garbage collector from cleaning up. This is what the `_bindings` list is for.
|
||||
|
||||
As well as binding the properties, we need to bind the command. This is done using the `SetCommand` extension method. This takes the name of the event and a command to bind to. It will find the event with the given name on the object the extension method is called on, and when that event is fired it will execute the command. It also checks the `CanExecute` status of the command and will enable or disable the button depending on the value. This is a nice way to enable or disable UI functionality from the view model.
|
||||
|
||||
Almost there - we now have an activity with a UI and a view model. Now we need to navigate to it.
|
||||
|
||||
###### Navigating to the new activity
|
||||
Navigation in MVVMLight is based around the idea of registering an activity against a key in your platform specific code, and navigating to that key in the portable code.
|
||||
Registration happens when we first register the navigation service in the `MainActivity` constructor. We call the `Configure` method passing in the key we defined in our `ViewModelLocator` earlier, and the type of Activity we want created when we navigate to this key.
|
||||
|
||||
```
|
||||
public MainActivity()
|
||||
{
|
||||
....
|
||||
var navigationService = new AppCompatNavigationService();
|
||||
navigationService.Configure(ViewModelLocator.NewCounterPageKey, typeof(NewCounterActivity));
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
We can then add a command to the `CountersViewModel` to do this navigation using an injected `INavigationService`.
|
||||
```
|
||||
public CountersViewModel(IDatabaseHelper databaseHelper, INavigationService navigationService)
|
||||
{
|
||||
...
|
||||
_navigationService = navigationService;
|
||||
...
|
||||
}
|
||||
|
||||
private RelayCommand _addNewCounterCommand;
|
||||
public RelayCommand AddNewCounterCommand => _addNewCounterCommand ?? (_addNewCounterCommand = new RelayCommand(AddNewCounter));
|
||||
|
||||
private void AddNewCounter()
|
||||
{
|
||||
_navigationService.NavigateTo(ViewModelLocator.NewCounterPageKey);
|
||||
}
|
||||
```
|
||||
|
||||
This command when executed will call the `NavigateTo` method on the `INavigationService` passing the key that we defined earlier. Once we make this call the navigation service does everything for us - creating the activity and pushing it onto the top of the navigation stack. And because we're good developers we can add a unit test to our `CountersViewModelTests` to verify this as well.
|
||||
|
||||
```
|
||||
[Test]
|
||||
public void ExecutingAddNewCounterCommandShouldNavigateToTheNewCounterActivity()
|
||||
{
|
||||
var vm = new CountersViewModel(_mockDatabaseHelper.Object, _mockNavigationService.Object);
|
||||
vm.AddNewCounterCommand.Execute(null);
|
||||
_mockNavigationService.Verify(n => n.NavigateTo(ViewModelLocator.NewCounterPageKey), Times.Once);
|
||||
}
|
||||
```
|
||||
|
||||
Finally we need to bind this command to the UI inside the `CountersFragment` using the MVVMLight `SetCommand` extension method we discussed above.
|
||||
|
||||
```
|
||||
private FloatingActionButton _floatingActionButton;
|
||||
|
||||
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
|
||||
{
|
||||
...
|
||||
_floatingActionButton = view.FindViewById<FloatingActionButton>(Resource.Id.floatingAddNewCounterButton);
|
||||
_floatingActionButton.SetCommand(nameof(FloatingActionButton.Click), ViewModelLocator.Counters.AddNewCounterCommand);
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Now this is all done we can run the app and test the navigation.
|
||||
|
||||
<div class="image-div" style="width: 400px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
###### Updating the counters in the UI
|
||||
If you run and test this you will be able to add a new counter, but the list of counters won't be updated. Recycler views are not able to automatically detect changes to the underlying collection. The final piece of the adding counters puzzle is to wire this up.
|
||||
The best way to handle this is from the database layer up - we can raise an event in our database helper when the counters are changed, detect this in the counters view model and update our UI.
|
||||
|
||||
Let's start with the database helper by adding an event to our `IDatabaseHelper` interface:
|
||||
```
|
||||
event EventHandler CountersChanged;
|
||||
```
|
||||
Then we implement it in `DatabaseHelper`.
|
||||
```
|
||||
public async Task AddOrUpdateCounterAsync(Counter counter)
|
||||
{
|
||||
...
|
||||
OnCountersChanged();
|
||||
}
|
||||
|
||||
public event EventHandler CountersChanged;
|
||||
|
||||
private void OnCountersChanged()
|
||||
{
|
||||
CountersChanged?.Invoke(this, new EventArgs());
|
||||
}
|
||||
```
|
||||
Notice the new C# 6 null-conditional operator. This will check the value of `CountersChanged` and if this is null do nothing, otherwise it will call the `Invoke` method. Lovely clean code!
|
||||
|
||||
We have our event, so lets wire it up in our `CountersViewModel` to update the counters we expose in the view model.
|
||||
```
|
||||
public CountersViewModel(IDatabaseHelper databaseHelper, INavigationService navigationService)
|
||||
{
|
||||
...
|
||||
_databaseHelper.CountersChanged += async (s, e) => await LoadCountersAsync();
|
||||
...
|
||||
}
|
||||
|
||||
public async Task LoadCountersAsync()
|
||||
{
|
||||
_counters.Clear();
|
||||
...
|
||||
}
|
||||
```
|
||||
We're listening on the event and when it is fired reloading the counters from the SQLite database. We're re-using the `LoadCountersAsync` method so we have to tweak it to always clear the collection before populating it with the data loaded from the database helper.
|
||||
|
||||
We now have our observable collection changing when the database changes, so we can listen on this to update the view. This can be handled in the `CountersAdapter` - the component in the recycler view that adapts the counters collection to the UI.
|
||||
|
||||
```
|
||||
public CountersAdapter()
|
||||
{
|
||||
((INotifyCollectionChanged)ViewModelLocator.Counters.Counters).CollectionChanged += OnCollectionChanged;
|
||||
}
|
||||
|
||||
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs)
|
||||
{
|
||||
NotifyDataSetChanged();
|
||||
}
|
||||
```
|
||||
|
||||
In here we handle the collection change method and when fired call `NotifyDataSetChanged`, a method that tells the recycler view to update the items in the UI. The `Counters` property on the `CountersViewModel` is a `ReadOnlyObservableCollection` which implements `INotifyCollectionChanged` explicitly, so we have to cast it to get access to the `CollectionChanged` event.
|
||||
|
||||
Note that this is not the most performant way of doing this. Every action on the collection will cause the entire UI to be updated. The reload is a clear then add of items one by one, so for example if there are 5 counters the `CollectionChanged` event will be raised 6 times and the UI will be fully rebuilt 6 times. The use of `NotifyDataSetChanged` will also cause an entire UI rebuild - there are other notify methods that handle single item adds, deletes or moves which could be used to improve performance. We could also improve the performance by implementing our own collection and only raising the event once. In this case we don't have to worry too much - we don't need to be lightening fast as will only have a few items.
|
||||
|
||||
#### Lets test it all out
|
||||
Now everything is wired up, lets take the app for a spin.
|
||||
|
||||
<div class="image-div" style="width: 400px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<hr/>
|
||||
|
||||
#### Update
|
||||
|
||||
After feedback from the comments against this post, lets tweak the UI to have a button on the toolbar to create the counter instead of on the bottom of the view.
|
||||
|
||||
First we need to remove the button from the bottom of the `new_counter.axml` layout file - just need to delete the `Button` element.
|
||||
|
||||
Next we need a menu. The way extra items are added to the toolbar is by creating a menu and inflating it into the toolbar. There is a visual studio template for menus, so right click on the 'Resources/menu' folder and add a new item using the Android menu template called `new_counter_menu.xml`.
|
||||
|
||||
<div class="image-div" style="width: 700px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
In this menu we need to add one item for the new counter button.
|
||||
|
||||
```
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item android:id="@+id/action_create_counter"
|
||||
app:showAsAction="always"
|
||||
android:icon="@drawable/ic_done_white_48dp"/>
|
||||
</menu>
|
||||
```
|
||||
|
||||
The icon being used here is the done icon from Google's material icons mentioned above. Download this one and copy it to the various drawable folders.
|
||||
Notice as well the `showAsAction` property comes from the `xmlns:app="http://schemas.android.com/apk/res-auto"` namespace - if you use it from the `android` namespace then the menu item will appear on the overflow menu without an icon.
|
||||
|
||||
Now we have our menu, we need to wire it up in the `NewCounterActivity`. We can start here by deleting the `CreateCounter` button and all references to it. Then we need to create the new menu in the toolbar. This is done by overriding the `OnCreateMenuItems` method and loading our new menu into the toolbar in there.
|
||||
|
||||
```
|
||||
public override bool OnCreateOptionsMenu(IMenu menu)
|
||||
{
|
||||
base.OnCreateOptionsMenu(menu);
|
||||
Toolbar.InflateMenu(Resource.Menu.new_counter_menu);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
Last thing to do is to handle the menu click in the `OnOptionsItemSelected` method. When the new menu item is selected we need to execute the same command the previous button used.
|
||||
|
||||
```
|
||||
public override bool OnOptionsItemSelected(IMenuItem item)
|
||||
{
|
||||
switch (item.ItemId)
|
||||
{
|
||||
case Android.Resource.Id.Home:
|
||||
ViewModel.GoBackCommand.Execute(null);
|
||||
return true;
|
||||
case Resource.Id.action_create_counter:
|
||||
ViewModel.AddCounterCommand.Execute(null);
|
||||
return true;
|
||||
default:
|
||||
return base.OnOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Done!
|
||||
|
||||
<div class="image-div" style="width: 400px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
The code for this can be found in GitHub on the Part5 branch at https://github.com/jimbobbennett/StupendousCounter/tree/Part5
|
||||
|
||||
In the next part we'll work on deleting counters.
|
||||
|
||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 265 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 186 KiB |
@@ -0,0 +1,512 @@
|
||||
---
|
||||
author: "Jim Bennett"
|
||||
categories: ["xamarin", "xamarin.android", "mvvmlight", "Technology", "UI", "tutorial"]
|
||||
date: 2016-03-24T08:27:44Z
|
||||
description: ""
|
||||
draft: false
|
||||
images:
|
||||
- /blogs/building-a-xamarin-android-app-part-6-2/banner.png
|
||||
featured_image: banner.png
|
||||
slug: "building-a-xamarin-android-app-part-6-2"
|
||||
tags: ["xamarin", "xamarin.android", "mvvmlight", "Technology", "UI", "tutorial"]
|
||||
title: "Building a Xamarin Android app - part 6"
|
||||
|
||||
images:
|
||||
- /blogs/building-a-xamarin-android-app-part-6-2/banner.png
|
||||
featured_image: banner.png
|
||||
---
|
||||
|
||||
|
||||
This is the sixth part in the my series about building an Android app using Xamarin.Android. I highly recommend reading the rest of the series first.
|
||||
The previous parts are:
|
||||
|
||||
* [Creating the basic app](/blogs/building-an-android-app-part-1/)
|
||||
* [Defining our data](/blogs/building-an-android-app-part-2/)
|
||||
* [Building view models](/blogs/building-a-xamarin-android-app-part-3/)
|
||||
* [Binding the view models to the UI](/blogs/building-a-xamarin-android-app-part-4/)
|
||||
* [Adding the add button](/blogs/building-a-xamarin-android-app-part-5/)
|
||||
|
||||
#### Editing and deleting our counters
|
||||
So far we have a pretty nice but simple app - we can add new counters and increment them. The next thing we need to add is the ability to edit or delete a counter. Lets start at the bottom of our stack and work up to the UI.
|
||||
We need to be able to edit or delete the counter - so we need a way to update the record or delete the row in out SQLite db, we need commands on our ViewModel to expose this, and we need a nice UI to handle the user interaction.
|
||||
|
||||
###### Basic UI design
|
||||
Before we begin though, we should consider how the user will delete a counter, so that we can implement it in the correct way. Our recycler view showing the counters is our way to interact with them - tapping on the plus button will increment the counter, so it makes sense to use this to drive editing a counter. We already make use of tapping on the plus button, so a simple tap on the recycler view item may not be the best way to drive the edit (it would be too easy to tap the wrong thing and edit instead of incrementing the counter). Instead we can take advantage of one of Androids other UI paradigm - the long touch. If a user performs a long touch on the counter in our recycler view we can show a new activity where they can edit the name or description of the counter, or delete it (and we can re-use this screen later to show the counters history).
|
||||
|
||||
<div class="image-div" style="width: 300px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
Lets crack on with the implementation.
|
||||
|
||||
###### Database
|
||||
In our `DatabaseHelper` we already have the ability to update counters using the `AddOrUpdateCounterAsync` method, so all we need to do is add an async delete method that takes a counter and deletes the row from SQLite.
|
||||
This needs to be added to the interface:
|
||||
|
||||
```
|
||||
public interface IDatabaseHelper
|
||||
{
|
||||
Task DeleteCounterAsync(Counter counter);
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
And the implementation:
|
||||
```
|
||||
public async Task DeleteCounterAsync(Counter counter)
|
||||
{
|
||||
var connection = new SQLiteAsyncConnection(_dbPath);
|
||||
await connection.DeleteAsync(counter);
|
||||
OnCountersChanged();
|
||||
}
|
||||
```
|
||||
|
||||
After the row is deleted we also need to raise the `CountersChanged` event to notify anything that is interested, such as our `CountersViewModel` that the counters have changed.
|
||||
|
||||
And obviously as we are good developers we need some tests for this in our Android unit test suite in the `DatabaseHelperTests` fixture to cover the deleting of rows and raising the collection changed event:
|
||||
|
||||
```
|
||||
[Test]
|
||||
public void DeletingACounterShouldDeleteTheCounter()
|
||||
{
|
||||
var dbfile = Path.Combine(RootPath, Guid.NewGuid().ToString("N") + ".db3");
|
||||
|
||||
DatabaseHelper.CreateDatabase(dbfile);
|
||||
|
||||
var db = new DatabaseHelper();
|
||||
var counter = new Counter
|
||||
{
|
||||
Name = "TestCounter",
|
||||
Description = "A test counter"
|
||||
};
|
||||
|
||||
var res = Task.Run(async () =>
|
||||
{
|
||||
await db.AddOrUpdateCounterAsync(counter);
|
||||
return 0;
|
||||
}).Result;
|
||||
|
||||
var counters = Task.Run(async () => await db.GetAllCountersAsync()).Result;
|
||||
|
||||
counters.Should().HaveCount(1);
|
||||
|
||||
res = Task.Run(async () =>
|
||||
{
|
||||
await db.DeleteCounterAsync(counter);
|
||||
return 0;
|
||||
}).Result;
|
||||
|
||||
counters = Task.Run(async () => await db.GetAllCountersAsync()).Result;
|
||||
counters.Should().HaveCount(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DeletingACounterShouldRaiseTheCollectionChangedEvent()
|
||||
{
|
||||
var dbfile = Path.Combine(RootPath, Guid.NewGuid().ToString("N") + ".db3");
|
||||
|
||||
DatabaseHelper.CreateDatabase(dbfile);
|
||||
|
||||
var db = new DatabaseHelper();
|
||||
var counter = new Counter
|
||||
{
|
||||
Name = "TestCounter",
|
||||
Description = "A test counter"
|
||||
};
|
||||
|
||||
var res = Task.Run(async () =>
|
||||
{
|
||||
await db.AddOrUpdateCounterAsync(counter);
|
||||
return 0;
|
||||
}).Result;
|
||||
|
||||
var eventRecorder = new EventRecorder(db, nameof(DatabaseHelper.CountersChanged));
|
||||
eventRecorder.RecordEvent();
|
||||
|
||||
res = Task.Run(async () =>
|
||||
{
|
||||
await db.DeleteCounterAsync(counter);
|
||||
return 0;
|
||||
}).Result;
|
||||
|
||||
eventRecorder.Should().HaveCount(1);
|
||||
}
|
||||
```
|
||||
|
||||
###### ViewModel
|
||||
What we need here is a ViewModel that exposes properties for the name and description of our counter, as well as providing a mechanism to save or delete it. We could build a new ViewModel for this, but we already have 2 ViewModels that represent counters with various levels of functionality on them - a `NewCounterViewModel` that allows the setting of the `Name` and `Description` properties, as well as a command to save the new counter to the database, and a `CounterViewModel` that has a readonly view of the counters details with a command to increment it's value. Ideally we should follow the [DRY principle](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) and only have one, so instead of adding a new one, lets take this opportunity of refactoring the code to have only one ViewModel, and add our new requirements to it.
|
||||
|
||||
So lets take the `CounterViewModel` and make this work for all our scenarios. First we need to change the `Name` and `Description` properties. These are currently read only and just return the value from the counter. What we'll do with these is make them read/write properties with a property change notification and a backing field, and set them in the constructor. We don't want to use passthrough properties, we need a backing field. This is because we want to allow the user to cancel their changes when they edit, if we pass the value straight through to the counter when we set it we will have to store the original value somewhere to allow us to revert it. It's easier to save the values in a field and only apply them to the counter if the user saves the changes.
|
||||
|
||||
```
|
||||
public CounterViewModel(Counter counter, IDatabaseHelper databaseHelper)
|
||||
{
|
||||
...
|
||||
Name = counter.Name;
|
||||
Description = counter.Description;
|
||||
}
|
||||
|
||||
private string _name;
|
||||
public string Name
|
||||
{
|
||||
get { return _name; }
|
||||
set { Set(ref _name, value); }
|
||||
}
|
||||
|
||||
private string _description;
|
||||
public string Description
|
||||
{
|
||||
get { return _description; }
|
||||
set { Set(ref _description, value); }
|
||||
}
|
||||
```
|
||||
|
||||
The `NewCounterViewModel` also has commands to add a new counter and navigate back. The call on the database helper to add the new counter is the same as the one we will need to use to save the edits, `AddOrUpdateCounterAsync`, so lets copy the `AddCounterCommand` from the `NewCounterViewModel` and rename it to `SaveCommand` so it can be used for both new and edits. We'll also need to add the `IDialogService` and `INavigationService` to the constructor parameters so they can be used by this command.
|
||||
|
||||
```
|
||||
public CounterViewModel(Counter counter, IDatabaseHelper databaseHelper, IDialogService dialogService, INavigationService navigationService)
|
||||
{
|
||||
_dialogService = dialogService;
|
||||
_navigationService = navigationService;
|
||||
...
|
||||
}
|
||||
|
||||
private RelayCommand _saveCommand;
|
||||
public RelayCommand SaveCommand => _saveCommand ?? (_saveCommand = new RelayCommand(async () => await SaveAsync()));
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
if (string.IsNullOrEmpty(Name))
|
||||
{
|
||||
await _dialogService.ShowError("The name must be set", "No name", "OK", null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(Description))
|
||||
{
|
||||
await _dialogService.ShowError("The description must be set", "No description", "OK", null);
|
||||
return;
|
||||
}
|
||||
|
||||
_counter.Name = Name;
|
||||
_counter.Description = Description;
|
||||
await _databaseHelper.AddOrUpdateCounterAsync(_counter);
|
||||
_navigationService.GoBack();
|
||||
}
|
||||
```
|
||||
|
||||
There is one code change made here as well - in the `NewCounterViewModel` a new `Counter` was created with the values from the properties. Here we use the `_counter` field, and update the values from our properties.
|
||||
|
||||
We can then add the `GoBackCommand` as well.
|
||||
|
||||
```
|
||||
private RelayCommand _goBackCommand;
|
||||
public RelayCommand GoBackCommand => _goBackCommand ?? (_goBackCommand = new RelayCommand(() => _navigationService.GoBack()));
|
||||
```
|
||||
|
||||
Next we need to add the `DeleteCommand` to expose the new delete functionality we've added to the database helper.
|
||||
|
||||
```
|
||||
private RelayCommand _deleteCommand;
|
||||
public RelayCommand DeleteCommand => _deleteCommand ?? (_deleteCommand = new RelayCommand(async () => await DeleteAsync()));
|
||||
|
||||
private async Task DeleteAsync()
|
||||
{
|
||||
if (await _dialogService.ShowMessage($"Are you sure you want to delete {Name}?", "Delete counter", "Yes", "No", null))
|
||||
{
|
||||
await _databaseHelper.DeleteCounterAsync(_counter);
|
||||
_navigationService.GoBack();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Nothing unexpected or new here - we show a dialog confirming with the user that they want to delete, and if they say yes delete the counter from the database and navigate back.
|
||||
|
||||
To test this we also need to copy the tests from the `NewCounterViewModelTests` fixture, making the relevant changes to change the type of view model that is constructed. We can then also add some tests for the `DeleteCommand`, as well as ensuring the saving tests will copy the values to the counter.
|
||||
|
||||
```
|
||||
[Test]
|
||||
public void ExecutingTheSaveCommandUpdatesTheChangesOnTheCounter()
|
||||
{
|
||||
var counter = new Counter {Value = 10, Name = "Name", Description = "Description"};
|
||||
var vm = new CounterViewModel(counter, _mockDatabaseHelper.Object, _mockDialogService.Object, _mockNavigationService.Object);
|
||||
vm.Name = "New Name";
|
||||
vm.Description = "New Description";
|
||||
vm.SaveCommand.Execute(null);
|
||||
counter.Name.Should().Be("New Name");
|
||||
counter.Description.Should().Be("New Description");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExecutingTheSaveCommandUpdatesTheCounterInTheDatabase()
|
||||
{
|
||||
var counter = new Counter { Value = 10, Name = "Name", Description = "Description" };
|
||||
var vm = new CounterViewModel(counter, _mockDatabaseHelper.Object, _mockDialogService.Object, _mockNavigationService.Object);
|
||||
vm.Name = "New Name";
|
||||
vm.Description = "New Description";
|
||||
vm.SaveCommand.Execute(null);
|
||||
_mockDatabaseHelper.Verify(d => d.AddOrUpdateCounterAsync(counter));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExecutingTheSaveCommandNavigatesBack()
|
||||
{
|
||||
var vm = CreateCounterViewModel();
|
||||
vm.SaveCommand.Execute(null);
|
||||
_mockNavigationService.Verify(n => n.GoBack());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExecutingTheDeleteCommandConfirmsTheDelete()
|
||||
{
|
||||
var vm = CreateCounterViewModel();
|
||||
vm.DeleteCommand.Execute(null);
|
||||
_mockDialogService.Verify(d => d.ShowMessage($"Are you sure you want to delete {vm.Name}?", "Delete counter", "Yes", "No", null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExecutingTheDeleteCommandAndSelectingYesOnTheDialogDeletesTheCounter()
|
||||
{
|
||||
var counter = new Counter { Value = 10, Name = "Name", Description = "Description" };
|
||||
var vm = new CounterViewModel(counter, _mockDatabaseHelper.Object, _mockDialogService.Object, _mockNavigationService.Object);
|
||||
_mockDialogService.Setup(d => d.ShowMessage($"Are you sure you want to delete {vm.Name}?", "Delete counter", "Yes", "No", null))
|
||||
.ReturnsAsync(true);
|
||||
vm.DeleteCommand.Execute(null);
|
||||
_mockDatabaseHelper.Verify(d => d.DeleteCounterAsync(counter), Times.Once);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExecutingTheDeleteCommandAndSelectingNoOnTheDialogDoesNotDeleteTheCounter()
|
||||
{
|
||||
var counter = new Counter { Value = 10, Name = "Name", Description = "Description" };
|
||||
var vm = new CounterViewModel(counter, _mockDatabaseHelper.Object, _mockDialogService.Object, _mockNavigationService.Object);
|
||||
_mockDialogService.Setup(d => d.ShowMessage($"Are you sure you want to delete {vm.Name}?", "Delete counter", "Yes", "No", null))
|
||||
.ReturnsAsync(false);
|
||||
vm.DeleteCommand.Execute(null);
|
||||
_mockDatabaseHelper.Verify(d => d.DeleteCounterAsync(counter), Times.Never);
|
||||
}
|
||||
```
|
||||
|
||||
After doing this we can tidy up by deleting the `NewCounterViewModel` and `NewCounterViewModelTests` classes. We also need to make the new code work. At the moment the `NewCounterActivity` uses a `NewCounterViewModel`, and gets it from the `ViewModelLocator`, clearing the values each time. We can delete it from the `ViewModelLocator`, change the type of the `ViewModel` property on the activity to be `CounterViewModel`, change the command that gets called to save, and create a new one with a new counter.
|
||||
|
||||
```
|
||||
public CounterViewModel ViewModel { get; private set; }
|
||||
|
||||
protected override void OnCreate(Bundle savedInstanceState)
|
||||
{
|
||||
...
|
||||
ViewModel = GetCounterViewModel();
|
||||
...
|
||||
}
|
||||
|
||||
protected virtual CounterViewModel GetCounterViewModel()
|
||||
{
|
||||
return new CounterViewModel(new Counter(),
|
||||
ViewModelLocator.DatabaseHelper,
|
||||
ViewModelLocator.DialogService,
|
||||
ViewModelLocator.NavigationService);
|
||||
}
|
||||
|
||||
public override bool OnOptionsItemSelected(IMenuItem item)
|
||||
{
|
||||
switch (item.ItemId)
|
||||
{
|
||||
...
|
||||
case Resource.Id.action_save_counter:
|
||||
ViewModel.SaveCommand.Execute(null);
|
||||
return true;
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notice how the `GetCounterViewModel` is protected and virtual? We'll see why later when we look at creating the views.
|
||||
|
||||
We then need to update the `CountersViewModel` to pass the new parameters to the constructor for our `CounterViewModel`. These extra parameters need to be added to the constructor so that the IoC container can populate them, then passed through once the `CounterViewModels` are created.
|
||||
|
||||
```
|
||||
public CountersViewModel(IDatabaseHelper databaseHelper, INavigationService navigationService, IDialogService dialogService)
|
||||
{
|
||||
_navigationService = navigationService;
|
||||
_dialogService = dialogService;
|
||||
...
|
||||
}
|
||||
|
||||
public async Task LoadCountersAsync()
|
||||
{
|
||||
...
|
||||
foreach (var counter in counters)
|
||||
_counters.Add(new CounterViewModel(counter, _databaseHelper, _dialogService, _navigationService));
|
||||
}
|
||||
```
|
||||
|
||||
Also here the `CountersViewModelTests` should be updated to mock up these extra parameters and pass them through when constructing the `CounterViewModels`.
|
||||
|
||||
We're almost there with the ViewModels part. The last thing to think about is how we are actually going to start the editing process. Android has a UI paradigm of a long press on an item in a list that leads to extra functionality, such as editing. We can use this here - the user can long press on the counters in our recycler view and we can open an edit activity. To make this work we need to add a command to our `CounterViewModel` that using the `INavigationService` to navigate to a new screen, passing the counter through. First we define a new key for this in our `ViewModelLocator`.
|
||||
|
||||
```
|
||||
public const string EditCounterPageKey = "EditCounterPage";
|
||||
```
|
||||
|
||||
Then we add the command to our `CounterViewModel`.
|
||||
|
||||
```
|
||||
private RelayCommand _editCommand;
|
||||
public RelayCommand EditCommand => _editCommand ?? (_editCommand = new RelayCommand( Edit));
|
||||
|
||||
private void Edit()
|
||||
{
|
||||
_navigationService.NavigateTo(ViewModelLocator.EditCounterPageKey, _counter);
|
||||
}
|
||||
```
|
||||
|
||||
Then we add a test to the `CounterViewModelTests`.
|
||||
|
||||
```
|
||||
[Test]
|
||||
public void ExecutingTheEditCommandNavigatesToTheEditScreen()
|
||||
{
|
||||
var counter = new Counter();
|
||||
var vm = new CounterViewModel(counter, _mockDatabaseHelper.Object, _mockDialogService.Object, _mockNavigationService.Object);
|
||||
vm.EditCommand.Execute(null);
|
||||
|
||||
_mockNavigationService.Verify(n => n.NavigateTo(ViewModelLocator.EditCounterPageKey, counter));
|
||||
}
|
||||
```
|
||||
|
||||
Done. We now have everything in our ViewModels ready to start on the views.
|
||||
|
||||
###### Views
|
||||
|
||||
We need to do a few things with our views to get editing working - we need to handle the long press on the recycler view to launch the editing, then create a new activity and layout for the editing screen.
|
||||
|
||||
Lets start with the long press. This is easy enough to implement in our existing `CounterViewHolder` class - we just add an event handler and execute our command from there.
|
||||
|
||||
```
|
||||
public CounterViewHolder(View itemView) : base(itemView)
|
||||
{
|
||||
..
|
||||
itemView.LongClick += ItemLongClick;
|
||||
}
|
||||
|
||||
private void ItemLongClick(object sender, View.LongClickEventArgs e)
|
||||
{
|
||||
_counterViewModel.EditCommand.Execute(null);
|
||||
}
|
||||
```
|
||||
|
||||
Android also supports a nice animation for this - tap and see a highlight starting at the touch point and spreading out.
|
||||
|
||||
<div class="image-div" style="width: 600px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
This is easy enough to implement, we just need to change the foreground of the `CardView` in the `counter_view.axml` layout file to use a stock Android selectable item background.
|
||||
|
||||
```
|
||||
<android.support.v7.widget.CardView
|
||||
...
|
||||
android:foreground="?android:attr/selectableItemBackground">
|
||||
```
|
||||
|
||||
We've got the long click looking awesome, and we've got the command executing. Now lest create an activity that the command can launch. For this we can create `EditCounterActivity`, and instead of deriving from `BaseActivity` like all the others, instead we can derive from `NewCounterActivity`. This activity already has most of what we need - it has a layout that shows the name and description in edit controls which we can reuse, it has a save button, it uses the right ViewModel type. All we need to change when we override it is how it creates the ViewModel, and add an extra menu item for the delete button.
|
||||
|
||||
Remember earlier we created a protected virtual method in the `NewCounterActvity` that creates a new `CounterViewModel`? Well the reason for creating it as a virtual is that we can override it in our `EditCounterViewModel` and create a new one wrapping the counter passed to our navigation call in the `EditCommand` of the `CounterViewModel`.
|
||||
|
||||
```
|
||||
protected override CounterViewModel GetCounterViewModel()
|
||||
{
|
||||
var navigationService = ViewModelLocator.NavigationService;
|
||||
var counter = (Counter) ((AppCompatNavigationService) navigationService).GetAndRemoveParameter(Intent);
|
||||
return new CounterViewModel(counter,
|
||||
ViewModelLocator.DatabaseHelper,
|
||||
ViewModelLocator.DialogService,
|
||||
navigationService);
|
||||
}
|
||||
```
|
||||
|
||||
We have to cate the `INavigationService` to a `AppCompatNavigationService` to get data out of it. This is because data passing is very platform specific, we need to give it the `Intent` that was used to launch the activity so that it can get the data out, and this is Android specific. The interface is designed to be portable so contains no platform specific code, so we cast to the Android variant to get the data out.
|
||||
|
||||
Next we need the delete button on the menu. For this we need a new menu called `edit_counter_menu.xml` containing this button.
|
||||
|
||||
```
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item android:id="@+id/action_delete_counter"
|
||||
app:showAsAction="always"
|
||||
android:icon="@drawable/ic_delete_white_48dp"/>
|
||||
</menu>
|
||||
```
|
||||
|
||||
Th image here, `ic_delete_white_48dp` is once again dowloaded from [Google's material icons](https://design.google.com/icons/) and dropped into all the drawable folders.
|
||||
|
||||
In our activity we now need to create the menu, and handle when it gets pressed.
|
||||
|
||||
```
|
||||
public override bool OnCreateOptionsMenu(IMenu menu)
|
||||
{
|
||||
base.OnCreateOptionsMenu(menu);
|
||||
Toolbar.InflateMenu(Resource.Menu.edit_counter_menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool OnOptionsItemSelected(IMenuItem item)
|
||||
{
|
||||
switch (item.ItemId)
|
||||
{
|
||||
case Resource.Id.action_delete_counter:
|
||||
ViewModel.DeleteCommand.Execute(null);
|
||||
return true;
|
||||
default:
|
||||
return base.OnOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
These work the same way as the `NewCounterActivity` described in an earlier post.. The base calls in both these methods call down to the `NewCounterActivity` so that the original menu with the save button is created and handled.
|
||||
|
||||
The last little but we need to do is a UI tweak. The `NewCounterActivity` sets the title to **New Counter**, but it would look nicer if we show that we are editing a counter. Also by default Android will set the focus on the first edit view, where MVVMLight sets the text, the focus ends up at the start of the text instead of the end. We can get round this by calling `SetSelection` on the `Name` edit view in our `OnCreate` method after we bind the data in the base `OnCreate` method.
|
||||
|
||||
```
|
||||
protected override void OnCreate(Bundle savedInstanceState)
|
||||
{
|
||||
base.OnCreate(savedInstanceState);
|
||||
Title = $"Edit {ViewModel.Name}";
|
||||
Name.SetSelection(Name.Text.Length);
|
||||
}
|
||||
```
|
||||
|
||||
The last thing we need to do is wire up this activity to the new key we created for navigation in the `ViewModelLocator`. All our wire ups are done in the `MainActivity` so lets add another entry.
|
||||
|
||||
```
|
||||
public MainActivity()
|
||||
{
|
||||
...
|
||||
navigationService.Configure(ViewModelLocator.EditCounterPageKey, typeof(EditCounterActivity));
|
||||
}
|
||||
```
|
||||
|
||||
Done!
|
||||
|
||||
###### Lets try it all out
|
||||
|
||||
We have our database code written, we have our ViewModels working and our UI wired up. Lets try it out.
|
||||
|
||||
<div class="image-div" style="width: 400px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
The code for this can be found in GitHub on the Part6 branch at https://github.com/jimbobbennett/StupendousCounter/tree/Part6
|
||||
|
||||
In the next part we'll look at showing the history information that we're storing against each counter.
|
||||
|
||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 207 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |