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 most fundamental pieces of a time tracker are activities and time segments. Activities are the things you’re working on, and time segments are the times throughout the day when you work on them. I’ve been discussing the implementation of activities for the past few weeks. This week, I’m starting on time segments.
Time Segments
Activities and time segments have a one-to-many relationship with each other. Each activity is associated with one or more time segments. A time segment is associated with exactly one activity. (In Time Tortoise, you aren’t allowed to work on two activities at the same time).
The most common Time Tortoise workflows are:
- Selecting an activity and starting the timer, or
- Stopping a currently running timer.
The former action creates a new time segment, associates an activity with it, and sets its start date/time. The latter action sets the end date/time for the current segment.
A list of time segments is a convenient data structure for answering time-related questions such as these:
- What did you do today?: Display time segments in chronological order, with their associated activities.
- How much time did you spend on a particular activity?: Sum time segment durations for a selected activity (for a day or week).
- How much time did you spend working?: Sum time segment durations for all activities (for a day or week).
Time Segment Implementation
To add time segment functionality to Time Tortoise this week, I had to add it to these layers.
Model
In code-first Entity Framework development, the model drives changes to the database schema. I made these model changes:
Add a new
TimeSegment
class with the following fields:Id
: primary keyActivityId
: foreign key toActivity.Id
StartTime
: beginning of the time segmentEndTime
: end of the time segment
In the
Activity
class, add aList<TimeSegment>
to hold the time segment for an activity. Because of the way that relational databases handle one-to-many relationships, this list doesn’t cause any changes to theActivity
table in the database. It still has justId
andName
fields.
Database
Entity Framework isolates developers from direct manipulation of the database. But the physical database still exists, and we can look at it. In previous weeks, the database just had an Activities
table, which contains a primary key Id
and a Name
text field.
The model changes this week caused a TimeSegments
table to be created with an Id
, StartTime
and EndTime
, and an ActivityId
foreign key pointing to the associated activity. These fields match the properties in the TimeSegment
C# class.
Data Access Layer
The Context
class requires DbSet
s corresponding to table data that we want to retrieve, so I added a DbSet<TimeSegment>
called TimeSegments
. However, this DbSet
isn’t retrieved automatically. It requires some supporting code in the Repository
class. Previously, Repository
had a LoadActivities
method containing this code:
return _context.Activities.ToList();
Although the database knows that activities and time segments are related, Entity Framework doesn’t automatically retrieve child objects when a parent object is retrieved. However, there is a concise change to the statement above that retrieves activities along with their associated time segments:
return _context.Activities.Include(a => a.TimeSegments).ToList();
Migrations
Entity Framework uses the concept of migrations to keep the database schema up to date. Once I made the changes required to support time segments, I ran the following command in the Visual Studio’s Package Manager Console window:
Add-Migration TimeSegments -Project TimeTortoise.DAL -Context SqliteContext
The parts of the command are as follows:
- Add-Migration: Generate C# code to update the database schema based on model changes made since the previous migration. This is known as code first migration.
TimeSegments
: This is the name of the class that EF creates to perform the migration.TimeTortoise.DAL
: This is the name of the project that contains myDbContext
classes. The Package Manager Console points by default to the startup project,TimeTortoise.UWP
. Rather than switch it in the Visual Studio UI, I find it easier to use this parameter. If I don’t point to the DbContext project, Add-Migration fails with the following message: “No DbContext was found in assembly ‘TimeTortoise.UWP’. Ensure that you’re using the correct assembly and that the type is neither abstract nor generic.”SqliteContext
: Since my data access layer contains multipleDbContext
s, I have to specify which one to use. Here are a few error messages that can occur if this parameter isn’t set correctly:- “More than one DbContext was found. Specify which one to use.” This happens if the parameter is left out and there are multiple classes that directly or indirectly inherit from
DbContext
. - “System.InvalidOperationException: No database provider has been configured for this DbContext.” The
DbContext
class used for migration must overrideOnConfiguring
, so that EF knows where to find the database to be migrated.
- “More than one DbContext was found. Specify which one to use.” This happens if the parameter is left out and there are multiple classes that directly or indirectly inherit from
One more error that I ran into: “System.DllNotFoundException: Unable to load DLL ‘sqlite3’: This operation is only valid in the context of an app container.” According to Brice Lambson from the EF team, “The version of sqlite3.dll
used at runtime on UWP won’t work at design-time.” I followed his advice, and added the SQLite DLL to my data access layer project.
Once Add-Migration
is successful, it adds a new class to the Migrations folder in the selected project, and also updates the ModelSnapshot
class, which contains a history of database schema changes. The migration can be run manually or, as I have it configured, can run automatically when the app starts and Database.Migrate()
is called in the SqliteContext
constructor.
View Model
The purpose of the View Model is to prepare data for the View. So we need Time Segment changes to show up in the View Model in an appropriate way for use in a UWP app.
To get a list of time segments to show up a UWP view and stay up to date with changes, we need an ObservableCollection<TimeSegment>
. So I added that to ActivityViewodel
.
MainViewModel
supports the main Time Tortoise view (which currently corresponds to the single app window). It needs a couple of changes:
LoadActivities
takes data retrieved from the model, and adds it to anObservableCollection<ActivityViewModel>
. SinceActivityViewModel
now has a child list ofTimeSegment
, we needLoadActivities
to populate this list as well.StartStop
is bound to a button in the UI. If nothing is currently being timed, then pressing the button (calling the method) creates a new time segment and assigns a start time. Otherwise, it stops timing and sets the end time.
Unit Tests
As I explained last week, software that depends on the system clock shouldn’t just call System.DateTime()
directly, since that will make it difficult to write reliable unit tests. Instead, components that need the date and time should depend on IDateTime
, an interface that exposes date/time-related members.
The tricky part of that approach is how to tell components which concrete implementation of IDateTime
to use. For now, I’m manually passing the implementation to the MainViewModel
constructor. That means this constructor now accepts two interfaces, IRepository
and IDateTime
. It may eventually make sense to use an Inversion of Control container like StructureMap, but for now it’s simple enough to pass them manually.
Production code and tests that don’t care about the time (like Activity
tests) can pass SystemDateTime
, which exposes the real time. Tests that do care about the time (like the TimeSegment
tests from this week) can ask Moq to create a fixed time value, like this:
var startTime = new DateTime(2017, 3, 1, 10, 0, 0);
mockTime.Setup(x => x.Now).Returns(startTime);
mockTime.Object
then contains an IDateTime
that doesn’t change. With that approach, I added tests to start and stop timing, and verify that the time segments contain the correct date/time values.
Integration Tests
I like to use integration tests to verify that there are no unexpected results when I actually hit the database. This week, I wrote one test to verify that time segments have the correct values when they are saved to the database and then retrieved. It was a useful way to learn about one-to-many relationships in Entity Framework.
View
The next step is to add time segment support to the View. I’ll do that next week, and see if my unit tests anticipated all of the issues that come up in the UI.