5/29-30 de:code セッション SV-007 “パワフル モバイル アプリ開発 ~ 最新 Microsoft Azure Mobile Services をフル活用しよう!~ ” フォローアップ (3)
皆様、こんにちは!さっそく多くの方に (1) と (2) を読んでいただいて、大変感謝しています。
続いて、今回は、3番目のセッションフォローアップである、オフラインサポートの実装、に行きたいと思います。
サービス側のデータの確認
スーパーマーケット従業員のToDo リストを作ります。まずは、(1)でご紹介した Mobile Services 側のアプリで、忘れずに ToDoItem テーブルに適当な初期データを作成しておいてください。もし未だの場合には、下記の通り、WebApiConfig.cs の Seed メソッド内に追加し、もう一度、Azure に発行しておいてください。下記がその例です。このデータは、フォローアップ(5)の Xamarin 対応の箇所でも使います。
1: ・・・
2: List<TodoItem> todoItems = new List<TodoItem>
3: {
4: new TodoItem { Id = "1", Text = "商品の棚卸をする", Complete = false },
5: new TodoItem { Id = "2", Text = "レジ周りを片付ける", Complete = false },
6: new TodoItem { Id = "3", Text = "商品棚を整理する", Complete = false },
7: new TodoItem { Id = "4", Text = "フロア全体のレイアウトをチェックする", Complete = false },
8: new TodoItem { Id = "5", Text = "棚ごとに掃除を実施する", Complete = false },
9: };
10:
11: foreach (TodoItem todoItem in todoItems)
12: {
13: context.Set<TodoItem>().Add(todoItem);
14: }
15:
16: base.Seed(context);
17: ・・・
クライアントアプリの作成
Microsoft Azure ポータルにログインし、Mobile Services タブから、(1)(2) で利用しているアプリのクイックスタート画面を開き、プラットフォームの選択で、Windows ストアをタップします。”新しい Windows ストア アプリを作成する”を展開し、ソリューションファイル一式をダウンロードしてください。これを編集していきます。
クライアントアプリの編集(オフライン対応)
オフラインデータ対応は、Mobile Services の新機能の一つで、ローカルデータベースである SQLite を使ってオフラインシナリオを実現します。アプリ内でこの機能を使うには、MobileServiceClient.SyncContext をローカルストレージに対して初期化します。その後、IMobileServiceSyncTable インターフェースを使ってテーブルへの参照を行います。
まずは、SQLite をインストールします。このリンクからインストールできます。SQLite for Windows 8.1
.vsix ファイルをダブルクリックしてインストールしてください。
Visual Studio で、上記のストアアプリのプロジェクトを開いて、ソリューションエクスプローラー内で、プロジェクトの下の参照設定を右クリックして、参照の追加でWindows の下の 拡張タブにある、SQLite for Windows Runtime を追加します。
SQLite ランタイムには、注意点があります。ビルド構成で、プロセッサーのアーキテクチャーを、x86, x64, または ARM のいずれかに決めないといけません。Any CPU は未サポートです。ビルドメニュー→構成マネージャーで適宜変更してください。
次に、ソリューションエクスプローラーで、クライアントアプリのプロジェクトを右クリックして、Nuget Package の管理 をクリックします。NuGet Package Manager が起動しますので、SQLiteStore と検索ボックスにタイプして、WindowsAzure.MobileServices.SQLiteStore パッケージをインストールしてください。
MainPage.xaml.cs を開き、下記の using 句を一番上のセクションに追加しておきます。
1: using Microsoft.WindowsAzure.MobileServices.SQLiteStore;
2: using Microsoft.WindowsAzure.MobileServices.Sync;
3: using Newtonsoft.Json.Linq;
同じく、Mainpage.xaml.cs で、IMobileServicesSyncTable インターフェースを使って todoTable の定義を更新します。テーブルの呼び出しには、MobileServicesClient.GetSyncTable() を使います。
1: //private IMobileServiceTable<TodoItem> todoTable = App.MobileService.GetTable<TodoItem>();
2: private IMobileServiceSyncTable<TodoItem> todoTable = App.MobileService.GetSyncTable<TodoItem>();
次に、TodoItem クラスを更新し、Version システムプロパティを含むようにします。
1: public class TodoItem
2: {
3: public string Id { get; set; }
4: [JsonProperty(PropertyName = "text")]
5: public string Text { get; set; }
6: [JsonProperty(PropertyName = "complete")]
7: public bool Complete { get; set; }
8: [Version]
9: public string Version { get; set; }
10: }
次に、OnNavigatedTo イベントハンドラを更新し、async メソッドを使い、クライアントのSyncコンテキストが、SQLite ストレージになるように構成します。適当な名前を付けるだけでOKです。当該 SQLite ストレージは、テーブルと一緒に作成され、Mobile Services のテーブルのスキーマと一致するように構成されます。 しかし、上記に追加した、Version システムプロパティは含むことになります。
1: protected async override void OnNavigatedTo(NavigationEventArgs e)
2: {
3: if (!App.MobileService.SyncContext.IsInitialized)
4: {
5: var store = new MobileServiceSQLiteStore("localsync12.db");
6: store.DefineTable<TodoItem>();
7: await App.MobileService.SyncContext.InitializeAsync(store, new MobileServiceSyncHandler());
8: }
9: RefreshTodoItems();
10: }
次に、MainPage.xaml を開き、全て選んで削除し、下記のように変更します(Grid 要素を変更します。2つのボタンも追加し、クリックイベントハンドラ―も追加し、 Push および Pull のオペレーションを追加します。色やフォントの大きさも変更します)。
1: <Page
2: x:Class="GetStartedWithOffline.MainPage"
3: IsTabStop="false"
4: xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
5: xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
6: xmlns:local="using:GetStartedWithOffline"
7: xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
8: xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
9: mc:Ignorable="d">
10:
11: <Grid Background="Green">
12:
13: <Grid Margin="50,50,10,10">
14: <Grid.ColumnDefinitions>
15: <ColumnDefinition Width="Auto" />
16: <ColumnDefinition Width="*" />
17: </Grid.ColumnDefinitions>
18: <Grid.RowDefinitions>
19: <RowDefinition Height="Auto" />
20: <RowDefinition Height="*" />
21: </Grid.RowDefinitions>
22:
23: <Grid Grid.Row="0" Grid.ColumnSpan="2" Margin="0,0,0,20">
24: <StackPanel>
25: <TextBlock Foreground="#0094ff" FontSize="32" FontFamily="Segoe UI Light" Margin="0,0,0,6">MICROSOFT AZURE MOBILE SERVICES</TextBlock>
26: <TextBlock Foreground="WhiteSmoke" FontFamily="Segoe UI Light" FontSize="48" >オフラインサポートの実装</TextBlock>
27: </StackPanel>
28: </Grid>
29:
30:
31: <Grid Grid.Row="1">
32: <StackPanel>
33: <local:QuickStartTask Number="1" Title="作業項目を入力" Description="スーパーマーケット店舗業務のTODO項目を入力" FontSize="36" Foreground="WhiteSmoke"/>
34:
35: <StackPanel Orientation="Horizontal" Margin="72,0,30,0">
36: <TextBox Name="TextInput" Margin="5" MinWidth="300" KeyDown="TextInput_KeyDown" FontSize="36"></TextBox>
37: <Button Name="ButtonSave" Click="ButtonSave_Click">
38: <StackPanel Orientation="Horizontal">
39: <SymbolIcon Symbol="Add"/>
40: <TextBlock Margin="5" FontSize="32">保存</TextBlock>
41: </StackPanel>
42: </Button>
43: </StackPanel>
44:
45: </StackPanel>
46: </Grid>
47:
48: <Grid Grid.Row="1" Grid.Column="1">
49: <Grid.ColumnDefinitions>
50: <ColumnDefinition Width="Auto" MinWidth="162" />
51: <ColumnDefinition Width="Auto" MinWidth="641" />
52: </Grid.ColumnDefinitions>
53: <Grid.RowDefinitions>
54: <RowDefinition Height="Auto" />
55: <RowDefinition Height="Auto" />
56: <RowDefinition Height="*" />
57: </Grid.RowDefinitions>
58: <StackPanel Grid.Row="0" Grid.ColumnSpan="2" Margin="0,0,68,0">
59: <local:QuickStartTask Number="2" FontSize="36" Title="データを検索し、更新し、Sync" Description="Pull ボタンと Push ボタンで Local データベースとサービスをSync" Foreground="WhiteSmoke" />
60: </StackPanel>
61:
62: <Button Grid.Row="1" Grid.Column="0" Margin="72,5,0,-3" Name="ButtonPush" Click="ButtonPush_Click" Width="151" Grid.ColumnSpan="3" Height="74">
63: <StackPanel Orientation="Horizontal">
64: <SymbolIcon Symbol="Upload"/>
65: <TextBlock Margin="5,7" FontSize="32">Push</TextBlock>
66: </StackPanel>
67: </Button>
68:
69: <Button Grid.Row="1" Grid.Column="1" Margin="84,19,0,9" Name="ButtonPull" Click="ButtonPull_Click" Width="147">
70: <StackPanel Orientation="Horizontal">
71: <SymbolIcon Symbol="Download"/>
72: <TextBlock Margin="5,7" FontSize="32">Pull</TextBlock>
73: </StackPanel>
74: </Button>
75:
76: <ListView Name="ListItems" SelectionMode="None" Margin="62,10,0,0" Grid.ColumnSpan="2" Grid.Row="2" >
77: <ListView.ItemTemplate>
78: <DataTemplate>
79: <StackPanel Orientation="Horizontal">
80: <CheckBox Name="CheckBoxComplete" IsChecked="{Binding Complete, Mode=TwoWay}" Checked="CheckBoxComplete_Checked"
81: Content="{Binding Text}" Foreground="Yellow" FontSize="36" Margin="10,5" VerticalAlignment="Center"/>
82: </StackPanel>
83: </DataTemplate>
84: </ListView.ItemTemplate>
85: </ListView>
86: </Grid>
87:
88: </Grid>
89: </Grid>
90: </Page>
次に、Push と Pull ボタンに割り当てられているイベントハンドラを記述し、保存します。
1: private async void ButtonPull_Click(object sender, RoutedEventArgs e)
2: {
3: Exception pullException = null;
4: try
5: {
6: await todoTable.PullAsync();
7: RefreshTodoItems();
8: }
9: catch (Exception ex)
10: {
11: pullException = ex;
12: }
13: if (pullException != null) {
14: MessageDialog d = new MessageDialog("Pull failed: " + pullException.Message +
15: "\n\nIf you are in an offline scenario, " +
16: "try your Pull again when connected with your Mobile Serice.");
17: await d.ShowAsync();
18: }
19: }
20: private async void ButtonPush_Click(object sender, RoutedEventArgs e)
21: {
22: string errorString = null;
23: try
24: {
25: await App.MobileService.SyncContext.PushAsync();
26: RefreshTodoItems();
27: }
28: catch (MobileServicePushFailedException ex)
29: {
30: errorString = "Push failed because of sync errors: " +
31: ex.PushResult.Errors.Count() + ", message: " + ex.Message;
32: }
33: catch (Exception ex)
34: {
35: errorString = "Push failed: " + ex.Message;
36: }
37: if (errorString != null) {
38: MessageDialog d = new MessageDialog(errorString +
39: "\n\nIf you are in an offline scenario, " +
40: "try your Push again when connected with your Mobile Serice.");
41: await d.ShowAsync();
42: }
43: }
未だここでアプリを実行しないでください。プロジェクトをリビルドし、エラーがないことを確認しておいてください。
オフラインシナリオでアプリをテストする
ここでは、アプリケーションの Mobile Services への接続を無効化して、 オフラインシナリオをシミュレートします。そこで、いくつかのデータを追加し、ローカルのストレージに保存します。
※ したがって、ここで Push および Pull ボタンを押すと、例外が発生します。次のパートで、このアプリの Mobile Services への接続を有効化しますので、そこで、Push および Pull 操作をおこない、ローカルストレージ(SQLite)と、Mobile Services のデータベースとを Sync します。
App.xaml.cs を開いて、MobileServiceClient の初期化部分を変更し、でたらめなエンドポイントアドレスを入力します。たとえば、"azure-mobile.net" を "azure-mobile.xxx" にして、保存します。
1: public static MobileServiceClient MobileService = new MobileServiceClient(
2: "https://your-mobile-service.azure-mobile.xxx/",
3: "AppKey"
4: ;
F5 を押してアプリを実行し、いくつか新しい ToDoItem を入力し、保存ボタンをクリックします。当該新項目は、Push ボタンを押すまでは、ローカルストレージの中だけで存在しています。このストアアプリは、Mobile Services に接続されている時と同じように、データベースに対する全ての CRUD(新規作成、読み取り、更新、削除)操作をサポートします。
試しに Pull ボタンを押すと、このようにエラーが出力されます。
アプリを終了し、再起動して、ローカルストレージにデータが存在していることを確認してください。
アプリを更新してMobile Services に再接続
アプリをMobile Services に再接続します。アプリがオフラインの状態から、Mobile Services に接続されたオンラインの状態になることをシミュレートします。
App.xaml.cs を開いて、MobileServiceClient の初期化部分を、正しいエンドポイントアドレスに変更します。 "azure-mobile.xxx" を "azure-mobile.net" に変更し、保存します。
1: public static MobileServiceClient MobileService = new MobileServiceClient(
2: "https://your-mobile-service.azure-mobile.net/",
3: "Your AppKey"
4: ;
Mobile Services に接続してアプリをテスト
Push および Pull ボタンそれぞれの操作により、ローカルストレージ(SQLite)を、Mobile Services データベース(SQL Database)と、Sync します。アプリをリビルドして実行します。Mobile Services につながっていても、アプリを起動した状態は、オフラインの時と同じようになっているはずです。これは、このアプリが常に、 ローカルストレージをポイントしている IMobileServiceSyncTable とともに動くためです。
.NET バックエンドなので、この Mobile Services に使われているデータベースの中身を確認するには、Azure ポータルにログオンするよりは、SQL Management Studio で、SQL Database にログインした方がわかりやすいです(オフラインシナリオはそれ自体は Node.js バックエンドでも使えます)。
未だローカル側の2件のデータ(食品棚を整理する、鮮魚棚の整頓)がサービス側に反映されていないことを確認します。
Push ボタンを押します。これにより、 MobileServiceClient.SyncContext.PushAsync が呼ばれ、RefreshTodoItems でローカルのデータを使ってアプリのデータを再ロードします。この Push 操作により、Mobile Services データベースが、ローカルストレージ側のデータを受信します。しかしながら、ローカルストレージは、Mobile Services 側データベースのデータを受信しません。
Push ボタン操作は、IMobileServicesSyncTable の代わりに、MobileServiceClient.SyncContext により、実行されます。Push は、SyncContext にかかわるすべてのテーブルを変更します。もちろん、テーブルに Relationship が設定されている場合も、カバーされます。
画面はこう変わります。
Mobile Services の SQL Database は、こうなっているはずです。
次に、Pull ボタンを押してみましょう。画面はこう変わります。このアプリが呼ぶのは、IMobileServiceSyncTable.PullAsync() と、 RefreshTodoItems だけです。全ての Mobile Services 側のデータがローカルストレージに Pull されて、アプリに表示されているのがわかります。また、すべてのローカルストレージにあるデータは Mobile Services 側のデータベースに Push されていることもわかります。これは、pull すると必ず最初に push を行うためです。
まとめ
Mobile Services の新機能である、オフラインサポートを使うためには、IMobileServiceSyncTable インターフェースと、ローカルストレージと一緒に初期化される、 MobileServiceClient.SyncContext が必要です。この場合のローカルストレージは、 SQLite データベースになります。
通常の Mobile Services 向けの CRUD 操作は、Mobile Services に接続しているときは SQL Database に対して、接続していないときでもローカルストレージの SQLite に対して、そのまま実行できます。
ローカルストレージのデータを、Mobile Services 側のデータと同期したい場合には、IMobileServiceSyncTable.PullAsync と MobileServiceClient.SyncContext.PushAsync メソッドを使います。
サービス側に変更を Push したいときは、IMobileServiceSyncContext.PushAsync() をコールします。 このメソッドは、 IMobileServicesSyncContext のメンバーであり、Sync Table の代わりに使われます。すべてのテーブルにわたる変更を Push するためです。(CRUD 操作の中でも)ローカルストレージでされたある意味些細な変更も、サービス側に送られます。
サービス側のテーブルにあるデータをローカルのアプリに Pull するには、IMobileServiceSyncTable.PullAsync() をコールします。pull すると必ず最初に push を行います。
いかがでしょう?非常に簡単ですよね。今回のプロジェクトのソースコードはこちらです。
--- 参考情報 ---
セッションでもご紹介した通り、この次は、チュートリアルにある、コンフリクト制御の箇所を見てやってみてください。
Handling conflicts with offline support for Mobile Services
サンプルはいろいろありますので、さらに高度なものも試してみて戴くと良いかと思います。
Build 2014 のデモ(The Phone Company) : https://github.com/lindydonna/mobile-services-samples/tree/master/ThePhoneCompany
Insurance Company: https://code.msdn.microsoft.com/Azure-Mobile-Service-678a5855
------
次回は、(1) と (2) のデモアプリの方に戻り、 (4) Microsoft Azure Active Directory による認証の解説に行きます。
鈴木章太郎