EF Code First and WPF with the Chinook database. Part 3 – Styles and DataTemplates 101

I have decided to split out the styling work into 2 posts as it would be a huge post to restyle even something so simple.  so this post is going to change the controls to be more style-conscious and introduce basic style and the next post will introduce some more advance concepts such as using adorners to display additional details.

Going back to the previous post we had a basic app as follows:

This is slightly ugly and needs a little work so the first thing I want to add is  a style that will tidy up the artist list.

I want the artist list to have a nice look and feel with rounded button type items and also contain a count of the number of albums – I want each item in the list to look something like this:

image

The first step to getting this design in place is adding the DataTemplate so that it is picked up by the item.  A DataTemplate is a template that gets applied to an object that has no default way of showing itself and manifests itself as its type name.

NOTE – this can also be be done by overriding Object.ToString() but this is a client side post so I don’t want to have to change model code to do something.

I have added a simple DataTemplate to the Application.Resources section in App.xaml as follows:

<DataTemplate DataType="{x:Type business:Artist}">
    <TextBlock x:Name="contentHolder">
        <TextBlock.Text>
            <MultiBinding StringFormat="{}{0} - {1} albums">
                <Binding Path="Name"/>
                <Binding Path="Albums.Count" />
            </MultiBinding>
        </TextBlock.Text>                
    </TextBlock>
    <DataTemplate.Triggers>
        <DataTrigger Binding="{Binding Albums.Count}" Value="1">
            <DataTrigger.Setters>
                <Setter Property="TextBlock.Text" TargetName="contentHolder">
                    <Setter.Value>
                        <MultiBinding StringFormat="{}{0} - {1} album">
                            <Binding Path="Name"/>
                            <Binding Path="Albums.Count" />
                        </MultiBinding>
                    </Setter.Value>
                </Setter>
            </DataTrigger.Setters>
        </DataTrigger>
    </DataTemplate.Triggers>
</DataTemplate>

There are several things going on here:

  1. the first line declares the type that this template is associated with – you have to add the namespace to this file first in order to make this work:
    xmlns:business="clr-namespace:MusicApp.Model;assembly=MusicApp.Model"
    
  2. The MultiBinding has an associated StringFormat – this is the same as a normal format string except for the “{}” after the equals sign – this simply escapes the format string.
  3. The binding to Albums.Count – yes you can bind to “built-in” properties as well.
  4. The DataTrigger is a simple way of saying that if we have only one album then the trailing text shouldn’t have an ‘s’ on the end.

It is possible to do the whole string in a converter but I like the declarative way as you can see everything going on in one place.  For those that like using converters you would bind the TextBlock’s Text property to the whole Artist ({Binding}) and then use a converter like the following:

public class ArtistStringConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        string retVal = string.Empty;
        var artist = value as Artist;
        if (artist != null)
        {
            retVal += string.Format("{0} - {1} {2}", artist.Name, artist.Albums.Count, artist.Albums.Count == 1 ? "artist" : "artists");
        }

        return retVal;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

this is then used from the xaml as follows:

<local:ArtistStringConverter x:Key="artistConverter"/>
<DataTemplate DataType="{x:Type business:Artist}">
    <TextBlock x:Name="contentHolder" Text="{Binding Converter={StaticResource artistConverter}}" />
</DataTemplate>

Once this is added (either option) you need to remove the DisplayMemberPath property from the ArtistList.xaml view so that the template is used instead.

I now have a list of Artists that show the name and the total albums they have so I can work on the actual style to make the list items look like the above image.

I want all three lists to look the same so I am adding a style with no key to the app.xaml file.  If you want specific styles for your controls then you can add ‘x:Key=”YourKeyName”’ and reference it using ‘{StaticResource YourKeyName}’ (in this instance the reference would be in the ItemContainerStyle property of the ListBox).

<Style TargetType="ListBoxItem">
    <Style.Setters>
        <Setter Property="Margin" Value="5,2" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="ListBoxItem">
                    <Grid>
                        <Rectangle Opacity="0.5" Height="30" StrokeThickness="1" x:Name="backBox"
                                        Stroke="Silver" RadiusX="5" RadiusY="5" Fill="Azure"/>
                        <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
                    </Grid>
                    <ControlTemplate.Triggers>
                        <Trigger Property="Selector.IsSelected" Value="True">
                            <Setter TargetName="backBox" Property="Fill" Value="Silver"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style.Setters>
