Disclaimer
I am in the very early early stages of toying with Xamarin and it may well be that whatever I am doing here, is not the smartest thing in the world. This is a much as a report of my learning (may ‘struggle’ is a better word) as how-to. But what I describe here works – more or less – although it was a mighty hassle to actually get it working.
The objective
While I was having my first trials with Xamarin stuff, I turned to Entity Framework Code First because, well, when setting up a backend I am lazy. Without much thinking I made the models, using data annotations, and soon found out I had painted myself into a corner with a model that (very simplified) looks like this
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace DemoShared.Models { [Table("Pictures")] public class Photo { public long ID { get; set; } [Required] public string Name { get; set; } } }
Guess what - data annotations don't work in PCL. Now what? Make a shadow class library for use on the client? That kind of went against my principles. So I decided to use the Shared Reference Project Manager. This is a Visual Studio extension that can make basically any project shared, in stead of only between Windows Phone 8.1 and Windows 8.1 You can get it here.
Setting up the initial application
Using this tutorial by Laurent Bugnion I did set up my first Xamarin MVVM application. I created an app "DemoShared" and used the tutorial it up to the point where he starts making a new new xaml page (“Creating a new XAML page”).
Try to build the project. If the Windows Phone project fails with “The 'ProductID' attribute is invalid.. “ (etc), manually open it’s Properties/WMAppManifest.xml, for instance with NotePad, find “ProductID and “PubisherID” and place the generate GUIDs between accolades. This is a bug the Xamarin project template that I already reported.
Basic setup of the backend
- File/New Project/Web/ASP.Net Web application (it’s the only choice you have)
- Choose a name (I chose DemoShared.Backend) and hit OK
- Choose “Web Api” and UNSELECT “Host in the cloud”
- Go to the NuGet Package manager and update all the packages, because a lot of them will be horribly outdated. Hit “accept” or “yes” on any questions
- Delete the all folders except “App_Data”, “App_Start” and “Controllers” because we don’t need them
- Delete BundleConfig.cs from App_Start, and remove the reference to it from Global.asax.cs as well
- Add a (.NET) class library DemoShared.DataAccess
- Install NuGet Package “EntityFrameWork” in DemoShared.Backend and DemoShared.DataAccess
- Create a project DemoShared.Models of type Shared Project (Empty) of type Visual C#:
- Right-click the DemoShared (Portable) project, select Add, then “Add Shared Project reference”, then select “DemoShared.Models
- Repeat this procedure for DemoShared.DataAccess.
- Also add a reference to System.ComponentModel.DataAnnotations to DemoShared.DataAccess.
- Add DemoShared.DataAccess as a reference to DemoShared.Backend
Re-defining the models
The key point of this whole exercise is to re-use models. For one model class this is quite some overkill, but if you have a large collection of models, things becomes quite different. So anyway. I redefined the model class as follows:
#if NET45 using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; #endif namespace DemoShared.Models { #if NET45 [Table("Pictures")] #endif public class Photo { public long ID { get; set; } #if NET45 [Required] #endif public string Name { get; set; } } }
Now in order to make this compile work the way it is intended on server, you will need to add the conditional compilation symbol “NET45” to all configurations of DemoShared.DataAccess
And then it compiles both into PCL and into the DataAccess project – without the attributes in the first, but with the attributes the second. Remember, this is just a really nifty formalized way of ye olde way of file linking.
Kick starting the Entity Framework
Following this tutorial – more or less, I added the following class to the DataAccess project:
using System.Data.Entity; using DemoShared.Models; namespace DemoShared.DataAccess { public class DemoContext : DbContext { public DemoContext() : base("DemoContext") { } public DbSet<Photo> Photos { get; set; } } }
And then to the web.config I added this rather hopeless long connection string:
<connectionStrings> <add name="DemoContext" connectionString="Data Source=(localdb)\v11.0;AttachDbFilename=|DataDirectory|DemoDb.mdf;Initial Catalog=DemoDb;Integrated Security=True;MultipleActiveResultSets=True" providerName="System.Data.SqlClient" /> </connectionStrings>
Rebuild the project. Now go to the Backend Controller folder, right click Add/Controller, select “Web API 2 Controller with actions, using Entity Framework” and in the following dialog, select the Model and Data Context class as displayed to the right
Now if you set “DemoShared.Backend” as start-up project, set it’s startup URL to “api/photos” and start the project, you will automatically get a database “DemoDb” in “App_Data”.Is EF code first cool or what? And my data annotations are used correctly – the Photo type is stored in the “Pictures” table, just as I wanted.
Now I manually entered some data in the tables to get something to show, but of course you can also write an initializer like the EF tutorial describes, or write a separate project that prefills the the database. This is what I usually do, and that’s why I have put the DemoContext in a separate assembly and not in the web project – to be able to reference it form another project.
And now finally, to the Xamarin app
Adding the view model
First of all – start the NuGet package manager and add Newtonsoft Json.NET to DemoShared (Portable).
Then, add a “ViewModels” folder to DemoShared (Portable) to hold this class:
using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Net; using DemoShared.Models; using GalaSoft.MvvmLight; using GalaSoft.MvvmLight.Command; using Newtonsoft.Json; namespace DemoShared.ViewModels { public class MainViewModel : ViewModelBase { public MainViewModel() { Photos = new ObservableCollection<Photo>(); } public string DataUrl { get { return "http://169.254.80.80:28552/api/Photos"; } } public ObservableCollection<Photo> Photos { get; set; } private RelayCommand loadCommand; public RelayCommand LoadCommand { get { return loadCommand ?? (loadCommand = new RelayCommand( () => { var httpReq = (HttpWebRequest)WebRequest.Create(new Uri(DataUrl)); httpReq.BeginGetResponse((ar) => { var request = (HttpWebRequest)ar.AsyncState; using (var response = (HttpWebResponse)request.EndGetResponse(ar)) { if (response.StatusCode == HttpStatusCode.OK) { using (var reader = new StreamReader(response.GetResponseStream())) { string content = reader.ReadToEnd(); if (!string.IsNullOrWhiteSpace(content)) { var result = JsonConvert.DeserializeObject<List<Photo>>(content); foreach (var g in result) { Photos.Add(g); } } } } } }, httpReq); })); } } private static MainViewModel instance; public static MainViewModel Instance { get { CreateNew(); return instance; } set { instance = value; } } public static MainViewModel CreateNew() { if (instance == null) { instance = new MainViewModel(); } return instance; } } }
A rather standard MVVMLight viewmodel I might say, with the usual singleton pattern I use (I am not a big fan of "Locators"). When the command is fired, data is loaded from the WebApi url, deserialized to the same model code as was used to create it on the server, and loaded into the ObservableCollection. Nothing much special here.
There's one fishy detail - in stead of a computer name or “localhost”, there is this hard coded IP adress. We will get to that later.
Adding the Forms XAML page
Add a new Forms XAML page like this
Be aware that you have to enter the name manually, that’s a bug in the Xamarin tools, like Laurent Bugnion already described in his tutorial
Now, go to the DemoPage.Xaml.cs and set up the data context by adding one line of code to the constructor of the page:
public DemoPage() { InitializeComponent(); BindingContext = MainViewModel.Instance; }
This will of course require a “using DemoShared.ViewModels;” at the top. Then we add our “XAML” to start page
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="DemoShared.DemoPage"> <StackLayout Orientation="Vertical" Spacing="0"> <Button Text="Click here" Command="{Binding LoadCommand}" VerticalOptions="Start" HorizontalOptions="Center" /> <ListView ItemsSource="{Binding Photos}" RowHeight="50"> <ListView.ItemTemplate> <DataTemplate> <ViewCell> <ViewCell.View> <StackLayout Padding="5, 5, 0, 5" Orientation="Vertical" Spacing="2"> <Label Text="{Binding ID}" Font="Bold, Large" TextColor="Red"/> </StackLayout> </ViewCell.View> </ViewCell> </DataTemplate> </ListView.ItemTemplate> </ListView> </StackLayout> </ContentPage>
Now this looks kind of familiar, but also kind of - not really. Binding looks kind of you would expect, some controls have weird options, as some... well, have you ever seen a ViewCell before? Working with this stuff really makes you appreciate things like IntelliSense and Blend, for you have none of the above. Good old handcrafted XML. But - it does carry the magic of Xamarin Forms.
Finally, we make our start page the start page of the app. Go to App.cs in DemoShared (Portable) and change the GetMainPage method to:
public static Page GetMainPage() { return new DemoPage(); }
Then I set a multiple startup project, setting both the Windows Phone project as well as the backend project as startup.
And sure enough, there’s the Windows Phone startup screen with one button. And if I click it…
I get an error.
And now for the fishy detail
So your backend is a website - running under IISExpress. By default, that’s not accessible by anything else but localhost, and not by IP adress. What you have to do is explained mostly here. First, find your applicationhost.config. It’s usually in
- C:\Users\%USERNAME%\Documents\IISExpress\config or
- D:\Users\%USERNAME%\Documents\IISExpress\config
In my case it’s the second one. Now before you start messing around in it, you might want to make a backup. Then open the file, and search for the text "*:28552”. This should yield you the following line:
<binding protocol="http" bindingInformation="*:28552:localhost" />
Make a copy of this line, directly below it. Replace localhost by the IP adress you found earlier using "ping -t –4 <hostname>". Net result, in my case:
<binding protocol="http" bindingInformation="*:28552:localhost" />
<binding protocol="http" bindingInformation="*:28552:169.254.80.80" />
Then – very important – close Visual Studio and restart as Administrator. For then and only then IISExpress will be allowed to bind to something else than localhost. That crucial bit of information is unfortunately hard to find.
And then, sure enough:
It runs on Windows Phone and Android. And I am sure, on Apple stuff too but lacking Apple hardware and developer account I could not test it.
BTW – and alternative to messing around with IIS Express settings is of course host the website in IIS. But that requires apparently the database being hosted in SQL*Server Express, so it can’t be in App_Data like this. Or something like that. I found this made a quick test easier.
One more thing
If you want to test this from outside your computer, on a phone, you might want to open the port for TCP traffic:
Conclusion
Using Xamarin allowed us to shared code over multiple platform, but the Shared Reference Project Manager we could also share with the server. And the ease with which you can get a database powered backend up and running using EF code first was also quite impressive to me. Now tiny details, like security and upgrades, I leave as exercise to the reader.
Post scriptum
My fellow MVP Corrado Cavelli pointed out to me that the LoadCommand method in the MainViewModel can be made considerably less complex by using the Microsoft ASP.NET Web API 2.2 Client NuGet Package. He turns out to be quite right. When you add this to the portable class library you can rewrite the command to a relatively simple
public RelayCommand LoadCommand { get { return loadCommand ?? (loadCommand = new RelayCommand( async () => { var client = new HttpClient { BaseAddress = new Uri("http://169.254.80.80:28552") }; var response = await client.GetAsync("api/Photos"); var result = await response.Content.ReadAsAsync<List<Group>>(); foreach (var g in result) { Groups.Add(g); } })); } }This requires "using System.Net.Http;" and "using System.Threading.Tasks;" to be added to the file, but it's a lot less cluttered than my original - copied somewhere from the interwebz - code. Thanks Corrado!