読者です 読者をやめる 読者になる 読者になる

atelier:mitsuba

i love UI/UX, Blend, XAML, Behavior, P5, oF, Web, Tangible Bits and Physical computing. なにかあればお気軽にご連絡ください。atelier@c-mitsuba.com

WPFとかUWPのTimerTriggerのMillisecondsPerTickを動的に変えたい。

Expression Blend C#

WPFとかUWPのTriggerにはTimerTrgigerっていう一定間隔でActionを実行してくれる便利なTriggerがあります。
使い方はこっち
http://c-mitsuba.hatenablog.com/entry/20120228/1330420575

実行間隔はMillisecondsPerTickで設定できるんですが、こいつはDependencyPropertyのくせに、Bindingで途中で変更しても、最初に設定した値の間隔で実行しつづけます。

というわけで、MillisecondsPerTickをBindingで動的に変えたいっていうおはなし。

まずはじめにTimerTrigerの実装をみてみましょう。
と思ったけど、残念ながらTimerTrigerが含まれるMicrosoft.Expression.Interactivityはsource referenceには公開されていません。
http://referencesource.microsoft.com/

というわけで、ILSpyでのぞいちゃいます。
TimerTriggerはMicrosoft.Expression.InteractivityのMicrosoft.Expression.Interactivity.Coreの中にあります。

f:id:c-mitsuba:20160204184248p:plain

でてきたコードをまるっとコピーしちゃいます。
継承してるInterfaceも一緒に。

using System;
using System.Windows;
using System.Windows.Interactivity;
using System.Windows.Threading;

namespace Microsoft.Expression.Interactivity.Core
{
	/// <summary>
	/// ソース上で発生する指定イベントによりトリガーされ、イベントが起動されたときに一定時間遅れて起動するトリガー。
	/// </summary>
	public class TimerTrigger : System.Windows.Interactivity.EventTrigger
	{
		internal class DispatcherTickTimer : ITickTimer
		{
			private DispatcherTimer dispatcherTimer;

			public event EventHandler Tick
			{
				add
				{
					this.dispatcherTimer.Tick += value;
				}
				remove
				{
					this.dispatcherTimer.Tick -= value;
				}
			}

			public TimeSpan Interval
			{
				get
				{
					return this.dispatcherTimer.Interval;
				}
				set
				{
					this.dispatcherTimer.Interval = value;
				}
			}

			public DispatcherTickTimer()
			{
				this.dispatcherTimer = new DispatcherTimer();
			}

			public void Start()
			{
				this.dispatcherTimer.Start();
			}

			public void Stop()
			{
				this.dispatcherTimer.Stop();
			}
		}

		public static readonly DependencyProperty MillisecondsPerTickProperty = DependencyProperty.Register("MillisecondsPerTick", typeof(double), typeof(TimerTrigger), new FrameworkPropertyMetadata(1000.0));

		public static readonly DependencyProperty TotalTicksProperty = DependencyProperty.Register("TotalTicks", typeof(int), typeof(TimerTrigger), new FrameworkPropertyMetadata(-1));

		private ITickTimer timer;

		private EventArgs eventArgs;

		private int tickCount;

		/// <summary>
		/// 目盛の間の待ち時間 (ミリ秒) を取得または設定します。これは依存関係プロパティです。
		/// </summary>
		public double MillisecondsPerTick
		{
			get
			{
				return (double)base.GetValue(TimerTrigger.MillisecondsPerTickProperty);
			}
			set
			{
				base.SetValue(TimerTrigger.MillisecondsPerTickProperty, value);
			}
		}

		/// <summary>
		/// トリガーが終了する前に起動される目盛の合計数を取得または設定します。これは依存関係プロパティです。
		/// </summary>
		public int TotalTicks
		{
			get
			{
				return (int)base.GetValue(TimerTrigger.TotalTicksProperty);
			}
			set
			{
				base.SetValue(TimerTrigger.TotalTicksProperty, value);
			}
		}

		/// <summary>
		/// <see cref="T:Microsoft.Expression.Interactivity.Core.TimerTrigger" /> クラスの新しいインスタンスを初期化します。
		/// </summary>
		public TimerTrigger() : this(new TimerTrigger.DispatcherTickTimer())
		{
		}

		internal TimerTrigger(ITickTimer timer)
		{
			this.timer = timer;
		}

		protected override void OnEvent(EventArgs eventArgs)
		{
			this.StopTimer();
			this.eventArgs = eventArgs;
			this.tickCount = 0;
			this.StartTimer();
		}

		protected override void OnDetaching()
		{
			this.StopTimer();
			base.OnDetaching();
		}

		internal void StartTimer()
		{
			if (this.timer != null)
			{
				this.timer.Interval = TimeSpan.FromMilliseconds(this.MillisecondsPerTick);
				this.timer.Tick += new EventHandler(this.OnTimerTick);
				this.timer.Start();
			}
		}

		internal void StopTimer()
		{
			if (this.timer != null)
			{
				this.timer.Stop();
				this.timer.Tick -= new EventHandler(this.OnTimerTick);
			}
		}

		private void OnTimerTick(object sender, EventArgs e)
		{
			if (this.TotalTicks > 0 && ++this.tickCount >= this.TotalTicks)
			{
				this.StopTimer();
			}
			base.InvokeActions(this.eventArgs);
		}
	}
	
		internal interface ITickTimer
	{
		event EventHandler Tick;

		TimeSpan Interval
		{
			get;
			set;
		}

