かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Azure MobileApps + Xamarin.Forms開発の始め方(.NETバックエンド + Prism.Forms)

というテーマで書きます!

サーバーサイド(Azure)

Microsoft Azureには、モバイルバックエンドの開発をお手軽にするための、Mobile Appsというものがあります。 これは、サーバーサイドをnodejsのexpressベースのフレームワークで開発するか、ASP.NET WebAPIベースのフレームワークで開発するか選べます。 どちらを選択しても少ないコード量でデータベースへのCRUD処理が作成できる点がポイントです。

特徴をざっとあげると

  • DBのCRUD処理が簡単にかける
  • データのオフライン同期機能が組み込みである
  • 認証機能が簡単に作れる
  • プッシュ通知が簡単に作れる
  • カスタムAPIで好きな処理を書くこともできる

特に最後のカスタムAPIで好きな処理をかけるという余地が残されている点がとても心強い点です。 カスタムAPIでも認証なんかは有効にできるので、いい感じです。

クライアントサイド(Xamarin)

Xamarinといえば、Nativeの開発もありですが最近Previewでネイティブコントロールの埋め込みもサポートされたりと何かとアツイXamarin.Formsを個人的に推してます。

実際開発するときはXamarin.FormsかXamarinのNativeか、きちんと判断が必要ですが軽いプロトタイピングとかならFormsでもそろそろ十分ではないのでしょうかという気がしてます。

Xamarin.Formsでは特にPrismというライブラリを使うとMVVMにのっとった開発が簡単にできる下準備も整えられるため個人的におすすめしておきます。

Mobile Appsにアクセスするためのライブラリも提供されていて、CRUDや認証、オフライン同期、API呼び出しなどが簡単にかけるようになっています。サーバーサイドの処理の呼び出しが最低限のコードで書けるようになっている点が個人的にとてもいいと思います。

ということで

ということで、このBlogでバックエンドとフロントエンドをC#でMobile Apps + Xamarin.Formsの組み合わせでどんなことができるのか簡単に解説していこうと思います。

サーバーサイドのプロジェクトの作成

Azure SDKを入れていると、Mobile AppsのプロジェクトテンプレートがCloudカテゴリに作成されています。

f:id:okazuki:20160920123609p:plain

とりあえずSampleMobileAppという名前でプロジェクトを作成します。クラウドにホストするは後でやるとして、新規作成しましょう。

f:id:okazuki:20160920123951p:plain

TodoItemを追加削除する機能を持ったサービスが作成されます。

プロジェクトの中を見てみよう

Models名前空間

MobileServiceContextクラスが作成されています。これは何てことはない、ただのEntityFrameworkのDbContextを継承したクラスになります。見慣れないAttributeToColumnAnnotationConventionというものがありますが、これはMibole AppsがDBのテーブルを生成するときに、ちょっと仕掛けを入れてるので、そこで使うやつみたいなので、気にする必要はありません。

TodoItemというテーブルが定義されていることが確認できます。

using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;
using System.Linq;
using Microsoft.Azure.Mobile.Server;
using Microsoft.Azure.Mobile.Server.Tables;
using SampleMobileApp.DataObjects;

namespace SampleMobileApp.Models
{
    public class MobileServiceContext : DbContext
    {
        // You can add custom code to this file. Changes will not be overwritten.
        // 
        // If you want Entity Framework to alter your database
        // automatically whenever you change your model schema, please use data migrations.
        // For more information refer to the documentation:
        // http://msdn.microsoft.com/en-us/data/jj591621.aspx
        //
        // To enable Entity Framework migrations in the cloud, please ensure that the 
        // service name, set by the 'MS_MobileServiceName' AppSettings in the local 
        // Web.config, is the same as the service name when hosted in Azure.

        private const string connectionStringName = "Name=MS_TableConnectionString";

        public MobileServiceContext() : base(connectionStringName)
        {
        }

        public DbSet<TodoItem> TodoItems { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Conventions.Add(
                new AttributeToColumnAnnotationConvention<TableColumnAttribute, string>(
                    "ServiceTableColumn", (property, attributes) => attributes.Single().ColumnType.ToString()));
        }
    }
}

