しっぽを追いかけて

ぐるぐるしながら考えています

Unity と猫の話題が中心   掲載内容は個人の私見であり、所属組織の見解ではありません

Xamarin.Forms でセッションデータを保存・復元したい

とりあえず Xamarin.Forms の Application に追加されたイベントハンドラを利用すれば、アプリの状態遷移時に処理を行えることはわかりました

では、セッションデータを保存してアプリの再開時などに復元するためにはどうしたらよいか

・・・っと思ったら Application にそういった用途に便利なデータストアが用意されていました!

Working with the App Lifecycle - Xamarin

使い方はすごくカンタン

// 保存する
Application.Current.Properties ["id"] = someClass.ID;

// 復元する
if (Application.Current.Properties.ContainsKey("id"))
{
    var id = Application.Current.Properties ["id"] as int;
    // do something with id
}

これだけみたいです

一応動作を確かめねば!・・・ということでサンプルを作って試してみます

少し前の状態遷移確認用サンプル のコードを流用して確認してみます

とりあえずインタフェースを定義

/// <summary>
/// セッションデータリポジトリのインタフェース
/// </summary>
public interface ISessionRepository
{
    /// <summary>
    /// データを設定します
    /// </summary>
    /// <typeparam name="T">データの型</typeparam>
    /// <param name="value">データ</param>
    void SetValue<T>(T value) where T : class;

    /// <summary>
    /// データを取得します
    /// </summary>
    /// <typeparam name="T">データの型</typeparam>
    /// <returns>データ</returns>
    T GetValue<T>() where T : class;

    /// <summary>
    /// 初期化します
    /// </summary>
    /// <returns>成功した場合<code>true</code>、それ以外は<code>false</code></returns>
    bool Initilize();

    /// <summary>
    /// セッションデータを読み込みます
    /// </summary>
    /// <returns>成功した場合<code>true</code>、それ以外は<code>false</code></returns>
    Task<bool> LoadAsync();

    /// <summary>
    /// セッションデータを保存します
    /// </summary>
    /// <returns>成功した場合<code>true</code>、それ以外は<code>false</code></returns>
    Task<bool> SaveAsync();
}

これを Application.Properties を利用して実装

/// <summary>
/// アプリケーション Properties
/// </summary>
public class ApplicationProperties : ISessionRepository
{
    /// <summary>
    /// データを設定します
    /// </summary>
    /// <typeparam name="T">データの型</typeparam>
    /// <param name="value">データ</param>
    public void SetValue<T>(T value) where T : class
    {
        App.Current.Properties[typeof(T).FullName] = value;
    }

    /// <summary>
    /// データを取得します
    /// </summary>
    /// <typeparam name="T">データの型</typeparam>
    /// <returns>データ</returns>
    public T GetValue<T>() where T : class
    {
        if (!App.Current.Properties.ContainsKey(typeof (T).FullName))
        {
            return null;
        }
        return App.Current.Properties[typeof(T).FullName] as T;
    }

    /// <summary>
    /// 初期化します
    /// </summary>
    /// <returns>成功した場合<code>true</code>、それ以外は<code>false</code></returns>
    public bool Initilize()
    {
        try
        {
            App.Current.Properties.Clear();
            return true;
        }
        catch (Exception)
        {
            return false;
        }
    }

    /// <summary>
    /// セッションデータを読み込みます
    /// </summary>
    /// <returns>成功した場合<code>true</code>、それ以外は<code>false</code></returns>
    public Task<bool> LoadAsync()
    {
        // 読み込み不要
        return Task.FromResult(true);
    }

    /// <summary>
    /// セッションデータを保存します
    /// </summary>
    /// <returns>成功した場合<code>true</code>、それ以外は<code>false</code></returns>
    public Task<bool> SaveAsync()
    {
        // 保存不要
        return Task.FromResult(true);
    }
}

これを利用してアプリケーション基盤クラスを変更

/// <summary>
/// アプリケーション基盤クラス
/// </summary>
public class App : Application
{
    /// <summary>
    /// セッションデータリポジトリ
    /// </summary>
    private ISessionRepository sessionRepository = null;

    /// <summary>
    /// DI コンテナ
    /// </summary>
    public static UnityContainer Container = new UnityContainer();