		void Start();

		void Stop();
	}
}

こんなかんじ。

で、MillisecondPerTickのプロパティをみてみると。。
PropertyChangedCallBackがないんですよね。

		public static readonly DependencyProperty MillisecondsPerTickProperty = DependencyProperty.Register("MillisecondsPerTick", typeof(double), typeof(TimerTrigger), new FrameworkPropertyMetadata(1000.0));

このやる気ない感じ。

なので、PropertyChangedCallBackを書き足して、変更処理を追記します。

        public static readonly DependencyProperty MillisecondsPerTickProperty = DependencyProperty.Register("MillisecondsPerTick", typeof(double), typeof(TimerTrigger), new FrameworkPropertyMetadata(1000.0,PropertyChangedCallback));

    private static void PropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
        {
            var self = dependencyObject as TimerTrigger;
            self._timer.Stop();
            self._timer.Interval = TimeSpan.FromMilliseconds(self.MillisecondsPerTick);
            self._timer.Start();
        }

変更があったら一旦タイマーをStopして、Intervalを再設定して、もう一回Startするかんじで。
あとはnamespaceをあわせたり、クラス名を替えたりして、既存のと被らないようにします。
全文はこんなかんじ。

using System;
using System.Windows;
using System.Windows.Threading;

namespace FetishSister
{
    /// <summary>
    /// ソース上で発生する指定イベントによりトリガーされ、イベントが起動されたときに一定時間遅れて起動するトリガー。
    /// </summary>
    public class TimerTrigger : System.Windows.Interactivity.EventTrigger
    {
        internal class DispatcherTickTimer : ITickTimer
        {
            private DispatcherTimer dispatcherTimer;

            public event EventHandler Tick
            {
                add
                {
                    this.dispatcherTimer.Tick += value;
                }
                remove
                {
                    this.dispatcherTimer.Tick -= value;
                }
            }

            public TimeSpan Interval
            {
                get
                {
                    return this.dispatcherTimer.Interval;
                }
                set
                {
                    this.dispatcherTimer.Interval = value;
                }
            }

            public DispatcherTickTimer()
            {
                this.dispatcherTimer = new DispatcherTimer();
            }

            public void Start()
            {
                this.dispatcherTimer.Start();
            }

            public void Stop()
            {
                this.dispatcherTimer.Stop();
            }
        }

        public static readonly DependencyProperty MillisecondsPerTickProperty = DependencyProperty.Register("MillisecondsPerTick", typeof(double), typeof(TimerTrigger), new FrameworkPropertyMetadata(1000.0,PropertyChangedCallback));

        private static void PropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
        {
            var self = dependencyObject as TimerTrigger;
            self._timer.Stop();
            self._timer.Interval = TimeSpan.FromMilliseconds(self.MillisecondsPerTick);
            self._timer.Start();
        }

        public static readonly DependencyProperty TotalTicksProperty = DependencyProperty.Register("TotalTicks", typeof(int), typeof(TimerTrigger), new FrameworkPropertyMetadata(-1));

        private ITickTimer _timer;

        private EventArgs _eventArgs;

        private int _tickCount;

        /// <summary>
        /// 目盛の間の待ち時間 (ミリ秒) を取得または設定します。これは依存関係プロパティです。
        /// </summary>
        public double MillisecondsPerTick
        {
            get
            {
                return (double)base.GetValue(MillisecondsPerTickProperty);
            }
            set
            {
                base.SetValue(MillisecondsPerTickProperty, value);
            }
        }

        /// <summary>
        /// トリガーが終了する前に起動される目盛の合計数を取得または設定します。これは依存関係プロパティです。
        /// </summary>
        public int TotalTicks
        {
            get
            {
                return (int)base.GetValue(TotalTicksProperty);
            }
            set
            {
                base.SetValue(TotalTicksProperty, value);
            }
        }

        /// <summary>
        /// <see cref="T:WpfApplication11.TimerTrigger" /> クラスの新しいインスタンスを初期化します。
        /// </summary>
        public TimerTrigger(): this(new DispatcherTickTimer())
        {
        }

        internal TimerTrigger(ITickTimer timer)
        {
            this._timer = timer;
        }

        protected override void OnEvent(EventArgs eventArgs)
        {
            this.StopTimer();
            this._eventArgs = eventArgs;
            this._tickCount = 0;
            this.StartTimer();
        }

        protected override void OnDetaching()
        {
            this.StopTimer();
            base.OnDetaching();
        }

        internal void StartTimer()
        {
            if (this._timer != null)
            {
                this._timer.Interval = TimeSpan.FromMilliseconds(this.MillisecondsPerTick);
                this._timer.Tick += this.OnTimerTick;
                this._timer.Start();
            }
        }

        internal void StopTimer()
        {
            if (this._timer != null)
            {
                this._timer.Stop();
                this._timer.Tick -= this.OnTimerTick;
            }
        }

        private void OnTimerTick(object sender, EventArgs e)
        {
            if (this.TotalTicks > 0 && ++this._tickCount >= this.TotalTicks)
            {
                this.StopTimer();
            }
            base.InvokeActions(this._eventArgs);
        }
    }

    internal interface ITickTimer
    {
        event EventHandler Tick;

        TimeSpan Interval
        {
            get;
            set;
        }

        void Start();

        void Stop();
    }
}

これでBindingで間隔を変更できるようになりましたとさ。
あ、でも0以下とか突っ込むと落ちる気もするから、そのへんはよしなに。