The Commuter View Model interface is written. The next step is to hook it to a view.
I like to think of the view as just a convenience. If the user could access properties directly, all they would need is the View Model interface. Thinking about the View Model this way helps me ensure that it is complete and targeted.
So I created the interface as a way of describing what would be on the view. A designer given the interface and a wireframe could build a view. Not having a designer handy, I did it myself. Here's how.
Create a mock
The first step was to create a mock implementation of the View Model interface. In truth, it's not just one interface; it's a set of interrelated interfaces. So I have interrelated mocks. Most of them are simple data access objects: collections of properties with no behavior. Create a read/write property for every property of the interface, even if the interface property is read-only. So for the IPodcastEpisode interface:
public interface IPodcastEpisode
{
string Name { get; }
string PodcastName { get; }
string DurationAsString { get; }
}
I created a PodcastEpisodeSample class:
public class PodcastEpisodeSample : IPodcastEpisode
{
public string Name { get; set; }
public string PodcastName { get; set; }
public string DurationAsString { get; set; }
}
The root class has a little more to it. It has to initialize itself and its children in its constructor. I don't like putting code in a constructor, but you don't have another chance to set your properties.
public class CommuterViewModelSample : ICommuterViewModel
{
public CommuterViewModelSample()
{
// Set up some sample data.
List<IPlaylist> playlists = new List<IPlaylist>();
playlists.Add(new PlaylistSample() { Name = "Favorites" });
PlaylistSample commutePlaylist = new PlaylistSample() { Name = "Commute" };
playlists.Add(commutePlaylist);
playlists.Add(new PlaylistSample() { Name = "Energy Songs" });
Playlists = playlists;
SelectedPlaylist = commutePlaylist;
List<IPodcast> podcasts = new List<IPodcast>();
podcasts.Add(new PodcastViewModel(new Podcast() { Name = ".NET Rocks!", Selected = false, Rank = 0 }));
podcasts.Add(new PodcastViewModel(new Podcast() { Name = "Hanselminutes", Selected = true, Rank = 3 }));
podcasts.Add(new PodcastViewModel(new Podcast() { Name = "Deep Fried Bytes", Selected = true, Rank = 4 }));
podcasts.Add(new PodcastViewModel(new Podcast() { Name = "They Might Be Giants", Selected = true, Rank = 5 }));
Podcasts = podcasts;
List<IPodcastEpisode> queue = new List<IPodcastEpisode>();
queue.Add(new PodcastEpisodeSample() { Name = "5-A", PodcastName = "They Might Be Giants", DurationAsString = "9:30" });
queue.Add(new PodcastEpisodeSample() { Name = "JavaFX and the Web's Four Virtual Machines", PodcastName = "Hanselminutes", DurationAsString = "40:24" });
PodcastEpisodeSample deepFriedBytesEpisode = new PodcastEpisodeSample() { Name = "New Ideas for the Web with Thomas Krotkiewski", PodcastName = "Deep Fried Bytes", DurationAsString = "53:15" };
queue.Add(deepFriedBytesEpisode);
Queue = queue;
SelectedEpisode = deepFriedBytesEpisode;
TotalDurationAsString = "1:43:09";
}
public IEnumerable<IPlaylist> Playlists { get; set; }
public IPlaylist SelectedPlaylist { get; set; }
public IEnumerable<IPodcast> Podcasts { get; set; }
public IEnumerable<IPodcastEpisode> Queue { get; set; }
public IPodcastEpisode SelectedEpisode { get; set; }
public string TotalDurationAsString { get; set; }
public ICommand Skip { get; set; }
}
You can hook up the mock in Blend. In the Window's properties, press the "New" button next to DataContext. Select the CommuterViewModelSample class from the solution. Now Blend will show the data from your mock object while you edit the view.
Overall structure: Grid
The structure of the Window itself is defined by a Grid. Grid is the best container for a window, because it supports "star" sizing. You can be specific about the size of some components, and then leave the rest of the real estate to be carved up proportionally. A Grid is the default layout root that Blend puts into a window, but if you have something else there (like a StackPanel for example), you can right-click on it and select "Change Layout Type".
Select the Grid in the object pane (where you right-clicked to change the layout type) and expand the layout properties. Hit the ellipsis button to set the RowDefinitions property. For Commuter I added 8 rows. I set the height of all but one to "Auto"; the remaining one I left at "1 Star". This one row will resize with the windows, and the others will be the height of their contents.
I populated the rows with controls, alternating between TextBlocks for labels and other controls for inputs. To tell you the truth, it was easier to drop into XML for this part. If you've set the row sizes to "Auto", then there is no space into which to drop controls. And if you haven't you spend a lot of time resetting Margin, HorizontalAlignment, and VerticalAlignment after you drag a control into the right row. Here's the structure so far.
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Commuter.Window1"
Title="Commuter" Height="420" Width="540" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" xmlns:Commuter="clr-namespace:Commuter">
<Window.DataContext>
<Commuter:CommuterViewModelSample/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
</Grid>
</Window>
Non-resizeable list: ListBox of Grid
The Podcasts list contains for each podcast a selection box, rating buttons, and the name of the podcast. The selection box and rating buttons are all fixed width. The only thing that varies is the width of the name. This sounds like a job for Grid again. The buttons can assume the width of their contents ("Auto"), while the name can consume all remaining space ("1 Star").
I dropped a ListBox into the appropriate row and set its Height property to a number I liked. Dragging the handle didn't do the right thing, because Blend couldn't tell whether I wanted to resize the row or not. When the property is set explicitly, it's obvious what you intend.
The next step was to bind the ItemsSource to the Podcasts property of the View Model. Click on the little gray dot to the right of the ItemsSource property and select "Data Binding". You'll want the "Explicit Data Context" tab, though I'm not sure why it's called that. You're using the DataContext inherited from the parent Window. Open up the View Model object and select the collection you want to show. I chose the Podcasts property.
Now you need to define how each item in the list looks. Right-click on the ListBox and select "Edit Control Parts (Template)", "Create Empty". Give the template an appropriate name (like PodcastsTemplate), leave "This Window" selected, and hit "OK". This creates a data template resource within the Window itself. If you had created it in the Application, it could be shared among windows. Since I don't see a need for that, I kept it local.
I'm not sure why this is called a "data template". It seems that it should be called a "view template". It's not a template for the data: that's defined by the bound interface property. Anyway...
From here, you can use a Grid layout just like you did for the Window, but set the ColumnDefinitions instead of the RowDefinitions. When you're done, click the button in the picture on the right to go back to the window. I can't tell you how long it took me to discover this thing.
You may notice that each of the rows is squashed at this point. Set the HorizontalContentAlignment property to "Stretch" to fix this.
Here's the resulting XAML.
<DataTemplate x:Key="PodcastsTemplate">
<Grid Height="18">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="38"/>
<ColumnDefinition Width="38"/>
<ColumnDefinition Width="38"/>
<ColumnDefinition Width="38"/>
<ColumnDefinition Width="38"/>
</Grid.ColumnDefinitions>
<CheckBox Width="13" VerticalAlignment="Center" Grid.Column="0"/>
<TextBlock Width="Auto" Text="{Binding Path=Name, Mode=Default}" Grid.Column="1" VerticalAlignment="Center"/>
<CheckBox Grid.Column="2" Template="{DynamicResource CarCheckBox}" IsChecked="{Binding Path=Rank1, Mode=Default}"/>
<CheckBox Grid.Column="3" Template="{DynamicResource CarCheckBox}" IsChecked="{Binding Path=Rank2, Mode=Default}"/>
<CheckBox Grid.Column="4" Template="{DynamicResource CarCheckBox}" IsChecked="{Binding Path=Rank3, Mode=Default}"/>
<CheckBox Grid.Column="5" Template="{DynamicResource CarCheckBox}" IsChecked="{Binding Path=Rank4, Mode=Default}"/>
<CheckBox Grid.Column="6" Template="{DynamicResource CarCheckBox}" IsChecked="{Binding Path=Rank5, Mode=Default}"/>
</Grid>
</DataTemplate>
...
<ListBox ItemsSource="{Binding Path=Podcasts, Mode=Default}" ItemTemplate="{DynamicResource PodcastsTemplate}" HorizontalContentAlignment="Stretch" Height="76" Grid.Row="3"/>
Resizable list: ListView
The list of episodes contains three string columns. It makes sense to let the user individually resize these, unlike the single text field among a bunch of fixed-width buttons. So I decided on a ListView for this control.
You'll probably have to click the chevrons on the bottom of the toolbar to find the ListView tool. It's not one that's visible out-of-the-box. Set the ItemsSource just as before, But this time, rather than defining a data template, you need to set the View. Expand the "View" property hiding in "Miscellaneous". Click on the "Columns" button to add three columns. Set the Header property and bind the DisplayMemberBinding.
Here's one frustrating part of Blend. Even though we've bound the ItemsSource, Blend presents us with the properties of the CommuterViewModelSample. It should be able to recognize that the ItemsSource is a collection of IPodcastEpisode and give us the properties of that interface. Since it can't we just have to manually enter a path expression. Check the box at the bottom and enter the property name.
<ListView ItemsSource="{Binding Path=Queue, Mode=Default}" SelectedItem="{Binding Path=SelectedEpisode, Mode=Default}" Grid.Row="5" HorizontalContentAlignment="Stretch">
<ListView.View>
<GridView>
<GridViewColumn Width="300" Header="Name" DisplayMemberBinding="{Binding Name}"/>
<GridViewColumn Width="140" Header="Podcast" DisplayMemberBinding="{Binding PodcastName}"/>
<GridViewColumn Width="50" Header="Duration" DisplayMemberBinding="{Binding DurationAsString}"/>
</GridView>
</ListView.View>
</ListView>
Spacing: Style
By default, all of the controls in a grid are pushed right up next to each other. To add spacing, I defined a set of styles. Each style targets a control type in the grid, and sets the Margin property.
<Window.Resources>
<Style TargetType="{x:Type ComboBox}">
<Setter Property="Margin" Value="3"/>
</Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Margin" Value="3"/>
</Style>
<Style TargetType="{x:Type ListBox}">
<Setter Property="Margin" Value="3"/>
</Style>
<Style TargetType="{x:Type ListView}">
<Setter Property="Margin" Value="3"/>
</Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Margin" Value="3"/>
</Style>
</Window.Resources>
Going back to Visual Studio
These patterns covered what I needed for this simple view. I suspect that they will cover most of my needs for data entry windows.
Overall, I find Blend to be a bit cumbersome to work in. It was great for the geometry of the car buttons, but it gets in the way when laying out a form. Dropping a control in the right place is not possible when the container size is Auto. Several extra properties get set on your behalf, which you then need to hunt down and reset. And even though the XAML that I need is small, the Blend UI hides many of the properties behind expanders, buttons, and tabs. I have to navigate several pages just to see what is immediately visible in a single line of XAML.
As a XAML editor, Blend is poor. It offers no intellisense the way that Visual Studio does. So now that I know the XAML that I need, I expect I'll be spending a lot more time in Visual Studio hand-crafting XAML.