読者です 読者をやめる 読者になる 読者になる

かずきのBlog@hatena

XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

Azure MobileApps + Xamarin.Forms開発の始め方(.NETバックエンド + Prism.Forms)「プッシュ通知(Androidだけ)」

Azure Xamarin

過去記事

プッシュ通知

そろそろ、プッシュ通知してみたいと思います。 先日、このためにGoogleの開発者登録しました。流石にMac持ってないのでAppleの開発者登録はしてないのでiOS側はプッシュ通知試せてないです。すいません。

ということでXamarin.FormsのAndroidのプッシュ通知を試してみたいと思います。

通知ハブの作成

通知を使うには通知ハブを作成しないといけません。Azure上に作成したMobile AppsのPushを選んで作ります。

f:id:okazuki:20160924071028p:plain

名前とネームスペースを入力して作成します。

f:id:okazuki:20160924071221p:plain

Web.configのAppSettingsにあるMS_NotificationHubNameに作成したNotificationHubの名前を入れておきます。

<add key="MS_NotificationHubName" value="okazuki0920-hub" />

ライブラリのインストール

サーバーサイドのプロジェクトにNuGetから以下のライブラリをインストールします。

  • Microsoft.Azure.NotificationHubs

プッシュ通知処理の追加

データが追加されたらプッシュ通知をしてみましょう。InsertAsyncお呼び出しの後にコードを追加します。

// POST tables/TodoItem
public async Task<IHttpActionResult> PostTodoItem(TodoItem item)
{
    item.UserId = (this.User as ClaimsPrincipal)?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
    TodoItem current = await InsertAsync(item);

    await PushAsync(item);

    return CreatedAtRoute("Tables", new { id = current.Id }, current);
}

private async Task PushAsync(TodoItem item)
{
    var settings = this.Configuration.GetMobileAppSettingsProvider().GetMobileAppSettings();
    var hubName = settings.NotificationHubName;
    var connectionString = settings.Connections[MobileAppSettingsKeys.NotificationHubConnectionString].ConnectionString;
    var hub = NotificationHubClient.CreateClientFromConnectionString(connectionString, hubName);

    var templateParams = new Dictionary<string, string>
    {
        { "messageParam",$"{item.Text}が追加されました" }
    };
    
    try
    {
        await hub.SendTemplateNotificationAsync(templateParams);
    }
    catch (Exception ex)
    {
        this.Configuration.Services.GetTraceWriter().Error(ex);
    }
}

そして、Azureへ発行しておきます。

Androidプロジェクトの編集

Googleのデベロッパーコンソールというやつに移動します。

console.developers.google.com

そして、プロジェクトを作ります。

f:id:okazuki:20160924073428p:plain

プロジェクト名と、質問事項にこたえて作成します。

そして、ここに行きます。

console.firebase.google.com

プロジェクトを作成するボタンを押して、先ほど作成したプロジェクトにFireBaseを追加してください。

設定を選びます。クラウドメッセージングを選びます。 f:id:okazuki:20160924080851p:plain

送信者IDとサーバーキーが表示されるので控えておきます。

Azureのポータルで作成したMobile AppsからPushを選択してGMSを選ぶとAPIキーの入力を求められるので入力を行います。

f:id:okazuki:20160924074839p:plain

そして、DroidプロジェクトのComponentsからGet More ComponentでGoogle Cloud Messaging Clientをインストールします。 (Xamarinのアカウントでのログインを求められたけど、これって新規で使ってる人とかどうするんだろう…)

コードを書こう

設定が終わったのでコードを書いていきます。 まず、送信者IDの定数を定義しておきましょう。

namespace PrismMobileApp.Droid
{
    public static class Consts
    {
        public static string[] SenderIds { get; } = new[] { "送信者ID" };
    }
}

そして、MainActivityのLoadApplicationの呼び出しの後に以下のコードを追加します。

try
{
    GcmClient.CheckDevice(this);
    GcmClient.CheckManifest(this);
    GcmClient.Register(this, Consts.SenderIds);
}
catch (Java.Net.MalformedURLException)
{
    Log.Debug(nameof(MainActivity), "Error");
}
catch (Exception ex)
{
    Log.Debug(nameof(MainActivity), ex.ToString());
}

GcmServiceというクラスを作成して、usingとnamespaceの間に以下のコードを追加します。

[assembly: Permission(Name = "@PACKAGE_NAME@.permission.C2D_MESSAGE")]
[assembly: UsesPermission(Name = "@PACKAGE_NAME@.permission.C2D_MESSAGE")]
[assembly: UsesPermission(Name = "com.google.android.c2dm.permission.RECEIVE")]
[assembly: UsesPermission(Name = "android.permission.INTERNET")]
[assembly: UsesPermission(Name = "android.permission.WAKE_LOCK")]
//GET_ACCOUNTS is only needed for android versions 4.0.3 and below
[assembly: UsesPermission(Name = "android.permission.GET_ACCOUNTS")]

次に、PushHandlerBoroadcastReceiverというクラスを定義します。

[BroadcastReceiver(Permission = Gcm.Client.Constants.PERMISSION_GCM_INTENTS)]
[IntentFilter(new string[] { Gcm.Client.Constants.INTENT_FROM_GCM_MESSAGE }, Categories = new string[] { "@PACKAGE_NAME@" })]
[IntentFilter(new string[] { Gcm.Client.Constants.INTENT_FROM_GCM_REGISTRATION_CALLBACK }, Categories = new string[] { "@PACKAGE_NAME@" })]
[IntentFilter(new string[] { Gcm.Client.Constants.INTENT_FROM_GCM_LIBRARY_RETRY }, Categories = new string[] { "jp.okazuki.pushsample" })]
public class PushHandlerBroadcastReceiver : GcmBroadcastReceiverBase<GcmService>
{
}

そして、GcmServiceBaseクラスを継承したGcmServiceクラスを作成します。

[Service]
public class GcmService : GcmServiceBase
{
    public GcmService() : base(Consts.SenderIds)
    {
    }

    protected override void OnMessage(Context context, Intent intent)
    {
        throw new NotImplementedException();
    }

    protected override void OnError(Context context, string errorId)
    {
        throw new NotImplementedException();
    }

    protected override void OnRegistered(Context context, string registrationId)
    {
        throw new NotImplementedException();
    }

    protected override void OnUnRegistered(Context context, string registrationId)
    {
        throw new NotImplementedException();
    }
}

このOnRegisteredで、渡されたregistrationIdを使って登録処理をしないといけないのですが、認証が有効になってると、ここで登録処理を行ってもアクセス権がないと怒られます。認証が終わった後にプッシュ通知に登録を行わないといけません。今回は、認証処理を行うIAuthenticatorに認証後に何かやるという処理を登録できる仕組みを追加しました。

using System;
using System.Threading.Tasks;

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

        void RegisterPostProcess(Func<Task> action);
    }
}

こんな感じにインターフェースを変えて

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

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

        private List<Func<Task>> PostProcesses { get; } = new List<Func<Task>>();

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

        public async Task<bool> AuthenticateAsync()
        {
            try
            {
                var user = await this.Client.LoginAsync(Forms.Context, MobileServiceAuthenticationProvider.Twitter);
                if (user != null)
                {
                    await Task.WhenAll(
                        this.PostProcesses.Select(x => x()));
                }

                return user != null;
            }
            catch (Exception ex)
            {
                Log.Debug(nameof(Authenticator), ex.ToString());
                return false;
            }
        }

        public void RegisterPostProcess(Func<Task> action)
        {
            this.PostProcesses.Add(action);
        }
    }
}

こんな感じに認証に成功したらPostProcessesを全部実行するという感じにしました。

下準備ができたので各オーバーライドメソッドを実装します。

[Service]
public class GcmService : GcmServiceBase
{
    public GcmService() : base(Consts.SenderIds)
    {
    }

    protected override void OnMessage(Context context, Intent intent)
    {
        var message = intent.Extras.GetString("message");
        if (!string.IsNullOrEmpty(message))
        {
            this.CreateNotification("New todo item!", $"Todo item: {message}");
            return;
        }
    }

