This is one in a series of articles about Time Tortoise, a Universal Windows Platform app for planning and tracking your work schedule. For more on the development of this app and the ideas behind it, see my Time Tortoise category page.
Consider the basic Time Tortoise workflow:
- Select an activity
- Start the timer
- Do some work
- Stop the timer
This will result in a single time segment that has the appropriate start and end times and is associated with the selected activity. The duration will be calculated and displayed in the Time Segment list view, so you’ll know how much time you spent working. Everything looks good.
But there’s one problem: in order to see your elapsed time, you have to stop the timer. That’s inconvenient. When I’m timing an activity, I like to periodically glance at the timer to see how I’m doing, especially if I’m working towards a time goal.
So it would be nice if the active time segment would update dynamically while the timer was running. That’s the goal for this week.
.NET Timers
System.Threading.Timer
The purpose of a timer in .NET is to execute code at regular intervals. For Time Tortoise, I want a timer that updates once per second, since that’s how often people expect a clock to update when seconds are visible. According to the .NET docs, “The .NET Framework Class Library includes four classes named Timer
, each of which offers different functionality.” Of the four timers listed, two are associated with specific user interfaces (one for Windows Forms and one for ASP.NET) and two are “intended for use as a server-based or service component.”
Although Time Tortoise is a UWP app, my goal is to keep most of the code in .NET Standard class modules, to facilitate unit testing and get the most out of the MVVM pattern. So I decided to try locating the timer in the view model. The following sequence of events seemed like it should work:
- Timer fires an event, which calls a callback method
- Callback method updates the current time segment
- XAML data binding updates the UI
Of the two timers intended to run in services (like the class libraries that support the Time Tortoise UI), only one is included in .NET Standard, my target API. I confirmed this using the new .NET API Browser, which lets you find which APIs are available in which .NET versions. At the moment, Time Tortoise targets .NET Standard v1.4, which supports System.Threading.Timer
.
I added a Timer
instance and callback method to TimeSegmentViewModel
, and configured the timer to fire every second. In the callback function, I updated the end time of the segment to the current time. A few other modifications ensured that the timer was only enabled when the user was timing an activity.
Unfortunately, the data binding infrastructure was not happy with this setup:
System.Exception: ‘The application called an interface that was marshalled for a different thread. (Exception from HRESULT: 0x8001010E (RPC_E_WRONG_THREAD))’
The problem has something to do with the timer executing its callback method on a thread that is not the UI thread. This Stack Overflow answer explains how to solve the problem for UWP apps, but the answer doesn’t apply to .NET Standard class libraries. So I could try it inside my UWP project. But if I’m going to add a timer to my UWP code anyway, there’s another option.
Windows.UI.Xaml.DispatcherTimer
DispatcherTimer provides similar functionality to System.Threading.Timer. But methods that handle tick events from DispatcherTimer are allowed to change the UI, because they run on “the same thread that produces the UI thread.”
MainPage.xaml.cs
, the code-behind file for the Time Tortoise main page, is a small file. This is intentional, since no unit tests target it, and I want to minimize untested code. Fortunately, setting up the timer doesn’t add much:
Create a local variable to hold a reference to the timer while the program is running:
private readonly DispatcherTimer _dispatcherTimer;
In the page constructor, set the timer to tick once per second, and define the callback method:
_dispatcherTimer = new DispatcherTimer {
Interval = new TimeSpan(0, 0, 1)
};
_dispatcherTimer.Tick += DispatcherTimer_Tick;
In the callback method, update the end time of the selected segment. (Data binding will take care of updating the UI).
Main.SelectedTimeSegmentEndTime =
DateTime.Now.ToString(CultureInfo.CurrentCulture);
I also added a few lines to start or stop the timer when the Start/Stop button is pressed.
Using this approach, everything works fine with no thread-related exceptions. While the timer is running, the selected segment end time and the duration update every second, letting the user know that things are working.
Data Binding with Nullable Fields
I try to add at least one new Time Tortoise feature each week, but I also like to refactor the code when I see something that isn’t as convenient as it could be. This week, in addition to the timer, I was doing some UI testing to make sure add/remove time segment worked the same way as add/remove activity. I found a few bugs, so I fixed some code and added some unit tests.
As I was doing this, I noticed something less than ideal about how I was keeping track of the selected activity and time segment: in MainViewModel
, I was depending on SelectedActivity
and SelectedTimeSegment
never being null. But in some cases I didn’t have a valid activity and/or time segment. For example, a new activity doesn’t have any time segments, so when that activity is selected, there’s no time segment to select.
My approach in this situation was to construct a new object when necessary. For example, when the user created a new activity, I would also create a new time segment. The main reason to use this approach is to simplify data binding. If I know that a property always contains a valid object, I can always bind to it. So that’s why I originally took that approach.
But there’s also something not quite accurate about this approach. Although SelectedTimeSegment
refers to a valid time segment in this case, it’s not one that the user created, and it doesn’t show up in the time segment list in the UI (since it’s not in the observable collection). So as I was tracking down bugs in time segment add/delete, I decided to change my design to set SelectedActivity
and SelectedTimeSegment
to null if they didn’t contain an actual selected item.
Of course, this immediately caused data binding to crash in certain situations, because I was binding to fields like SelectedTimeSegment.StartTime
, where SelectedTimeSegment
could be null. XAML has features like TargetNullValue
and FallbackValue
that seem to be designed to deal with this, but since I don’t have very many fields with this problem, I decided to just add a few new MainViewModel
fields that do their own null checks, and bind to those. So instead of binding to SelectedTimeSegment.StartTime
, I bind to SelectedTimeSegmentStartTime
. Problem solved.