DataObjects名前空間

DataObjects名前空間にテーブルとマッピングされるクラスの実体が定義されています。このクラスはIdやUpdatedAt、CreatedAtなどのMobile Appsのお決まりのテーブルのカラムが定義されたEntityDataクラスを継承している点が特徴です。自分でクラスを定義するときも、このクラスを継承する形で定義して、MobileServiceContextに追加するといいでしょう。

using Microsoft.Azure.Mobile.Server;

namespace SampleMobileApp.DataObjects
{
    public class TodoItem : EntityData
    {
        public string Text { get; set; }

        public bool Complete { get; set; }
    }
}

Controllers名前空間

一番の問題はControllers名前空間です。TodoItemControllerValuesControllerが定義されています。

TodoItemControllerが曲者です。コードから見てみましょう。

using System.Linq;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.OData;
using Microsoft.Azure.Mobile.Server;
using SampleMobileApp.DataObjects;
using SampleMobileApp.Models;

namespace SampleMobileApp.Controllers
{
    public class TodoItemController : TableController<TodoItem>
    {
        protected override void Initialize(HttpControllerContext controllerContext)
        {
            base.Initialize(controllerContext);
            MobileServiceContext context = new MobileServiceContext();
            DomainManager = new EntityDomainManager<TodoItem>(context, Request);
        }

        // GET tables/TodoItem
        public IQueryable<TodoItem> GetAllTodoItems()
        {
            return Query();
        }

        // GET tables/TodoItem/48D68C86-6EA6-4C25-AA33-223FC9A27959
        public SingleResult<TodoItem> GetTodoItem(string id)
        {
            return Lookup(id);
        }

        // PATCH tables/TodoItem/48D68C86-6EA6-4C25-AA33-223FC9A27959
        public Task<TodoItem> PatchTodoItem(string id, Delta<TodoItem> patch)
        {
            return UpdateAsync(id, patch);
        }

        // POST tables/TodoItem
        public async Task<IHttpActionResult> PostTodoItem(TodoItem item)
        {
            TodoItem current = await InsertAsync(item);
            return CreatedAtRoute("Tables", new { id = current.Id }, current);
        }

        // DELETE tables/TodoItem/48D68C86-6EA6-4C25-AA33-223FC9A27959
        public Task DeleteTodoItem(string id)
        {
            return DeleteAsync(id);
        }
    }
}

これは、テーブルに紐づくコントローラのひな型です。ちょっとメンドクサイですよね…。まぁでも、ここにいろいろ処理を挟みこむことで処理をフックできます。

ValuesControllerは、ただのApiControllerを継承してMobileAppController属性をつけただけのクラスです。こうやってカスタムのAPIを定義します。

using System.Web.Http;
using Microsoft.Azure.Mobile.Server.Config;

namespace SampleMobileApp.Controllers
{
    // Use the MobileAppController attribute for each ApiController you want to use  
    // from your mobile clients 
    [MobileAppController]
    public class ValuesController : ApiController
    {
        // GET api/values
        public string Get()
        {
            return "Hello World!";
        }

        // POST api/values
        public string Post()
        {
            return "Hello World!";
        }
    }
}

Azureへのデプロイ

AzureでMobile Appsを作成します。Quickstartじゃないほうですね。SQL Databaseも作成しましょう。

続けてプロジェクトを発行します。プロジェクトの右クリックメニューから公開を選んでAzureで作成したMobile Appsを選択します。

Azureで作成したMobile AppsのApplication settingsからConnection StringsにMS_TableConnectionStringを追加します。接続文字列は先ほど作成したDBになります。

f:id:okazuki:20160920125340p:plain

以上でデプロイは完了です。

クライアントの作成

今回はXamarin.Formsでいってみましょう。Prism Template Packをインストールした状態ですとPrismのカテゴリにPrism.Formsを使ったプロジェクトのテンプレートが追加されるため、それを使ってプロジェクトを作っていこうと思います。

f:id:okazuki:20160920201313p:plain

プロジェクトを作成したら、Microsoft.Azure.Mobile.ClientをNuGetパッケージマネージャーから追加します。