</Style>

Again I have a couple of things going on inside this style:

  1. It targets ListBoxItem as this is what we want to style (not the ListBox itself).
  2. The margin separates the items nicely.
  3. The actual style part is contained inside a ControlTemplate which is then assigned  the Template property of the ListBoxItem.
  4. The ContentPresenter will present whatever content it is given, in this case it is presenting the passed in DataTemplate.
  5. The trigger here indicates that when the ListBoxItem is selected we want to change the fill colour of the rectangle to indicate we have selected an item.

To finish I have added a new DataTemplate for the albums – this looks the same with the type and property names changed to display the correct things and I have also rearranged the view as follows:

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="0.5*" />
        <ColumnDefinition Width="0.5*" />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="0.5*"/>
        <RowDefinition Height="0.5*"/>
    </Grid.RowDefinitions>

    <ListBox ItemsSource="{Binding Path=Artists}" Margin="5" 
                Grid.Column="0" Grid.Row="0"
                Grid.RowSpan="2"
                SelectedItem="{Binding SelectedArtist}" />
        
    <ListBox ItemsSource="{Binding Path=SelectedArtist.Albums}" Margin="5" 
                Grid.Column="1" Grid.Row="0" 
                SelectedItem="{Binding SelectedAlbum}" />
        
    <ListBox ItemsSource="{Binding Path=SelectedAlbum.Tracks}" Margin="5" 
                DisplayMemberPath="Name" 
                Grid.Column="1" Grid.Row="1" />
</Grid>

This now gives me the following view which I am sure you will agree looks a lot better and is easily achievable by simply using XAML – we could have done this in blend as well but for the simple changes we have made I prefer to edit the XAML directly.

image

In the next post I will be adding some adorner goodness to the screen for displaying album and track details (which also covers changes to the model).

EF Code First and WPF with the Chinook database. Part 2 – The client

Now we have the data fetching work sorted out we can turn our attentions to the front-end.

I am going to make a very simple view in this post listing the artists and associated albums with their tracks.  It is going to look like this:

image

Now, this is very basic but it serves its purpose to show the tools we are going to be using – a later post will use the power of WPF to style and transform the screen into something more palatable.

The first thing to do is add a WPF project(I have named mine MusicApp.WPF.Client).  If you rename the the MainWindow.xaml file then don’t forget to change the startup uri in app.xaml

<Application x:Class="MusicApp.WPF.Client.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="Main.xaml">

In terms of the structure for the project I have separate folders for views, view models and common files.

The View

Once the basic structure is in place we can add our view to the Views folder.  The view in this case is a user control that we will add to the main window.

The view itself is very simplistic – 1 grid containing 3 ListBoxes and is created as follows

<UserControl x:Class="MusicApp.WPF.Client.Views.ArtistList"
              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              Height="Auto" Width="Auto">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="0.5*" />
            <ColumnDefinition Width="0.5*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="0.7*"/>
            <RowDefinition Height="0.3*"/>
        </Grid.RowDefinitions>
        <ListBox ItemsSource="{Binding Path=Artists}" Margin="5" 
                 DisplayMemberPath="Name" Grid.Column="0" 
                 Grid.ColumnSpan="2" Grid.Row="0" 
                 SelectedItem="{Binding SelectedArtist}" />
        
        <ListBox ItemsSource="{Binding Path=SelectedArtist.Albums}" Margin="5" 
                 DisplayMemberPath="Title" Grid.Column="0" Grid.Row="1" 
                 SelectedItem="{Binding SelectedAlbum}" />
        
        <ListBox ItemsSource="{Binding Path=SelectedAlbum.Tracks}" Margin="5" 
                 DisplayMemberPath="Name" Grid.Column="1" Grid.Row="1" />
    </Grid>
</UserControl>

This is a view with a 2 x 2 grid with columns of equal width (1/2 of the grid each) and one row at 70% width and one at 30%.  Inside the grid there is a ListBox that takes up the whole top row (ColumnSpan=2) and one ListBox in each of the columns in the lower row.

