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.
Earlier this month, I wrote about time segments, a fundamental part of time tracking. In that article, I described the implementation of the database schema, model, and view model for time segments. This week, I’ll be focusing on the user interface.
A Visual Studio Designer Bug?
For a while now I have been working around what seems to be a bug in the Visual Studio designer. I haven’t found a perfect solution (that would probably require a code fix in Visual Studio) but this week I found a better workaround.
The bug in question relates to a XAML feature that allows data from user-defined types to be displayed in a list. For example, the main Time Tortoise page shows a list of activities. Here’s how that list is set up in MainPage.xaml
:
- A
ListView
control on the page presents a list of clickable rows. - The attribute
ItemsSource="{x:Bind Main.Activities, Mode=OneWay}"
indicates that theListView
is bound toMain.Activities
, anObservableCollection
that contains elements of typeActivityViewModel
. It is bound in OneWay mode, meaning that the binding target (theListView
) is updated when the binding source (theObservableCollection
) raises an event indicating that it has changed. Since the binding is one-way, we can’t edit activity names directly in theListView
.
Because ActivityViewModel
is a user-defined type, the ListView
doesn’t automatically know how to display it. That’s where the ItemTemplate
comes in. It is defined as follows:
<ListView.ItemTemplate>
<DataTemplate x:DataType="viewModel:ActivityViewModel">
<Grid>
<TextBlock Text="{x:Bind Name, Mode=OneWay}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
x:DataType="viewModel:ActivityViewModel"
specifies the type for which this template defines the display rules. ActivityViewModel
is the type name. viewModel
is a namespace name, defined as follows inside the Page
element at the top of the .xaml
file:
xmlns:viewModel="using:TimeTortoise.ViewModel"
<TextBlock Text="{x:Bind Name, Mode=OneWay}" />
indicates that, of the fields in ActivityViewModel
, we only want to display the Name
field. Since binding is again one-way, the name will be updated in the TextBlock
(the target) whenever it’s updated in the view model Name
field (the source).
All of this mostly works fine, except for one problem: the designer seems to get confused when ItemTemplate
is used in a multi-project solution set up the way TimeTortoise.sln
is. To maximize the benefits of the MVVM pattern, the Time Tortoise solution contains separate projects for the model, view, view model, and data access layer. This means that the ItemTemplate
, which is contained on a page in the view project, refers to a second project when it uses a field in ActivityViewModel
. Then the view model refers to a third project when it uses fields from the Activity
model. It’s that third project that seems to trip up the designer.
Here’s the error message that occurs on the first line of the DataTemplate
:
The name "ActivityViewModel" does not exist in the namespace
"using:TimeTortoise.ViewModel".
The error is clearly specific to the designer, because the project compiles and runs fine, and IntelliSense even correctly suggests the view model name. But unfortunately, the error causes the designer to crash with:
Invalid Markup. Check the Error List for more information.
This error makes it impossible to use the graphical designer to edit the page (though editing the XAML directly still works). My initial workaround was to remove the entire <ListView.ItemTemplate>
section while using the designer for editing, and then add it back for testing.
When I upgraded Time Tortoise to VS2017, the problem didn’t go away as I had hoped. However, the designer did show the following more useful error message:
Microsoft.MetadataReader.UnresolvedAssemblyException
Type universe cannot resolve assembly: TimeTortoise.Model,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.
Sure enough, when I added a reference from TimeTortoise.UWP to TimeTortoise.Model, the designer error disappeared. So even though the View project doesn’t directly reference anything in the Model project (and it shouldn’t, according to MVVM), the designer thinks it needs a direct reference to the Model, presumably because it sees references to a Model class in the ViewModel.
This extra reference is not ideal, since it could lead to unintentional use of Model classes directly in the View. But adding an extra reference in order to use the designer is better than deleting and re-adding code, so I’m sticking with it for now. This designer error seems to be a common one, and Stack Overflow lists many other potential solutions (e.g., in these questions). Since I haven’t tried all of them, it’s possible that a better solution exists.
Saving Parent and Child Objects using Entity Framework
In previous iterations of the app, with only activity names to save and retrieve, the Entity Framework add/save process worked as follows:
- The user clicks the Add button on the main page. The button is bound to
MainViewModel.Add()
. - The
Add
method creates a newActivity
instance, adds it to the local activity collection, and callsIRepository.AddActivity()
. - The
AddActivity
method callsDbContext.Add(activity)
. This Add method is implemented by Entity Framework. It tells EF to start tracking the given activity. This means the activity will be inserted into the database whenDbContext.SaveChanges()
is called. - Eventually, the user clicks the Save button to save the activity name. The button is bound to
MainViewModel.Save()
, which callsIRepository.SaveActivity()
, which callsDbContext.SaveChanges()
, which updates the database.
The addition of the time segment class creates a parent/child relationship in the object graph. The parent is Activity
, and it contains a list of time segments, so TimeSegment
is the child class.
Conveniently, Entity Framework knows how to persist object graphs: the EF code used to save an Activity
also saves its associated TimeSegment
list. No new EF code is required for saving the time segments. Here’s how the process works:
- The user selects an activity.
- The user creates a time segment by clicking Start when they start working and Stop when they finish. The
StartStop
method inMainViewModel
creates and updates the time segment in theTimeSegments
collection. DbContext.Add(activity)
andDbContext.SaveChanges()
, as described above, add and save both theActivity
and itsTimeSegments
collection. This works whether the activity was previously saved in the database, or whether it is saved for the first time along with its time segments. The time segments don’t need to be explicitly referenced withDbConext.Add
. When a parent activity is added to the context, any child segments are also added.
More Time Segment Features
Now that the basics of time segments are in place, I have a foundation to build other time segment features, including:
- Showing a live timer when timing is active.
- Showing duration values for each segment and for the total of all of an activity’s segments.
- Editing segments (without allowing invalid data to be introduced).
- Deleting segments.
- Handling segments correctly when the app exits and restarts while timing is in progress.
- Preventing multiple segments from being active simultaneously.
That all starts next week.