追加したら、MobileServiceClientAppクラスで、UnityのDIコンテナに登録しましょう。

using Microsoft.Practices.Unity;
using Microsoft.WindowsAzure.MobileServices;
using Prism.Unity;
using PrismMobileApp.Views;
using System;
using System.Net.Http;

namespace PrismMobileApp
{
    public partial class App : PrismApplication
    {
        public App(IPlatformInitializer initializer = null) : base(initializer) { }

        protected override async void OnInitialized()
        {
            InitializeComponent();

            await this.NavigationService.NavigateAsync("MainPage?title=Hello%20from%20Xamarin.Forms");
        }

        protected override void RegisterTypes()
        {
            this.Container.RegisterTypeForNavigation<MainPage>();
            // MobileServiceClientをシングルトンで登録
            this.Container.RegisterType<MobileServiceClient>(
                new ContainerControlledLifetimeManager(),
                new InjectionConstructor(
                    new Uri("https://okazuki0920.azurewebsites.net"),
                    new HttpMessageHandler[] { }));
        }
    }
}

そして、Models名前空間にTodoItemクラスを追加します。

using System;

namespace PrismMobileApp.Models
{
    public class TodoItem
    {
        public string Id { get; set; }

        public string Text { get; set; }

        public bool Complete { get; set; }

        public byte[] Version { get; set; }

        public DateTimeOffset? CreatedAt { get; set; }

        public DateTimeOffset? UpdatedAt { get; set; }
    }
}

続けてMainPageViewModelを作っていきます。これはコンストラクタでMobileClientServiceを受け取って、リフレッシュと追加処理を行っています。

using Microsoft.WindowsAzure.MobileServices;
using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using PrismMobileApp.Models;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace PrismMobileApp.ViewModels
{
    public class MainPageViewModel : BindableBase, INavigationAware
    {
        private MobileServiceClient Client { get; }

        private IEnumerable<TodoItem> todoItems;

        public IEnumerable<TodoItem> TodoItems
        {
            get { return this.todoItems; }
            set { this.SetProperty(ref this.todoItems, value); }
        }

        private string text;

        public string Text
        {
            get { return this.text; }
            set { this.SetProperty(ref this.text, value); }
        }

        public DelegateCommand AddCommand { get; }

        public MainPageViewModel(MobileServiceClient client)
        {
            this.Client = client;
            this.AddCommand = new DelegateCommand(async () =>
                {
                    await this.Client.GetTable<TodoItem>()
                        .InsertAsync(new TodoItem { Text = this.Text });
                    this.Text = "";
                    await this.RefreshAsync();
                }, () => !string.IsNullOrWhiteSpace(this.Text))
                .ObservesProperty(() => this.Text);
        }

        private async Task RefreshAsync()
        {
            this.TodoItems = await this.Client.GetTable<TodoItem>().CreateQuery().ToListAsync();
        }

        public void OnNavigatedFrom(NavigationParameters parameters)
        {

        }

        public async void OnNavigatedTo(NavigationParameters parameters)
        {
            await this.RefreshAsync();
        }
    }
}

あとは、XAMLで画面を作るだけです。

<?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:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="PrismMobileApp.Views.MainPage"
             Title="MainPage">
  <ContentPage.Padding>
    <OnPlatform x:TypeArguments="Thickness" 
                iOS="0,20,0,0"
                Android="0"/>
  </ContentPage.Padding>
  <StackLayout>
    <StackLayout Orientation="Horizontal">
      <Entry Text="{Binding Text, Mode=TwoWay}" 
             HorizontalOptions="FillAndExpand" />
      <Button Text="Add" 
              Command="{Binding AddCommand}"/>
    </StackLayout>
    <ListView ItemsSource="{Binding TodoItems}">
      <ListView.ItemTemplate>
        <DataTemplate>
          <ViewCell>
            <StackLayout Orientation="Horizontal">
              <Label Text="{Binding Text}"
                     HorizontalOptions="FillAndExpand" />
              <Switch IsToggled="{Binding Complete}" />
            </StackLayout>
          </ViewCell>
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>
  </StackLayout>
</ContentPage>

これで実行すると以下のような画面になります。

