しっぽを追いかけて

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

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

Suspended 中の経過時間を把握する

円弧シェイプを利用した数値の表現 - しっぽを追いかけて で作成したコントロールに少し手を入れて、下記のようなタイマーアプリを作ろうとしたんですが、思わぬところで壁にぶつかりました

f:id:matatabi_ux:20140817141517p:plain

アプリが何らかの形で中断(Suspending)状態になった場合、タイマーも停止するので再開した際に中断時に経過した時間が減算されていないという・・・

最初バックグラウンドタスクで何とかするか?とも思ったのですが、バックグラウンドタスクを普通に利用した場合 15 分に一度しか実行できず、タイマーとしてバッググラウンドで実行するにはサーバーとかないと無理そうなので別の方法を考えました

※このコードは Prism MVPVM のアーキテクチャで作成しています

MVPVM の詳細については Prism を MVPVM 化する(1)ViewModel と Model を Portable Class Library プロジェクトに配置する - しっぽを追いかけて を参照ください

まず Presenter クラスの OnNavigatedFrom で中断時の処理を入れます

#region Privates

/// <summary>
/// タイマー
/// </summary>
private DispatcherTimer timer = new DispatcherTimer();

#endregion //Privates

/// <summary>
/// 画面から遷移するときの処理
/// </summary>
/// <param name="viewModelState">画面状態データ</param>
/// <param name="suspending">中断フラグ</param>
public override void OnNavigatedFrom(Dictionary<string, object> viewModelState, bool suspending)
{
    if (suspending)
    {
        this.timer.Stop();

        // 中断時の経過時間を記憶
        ApplicationData.Current.LocalSettings.Values["SuspendTime"] = Environment.TickCount;

        Application.Current.Resuming += this.View.OnResuming;
    }
    base.OnNavigatedFrom(viewModelState, suspending);
}

Prism フレームワーク側で suspending の引数を渡してくれるので、このフラグが true の場合中断時と判定し、タイマーを停止して再開時のイベントハンドラを登録します

この際にアプリが起動してからの総経過時間(ミリ秒)をローカルストレージに保存しておきます

次に再開時のイベントハンドラでの処理

/// <summary>
/// トップ画面
/// </summary>
[PresenterView(typeof(TopPagePresenter))]
public sealed partial class TopPage : VisualStateAwarePage, IPresenterView<TopPagePresenter>
{
    /// <summary>
    /// コンストラクタ
    /// </summary>
    public TopPage()
    {
        this.InitializeComponent();
    }

    #region IPresenterView<TPresenter>

    /// <summary>
    /// この画面の Presenter
    /// </summary>
    public TopPagePresenter Presenter
    {
        get { return this.GetPresenter<TopPagePresenter>(); }
    }

    /// <summary>
    /// 再開時の処理
    /// </summary>
    /// <param name="sender">イベント発行者</param>
    /// <param name="e">イベント引数</param>
    public void OnResuming(object sender, object e)
    {
        Application.Current.Resuming -= this.OnResuming;
        this.Presenter.View = this;
        this.Presenter.ViewModel = this.DataContext as TopPageViewModel;
        this.Presenter.Resume();
    }

    #endregion //IPresenterView<TPresenter>
}

どういうわけか Presenter にイベントハンドラを設定すると Presenter に設定しておいた View や ViewModel が消えてしまう(このあたりの動作はいつか調べたい)ので、View 側から再開するようにします

View と ViewModel を設定しなおして Presenter の再開処理を呼び出します

/// <summary>
/// 再開時の処理
/// </summary>
public void Resume()
{
    // タイマー稼働中だったの場合
    if (this.ViewModel.IsTimerStated && !this.ViewModel.IsTimerPaused)
    {
        // 中断から再開までに経過した時間分を減算
        var suspendSpan = Environment.TickCount - (int)ApplicationData.Current.LocalSettings.Values["SuspendTime"];
        this.ViewModel.TimerValue -= (int)Math.Floor((double)(suspendSpan / 1000d));

        if (this.ViewModel.TimerValue < 1)
        {
            this.ViewModel.TimerValue = 0;
            this.OnStopClick(this, null);
        }
        else
        {
            this.timer.Start();
        }
    }
}

アプリ起動後の経過時間が取得できる Environment.TickCount というプロパティですが、なんと中断中も経過時間が加算される上に OS の現在時刻設定に影響されません!

これを利用して Presenter の再開処理ではそれまでタイマーが稼働中だったらローカルストレージから経過時間を読み込んで、現在の総経過時間との差から中断時間を把握します

あとは中断時間に応じてタイマーを減算するという寸法です・・・これで中断していてもタイマーが経過した(ように)見えますね!