Introduction
I wanted to put a simple timer on a window that is displayed when the system is busy. It seemed to me that this really did not belong in the ViewModel
, and seemed like it should not use code-behind. The solution was this behavior.
The Dependency Property
This behaviour has really two parts.
The static
part is two DependencyProperty
s, a public
IsEnabled
DependencyProperty
to enable the timer, and a private
TimerBehaviour
DependencyProperty
to save an instance of the class
.
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached("IsEnabled", typeof(bool),
typeof(TimerBehaviour), new UIPropertyMetadata(false, OnValueChanged));
public static bool GetIsEnabled(FrameworkElement o) { return (bool)o.GetValue(IsEnabledProperty); }
public static void SetIsEnabled(FrameworkElement o, bool value) { o.SetValue(IsEnabledProperty, value); }
private static readonly DependencyProperty TimerBehaviourProperty =
DependencyProperty.RegisterAttached("TimerBehaviour", typeof(TimerBehaviour),
typeof(TimerBehaviour), new UIPropertyMetadata(null));
private static TimerBehaviour GetTimerBehaviour(FrameworkElement o)
{ return (TimerBehaviour)o.GetValue(TimerBehaviourProperty); }
private static void SetTimerBehaviour(FrameworkElement o, TimerBehaviour value)
{ o.SetValue(TimerBehaviourProperty, value); }
private static void OnValueChanged(DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs e)
{
var frameworkElement = dependencyObject as FrameworkElement;
if (frameworkElement == null) return;
if (e.NewValue is bool && (bool)e.NewValue)
{
SetTimerBehaviour(frameworkElement, new TimerBehaviour(frameworkElement));
}
else
{
GetTimerBehaviour(frameworkElement).Dispose();
SetTimerBehaviour(frameworkElement, null);
}
}
Only the IsEnabled
DependencyProperty
has an event handler which creates a new instance of the TimerBehaviour
class and saves it in the TimerBehaviour
DependencyProperty
when IsEnabled
is set to true
.
The non-static
part of the class
has a constructor that saves information about the FrameworkElement
to which this DependencyProperty
is attached, creates a new DispatcherTimer
instance that will trigger every second and call the event handler, SecondEvent
, and saves the current time, which is used to determine the elapsed time. It then starts the DispatcherTimer
.
private readonly DateTime _startTime;
private readonly FrameworkElement _frameworkElement;
private readonly DispatcherTimer _timer;
public TimerBehaviour(FrameworkElement frameworkElement)
{
_frameworkElement = frameworkElement;
_startTime = DateTime.Now;
_timer = new DispatcherTimer(new TimeSpan(0, 0, 1), DispatcherPriority.Render,
SecondEvent, frameworkElement.Dispatcher);
_timer.Start();
SecondEvent(_timer, new EventArgs());
}
private void SecondEvent(object sender, EventArgs e)
{
var timerString = DateTime.Now.Subtract(_startTime).ToString("mm\\:ss");
if (_frameworkElement is TextBlock)
((TextBlock)_frameworkElement).Text = timerString;
}
The SecondEvent
event handler then subtracts the saved time from the current time, and, if the FrameworkElement
is a TextBlock
, will set the Text
property of the TextBlock
to the elapsed time in minutes and seconds.
Using the Code
All that needs to be done is to include a TextBlock
in the XAML file, and then include the TimerBehaviour
and set its IsEnabled
property to true
:
<TextBlock local:TimerBehaviour.IsEnabled="True" />
Conclusion
This behaviour allows a timer to easily be added to any WPF form by only adding a TextBox
and attaching this behaviour.
I can easily see where this can be enhanced, and also see where it might be modified to display just the time of day, which actually may be more useful considering that there is more need to display the time than just a timer. I could also add the ability to reset the timer, or allow flexibility of display.
History
- 03/18/2016: Initial version
- 03/22/2016: Updated code to show zero time when initialized
- 09/13/2017: Fixed bugs when cursor is at last position