概要
Xamarin.FormsのPCLプロジェクトにおいて、定期的に処理を実行する「タイマー」を使用したいケースがあります。
そのような時は、Deviceクラスに用意されているStartTimerメソッドを使用します。
★注意★
ここに掲載したプログラムコードは全てサンプルです。
そのまま利用した場合は誤動作が発生する恐れもありますのでご注意願います。
サンプルケース
ここでは次の機能の実現を目指します。
- 毎分「00秒目」に処理を実行したい。
- ミリ秒単位の誤差は容認するが、「00秒〜01秒目」の間で処理を実行できる程度の精度は欲しい。
- 処理の定期的な呼び出しは「開始メソッド(Start)」にて開始し、「停止メソッド(Stop)」にて停止したい。
- 定期的に呼び出される処理では、UI部品の操作・更新を実施できるようにしておく。
Deviceクラス
Deviceクラスに用意されている以下のメソッドを使用します。
- StartTimer
- BeginInvokeOnMainThread
(1) StartTimer (TimeSpan interval, Func callback);
interval : タイマーがタイムアウトし、ハンドラメソッドを呼び出すまでの時間間隔。
callback : タイムアウト時に呼び出されるハンドラメソッド。
指定した時間を経過してタイムアウトとなった後、callbackハンドラメソッドを呼び出します。
引き続き次のタイムアウトを待つ場合は callbackハンドラメソッド終了時の戻り値をtrueにします。
次回のタイムアウトを待たずタイマーの使用を終了する場合は callbackハンドラメソッド終了時の戻り値をfalseにします。
(2) BeginInvokeOnMainThread (Action action);
action : UIスレッドを介して呼び出されるハンドラメソッド。
UIスレッドを介してactionハンドラメソッドを呼び出すので、同メソッド(および そこから呼び出される処理)から画面部品を操作・更新することができます。
1分単位の周期タイマーについて
StartTimerのinterval値を「1分」に設定しておけば分刻みのタイマーとなりますが、callbackハンドラメソッドの処理が著しく遅延した場合を始めとする様々な要因で、タイマーそのものが少しづつ遅延していく可能性が考えられます。
また、このタイマー機能自体がマルチメディア(映像や音声)を制御できるほどの高精度を保っている訳ではなさそうなので、継続して正確性を保つのはやはり難しいと思われます。
したがって、ここでは次の仕様を定めて実用上問題のない精度を実現することにします。
- StartTimerで設定したタイマーは、1回のタイムアウト発生で使用を終了する。
(callbackハンドラの戻り値は常にfalseとする) - callbackハンドラ処理の最後でStartTimerメソッドを呼び出し、新しいタイマーをタイムアウト毎に再設定する。
- StartTimerのinterval値は「60秒」の固定値ではなく、StartTimer呼び出し時点の秒数値を「60」から差し引いた値を指定する。
(実際の時刻の「00秒目」にタイムアウトするよう、時間間隔をその都度 再計算する)
UIスレッドを介したactionハンドラメソッドの呼び出しについて
Xamarin.FormsのDevice.BeginInvokeOnMainThreadメソッドは、プラットフォーム共通の提供メソッドです。
AndroidやiOSなどのプラットフォームの違いをメソッド内で吸収し、actionハンドラメソッドをUI(画面部品)の操作・更新可能な状態で呼び出してくれます。
(つまり、UIスレッド経由で同ハンドラメソッドを呼び出してくれます)
タイマークラスのサンプル
using System;
using Xamarin.Forms;
namespace sample
{
/// <summary>
/// 毎分「00秒」にタイムアウトする分刻みのタイマー機能を提#供するクラス。
/// </summary>
public class MinutesTimer
{
/// <summary>分刻みタイマーのタイムアウトイベント用デリゲート宣言。</summary>
public delegate void TimeOutHandler (EventArgs e);
/// <summary>分刻みタイマーのタイムアウトイベント。</summary>
public event TimeOutHandler TimeOutEvent;
/// <summary>直前のタイマー起動日時。</summary>
private DateTime _startDateTime;
/// <summary>タイマー起動中は true。 それ以外の時は false。</summary>
private bool _timerRunning;
//=============================================================================
/// <summary>
/// <see cref="MinutesTimer"/> クラスの新しいインスタンスを初期化します。
/// </summary>
public MinutesTimer ()
{
this._timerRunning = false;;
}
/// <summary>
/// タイマーを起動します。
/// </summary>
public void Start()
{
if (this._timerRunning == true)
return;
// 新たなタイマーを登録する。
RegistTimer (this.HandleFunc);
}
/// <summary>
/// タイマーを停止します。
/// </summary>
public void Stop()
{
this._timerRunning = false;
}
/// <summary>
/// 分刻みタイマーがタイムアウトした時に呼び出されるハンドラメソッド。
/// </summary>
/// <returns>
/// このメソッドを呼び出したタイマーの登録を解除する場合は false。 それ以外の場合は true。
/// </returns>
private bool HandleFunc ()
{
if (this._timerRunning == true) {
if (this.TimeOutEvent != null) {
// タイマー起動中 かつ タイムアウトイベントが登録済みの場合は、
// UIスレッド経由で登録されているイベントハンドラメソッドを
// 呼び出す。
Device.BeginInvokeOnMainThread (() => {
this.TimeOutEvent (new EventArgs ());
});
}
// 新たなタイマーを登録する。
RegistTimer (this.HandleFunc);
}
// ハンドラメソッドを呼び出したタイマーは、メソッド離脱と共に登録を解除する。
// (新たなタイマーを登録済みの場合は、それを契機にこのハンドラメソッドが再び呼び出される)
return false;
}
/// <summary>
/// 新しい分刻みタイマーを登録します。
/// </summary>
/// <param name="callback">タイマーのタイムアウト時に呼び出されるメソッド。</param>
private void RegistTimer (Func<bool> callback)
{
// 現在時を起点に、次の「00秒目」までの経過秒数を計算する。
this._startDateTime = DateTime.Now;
double spanSecond = 60 - this._startDateTime.Second;
// 分刻みタイマーを起動する。
this._timerRunning = true;
Device.StartTimer (TimeSpan.FromSeconds (spanSecond), callback);
}
}
}