    private void CreateNotification(string title, string description)
    {
        var notificationManager = this.GetSystemService(Context.NotificationService) as NotificationManager;
        var uiIntent = new Intent(this, typeof(MainActivity));

        var builder = new NotificationCompat.Builder(this);
        var notification = builder.SetContentIntent(PendingIntent.GetActivity(this, 0, uiIntent, 0))
            .SetSmallIcon(Android.Resource.Drawable.SymActionEmail)
            .SetTicker(title)
            .SetContentTitle(title)
            .SetContentText(description)
            .SetSound(RingtoneManager.GetDefaultUri(RingtoneType.Notification))
            .SetAutoCancel(true)
            .Build();

        notificationManager.Notify(1, notification);
    }

    protected override void OnError(Context context, string errorId)
    {
    }

    protected override void OnRegistered(Context context, string registrationId)
    {
        ((App)App.Current).Container.Resolve<IAuthenticator>().RegisterPostProcess(() =>
        {
            ((Activity)Forms.Context).RunOnUiThread(async () =>
            {
                try
                {
                    var client = ((App)App.Current).Container.Resolve<MobileServiceClient>();
                    var push = client.GetPush();

                    const string templateBodyGCM = @"{ ""data"": { ""message"":""$(messageParam)"" }}";
                    var template = new JObject();
                    template["genericMessage"] = new JObject
                    {
                        { "body", templateBodyGCM },
                    };

                    await push.RegisterAsync(registrationId, template);
                }
                catch (Exception ex)
                {
                    Log.Error(nameof(GcmService), ex.ToString());
                }
            });
            return Task.CompletedTask;
        });
    }

    protected override void OnUnRegistered(Context context, string registrationId)
    {
    }
}

OnRegisteredメソッドで先ほどの認証処理の後処理にプッシュの登録処理を追加しています。

動作確認

ローカルデバッグ用に構成してた、App.xaml.csの中身をクラウドように書き換えます。

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[] { }));
    //this.Container.RegisterType<MobileServiceClient>(
    //    new ContainerControlledLifetimeManager(),
    //    new InjectionConstructor(
    //        new Uri("http://169.254.80.80:1782/"),
    //        new HttpMessageHandler[] { }),
    //    new InjectionProperty(nameof(MobileServiceClient.AlternateLoginHost), new Uri("https://okazuki0920.azurewebsites.net")));
    this.Container.RegisterType<TodoApp>(new ContainerControlledLifetimeManager());
    this.Container.RegisterType<IDataSynchronizer, DataSynchronizer>(new ContainerControlledLifetimeManager());
    this.Container.RegisterType<ITodoItemRepository, TodoItemRepository>(new ContainerControlledLifetimeManager());
}

VSのAndroidエミュレータだとプッシュ通知できない?っぽいので実機デバッグします。

何か適当にデータを追加してPullToRefreshかけるとプッシュ通知が飛んできます。

f:id:okazuki:20160924102451j:plain

まとめ

認証とセットでプッシュ通知をやるときは、プッシュ通知の登録タイミングを気を付けよう。

Azure MobileApps + Xamarin.Forms開発の始め方(.NETバックエンド + Prism.Forms)「オフラインデータ同期」

Azure Xamarin

過去記事

オフラインデータ同期をしよう

モバイルアプリの特徴として、常にネットワーク通信が良好な状態で操作されるわけではないという特徴があります。 そんなときのため、ローカルDBにいったんデータをため込んで、ネットワークが生きてるときにサーバーと同期するということが行われたりします。

Azure Mobile Appを使うと、ここら辺の機能がデフォルトで組み込まれているので捗ります。

オフライン同期をするには

下準備

Xamarin.Forms(Android, iOS)でオフライン同期を有効にするには、以下のパッケージをNuGetからPCLとAndroidとiOSのプロジェクトに追加します。

  • Microsoft.Azure.Mobile.Client.SQLiteStore

2016/09/23現在、Xamarin.FormsのMobile Appsの同期のドキュメントは古くて、そこに書いてある通りに実行するとエラーになります(ひどい)

azure.microsoft.com

ちょっと処理が変わってて、SQLiteのパスを取得する処理がAndroidだとPersonalのパスをとってきて、そのファイルを作っておかないといけないという点と、iOSではSQLitePCL.Batteries.Init();が必要そうだというところです。ここらへんは、Xamarin.AndroidとXamarin.iOSのドキュメントを個別にみると書いてあります。

azure.microsoft.com

azure.microsoft.com

パスの指定方法が、プラットフォーム固有になってます。なので、DependencyServiceを使うか、Prism.FormsならPlatformInitializerを使う必要があります。 今回は、Prism.Formsを使ってるのでPlatformInitializerを使いたいと思います。

ISQLiteDBPathProviderというインターフェースを作成します。

namespace PrismMobileApp.Models
{
    public interface ISQLiteDBPathProvider
    {
        string GetPath();
    }
}

Droidプロジェクトで、以下のような実装を準備します。

using System;
using PrismMobileApp.Models;
using System.IO;

namespace PrismMobileApp.Droid.Models
{
    class SQLiteDBPathProvider : ISQLiteDBPathProvider
    {
        public string GetPath()
        {
            var path = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.Personal),
                "localstore.db");
            if (!File.Exists(path))
            {
                File.Create(path).Dispose();
            }

            return path;
        }
    }
}

iOSプロジェクトで以下のような実装を準備します。

using PrismMobileApp.Models;

namespace PrismMobileApp.iOS.Models
{
    class SQLiteDBPathProvider : ISQLiteDBPathProvider
    {
        public string GetPath()
        {
            SQLitePCL.Batteries.Init();
            return "localstore.db";
        }
    }
}

そして、各々のPlatformInitializerで以下のように登録します。

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


// iOS
public class iOSInitializer : IPlatformInitializer
{
    public void RegisterTypes(IUnityContainer container)
    {
        container.RegisterType<IAuthenticator, Authenticator>(new ContainerControlledLifetimeManager());
        container.RegisterType<ISQLiteDBPathProvider, SQLiteDBPathProvider>(new ContainerControlledLifetimeManager());
    }
}

これで下準備は完了です。

大規模リファクタリング

処理が複雑になってきたので、ちょっくらリファクタリングします。

using Microsoft.WindowsAzure.MobileServices;
using Microsoft.WindowsAzure.MobileServices.SQLiteStore;
using Prism.Mvvm;
using System.Collections.ObjectModel;
using System.Threading.Tasks;

namespace PrismMobileApp.Models
{
    public class TodoApp : BindableBase
    {
        private MobileServiceClient Client { get; }

        private ISQLiteDBPathProvider SQLiteDBPathProvider { get; }

        private ITodoItemRepository TodoItemRepository { get; }

        private IDataSynchronizer DataSynchronizer { get; }

        private IAuthenticator Authenticator { get; }

        public ObservableCollection<TodoItem> TodoItems { get; } = new ObservableCollection<TodoItem>();

        private bool isAuthenticated;

        public bool IsAuthenticated
        {
            get { return this.isAuthenticated; }
            set { this.SetProperty(ref this.isAuthenticated, value); }
        }

        private bool IsInitialized { get; set; }


        public TodoApp(MobileServiceClient client, 
            ISQLiteDBPathProvider sqliteDBPathProvider, 
            IAuthenticator authenticator,
            ITodoItemRepository todoItemRepository,
            IDataSynchronizer dataSynchronizer)
        {
            this.Client = client;
            this.Authenticator = authenticator;
            this.SQLiteDBPathProvider = sqliteDBPathProvider;
            this.TodoItemRepository = todoItemRepository;
            this.DataSynchronizer = dataSynchronizer;
        }

        public async Task InitializeAsync()
        {
            if (this.IsInitialized) { return; }

            var store = new MobileServiceSQLiteStore(this.SQLiteDBPathProvider.GetPath());
            store.DefineTable<TodoItem>();
            await this.Client.SyncContext.InitializeAsync(store);
            this.IsInitialized = true;
        }

        public async Task AuthenticateAsync()
        {
            if (this.IsAuthenticated) { return; }

            this.IsAuthenticated = await this.Authenticator.AuthenticateAsync();
        }

        public Task SyncAsync() => this.DataSynchronizer.SyncAsync();