f:id:okazuki:20160920205334p:plain

.NETバックエンドでの認証

認証もついでにやってみようと思います。.NETバックエンドでの認証もTwitterの設定は以前書いたものと同じようになります。

blog.okazuki.jp

サーバーの設定をしたら、TodoControllerAuthrize属性をつけて再デプロイします。

namespace SampleMobileApp.Controllers
{
    [Authorize]
    public class TodoItemController : TableController<TodoItem>
    {

あとの流れはnodejsバックエンドと同じですね。PCLプロジェクトに以下のような認証インターフェースを切ります。

using System.Threading.Tasks;

namespace PrismMobileApp.Models
{
    public interface IAuthenticator
    {
        Task<bool> AuthenticateAsync();
    }
}

Droidプロジェクトで以下のような実装を作り

using Android.Util;
using Microsoft.WindowsAzure.MobileServices;
using PrismMobileApp.Models;
using System;
using System.Threading.Tasks;
using Xamarin.Forms;

namespace PrismMobileApp.Droid.Models
{
    public class Authenticator : IAuthenticator
    {
        private MobileServiceClient Client { get; }

        public Authenticator(MobileServiceClient client)
        {
            this.Client = client;
        }

        public async Task<bool> AuthenticateAsync()
        {
            try
            {
                var user = await this.Client.LoginAsync(Forms.Context, MobileServiceAuthenticationProvider.Twitter);
                return user != null;
            }
            catch (Exception ex)
            {
                Log.Debug(nameof(Authenticator), ex.ToString());
                return false;
            }
        }
    }
}

MainActivityに定義されているAndroidInitializerクラスでUnityのDIコンテナに登録します。

public class AndroidInitializer : IPlatformInitializer
{
    public void RegisterTypes(IUnityContainer container)
    {
        container.RegisterType<IAuthenticator, Authenticator>(new ContainerControlledLifetimeManager());
    }
}

ちょっと入り組んでますがMainPageViewModelに認証処理を組み込みます。 これくらい入り組んでくると、ここらへんの処理もModelに切り出したほうがいいでしょうね。

using Microsoft.WindowsAzure.MobileServices;
using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using Prism.Services;
using PrismMobileApp.Models;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace PrismMobileApp.ViewModels
{
    public class MainPageViewModel : BindableBase, INavigationAware
    {
        private MobileServiceClient Client { get; }

        private IAuthenticator Authenticator { get; }

        private IEnumerable<TodoItem> todoItems;

        public IEnumerable<TodoItem> TodoItems
        {
            get { return this.todoItems; }
            set { this.SetProperty(ref this.todoItems, value); }
        }

        private string text;

        public string Text
        {
            get { return this.text; }
            set { this.SetProperty(ref this.text, value); }
        }

        public DelegateCommand AddCommand { get; }

        public MainPageViewModel(MobileServiceClient client, IAuthenticator authenticator)
        {
            this.Client = client;
            this.Authenticator = authenticator;
            this.PageDialogService = pageDialogService;

            this.AddCommand = new DelegateCommand(async () =>
                {
                    await this.Client.GetTable<TodoItem>()
                        .InsertAsync(new TodoItem { Text = this.Text });
                    this.Text = "";
                    await this.RefreshAsync();
                }, () => !string.IsNullOrWhiteSpace(this.Text) && this.Client.CurrentUser != null)
                .ObservesProperty(() => this.Text);
        }

        private async Task RefreshAsync()
        {
            if (this.Client.CurrentUser == null)
            {
                if (await this.Authenticator.AuthenticateAsync())
                {
                    await this.RefreshAsync();
                }
            }
            else
            {
                this.TodoItems = await this.Client.GetTable<TodoItem>().CreateQuery().ToListAsync();
            }
        }

        public void OnNavigatedFrom(NavigationParameters parameters)
        {

        }

        public async void OnNavigatedTo(NavigationParameters parameters)
        {
            await this.RefreshAsync();
        }
    }
}

実行するとログイン画面が出ます。

f:id:okazuki:20160920211048p:plain

サインインをすると、以下のようにデータが表示されます。ちゃんと取れてますね。

f:id:okazuki:20160920211153p:plain