Time Tortoise: Time Segments

Time Segments

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 key
    • ActivityId: foreign key to Activity.Id
    • StartTime: beginning of the time segment
    • EndTime: end of the time segment
  • In the Activity class, add a List<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 the Activity table in the database. It still has just Id and Name 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 DbSets 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 my DbContext 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 multiple DbContexts, 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 override OnConfiguring, so that EF knows where to find the database to be migrated.

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 an ObservableCollection<ActivityViewModel>. Since ActivityViewModel now has a child list of TimeSegment, we need LoadActivities 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.