5/29-30 de:code セッション SV-007 “パワフル モバイル アプリ開発 ~ 最新 Microsoft Azure Mobile Services をフル活用しよう!~ ” フォローアップ (3)

皆様、こんにちは!さっそく多くの方に (1) と (2) を読んでいただいて、大変感謝しています。

続いて、今回は、3番目のセッションフォローアップである、オフラインサポートの実装、に行きたいと思います。

サービス側のデータの確認

スーパーマーケット従業員のToDo リストを作ります。まずは、(1)でご紹介した Mobile Services 側のアプリで、忘れずに ToDoItem テーブルに適当な初期データを作成しておいてください。もし未だの場合には、下記の通り、WebApiConfig.csSeed メソッド内に追加し、もう一度、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 ストア アプリを作成する”を展開し、ソリューションファイル一式をダウンロードしてください。これを編集していきます。

mbs1

クライアントアプリの編集(オフライン対応)

オフラインデータ対応は、Mobile Services の新機能の一つで、ローカルデータベースである SQLite を使ってオフラインシナリオを実現します。アプリ内でこの機能を使うには、MobileServiceClient.SyncContext をローカルストレージに対して初期化します。その後、IMobileServiceSyncTable インターフェースを使ってテーブルへの参照を行います。

まずは、SQLite をインストールします。このリンクからインストールできます。SQLite for Windows 8.1

  .vsix ファイルをダブルクリックしてインストールしてください。

Visual Studio で、上記のストアアプリのプロジェクトを開いて、ソリューションエクスプローラー内で、プロジェクトの下の参照設定を右クリックして、参照の追加Windows の下の 拡張タブにある、SQLite for Windows Runtime を追加します。 image

SQLite ランタイムには、注意点があります。ビルド構成で、プロセッサーのアーキテクチャーを、x86, x64, または ARM のいずれかに決めないといけません。Any CPU は未サポートです。ビルドメニュー→構成マネージャーで適宜変更してください。image

次に、ソリューションエクスプローラーで、クライアントアプリのプロジェクトを右クリックして、Nuget Package の管理 をクリックします。NuGet Package Manager が起動しますので、SQLiteStore と検索ボックスにタイプして、WindowsAzure.MobileServices.SQLiteStore パッケージをインストールしてください。

image 

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>

次に、PushPull ボタンに割り当てられているイベントハンドラを記述し、保存します。

    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(新規作成、読み取り、更新、削除)操作をサポートします。

screenshot_06042014_143812 

試しに Pull ボタンを押すと、このようにエラーが出力されます。

screenshot_06042014_143809

アプリを終了し、再起動して、ローカルストレージにデータが存在していることを確認してください。

アプリを更新して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 とともに動くためです。

screenshot_06042014_143812

.NET バックエンドなので、この Mobile Services に使われているデータベースの中身を確認するには、Azure ポータルにログオンするよりは、SQL Management Studio で、SQL Database にログインした方がわかりやすいです(オフラインシナリオはそれ自体は Node.js バックエンドでも使えます)。

未だローカル側の2件のデータ(食品棚を整理する、鮮魚棚の整頓)がサービス側に反映されていないことを確認します。

image

Push ボタンを押します。これにより、 MobileServiceClient.SyncContext.PushAsync が呼ばれ、RefreshTodoItems でローカルのデータを使ってアプリのデータを再ロードします。この Push 操作により、Mobile Services データベースが、ローカルストレージ側のデータを受信します。しかしながら、ローカルストレージは、Mobile Services 側データベースのデータを受信しません。

Push ボタン操作は、IMobileServicesSyncTable の代わりに、MobileServiceClient.SyncContext により、実行されます。Push は、SyncContext にかかわるすべてのテーブルを変更します。もちろん、テーブルに Relationship が設定されている場合も、カバーされます。

画面はこう変わります。

image

Mobile Services の SQL Database は、こうなっているはずです。

werwer

次に、Pull ボタンを押してみましょう。画面はこう変わります。このアプリが呼ぶのは、IMobileServiceSyncTable.PullAsync() と、 RefreshTodoItems だけです。全ての Mobile Services 側のデータがローカルストレージに Pull されて、アプリに表示されているのがわかります。また、すべてのローカルストレージにあるデータは Mobile Services 側のデータベースに Push されていることもわかります。これは、pull すると必ず最初に push を行うためです。

screenshot_06042014_225357 

image

まとめ

Mobile Services の新機能である、オフラインサポートを使うためには、IMobileServiceSyncTable インターフェースと、ローカルストレージと一緒に初期化される、 MobileServiceClient.SyncContext が必要です。この場合のローカルストレージは、 SQLite データベースになります。

通常の Mobile Services 向けの CRUD 操作は、Mobile Services に接続しているときは SQL Database に対して、接続していないときでもローカルストレージの SQLite に対して、そのまま実行できます。

ローカルストレージのデータを、Mobile Services 側のデータと同期したい場合には、IMobileServiceSyncTable.PullAsyncMobileServiceClient.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 による認証の解説に行きます。

鈴木章太郎