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.
The typical way to create a time segment in Time Tortoise is to select an activity, start the timer, work on the activity for a while, and then stop the timer. That causes the app to add a time segment to the selected activity, with the appropriate start and end times.
But sometimes you need more control over your time segments. If you’re working on activity A, and someone interrupts you to discuss activity B, you might forget to switch activities. In another instance, you might start timing the wrong activity, and not notice it for a while. Whatever the reason, if you care about timing accuracy (and if you don’t, why are you using a time tracker?) you need a way to manually create, edit, and delete time segments.
This week, I worked on the most basic of these operations, editing an existing time segment.
A Review of XAML Data Binding
Here are the steps you would use to edit a time segment:
- Select a target activity and time segment.
- Enter a new start time and/or end time.
- Save the result.
Since Time Tortoise is a UWP app, accepting input from the user and displaying output to the screen happens through data binding. For example, here’s how data binding is configured to track the current activity:
- A
ListView
on the main page is bound to anObservableCollection
ofActivityViewModel
objects. - The
SelectedIndex
property of the list view is bound to the integer fieldSelectedActivityIndex
. - When the user selects an activity,
SelectedActivityIndex
is updated (through data binding) to reflect the index of the selected list view row. Code in its setter then updatesSelectedActivity
, a property of typeActivityViewModel
. - A
TextBox
on the main page is bound toSelectedActivity.Name
, the name of the current activity, usingTwoWay
binding. This means the text box contents are updated when the user selects an activity, and the user can update the activity name by typing in the text box. When the user clicks Save, the activity name is then updated in the database through Entity Framework.
This week, I added two data-bound text boxes for updating time segment start and end values. Data binding for these two text boxes works the same way as for the activity name: there’s a ListView
for the time segments, bound to an ObservableCollection
of TimeSegmentViewModel
objects. When you select an activity, the time segment list view displays time segments for that activity. A change to SelectedIndex
on the list view keeps a property called SelectedTimeSegment
up to date. And the text boxes are bound to the DateTime
properties of SelectedTimeSegment
.
UWP Input Validation
As soon as I wired up data binding for the start and end text boxes, I discovered that entering an invalid date/time string caused an exception. Since entering an invalid date or time is an easy mistake for the user to make, something clearly has to be fixed in order for the app to be usable.
A common solution to date/time input validation is to use a control that only allows the user to select valid dates and times. If I was targeting mobile devices, I would definitely use this approach. But Time Tortoise is intended for a programmer or other knowledge worker using a physical keyboard. In my experience, date/time selection controls are annoying in that environment, and I wouldn’t use any time tracking app where selection was the only option for entering dates and times.
Another idea is to find a UWP-compatible control that allows both selecting a valid date and time, and entering an arbitrary string to be validated by the control. That may eventually be the best option for Time Tortoise. But the standard XAML date and time picker controls from the Visual Studio Toolbox are oriented towards picking dates and times, not accepting and validating them. So I decided to go with the text box + validation approach.
Windows Presentation Foundation (WPF) data binding includes comprehensive data validation. But after unsuccessfully trying to get the examples from that article to work in my project, I did a bit of searching and found that built-in UWP input validation is only in the early design stages, and doesn’t currently work the same as WPF validation. UWP developers are instead using third-party solutions like Template10.
So I had another decision to make: whether or not to take a dependency on a UWP input validation library. While I’m in favor of adopting open-source components for the project, I decided for now to build my own very simple input validation system.
Simple Input Validation for Data-Bound Fields
Here’s a review of the problem to be solved: The TimeSegment
class has a StartTime
field and an EndTime
field, both of type DateTime
. These fields also appear in TimeSegmentViewModel
. When the user clicks on a row in the time segment list view, the value of each field from the view model appears in its corresponding text box through data binding. We want to allow the user to edit the date/time information in each textbox, and save it back into the start/end fields.
XAML data binding supports data type conversion, and it looks like it would work for UWP. But since the theme of this week is building things from scratch, I’m going to handle my own conversion in conjunction with my homemade data validation. Migrating to a more standard approach could be a work item for the future, especially as better data validation comes to the UWP platform.
The data conversion for the time segment scenario takes place in the StartTime
and EndTime
getters and setters in TimeSegmentViewModel
. The getters are simple: since the view model uses string
(to match the Text
property of the TextBox
) and the model uses DateTime
, we simply call ToString
in the getter before returning each value.
In the setter, we need to convert in the other direction. Fortunately, there’s a DateTime.Parse
method to convert the string representation of a date/time to an actual DateTime
. If Parse
can’t interpret the string representation, it throws a FormatException
so we need to handle that case.
When WPF data validation detects a problem, it causes a validation message to appear near the field that didn’t validate. This is standard UX behavior in modern web pages as well. While I wait for UWP validation to catch up with WPF, I’m using the simplified approach of writing all of the validation messages to a single TextBlock
.
To encapsulate the validation functionality, I created a new view model class called ValidationMessageViewModel
, which contains the following:
- A private
ValidationMessage
class, with the message itself (e.g., “Please enter a valid start date and time”) and a boolean indicating whether the message should be displayed. SetFieldValid
andSetFieldInvalid
methods to indicate whether a field has been found valid or invalid.- A
ValidationMessages
property containing all of the active validation messages (for fields that have been found invalid). - A
SetValidationMessages
method to update the consolidated message, based on which fields are marked as invalid.
Like the other Time Tortoise view models, ValidationMessageViewModel
inherits from NotificationBase
, which means it can participate in data binding. The bound field is a text block on the main page, which binds to ValidationMessages
and tells the user which fields need to be fixed.
With this validation view model in place, the StartTime
and EndTime
setters use the following process:
- Set the current field to valid (assume it’s valid before doing anything).
- Inside a
try
block, try to parse the date/time string in the bound text box. - If a
FormatException
occurs, set the current field to invalid. - In the
finally
block, callSetValidationMessages
to refresh the validation messages.
Since ValidationMessages
is a bound field, updating its value in the finally
block causes the UI to update as well, which presents the user with the current validation messages. If any field contains a value that causes it to be marked as invalid, the corresponding message appears. Once the user corrects the value, that message disappears from the text block.
Next: Adding and Deleting Time Segments
Now that time segment editing is in place, adding the ability to add and delete time segments will be comparatively easy. And the validation infrastructure from this week can also be used to validate other fields, like activity name.