   /// <summary>
    /// コンストラクタ
    /// </summary>
    public App()
    {
        this.MainPage = new TopPage();

        Container.RegisterType<ISessionRepository, ApplicationProperties>(new ContainerControlledLifetimeManager());
        this.sessionRepository = Container.Resolve<ISessionRepository>();
        this.sessionRepository.Initilize();
    }

    /// <summary>
    /// アプリ起動時処理
    /// </summary>
    protected override async void OnStart()
    {
        SimpleLogger.WriteLine("activated.");

        await this.sessionRepository.LoadAsync();
        var vm = this.sessionRepository.GetValue<TopPageViewModel>();

        if (vm != null)
        {
            this.MainPage.BindingContext = vm;
            SimpleLogger.WriteLine("Restore session data.");
        }
        else
        {
            this.MainPage.BindingContext = new TopPageViewModel();
            this.sessionRepository.SetValue<TopPageViewModel>(this.MainPage.BindingContext as TopPageViewModel);
            SimpleLogger.WriteLine("Create session data.");

            await this.sessionRepository.SaveAsync();
            SimpleLogger.WriteLine("Save session data.");
        }
    }

    /// <summary>
    /// アプリ中断時処理
    /// </summary>
    protected override async void OnSleep()
    {
        SimpleLogger.WriteLine("sleeped.");
    }

    /// <summary>
    /// アプリ再開時処理
    /// </summary>
    protected override async void OnResume()
    {
        this.sessionRepository = Container.Resolve<ISessionRepository>();
        await this.sessionRepository.LoadAsync();
        var vm = this.sessionRepository.GetValue<TopPageViewModel>();

        if (vm != null)
        {
            this.MainPage.BindingContext = vm;
            SimpleLogger.WriteLine("Restore session data.");
        }

        SimpleLogger.WriteLine("resumed.");
    }
}

意地悪にシリアライズしにくそうな DateTime や TimeSpan、入れ子 ViewModel を含めてみました

最後に 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:vm="clr-namespace:XamarinSessionRestore.ViewModels;assembly=XamarinSessionRestore"
             x:Class="XamarinSessionRestore.Views.TopPage">
  
  <ContentPage.BindingContext>
    <vm:TopPageViewModel/>
  </ContentPage.BindingContext>
  
  <Grid VerticalOptions="Center">
    <StackLayout Orientation="Vertical"
                 VerticalOptions="Start" 
                 HorizontalOptions="CenterAndExpand">
    <Label Text="This ViewModel Created at"
           HorizontalOptions="CenterAndExpand"/>
    <Label Text="{Binding DateTime, StringFormat='{0:yyyy/MM/dd (ddd) HH:mm:ss.fff}'}" 
           HorizontalOptions="CenterAndExpand"/>
    </StackLayout>
  </Grid>

</ContentPage>

ViewModel の DateTime を表示しているだけ

さてこのサンプルでホーム画面に戻ったり、一度終了して再度起動するなどの操作を行ってみました

操作の様子はこんな感じ

Android の出力ログは

App.OnStart: activated.
App.OnStart: Create session data.
App.OnSleep: Save session data.
TopPage.xaml.OnAppearing: TopPage Appearing.
App.OnSleep: sleeped.
App.OnResume: Restore session data.
App.OnResume: resumed.
App.OnResume: Restore session data.
App.OnResume: resumed.
App.OnSleep: sleeped.
App.OnStart: activated.
App.OnStart: Restore session data.
TopPage.xaml.OnAppearing: TopPage Appearing.

そしてこれが iOS

App.OnStart: activated.
App.OnStart: Create session data.
App.OnSleep: Save session data.
TopPage.xaml.OnAppearing: TopPage Appearing.
App.OnSleep: sleeped.
App.OnResume: Restore session data.
App.OnResume: resumed.
App.OnSleep: sleeped.
App.OnStart: activated.
App.OnStart: Restore session data.
TopPage.xaml.OnAppearing: TopPage Appearing.

最後に Windows Phone

App.OnStart: activated.
App.OnStart: Create session data.
App.OnSleep: Save session data.
TopPage.xaml.OnAppearing: TopPage Appearing.
App.OnSleep: sleeped.
App.OnResume: Restore session data.
App.OnResume: resumed.
App.OnSleep: sleeped.
App.OnStart: activated.
App.OnStart: Create session data.
App.OnSleep: Save session data.
TopPage.xaml.OnAppearing: TopPage Appearing.

AndroidiOS は再起動してもセッションデータが復元されましたが、Windows Phone は消えました!

足並みがそろわない・・・orz