        public async Task LoadTodoItemsAsync()
        {
            var todoItems = await this.TodoItemRepository.GetAllAsync();
            this.TodoItems.Clear();
            foreach (var item in todoItems)
            {
                this.TodoItems.Add(item);
            }
        }

        public async Task InsertTodoItemAsync(TodoItem todoItem)
        {
            await this.TodoItemRepository.InsertAsync(todoItem);
            await this.LoadTodoItemsAsync();
        }
    }
}

こんなメインのModelを用意します。InitializeAsyncの中身でローカル同期の初期化を行っています。

var store = new MobileServiceSQLiteStore(this.SQLiteDBPathProvider.GetPath());
store.DefineTable<TodoItem>();
await this.Client.SyncContext.InitializeAsync(store);

この処理は、先ほど作成したプラットフォーム固有のSQLiteDBPathProviderクラスからパスを取得して、それを使ってMobileServiceSQLiteStoreクラスを生成します。DefineTableメソッドでテーブル定義の初期化を行います。

このTodoAppクラス内で使われてるIDataSynchornizerと、ITodoItemRepositoryが新顔です。以下のような定義になります。

using System.Threading.Tasks;

namespace PrismMobileApp.Models
{
    public interface IDataSynchronizer
    {
        Task SyncAsync();
    }
}
using System.Collections.Generic;
using System.Threading.Tasks;

namespace PrismMobileApp.Models
{
    public interface ITodoItemRepository
    {
        Task<IEnumerable<TodoItem>> GetAllAsync();

        Task InsertAsync(TodoItem todoItem);
    }
}

こいつらの実装クラスは置いといて、MainPageViewModelTodoAppを使うように変更します。

using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using PrismMobileApp.Models;
using System.Collections.ObjectModel;

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

        private string text;

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

        private bool isRefreshing;

        public bool IsRefreshing
        {
            get { return this.isRefreshing; }
            set { this.SetProperty(ref this.isRefreshing, value); }
        }

        public ObservableCollection<TodoItem> TodoItems => this.TodoApp.TodoItems;

        public DelegateCommand AddCommand { get; }

        public DelegateCommand RefreshCommand { get; }

        public MainPageViewModel(TodoApp todoApp)
        {
            this.TodoApp = todoApp;

            this.AddCommand = new DelegateCommand(async () =>
                {
                    await this.TodoApp.InsertTodoItemAsync(new TodoItem { Text = this.Text });
                    this.Text = "";
                    await this.TodoApp.LoadTodoItemsAsync();
                }, () => !string.IsNullOrWhiteSpace(this.Text) && this.TodoApp.IsAuthenticated)
                .ObservesProperty(() => this.Text);

            this.RefreshCommand = new DelegateCommand(async () =>
            {
                await this.TodoApp.SyncAsync();
                await this.TodoApp.LoadTodoItemsAsync();
                this.IsRefreshing = false;
            });
        }

        public void OnNavigatedFrom(NavigationParameters parameters)
        {

        }

        public async void OnNavigatedTo(NavigationParameters parameters)
        {
            await this.TodoApp.InitializeAsync();
            await this.TodoApp.AuthenticateAsync();
            await this.TodoApp.SyncAsync();
            await this.TodoApp.LoadTodoItemsAsync();
        }
    }
}

同期処理

IDataSynchronizerの実装クラスに同期処理を書きます。同期処理は、プルとプッシュになります。プッシュがローカルで行われた変更を順次サーバーに反映していきます。これはデータ全体に対して行われます。プルは、サーバーからの変更を取り込みます。この取り込みは、テーブル単位で行います。プルするときに、プッシュしないといけないものがある場合は暗黙的にプッシュがされるという動きをします。

ということで、コードで簡単な同期処理を書くと以下のようになります。

using Microsoft.WindowsAzure.MobileServices;
using Microsoft.WindowsAzure.MobileServices.Sync;
using System;
using System.Diagnostics;
using System.Threading.Tasks;

namespace PrismMobileApp.Models
{
    class DataSynchronizer : IDataSynchronizer
    {
        private MobileServiceClient Client { get; }

        private IMobileServiceSyncTable<TodoItem> TodoItemTable { get; }

        public DataSynchronizer(MobileServiceClient client)
        {
            this.Client = client;
            this.TodoItemTable = this.Client.GetSyncTable<TodoItem>();
        }

        public async Task SyncAsync()
        {
            try
            {
                await this.Client.SyncContext.PushAsync();
                await this.TodoItemTable.PullAsync("allTodoItem", this.TodoItemTable.CreateQuery());
            }
            catch (MobileServicePushFailedException ex)
            {
                var errors = ex.PushResult?.Errors;
                if (errors != null)
                {
                    foreach (var error in errors)
                    {
                        if (error.OperationKind == MobileServiceTableOperationKind.Update && error.Result != null)
                        {
                            await error.CancelAndUpdateItemAsync(error.Result);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex);
            }
        }
    }
}

プッシュは、MobileServiceClientクラスのSyncContextに対してPushAsyncを呼ぶことで実行できます。 プルは、MobileServiceClientからGetSyncTableで取得したテーブルに対してPullAsyncを呼び出すことで実行できます。PullAsyncは、クエリ名とクエリを受け取ります。クエリ名を指定すると、最後に同期した後にサーバーで行われた変更を取り込む動きをします。nullを指定すると全同期になるみたいです。クエリ名は、一意になる必要があるっぽいです。

プッシュに失敗したら、MobileServicePushFailedExceptionが発生します。この例外をハンドリングすることでエラーに対していろいろできます。今回は更新処理に失敗して、サーバーからデータが返されたらコンフリクトしてるということなので、ローカルのデータをサーバーのデータで上書きしています。さよならローカル変更。それ以外は無視しています。

次に、ローカルDBに対する読み込みと書き込みをやります。

using Microsoft.WindowsAzure.MobileServices;
using Microsoft.WindowsAzure.MobileServices.Sync;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace PrismMobileApp.Models
{
    class TodoItemRepository : ITodoItemRepository
    {
        private IMobileServiceSyncTable<TodoItem> TodoItemTable { get; }

        public TodoItemRepository(MobileServiceClient client)
        {
            this.TodoItemTable = client.GetSyncTable<TodoItem>();
        }

        public Task<IEnumerable<TodoItem>> GetAllAsync()
        {
            return this.TodoItemTable.CreateQuery().OrderBy(x => x.Text).ToEnumerableAsync();
        }

        public Task InsertAsync(TodoItem todoItem)
        {
            return this.TodoItemTable.InsertAsync(todoItem);
        }
    }
}

これは、MobileServiceClientクラスにGetSyncTableで取得できるIMobileServiceSyncTableクラスを使うことで実現できます。使い方はGetTableで取得したIMobileServyceTableと同じ感覚で使えます。

コンテナへの登録

最後に、コンテナでいい感じにModelのクラスとかを組み立ててくれるように登録を行います。

using Microsoft.Practices.Unity;
using Microsoft.WindowsAzure.MobileServices;
using Prism.Unity;
using PrismMobileApp.Models;
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");
        }

        protected override void RegisterTypes()
        {
            this.Container.RegisterTypeForNavigation<MainPage>();
            // MobileServiceClientをシングルトンで登録
            this.Container.RegisterType<MobileServiceClient>(
                new ContainerControlledLifetimeManager(),
                new InjectionConstructor(
                    //new Uri("https://okazuki0920.azurewebsites.net"),
                    new Uri("http://169.254.80.80:1782/"),
                    new HttpMessageHandler[] { }),
                new InjectionProperty(nameof(MobileServiceClient.AlternateLoginHost), new Uri("https://okazuki0920.azurewebsites.net")));
            this.Container.RegisterType<TodoApp>(new ContainerControlledLifetimeManager());
            this.Container.RegisterType<IDataSynchronizer, DataSynchronizer>(new ContainerControlledLifetimeManager());
            this.Container.RegisterType<ITodoItemRepository, TodoItemRepository>(new ContainerControlledLifetimeManager());
        }
    }
}

Visual Studio Emuletor for AndroidでローカルサーバーにつなぐようにURLは構成しています。(iOS動かせる環境がないので未検証)

これで実行すると同期処理を実行するまでサーバーにデータが反映されないことが確認できます。

