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.
Incremental Build Model
Last week I made my first commit to the GitHub repository for Time Tortoise, the app that I’m working on this year.
I publish a blog post every Wednesday, so I have adopted that schedule for this project as well. In general, each blog post is associated with a GitHub commit. In the first few weeks of the year, I used example UWP projects to experiment with a few ideas. Starting last week, my commits have been adding functionality to the Time Tortoise app itself.
This approach is an example of the incremental build model of software development. The idea is to start with a small working program and build it over time through a series of small (incremental) improvements. After each increment, the program remains usable, and it has slightly more functionality.
In agile terms, I’m building Time Tortoise using a series of one-week sprints, with a release at the end of each sprint. Since the project team consists of one very part-time developer (me), the scope of each sprint is small. But thanks to the incremental build model, it will add up to something useful over time.
The scope for this week: create, update, and list activity names.
Create, Update, and List Activity Names
In Time Tortoise, an activity is defined as follows:
Activity: a type of work whose duration you want to measure.
For example, at this moment I’m tracking an activity that I could call blog post writing. Earlier this week I was tracking an activity called Time Tortoise development. I want to know how many hours I have spent on these and other activities today, this week, last week, and so on. Eventually, Time Tortoise will be able to answer these questions.
A basic Time Tortoise use case is to select an activity from a list and start tracking it. So the system needs a way to manage a list of activities. This week, I worked on a few basic activity management features:
- Add a new activity to the list.
- Update an existing activity.
- Display the current list of activities.
An activity will eventually be associated with a list of time segments. For now, I’m storing just the activity name. The database also assigns a unique ID to each activity, but the user doesn’t have to know about that.
MVVM Design
The Time Tortoise design is based on the Model-View-ViewModel pattern. As explained last week, I have separate .NET projects for the view, view model, model, data access layer, and unit tests. In last week’s commit, I just exercised the view model with test code and a mock repository. This week, I have an end-to-end scenario that includes a XAML UI and a SQLite database.
View
The view now has four controls:
ListView
A XAML ListView “displays data items in a vertical stack.” In this case, the data items are activity names.
One of the benefits of XAML is its data binding functionality. So the goal is to send the list of activity names to the UI through binding.
Currently the app has one view, MainPage.xaml. The codebehind for a view is a convenient place to reference the corresponding view model, like this:
public MainViewModel Main { get; set; }
Then in the XAML, we can bind to properties on the view model, using the Main
identifier. For example:
ItemsSource="{x:Bind Main.Activities, Mode=OneWay}"
This tells the ListView that it can get its list of activities from a collection called Main.Activities
, and that it should update itself when this collection changes.
To operate on an activity, the user first selects it from the list. We can use binding to keep track of which activity the user selects:
SelectedIndex="{x:Bind Main.SelectedIndex, Mode=TwoWay}"
If we only needed activity names, then Activities
could be a collection of strings. But I’m planning to include more information (e.g., time totals) in the list. Formatting a row with multiple columns is possible using an ItemTemplate. That approach allows each column to be bound to a separate text block, like this for the Name column:
<TextBlock Text="{x:Bind Name, Mode=OneWay}" />
TextBox
To add and update an activity name, we need a TextBox. Two-way binding ensures that the name displayed in the textbox stays in sync with the name selected in the list:
<TextBox Text="{x:Bind Main.SelectedActivity.Name, Mode=TwoWay}"/>
Button
Two buttons allow the user to add and save activities. The click event for each button is bound to a method in the view model, which I’ll discuss next:
<Button Click="{x:Bind Main.Add}" Content="Add" />
<Button Click="{x:Bind Main.Save}" Content="Save" />
ViewModel
The Time Tortoise solution currently has two view model classes.
ActivityViewModel
exposes data from the Activity
model class. Although this view model doesn’t currently do any special formatting or other processing, it’s still desirable to encapsulate Activity
rather than use it as-is. The reason: we don’t want to directly reference the Model from the View. That would break MVVM information hiding rules, whereby the View Model knows about both the Model and the View, but the Model and the View don’t know about each other.
MainViewModel
supports MainPage
, primarily by exposing these binding sources and targets:
Activities
is a property of type ObservableCollection, a special collection type that provides notifications that allow UI elements (like the ListView) to stay in sync with changes to the collection.SelectedIndex
contains the integer index for the currently selected activity.SelectedActivity
contains the corresponding activity (ActivityViewModel
) that is selected. It gets updated when the selected index changes.Save
calls the repository to save the currently selected activity.Add
adds a new activity to the collection. Binding then causes it to show up in the list.
The MainViewModel
constructor calls LoadActivities
, which calls the repository to initialize the Activities
collection.
Binding
Here’s a review of how binding works:
- Each view model class inherits from
NotificationBase
, which implements INotifyPropertyChanged, the .NET interface that provides notifications about changed properties. - Property getters in view model classes are implemented like normal getters. They just return the value of a private variable.
- Property setters don’t just update the private variable. Instead, they make a call like
SetProperty(ref _selectedActivity, value)
. In this case,_selectedActivity
is the private variable,value
is the new value provided by the caller, andSetProperty
is implemented inNotificationBase
. - Because
SetProperty
accepts_selectedActivity
by reference, it can update the value directly. But in addition to updating the value, it also raises a PropertyChanged event. This notifies bound clients that a new activity has been selected. For example, the text box on the view, which is bound toSelectedActivity.Name
, then knows to update its text value.
Model
The only change in the model since last week is to add an integer Id
field to the Activity class. This is an Entity Framework convention to identify the primary key for a database table.
Data Access Layer
Last week’s data access code consisted only of a mock repository that didn’t make any actual database calls. So the data access layer is almost entirely new for this week. Here’s the current state of the data access layer:
Repository
now has two methods:LoadActivities
returns theActivities
collection from the database context.SaveActivity
calls the database context to persist a singleActivity
object (new or previously saved).- The database context is now a hierarchy of classes: The base class is
DbContext
, which is provided by Entity Framework.Context
inherits fromDbContext
and contains only properties of type DbSet, which are used to persist and retrieve C# objects. Finally,SqliteContext
inherits fromContext
, and contains SQLite-specific details like the database connection string. SinceRepository
accepts an instance of typeContext
in its constructor, this allows a client such as an integration test to pass in a type that uses an in-memory connection string instead of pointing to the standard database file on disk. - The Migrations folder, as always, contains code to build the database schema.
Tracking an entity
Here’s an error message that I got while debugging the data access layer:
The instance of entity type ‘Activity’ cannot be tracked because another instance of this type with the same key is already being tracked. When adding new entities, for most key types a unique temporary key value will be created if no key is set (i.e. if the key property is assigned the default value for its type). If you are explicitly setting key values for new entities, ensure they do not collide with existing entities or temporary values generated for other new entities. When attaching existing entities, ensure that only one entity instance with a given key value is attached to the context.
This message was associated with a System.InvalidOperationException
exception that was thrown when I called DbContext.SaveChanges
to save an updated Activity.
As the message suggests, the problem was due to having two instances of the same activity record. When I load the current set of activities from the database in MainViewModel.LoadActivities
, I turn each one into an ActivityViewModel
so it can be used by the View. At one point, I was doing this by assigning each field individually in the ActivityViewModel
constructor. Not only is this more work than necessary. It also results in the above error. The simpler, and correct, approach is the following constructor:
public ActivityViewModel(Activity activity) : base(activity)
{}
ActivityViewModel
inherits from NotificationBase<Activity>
, which encapsulates an instance of Activity
in a protected field called This
that is initialized in the constructor. By always letting NotificationBase
keep track of the activity instance, we ensure that we have only one copy floating around, which keeps Entity Framework happy.
Tests
Since this week was mainly about research and experimentation to get the binding and data access code working, I didn’t write my tests first, as strict TDD rules would dictate. Getting to full test coverage will be a task for next week.