Friday, August 7, 2009

Ah... Data Templates, How I Love Thee....


What? You don't like them? Oh, but you will.... you will....

WPF has a number of different Templates which can be applied to controls, and the DataTemplate is the one that's used for things like ListViews, ListBoxes and ComboBoxes.

These things have great and wonderful powers. I use them a lot--mainly when I'm trying to display complex data via a ListView--complex data such as a history of addresses for an entity. Consider, an address has 4-8 fields of data associated with it: 2 or 3 Street lines, a city, a region, a postal code and possibly country and county data. In addition to the data, I want to have command buttons that allow me to perform EDIT/DELETE operations on that line of data.

Now, in a traditional ListView, all I would have to do is bind to my Address class, and override the ToString function to return the data in a single line, and the listview will display it. For this particular example, I'll use the following as my Data Object:


public class AddressViewModel : INotifyPropertyChanged
{

private string _line1;
private string _line2;
private string _city;
private string _state;
private string _postal;

public string Line1
{
get { return _line1; }
set { _line1 = value; onPropertyChanged("Line1"); }
}
public string Line2
{
get { return _line2; }
set { _line2 = value; onPropertyChanged("Line2"); }
}
public string City
{
get { return _city; }
set { _city = value; onPropertyChanged("City"); }
}
public string State
{
get { return _state; }
set { _state = value; onPropertyChanged("State"); }
}
public string Postal
{
get { return _postal; }
set { _postal = value; onPropertyChanged("Postal"); }
}

public string AddressString
{
get {
string s = "";
s = s + _line1;
s = s + "\n";
s = s + _city + ", " + _state + " " + _postal;
return s;
}
}

public event PropertyChangedEventHandler PropertyChanged;
protected void onPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}


}


The problem there is that it doesn't provide the edit controls that I like having on such complex data types. Plus, there is confusion on what action should be performed when the user selects the item.

So, I build myself the DataTemplate. I define that I want all these elements in a specific order, binding as needed. Additionally, since I have the command buttons in the data template, it's DataContext is automatically an instance of my Data Object, so I have a handle to that specific Data Object via the SENDER parameter of my Click Event.

The DataTemplate XAML is here:

<DataTemplate x:Key="AddressRepeater">
<DockPanel HorizontalAlignment="Stretch" >
<StackPanel Height="4" DockPanel.Dock="Top" HorizontalAlignment="Stretch" >
<Separator BorderBrush="Black"></Separator>
</StackPanel>
<Border BorderBrush="Black" BorderThickness="1" CornerRadius="0.5" >
<StackPanel DockPanel.Dock="Left" HorizontalAlignment="Left"
VerticalAlignment="Top" Orientation="Vertical" >
<Button Name="cmdEdit" Style="{StaticResource ClearAutoSizeButton}"
Height="20" Width="20" MaxHeight="20" MaxWidth="20"
MinHeight="20" MinWidth="20" Click="cmdEdit_Click"
HorizontalAlignment="Center" VerticalAlignment="Top"
ToolTip="Edit This Address" Margin="5,10,5,10">
<Image Style="{DynamicResource EditImage}" />
</Button>
<Button Name="cmdDelete" Style="{StaticResource ClearAutoSizeButton}"
Height="20" Width="20" MaxHeight="20" MaxWidth="20"
MinHeight="20" MinWidth="20" Click="cmdDelete_Click"
HorizontalAlignment="Center" VerticalAlignment="Top"
ToolTip="Delete This Address" Margin="5,0,5,10">
<Image Style="{DynamicResource DeleteImage}" />
</Button>
</StackPanel>
</Border>
<TextBlock Text="{Binding AddressString}" TextWrapping="Wrap" Margin="10,5,0,0" />
</DockPanel>
</DataTemplate>


Of course, that alone will give you a very ragged edge, as the controls won't stretch across the window as expected. Each line will only take up as much room as it needs, no more, no less. Which does not give you a pretty UI. To make that DataTemplate stretch across the screen, you've got to add some information to the ListView (or it's style).

First, you need to provide the HorizontalContentAlignment value to read "Stretch" Then, you need to set the horizontalAlignment of the ItemContainerStyle to "Stretch" as well.

But it's still not quite right. If your data extends beyond the size of your window, your controls won't wrap as expected (say you're binding to a TextBlock and you expect all the text to be displayed in the control's viewable space). To fix this, you need to tell the HorizontalScrollbar to be Disabled (which in the Listview is accessed via t he ScrollViewer.HorizontalScrollBarVisibility property).

The final prettification that you'll need on the control is if you want alternating background colors. This takes two steps. The First is to add the AlternationCount to the ListView. The second is back to the ItemContainerStyle, and you will need to add a Trigger on the ItemsControl.AlternationIndex. When the Value is 0, give it one color, when it is 1, give it another. Note that you can have as many alternation colors as you'd like.

The XAML for the "prettification" is:

<ListView Name="lsvAddresses" MinHeight="250" MinWidth="250"
HorizontalContentAlignment="Stretch" AlternationCount="2"
ItemTemplate="{DynamicResource AddressRepeater}"
Style="{DynamicResource ListViewRepeaterStyle}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
SelectionChanged="lsvAddresses_SelectionChanged"
>
<ListView.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Style.Triggers>
<Trigger Property="ItemsControl.AlternationIndex" Value="0">
<Setter Property="Background" Value="#FFFFFC"></Setter>
</Trigger>
<Trigger Property="ItemsControl.AlternationIndex" Value="1">
<Setter Property="Background" Value="WhiteSmoke"></Setter>
</Trigger>
</Style.Triggers>
</Style>
</ListView.ItemContainerStyle>
</ListView>

So, in the end, once we put all these things together, we'll get ourselves a window with a ListView which displays data like this:


And of course, the Source can be downloaded here

No comments:

Blog Widget by LinkWithin