I have included all of the bindings so that I know what is required on the view model when I write it.  I will need a collection property called ‘Artists’ and 2 properties for the selected artist and selected album.

The DisplayMemberPath details which property of the object will be displayed as text in the ListBoxItem.

Once the control has been written then we can add it to the main window xaml.  There are 2 things to do to add a usercontrol to another xaml file

  1. add the correct namespace.
  2. add the control

Adding the namespace requires that we add an xmlns to the top of the main window file

xmlns:views="clr-namespace:MusicApp.WPF.Client.Views"

Once we have this then we can simply add the control as a new object on the window

<views:ArtistList/>

which gives us a Main window with the following xaml

<Window x:Class="MusicApp.WPF.Client.Main"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:views="clr-namespace:MusicApp.WPF.Client.Views"
        Title="Main" Height="475" Width="425">
    <Grid>
        <views:ArtistList/>
    </Grid>
</Window>

The ViewModel

Now the view is done I can create a ViewModel to support it.  The ViewModel will need to implement INotifyPropertyChanged and I have the following snippet method that will fire the event implemented by it

private void Notify(string propertyName)
{
    if (PropertyChanged != null)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

Unity

I want to be able to use a different data provider here if I want to so I am going to introduce the Unity IoC container to the application at this point.  Unity can be installed into the current project directly from NuGet (install-package unity) or you can download directly. It needs a little configuration before we can use it.

I am going to use it to resolve a reference to the IDataProvider interface we created previously so I add a new app.config file to the client project and add the following configuration

<configuration>
    <configSections>
        <section name="unity"
                             type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration" />
    </configSections>
    <unity>
        <containers>
            <container>
                <type type="MusicApp.Model.IDataProvider, MusicApp.Model" mapTo="MusicApp.Model.EFCodeFirstDataProvider, MusicApp.Model" />
            </container>
        </containers>
    </unity>
</configuration>

This is simply adding a mapping between IDataProvider and EFCodeFirstDataProvider which allows me to resolve the former to the latter.

We need a way of referencing this container in our application so I use a factory class with a static member to hold the actual UnityContainer

class ContainerFactory
{
    private static UnityContainer container;

    public ContainerFactory()
    {
        if (container == null)
        {
            container = new UnityContainer();
            UnityConfigurationSection section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
            section.Containers.Default.Configure(container);
        }
    }

    public UnityContainer Container
    {
        get
        {
            return container;
        }
    }
}

I can then have a reference to an IDataProvider in the ViewModel and resolve it from the constructor as follows

ContainerFactory factory = new ContainerFactory();
provider = factory.Container.Resolve<IDataProvider>();

The whole ViewModel including properties now looks like this.

public class ArtistListViewModel : INotifyPropertyChanged
{
    private IDataProvider provider;
    private ObservableCollection<Artist> artists;
    private Artist selectedArtist;
    private Album selectedAlbum;

    public ArtistListViewModel()
    {
        ContainerFactory factory = new ContainerFactory();
        provider = factory.Container.Resolve<IDataProvider>();
        GetArtists();
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public ObservableCollection<Artist> Artists
    {
        get
        {
            return artists;
        }
        private set
        {
            artists = value;
            Notify("Artists");
        }
    }

    public Artist SelectedArtist
    {
        get
        {
            return selectedArtist;
        }
        set
        {
            selectedArtist = value;
            Notify("SelectedArtist");
        }
    }

    public Album SelectedAlbum
    {
        get
        {
            return selectedAlbum;
        }
        set
        {
            selectedAlbum = value;
            Notify("SelectedAlbum");
        }
    }

    private void GetArtists()
    {
        Artists = new ObservableCollection<Artist>(provider.GetArtists());
    }

    private void Notify(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Wire it up

Now we have the ViewModel and view in place we need to wire them together this is just a case of setting the views DataContext from its constructor

public ArtistList()
{
    InitializeComponent();
    this.DataContext = new ArtistListViewModel();
}

Wrapping up

Now we have these wired we simply need to add the ConnectionStrings to the config file (that we added the unity config to earlier), change the startup project to the new WPF app and run it.  This will now display the window shown at the start and I can change the artist to update the albums/tracks at the bottom.

Next time – making the view look good.