ここまでのソースをGitHubにあげておきました。私のAzure環境にアクセスできる状態の間はローカル環境で動かせるのではないかと思います。(近い将来消すので動かなくなります)

github.com

Azure MobileApps + Xamarin.Forms開発の始め方(.NETバックエンド + Prism.Forms)「ローカルデバッグ」

Azure Xamarin

過去記事

ローカルデバッグをしよう

今までAzureにデプロイしてデバッグして認証までつけました。 今回はローカル環境でのデバッグをしてみたいと思います。

SQL DatabaseはとりあえずAzureを使う

とりあえずSQL Databaseの接続文字列はAzureのを指してるので、そのままにしておきます。ポータルからSQL Databaseのファイアウォールに穴をあけておきましょう。

f:id:okazuki:20160922102423p:plain

認証をどうにかしよう

認証をどうにかしたいと思います。新規作成したプロジェクトは、そこらへんのおぜん立てもしてくれていて、以下のようなコードがStartup.MobileApp.csに記述されています。

app.UseAppServiceAuthentication(new AppServiceAuthenticationOptions
{
    // This middleware is intended to be used locally for debugging. By default, HostName will
    // only have a value when running in an App Service application.
    SigningKey = ConfigurationManager.AppSettings["SigningKey"],
    ValidAudiences = new[] { ConfigurationManager.AppSettings["ValidAudience"] },
    ValidIssuers = new[] { ConfigurationManager.AppSettings["ValidIssuer"] },
    TokenHandler = config.GetAppServiceTokenHandler()
});

SigningKey, ValidAudience, ValidIssuerをAppSettingsに設定すればOKです。後者2つは、アプリのURLです。

SigingKeyは秘密の情報でポータルからとってきます。「http://アプリの名前.scm.azurewebsites.net」にアクセスしてKuduを表示させます。「Environmant」をクリックします。WEBSITE_AUTH_SIGNING_KEYという値があるので、この値をSigningKeyに設定します。

Web.configのappSettingsに以下の値を設定します。

<add key="SigningKey"
     value="Kuduからとってきた値"/>
<add key="ValidAudience"
     value="https://okazuki0920.azurewebsites.net/"/>
<add key="ValidIssuer"
     value="https://okazuki0920.azurewebsites.net/"/>

エミュレータからの接続設定

ここを参考に頑張りました。

qiita.com

IIS Expressをちょっと弄ります。ソリューションの下の.vs\config\applicationhost.configがIIS Expressの設定ファイルになるので、それを書き換えます。以下のようなsiteタグがあるので、そこのbindingsにbindingを追加します。

<site name="SampleMobileApp" id="2">
    <application path="/" applicationPool="Clr4IntegratedAppPool">
        <virtualDirectory path="/" physicalPath="C:\Users\Kazuki\documents\visual studio 2015\Projects\SampleMobileApp\SampleMobileApp"
    </application>
    <bindings>
        <binding protocol="http" bindingInformation="*:1782:localhost" />
        <binding protocol="http" bindingInformation="*:1782:169.254.80.80" /> <!-- こいつを追加 -->
    </bindings>
</site>

ポート番号は各自のに合わせてね(ここでは1782が使われてたので1782)

Visual Studio 2015を管理者権限で起動しなおします。

Windowsファイアウォールでポートを開放します(今回の例の場合は1782番)。

ローカルホストにアクセスするようにURLを書き換える

PCLのプロジェクトのApp.xaml.csを開いて以下のようにURLを書き換えます。Visual Studio Emulator for AndroidでローカルホストにアクセスするIPアドレスですね。もう1つポイントは、AlternateLoginHostに本番のURLを突っ込んでおくことです。これでログインがうまく動くようになります。

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

実行して動作確認

これで実行すると、Mobile Appsをクラウドではなくローカルで動かした状態でデバッグができます。ちゃんとデータが取れてることが確認できます。

f:id:okazuki:20160923014103p:plain

ローカルDBの使用

あとはDBさえローカルにしてしまえば認証機能つきの状態で完全にローカルでテストができるようになります。 これは簡単で、ローカルにSQL Server Express Editionなどを入れて、接続文字列を、そこに指定するだけです。

こんな感じですね。

<connectionStrings>
  <add name="MS_TableConnectionString" connectionString="Data Source=localhost;Initial Catalog=okazuki0920;Integrated Security=True" providerName="System.Data.SqlClient" />
</connectionStrings>

実行して適当にデータを突っ込むとローカルDBにデータができていることが確認できます。

f:id:okazuki:20160923014538p:plain

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

Azure Xamarin

過去記事

DBの変更

JavaScriptがバックエンドのときはいい感じにDBの変更をしてくれる(それはそれで怖いっちゃ怖いけど)んですが、.NETバックエンドだとそういう感じにはなっていません。 というか、どうやるのか書いてません。書いてませんがEntityFramework 6.0を使ってるのでおそらくマイグレーションするのでしょう。

Entity Framework Code First Migrations

とりあえず、今回はローカルDBは考えてなかったのでWeb.configの接続文字列にクラウドの接続文字列を追加しましょう(これは悪手なので後で正しいやり方をフォローする記事を書く(予定))

そして、パッケージマネージャーコンソールを開いて、既定のプロジェクトをMobile Appsのサーバープロジェクトに変更して以下のコマンドを打ち込みます。

PM> Enable-Migrations

試しに、UserIdを追加したいので、TodoItemUserId列を追加します。

using Microsoft.Azure.Mobile.Server;

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

        public bool Complete { get; set; }

        public string UserId { get; set; }
    }
}

そして、TodoControllerにUserIdでのフィルタリング等を追加します。 ただ、このロジックはIDを知ってる人なら、その情報にはアクセスできるという前提で動いています。(それ以上堅牢なロジックの組み方がわからなかったので、これが限界なのかも)

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;
using System.Security.Claims;

namespace SampleMobileApp.Controllers
{
    [Authorize]
    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()
        {
            var sid = (this.User as ClaimsPrincipal)?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
            return Query().Where(x => x.UserId == sid);
        }

        // 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)
        {
            item.UserId = (this.User as ClaimsPrincipal)?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
            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);
        }
    }
}

そして、マイグレーションを実行します。以下のコマンドをパッケージマネージャーコンソールでたたきましょう。

Add-Migration AddUserId

こんな感じのファイルが作られます。

namespace SampleMobileApp.Migrations
{
    using System;
    using System.Data.Entity.Migrations;
    
    public partial class AddUserId : DbMigration
    {
        public override void Up()
        {
            AddColumn("dbo.TodoItems", "UserId", c => c.String());
        }
        
        public override void Down()
        {
            DropColumn("dbo.TodoItems", "UserId");
        }
    }
}

いい感じですね。ついでに、自動でマイグレーションしてくれるようにしましょう。

App_Startの下のStartup.MobileApp.csにあるMobileServiceInitializerを以下のように書き換えます。

class MobileServiceInitializer : MigrateDatabaseToLatestVersion<MobileServiceContext, SampleMobileApp.Migrations.Configuration>
{
}

これで自動でマイグレーション走るようになります。これが嫌なケースの場合は、別途マイグレーションのSQLを生成する方法を最初に示したマイグレーションについて書かれたページで参照してみてください。

発行して実行すると以下のようになります。

f:id:okazuki:20160921223917p:plain

何も違いがわかりませんね。なかなか複数アカウント持ってないので確認しづらいところではあります…。ということで、DBを見てみましょう。

f:id:okazuki:20160921224341p:plain

UserId列が追加されて、値が入ってますね!!そして、DBにいろいろ値が入ってるけどちゃんとフィルタリングした結果が出てるっぽいですね!やったね!

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

Azure Xamarin

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

サーバーサイド(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

gitで複数リポジトリに同時にプッシュしたい

Other

そんなときもありますよね?ということで先人がいました。

stackoverflow.com

>mkdir sample
>git init
>git remote add all https://github.com/runceel/test1.git

でリモートを追加

>git remote -v
all     https://github.com/runceel/test1.git (fetch)
all     https://github.com/runceel/test1.git (push)

こんな感じになってる。

そして、pushを書き換える。

>git remote set-url --add --push all https://github.com/runceel/test2.git
>git remote -v
all     https://github.com/runceel/test1.git (fetch)
all     https://github.com/runceel/test2.git (push)

もう一度オリジナルを追加すると…

>git remote set-url --add --push all https://github.com/runceel/test1.git
>git remote -v
all     https://github.com/runceel/test1.git (fetch)
all     https://github.com/runceel/test2.git (push)
all     https://github.com/runceel/test1.git (push)

こうなる。(なんでかは理解してない)

これで何かcommitしてpushすると

>git push all master
Counting objects: 6, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (6/6), 451 bytes | 0 bytes/s, done.
Total 6 (delta 0), reused 0 (delta 0)
To https://github.com/runceel/test2.git
 * [new branch]      master -> master
Counting objects: 6, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (6/6), 451 bytes | 0 bytes/s, done.
Total 6 (delta 0), reused 0 (delta 0)
To https://github.com/runceel/test1.git
 * [new branch]      master -> master

こんな感じで2つのリポジトリにpushされた。

追記

こういうことね

stackoverflow.com

Azureで個人的によく使うと思うサービス

Azure

Azureのサービスって鬼のようにありますよね。

こちらのサイトにあるように、ちょっとこれだけの機能を網羅的に使いこなすのはしんどいです…。

いまさら聞けない!Microsoft Azure 概要 – 第三回 – : Microsoft Azure 情報メディア「AZURE WAVE」

ということで、とっかかりに使うサービスとしてお勧めなのを以下に挙げます。

サービス名 概要 無料枠ある? 課金体系
Web Apps 名前の通りWebアプリケーションをホストするサービス 有り Web AppsをホストするAppサービスプランのCPUとメモリのプランと台数 × 時間
SQL Database 名前の通りSQL ServerのAzure版。 有り*1 DTUと呼ばれる性能をお金で買う感じ
Storage Service これも名前から想像がつくようにファイル置き場のように使えるサービスです。ファイル置き場のBlobサービスや、キーバリューストアのTableや、Queueと呼ばれるメッセージキューのサービス等があります。 無し 容量とアクセス回数
Virtual Machine これまではPaaSサービスばかりでしたが、既存の何かがあって、それを単純に乗せたいだけならIaaSのVirtual Machineを使うといいと思います。WindowsだけでなくLinuxもあるので潰しは効くはずです。 無し マシンタイプ(CPUやメモリの性能)×時間

個人的には上記サービスで割と事足りるんですが、それ以外にも、こんなものをMicrosoftは推してるっぽいです。

サービス名 概要 無料枠ある? 課金体系
Azure Machine Learning 何かと話題の機械学習です。GUIでとても簡単に各種アルゴリズムを使うことが出来るのでお勧めです。 有り データ量と学習時間とAPIの呼び出し回数
Azure Functions AWSのLambdaみたいなものみたいですね。利用者は完全にサーバーを意識することなく、プログラムを置くだけで実行できる感じです。色々なものをトリガーにして起動したりできるみたいですね。 知らない*2 知らない*3
Mobile Apps Xamarinに力を入れてる関係か、mBaaSと呼ばれるモバイルバックエンドを構築するためのサービスのMobile Appsも、最近推してるように思います。nodejsのexpressやASP.NET WebAPIベースのフレームワークでAPIやDBへのアクセスを簡単に作ることが出来ます。また、それらにアクセスするためのクライアントライブラリも色々提供されてて、簡単にありがちなパターンのアプリなら作れるようになっています。 有り Mobile AppsをホストするAppサービスプランのCPUとメモリのプランと台数 × 時間

*1:Mobile Apps QuickStartと同時に作成すると無料枠のDBを1つ作れるっぽい

*2:誰か教えて

*3:誰か教えて

Azureで使われてる用語を今更見てみよう5選

Azure

Microsoft Azureを長年使ってる人にとっては常識だとしても初見殺しっぽいような単語をピックアップしてみようと思います。

f:id:okazuki:20160917230148p:plain

では順番に行ってみたいと思います。

リソースグループ

個人的第1位はリソースグループです。

リソースグループとは、Microsoft Azure内の様々なサービスから作成したリソース(サーバーやデータベースなど)をまとめておく単位です。まとめておくと以下のような利点があります。

  • 関連するサービスを1まとめにできるため整理整頓できる
  • リソースグループ単位でアクセス権限が設定できるため管理を人に渡すときにやりやすい
  • まとめて消せる(サンプルとか作って消すようなときに凄い便利)

Appサービスプラン

第2位は、Appサービスプランです。 これは、ウェブアプリケーションをホストするためのWeb appsというサービス等をホストするサーバーのようなものです。

CPUやメモリなどのリソースは、Appサービスプラン単位で決めることが出来ます。無料のFreeから強いPremiumまで様々なタイプが用意されています。また、10インスタンスまでスケールアウトすることも可能です*1

Appサービス

プランの次はAppサービスです。 Appサービスは、以下のサービスの総称みたいなものです。

  • Web apps
  • Mobile apps
  • Logic apps
  • API apps

個人的な感想ですが、Web appsで自分の作ったWebアプリのホストが出来るということを覚えておけば、まずいいと思います。

サブスクリプション

Azureを使用する際の契約の単位のことです。 1つのMSアカウントに複数紐づけることが出来ます。 支払い単位も、このサブスクリプション単位になります。

クラウドサービス

あれ?Microsoft Azureってクラウドサービスじゃないの?クラウドサービスの中にクラウドサービス??って個人的に初見殺しな名前だと思うんですよ!

クラウドサービスは、Microsoft Azureのサービスが始まったころからあるWebアプリをホストするためのWebロールと、バッチ処理なんかをホストするためのWorkerロールがあります。最近は、Webアプリをホストするなら前述のAppサービスのWeb appsを使い、バッチ処理なんかをホストする場合はWebJobというサービスを使うので、もはや下位互換のために残ってるサービスと思ってもいいような気がします。

ただ、このサービスが強いのは、気合入れれば自分でミドルウェアとか仕込んだりできるので、そういう用途でデプロイ芸を極めたい人達にはいいのかもしれません。(ミドルウェア入れるなら仮想マシンのIaaSで、いいんじゃっていう気がしますが)

参考

以下のページも参考になります。

azure.microsoft.com

*1:10インスタンスで足りない場合はサポートに問い合わせると増やしてもらえるみたいです

Azure上でDBアクセスするWebアプリを作るためのとっかかり

Azure ASP.NET

表題のようなWebアプリケーションを作りたいとしてとっかかりどうするの?というのが気になる方もいると思います。 ということで手順をメモっておきます。

Azureのアカウントは持ってる前提で進めます。

Azureで器の作成

Web AppとSQL Databaseを作成しておきます。何をするにも、まず入れ物がないと話しにならないですからね。

リソースグループの作成

Azureポータルにアクセスして、リソースグループを新規作成します。 何かやるときは、リソースグループを作ってその中に色々作っておくとグルーピングできて便利です。 何が便利かってあとでまとめて消せるのが個人的に重宝してますね。

消す以外にも、リソースグループ単位でアクセス権限つけたりすると、管理が何かとはかどったりすることもあります。

さて、では作っていきましょう。

ポータルのResource groupsを選んで画面上部のAddを選択します。名前を決めるように言われるので任意の名前を入れます。ここではokazuki0917と入力したものとして話を進めます。そして、ローケーションも選ぶ必要があるので何事もなければ日本のリージョンを選んでおくといいでしょう。ここではJapan Westを選んだものとして話を進めます。

f:id:okazuki:20160917204121p:plain

SQL Databaseの作成(ポータルから作る場合)

次にSQL Databaseを作成します。`ちなみにVisual Studioから後で作ることもできるのでVisual Studioから作りたい人はこの手順を飛ばしてもかまいません。

okazuki0917リソースグループを選択して画面上部のAddを選択します。FilterにSQL Databaseと入力して検索します。SQL Databaseが出てくるので選択します。Create`ボタンを押すと作成のウィザードが始まります。

名前などを決めて(ここではokazuki0917-dbにしました)サーバーも作成します。サーバーのロケーションをリソースグループと揃えておくのに気を付けましょう。あと、サーバーの作成時に資格情報を入力するので、あとでDBにつなぐときに必要になるため控えておきます。

f:id:okazuki:20160917204803p:plain

Pricing Tierは、いくらお金を払って性能を買うかという話しなので小さなサンプルのときはBasic(Show allを押さないと出てこない)を選んでおくといいと思います。

Web appの作成(ポータルからやる場合)

次にWeb appを作成します。 これも、2通りのやり方があってVisual Studioからプロジェクト新規作成時に作ってしまうか、ポータル上で作っておいて後で紐づけるかの選択になります。 あとでVisual Studioで作る場合は、ここは飛ばしてしまって大丈夫です。

ポータルで先に作っておきたい人は、ここの手順をやりましょう。

リソースグループで画面上部のAddを選択してFilterでWeb appと入力します。Web Appが表示されるので選択します。Createを選択すると新規作成のウィザードが始まります。

適当に名前を決めます(ここではokazuki0917-app)。そして、App service planというものも作成する必要がある(これがサーバーの実体みたいなもの)のでこれも作成します。 ここでもお金で性能を買うかどうか選択させられるので、サンプル程度なら無料のFreeを選択するといいでしょう。App service planも名前を決めないといけないので適当に決めます。 (ここではokazuki0917-appserviceplanにしました)

f:id:okazuki:20160917205548p:plain

プロジェクトの作成

ASP.NET Web アプリケーションの作成を行います。新規作成時に色々作られるのが嫌いなので、必要最低限のものが生成されるように、ASP.NET MVCのEmptyプロジェクトを作ります。

ポータルでSQL DatabaseとWeb appを作った人はクラウドのホストするのチェックを外しておきましょう。

Visual Studioから作りたい人は、チェックを入れておきましょう。

f:id:okazuki:20160917210040p:plain

Visual StudioからのWeb appとSQL Databaseの作成

クラウドのホストするのチェックを入れると以下のような画面が出ます。各種項目を埋めましょう。 リソースグループとApp service planを新規作成するには、新規作成ボタンを押して名前などを入力します。

f:id:okazuki:20160917210917p:plain

次にSQL Databaseを作成します。左のタブからサービスを選択してSQL Databaseの+を押します。サーバーの新規作成を押して、適当に名前と資格情報を入力しておきます。

f:id:okazuki:20160917211238p:plain

そして作成を押します。

ポータルで作成した人向け手順

ポータルでWeb appとかを作成した人は、プロジェクト作成後に紐づけを行います。

プロジェクトを右クリックして公開を選びます。 プロファイルタブで、Microsoft Azure App Service`を選びます。

そうすると以下のように、どこに配備するか選ぶ画面が出てくるので先ほど作成したリソースグループのWeb appを選びましょう。

f:id:okazuki:20160917212033p:plain

Azureへの発行

プロジェクトの右クリックから公開を選んで発行を行うとAzure上にWebアプリケーションが展開されます。まだ何も作ってないので何も出ませんが…。

ページの作成

Controllersフォルダで右クリックの追加からコントローラを選びます。そしてMVC 5コントローラー 空を選びます。 コントローラ名を決めるように促されるのでデフォルトでアクセスされるHomeControllerという名前にします。

コントローラが作成されたらIndexメソッドに紐づくViewを作成します。Indexメソッドの名前のところで右クリックしてビューを追加を選択します。

f:id:okazuki:20160917212605p:plain

MVC 5ビューを選択しましょう。デフォルトのままOKをします。

f:id:okazuki:20160917212719p:plain

これで公開から発行をおこなうと以下のようなページが表示されます。

f:id:okazuki:20160917212850p:plain

DBアクセス処理の追加

簡単なDBアクセス処理を作ってみましょう。

NuGetパッケージマネージャーからEntityFrameworkを追加します。

f:id:okazuki:20160917213100p:plain

そして、Modelsフォルダに以下のようなクラスを追加します。

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web;

namespace FirstStepAzure.Models
{
    public class SampleContext : DbContext
    {
        public DbSet<Person> People { get; set; }
    }

    public class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}

Global.asax.csにDBの初期化処理を追加します。ここでは、何かモデルに変更があったらDBを作り直すように指定しています。(ここらへんはEntityFrameworkの勉強をしてね)

using FirstStepAzure.Models;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace FirstStepAzure
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            RouteConfig.RegisterRoutes(RouteTable.Routes);

            Database.SetInitializer(new DropCreateDatabaseIfModelChanges<SampleContext>());
        }
    }
}

コントローラにDBを操作する処理を追加してみましょう。とりあえず動作確認のため、ページにアクセスするたびにデータを追加して、それを表示するようにしてみたいと思います。

using FirstStepAzure.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace FirstStepAzure.Controllers
{
    public class HomeController : Controller
    {
        // GET: Home
        public ActionResult Index()
        {
            // データの追加
            using (var ctx = new SampleContext())
            {
                ctx.People.Add(new Person { Name = $"okazuki {DateTime.UtcNow}" });
                ctx.SaveChanges();
            }

            // データを読み込む
            IEnumerable<Person> people;
            using (var ctx = new SampleContext())
            {
                people = ctx.People.ToArray();
            }

            return View(people);
        }
    }
}

ViewでControllerから受け取ったデータを表示します。

@model IEnumerable<FirstStepAzure.Models.Person>
@{
    ViewBag.Title = "Index";
}

<h2>Index</h2>

@foreach (var p in this.Model)
{
    <p>@p.Name</p>
}

接続文字列の構成

Web.configに接続文字列を追加します。とりあえずローカルで動かしたい人は、ローカルのSQL Server Express Editionとかの接続文字列を入れておくといいでしょう。appSettingsの下あたりに以下のような感じで追加します。

<connectionStrings>
  <add name="SampleContext" 
       connectionString="DBへの接続文字列"
       providerName="System.Data.SqlClient"/>
</connectionStrings>

AzureへDB接続文字列を構成して発行

次に発行を行います。発行の設定に先ほど追加したSampleContextという名前のDB接続先が出来てるので、接続先DBを選択します。

f:id:okazuki:20160917214353p:plain

この発行先を実行時に使用するのチェックはローカルDBを使いたい人は外しておきましょう。

そして発行を行います。

動作確認

実行してF5を押すとデータが増えていきます。

f:id:okazuki:20160917214720p:plain

最後に

あとは、ASP.NET MVCとEntityFrameworkを勉強してアプリを組んでいくだけです。書籍的には以下をお勧めしておきます。

今さら入門するMVVMに必要な技術要素(Xamarin.Forms & UWP)

MVVM UWP Xamarin

Model View ViewModelパターン(以下MVVMパターン)が登場して約10年になります。 ここらへんで一度MVVMを実装するうえで必要になる技術要素を振り返ってみたいと思います。

その前にMVVM

MVVMは以下のWikipediaあたりでも見てください。

Model View ViewModel - Wikipedia

見た目と、それ以外にクラスを分離して、さらに見た目をXAMLで作りやすいようにViewとViewModelに分離したようなイメージです。

見ていこう

ということでMVVMで必要になる技術要素を見ていこうと思います。

INotifyPropertyChangedインターフェース

まずは、これが無いと始まりません。MVVMではViewはViewModelを監視して、ViewModelはModelを監視していることが多いです。その時に、クラスのプロパティが変わったことを通知するためにC#に用意されている共通のインターフェースがSystem.ComponentModel.INotifyPropertyChangedインターフェースになります。

このインターフェースはPropertyChangedイベントを持つだけのシンプルなクラスになります。このインターフェースを実装したクラスでは、プロパティのsetterでPropertyChangedイベントの引数にPropertyChangedEventArgsでプロパティ名を渡したものを使ってイベントを発火することで、外部に対してプロパティの変更通知が出来ます。

例えば、以下のような実装になります。

public class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private string name;
    public string Name
    {
        get { return this.name; }
        set
        {
            this.name = value;
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
        }
    }
}

以下のようなコードで動作が確認できます。これは、XamarinでもUWPでもない、プレーンなコンソールアプリケーションです。

public class Program
{
    public static void Main(string[] args)
    {
        var p = new Person();
        p.PropertyChanged += (_, e) => Console.WriteLine($"{e.PropertyName} changed.");
        p.Name = "tanaka";
        p.Name = "okazuki";
    }
}

Personクラスのインスタンスを作成して、PropertyChangedイベントを購読しています。購読した先では変更のあったプロパティ名を表示しています。そして、2回値を代入しています。実行すると以下のような結果になります。

Name changed.
Name changed.

とても単純ですね。単純さ故に無駄もあります。例えば、コードを以下のように書き換えてみましょう。

public class Program
{
    public static void Main(string[] args)
    {
        var p = new Person();
        p.PropertyChanged += (_, e) => Console.WriteLine($"{e.PropertyName} changed.");
        p.Name = "tanaka";
        p.Name = "tanaka";
    }
}

実行すると先ほどと同じように2回Name changed.が表示されます。値が変わってないのに値が変わったと表示されるのは無駄なので、Nameプロパティを以下のように書き換えます。

public class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private string name;
    public string Name
    {
        get { return this.name; }
        set
        {
            // 比較して同じなら何もしない
            if (EqualityComparer<string>.Default.Equals(this.name, value))
            {
                return;
            }
            this.name = value;
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
        }
    }
}

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

Name changed.

これで無駄なイベントの発行がなくなりました。

共通クラス化

MVVMでは、INotifyPropertyChangedを実装したクラスを大量に作成します。そのプロパティすべてに上記のような記述をするのは冗長です。そのため、以下のようなベースクラスを作成するのが一般的です。

public class BindableBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null) =>
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

    protected virtual bool SetProperty<T>(ref T field, T value, [CallerMemberName]string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) { return false; }
        field = value;
        this.OnPropertyChanged(propertyName);
        return true;
    }
}

このBindableBaseクラスを使うことでPersonクラスは以下のようになります。

public class Person : BindableBase
{
    private string name;

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

public string Name { get; set; }に比べると冗長ですが、現状これが精一杯です。

XAML

XAMLはMVVMパターンでViewを記述するのに使います。XAMLはXMLをベースとした言語で階層構造を持ったオブジェクトを組み立てることに特化した言語になります。基本的に以下のようなルールがあります。

  • XML名前空間はC#の名前空間に対応
  • タグ名はクラス名に対応
  • 属性はプロパティに対応

例えば、XamarinApp.Models名前空間にあるPersonクラスをインスタンス化してNameプロパティにtanakaと設定するコードはC#で書くと以下のようになると思います。

new Person
{
    Name = "tanaka"
}

これをXAMLで書くと以下のようになります。

<!-- Xamarin.Formsの場合 -->
<Models:Person xmlns:Models="clr-namespace:XamarinApp.Models"
    Name="tanaka" />

<!-- UWPの場合 -->
<Models:Person xmlns:Models="using:XamarinApp.Models"
    Name="tanaka" />

XML名前空間内でのC#の名前空間の記述方法が違う以外は同じ書き方になります。

プロパティのもう1つの書き方として属性ではなくタグを使用して書く方法があります。

<!-- Xamarin.Formsの場合 -->
<Models:Person xmlns:Models="clr-namespace:XamarinApp.Models">
    <Models:Person.Name>tanaka</Models.Person.Name>
</Models:Person>

<!-- UWPの場合 -->
<Models:Person xmlns:Models="using:XamarinApp.Models">
    <Models:Person.Name>tanaka</Models.Person.Name>
</Models:Person>

クラス名.プロパティ名という命名規約でタグを書くことで、そのタグの子要素がプロパティに設定されます。この書き方のいいところは、プロパティの値として、複雑なクラスを定義できるという点です。上記の例のような文字列では、ありがたみがわかりませんが、以下のようにPersonクラスに、Person型のChildプロパティがあって、そこに値を設定するといったときに効果を発揮します。

<!-- Xamarin.Formsの場合 -->
<Models:Person xmlns:Models="clr-namespace:XamarinApp.Models"
    Name="tanaka">
    <Models:Person.Child>
        <Models:Person Name="kimura" />
    </Models:Person.Child>
</Models:Person>

<!-- UWPの場合 -->
<Models:Person xmlns:Models="using:XamarinApp.Models"
    Name="tanaka">
    <Models:Person.Child>
        <Models:Person Name="kimura" />
    </Models:Person.Child>
</Models:Person>

XAMLには、コンテンツプロパティというものがあって、タグの直下に何も指定せずに書いた場合は、クラス単位に指定されたプロパティに値をセットするということが出来ます。例えば、上記の例でChildプロパティがコンテンツプロパティだった場合、以下のようにシンプルに書くことが出来ます。

<!-- Xamarin.Formsの場合 -->
<Models:Person xmlns:Models="clr-namespace:XamarinApp.Models"
    Name="tanaka">
    <Models:Person Name="kimura" />
</Models:Person>

<!-- UWPの場合 -->
<Models:Person xmlns:Models="using:XamarinApp.Models"
    Name="tanaka">
    <Models:Person Name="kimura" />
</Models:Person>

さらに、XAMLにはコレクションのプロパティに対して値を設定するときに、以下のようにタグを並べて定義することが出来ます。

<!-- Xamarin.Formsの場合 -->
<Models:Person xmlns:Models="clr-namespace:XamarinApp.Models"
    Name="tanaka">
    <Models:Person.Children>
        <Models:Person Name="kimura1" />
        <Models:Person Name="kimura2" />
        <Models:Person Name="kimura3" />
        <Models:Person Name="kimura4" />
    </Models:Person.Children>
</Models:Person>

<!-- UWPの場合 -->
<Models:Person xmlns:Models="using:XamarinApp.Models"
    Name="tanaka">
    <Models:Person.Children>
        <Models:Person Name="kimura1" />
        <Models:Person Name="kimura2" />
        <Models:Person Name="kimura3" />
        <Models:Person Name="kimura4" />
    </Models:Person.Children>
</Models:Person>

Childrenがコンテンツプロパティだった場合は、以下のように書くこともできます。

<!-- Xamarin.Formsの場合 -->
<Models:Person xmlns:Models="clr-namespace:XamarinApp.Models"
    Name="tanaka">
    <Models:Person Name="kimura1" />
    <Models:Person Name="kimura2" />
    <Models:Person Name="kimura3" />
    <Models:Person Name="kimura4" />
</Models:Person>

<!-- UWPの場合 -->
<Models:Person xmlns:Models="using:XamarinApp.Models"
    Name="tanaka">
    <Models:Person Name="kimura1" />
    <Models:Person Name="kimura2" />
    <Models:Person Name="kimura3" />
    <Models:Person Name="kimura4" />
</Models:Person>

XAMLには、オブジェクトに対して、別のオブジェクトのプロパティを設定するという添付プロパティというものがあります。 この添付プロパティは、レイアウトの情報をコントロールに追加するときによく使われます。例えば、Gridという格子状に領域を区切って、そこにコントロールを置くことが出来るレイアウトパネルがあります。このとき、Gridの中の要素に対して何行何列目に置くという指定がしたくなります。そういうようなケースで添付プロパティが活躍します。

添付プロパティは、クラス名.添付プロパティ名という形で指定します。

<Grid>
    <!-- 行と列の定義は省略 -->

    <Button Grid.Row="0" Grid.Column="1" ... />
    <Button Grid.Row="1" Grid.Column="1" ... />
</Grid>

上記の例では、最初のボタンではGrid.Rowという添付プロパティとGrid.Columnという添付プロパティを使って、0行1列目にボタンを置くという指定をしています。このように、Gridのプロパティを、あたかもButtonのプロパティであるかのように指定できるところが添付プロパティの特徴です。

最後にマークアップ拡張について説明します。 マークアップ拡張は、オブジェクトを組み立てるためのショートカットの記法です。普通ならXMLのタグを使って組み立てないといけないような複雑なオブジェクトを、属性に対して書いたりすることが出来るようになります。

マークアップ拡張は{}で括られたものになります。例えばリソースを参照するStaticResourceマークアップ拡張や、後述するデータバインディングで使用するBindingマークアップ拡張があります。マークアップ拡張を使ったケースと使わないケースの比較のために同じ意味合いのXAMLを書いてみます。

<Label Text="{Binding Name, Mode=OneWay}" />
<Label>
  <Label.Text>
    <Binding Path="Name"
             Mode="OneWay" />
  </Label.Text>
</Label>

データバインディング

MVVMにおいて、ViewとViewModelを紐づける方法としてデータバインディングが使われます。データバインディングをざっくりと説明すると、画面のコントロールのプロパティ(厳密にはXamarin.Formsではバインダブルプロパティ、UWPでは依存関係プロパティ)と、普通のクラスのオブジェクトのプロパティの同期をとる仕組みです。

Xamarin.Formsでは、BindableObjectで定義されたBindingContextプロパティに設定されたオブジェクトがBindingPathの起点になります。UWPでは、FrameworkElementで定義されたDataContextプロパティに設定されたオブジェクトがBindingPathの起点になります。

Pathの起点になるとは以下のような動作のことです。

例としてPersonクラスをバインドしてみます。

// さっき定義したBindableBaseクラスを使用
public class Person : BindableBase
{
    private string name;

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

Xamarin.Formsでは、以下のようになります。

<?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:Models="clr-namespace:XamarinApp.Models"
             x:Class="XamarinApp.Views.MainPage">
  <ContentPage.BindingContext>
    <Models:Person Name="tanaka" />
  </ContentPage.BindingContext>
  <StackLayout>
    <Entry Text="{Binding Name, Mode=TwoWay}" />
    <Label Text="{Binding Name, Mode=OneWay}" />
  </StackLayout>
</ContentPage>

UWPでは、以下のようになります。

<Page x:Class="UWPApp.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:UWPApp"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:Models="using:UWPApp.Models"
      mc:Ignorable="d">
    <Page.DataContext>
        <Models:Person Name="kimura" />
    </Page.DataContext>
    <StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <TextBox Text="{Binding Name, Mode=TwoWay}" />
        <TextBlock Text="{Binding Name, Mode=OneWay}" />
    </StackPanel>
</Page>

このプログラムを実行することで、入力した値が、下のテキストにPersonクラスのNameプロパティを経由して同期されます。このときの値の変更タイミングの検知には、最初に開設したINotifyPropertyChangedインターフェースが使用されています。

f:id:okazuki:20160915210945p:plain

f:id:okazuki:20160915210953p:plain

Bindingには、Modeがあります。Modeには以下のようなものがあります。

  • OneWay: ソース(C#のオブジェクト)からターゲット(画面のコントロール側)への1方向同期
  • TwoWay: ソースとターゲットの双方向同期
  • OneTime: ソースからターゲットへの1度きりの同期

指定しない場合は、デフォルトでOneWayになります。

UWP固有の機能

UWP固有の機能としてコンパイル時データバインディングというものがあります。ここでは詳しく述べませんが、以下の記事を参照してみてください。

blog.okazuki.jp

blog.okazuki.jp

コマンド

Viewで起きたイベントをViewModelに伝える手段としてCommandというものが用いられます。Commandの実態は、ICommandインターフェースという、自身が実行可能かどうかという状態と、実行するというExecuteメソッドを持っただけのシンプルなインターフェースになります。

public interface ICommand
{
    event EventHandler CanExecuteChanged;

    bool CanExecute(object parameter);

    void Execute(object parameter);
}

ExecuteメソッドとCanExecuteメソッドをデリゲートで指定可能な以下のようなDelegateCommandというクラスを定義して使うのが一般的です。

using System;
using System.Windows.Input;

namespace UWPApp
{
    public class DelegateCommand : ICommand
    {
        public event EventHandler CanExecuteChanged;

        private Func<bool> canExecute;

        private Action execute;

        public DelegateCommand(Action execute, Func<bool> canExecute = null)
        {
            this.execute = execute;
            this.canExecute = canExecute;
        }

        public bool CanExecute(object parameter) => this.canExecute?.Invoke() ?? true;

        public void Execute(object parameter) => this.execute();

        public void RaiseCanExecuteChanged() => this.CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }
}

Xamarin.Formsでは、何故かICommandの実装クラスであるXamarin.Forms.Commandクラスが提供されているので自分で実装する必要はありません。

Commandを使用したクラス例を見てみます。MainPageに対応するViewModelであるMainPageViewModelというクラスを以下のように定義します。

// Xamarin.Forms
using System;
using Xamarin.Forms;
using XamarinApp.Models;

namespace XamarinApp.ViewModels
{
    public class MainPageViewModel : BindableBase
    {
        private string now;

        public string Now
        {
            get { return this.now; }
            set { this.SetProperty(ref this.now, value); this.UpdateNowCommand.ChangeCanExecute(); }
        }

        public Command UpdateNowCommand { get; }

        public MainPageViewModel()
        {
            this.UpdateNowCommand = new Command(() =>
            {
                this.Now = DateTime.Now.ToString();
            },
            () => string.IsNullOrEmpty(this.Now));
        }

    }
}
// UWP
using System;
using UWPApp;
using UWPApp.Models;

namespace UWPApp.ViewModels
{
    public class MainPageViewModel : BindableBase
    {
        private string now;

        public string Now
        {
            get { return this.now; }
            set { this.SetProperty(ref this.now, value); this.UpdateNowCommand.RaiseCanExecuteChanged(); }
        }

        public DelegateCommand UpdateNowCommand { get; }

        public MainPageViewModel()
        {
            this.UpdateNowCommand = new DelegateCommand(() =>
            {
                this.Now = DateTime.Now.ToString();
            }, () => string.IsNullOrEmpty(this.Now));
        }

    }
}

Commandは、Buttonクラスが持っているCommandプロパティとデータバインディングすることで、ユーザーの操作をCommandに渡すことができます。XAMLを書いてみましょう。

<!-- Xamarin.Forms -->
<?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:ViewModels="clr-namespace:XamarinApp.ViewModels"
             x:Class="XamarinApp.Views.MainPage">
  <ContentPage.BindingContext>
    <ViewModels:MainPageViewModel />
  </ContentPage.BindingContext>
  <StackLayout>
    <Button Text="Update now"
            Command="{Binding UpdateNowCommand}" />
    <Label Text="{Binding Now}" />
  </StackLayout>
</ContentPage>

実行すると、以下のようになります。まず、ボタンだけが表示されます。

f:id:okazuki:20160915213026p:plain

ボタンを押すと、現在時刻が表示されボタンが押せなくなります。

f:id:okazuki:20160915213049p:plain

これはCommandの第二引数でNowが空の時しか押せないという条件を指定しているためです。また、Commandの状態が変わったことを通知するためにNowプロパティのsetterでコマンドの変更イベントを発行しているためUIとの同期がとられています。

UWP側もXAMLを見てみましょう。大体同じです。

<Page x:Class="UWPApp.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:UWPApp"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:ViewModels="using:UWPApp.ViewModels"
      mc:Ignorable="d">
    <Page.DataContext>
        <ViewModels:MainPageViewModel />
    </Page.DataContext>
    <StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Button Content="Update now"
                Command="{Binding UpdateNowCommand}" />
        <TextBlock Text="{Binding Now}" />
    </StackPanel>
</Page>

実行結果も同じになります。

f:id:okazuki:20160915213508p:plain

f:id:okazuki:20160915213516p:plain

ModelとViewModel間

MVVMでVMとVの間の連携はわかったのでModelとViewModelはどうなの?という疑問を持つかもしれません。

これは通常のC#のプログラミングになります。メソッド呼び出し、メソッドの戻り値、イベントなどを適切に使って実装することになります。

ここで書いてないけど必ずぶつかるもの

  • コレクションのバインドどうするの?→INotifyCollectionChangedの実装クラスであるObservableCollectionを使ってListViewなどのリスト表示系コントロールを使う
  • ViewModelからViewに何か通知をしたいんだけど→メッセンジャーパターンというものがあります。

まとめ

だらだらと書きましたが、まとめとしては…。 何かフレームワークを使いましょう。個人的にはPrismがおすすめです。ここで実装したようなBindableBaseクラスやDelegateCommandクラスが提供されている他に、画面遷移、ダイアログの表示など実際のアプリを作るうえで便利な機能が提供されています。Xamarin.FormsのPrismについては書いてないですが、こちらのリポジトリにWPFとUWPのPrismについては書いていたりします。

github.com

Xamarin.Forms版のPrismについては以下のBlogが詳しいです。

www.nuits.jp

また、まだ正式リリース前にPrismについて発表した資料とかがあります。

www.slideshare.net