Building a Multi-Column ListBox in Avalon

The really cool thing about Avalon is the integration.  Yeah, everybody has their favorite features but being able to take all the pieces and build something that's even better, even cooler is where it's at.  I've been lucky to have worked on a variety of different features in Avalon, so, I like to put some of those features together and see what can be done.

In the May CTP, you'll find a variety of controls but you won't find a multi-column ListBox.  Turns out that this isn't too hard to build with the pieces that *are* there, though.  So, this post is about building the multi-column ListBox.

Basic Requirements

Lets start with the basic requirements; what does a multi-column ListBox need?

  • Selection
  • Column headers & columns that size to the largest item in that column
  • Source data providing data for each row and column
  • Vertical "stacking" of items for layout
  • Horizontal scrolling of items including the headers
  • Vertical scrolling of items *excluding* the headers

The Tools

For selection, I'll just use a ListBox; that's its whole purpose in life.  For the column headers and sizing to the largest item in the column I'm going to use something called Grid SizeSharing.  My data will come from an XmlDataSource that I'll populate for this example.  I'll use a StackPanel for the vertical stacking.  Finally, I'll use a couple ScrollViewers to get the right scrolling behavior.

A Quick Peak Ahead

You can find the markup for this example next but I wanted to give a sneak peak.  Here's a screenshot.  Notice column sizes and the header that isn't scrolling.

The Finished Xaml

I'm just going to jump straight to the xaml because I've commented it heavily and should be self-explanatory.  Of course, you're free to ask me questions about it.  (note: there's at least one bug in the SharedSize feature in the May CTP that affects this demo; I know it's fixed internally but I'll see if I can't find a work around.)

<Border xmlns="https://schemas.microsoft.com/winfx/avalon/2005" xmlns:x="https://schemas.microsoft.com/winfx/xaml/2005">

<!--
//
//Root layout for this sample is a Grid
//
-->
<Grid >
<Grid.Resources>

   <!--
//
// I create an XmlDataSource containing all of the vital
// info about the Avalon bloggers including the BlogSite,
// their name, an artificial "OnlineStatus" property and
// the Url to their blog. This is a good basic set of data
// to show this example and will be the basis for each column.
//
-->

   <XmlDataSource x:Key="BlogData" XPath="Blogs/Blog">
<Blogs xmlns="">
<Blog>
<BlogSite>simplegeek.com</BlogSite>
<Blogger OnlineStatus="Offline">Chris Anderson</Blogger>
<Url>https://simplegeek.com</Url>
</Blog>
<Blog>
<BlogSite>fortes.com</BlogSite>
<Blogger OnlineStatus="Offline">Fil Fortes</Blogger>
<Url>https://fortes.com/work</Url>
</Blog>
<Blog>
<BlogSite>Longhorn Blogs</BlogSite>
<Blogger OnlineStatus="Online">Rob Relyea</Blogger>
<Url>https://www.longhornblogs.com/rrelyea/</Url>
</Blog>
<Blog>
<BlogSite>designerslove.net</BlogSite>
<Blogger OnlineStatus="Online">Nathan Dunlap</Blogger>
<Url>https://designerslove.net/</Url>
</Blog>
<Blog>
<BlogSite>blogs.msdn.com</BlogSite>
<Blogger OnlineStatus="Online">Karsten Januszewski</Blogger>
<Url>https://blogs.msdn.com/karstenj</Url>
</Blog>
<Blog>
<BlogSite>weblogs.asp.net</BlogSite>
<Blogger OnlineStatus="Online">Greg Schecter</Blogger>
<Url>https://weblogs.asp.net/greg_schechter</Url>
</Blog>
<Blog>
<BlogSite>blogs.msdn.com</BlogSite>
<Blogger OnlineStatus="Online">Tim Sneath</Blogger>
<Url>https://blogs.msdn.com/tims/</Url>
</Blog>

     <Blog>
<BlogSite>weblogs.asp.net</BlogSite>
<Blogger OnlineStatus="Offline">Marcelo Lopez-Ruiz </Blogger>
<Url>https://weblogs.asp.net/marcelolr/</Url>
</Blog>
<Blog>
<BlogSite>blogs.msdn.com</BlogSite>
<Blogger OnlineStatus="Online">Kevin Moore </Blogger>
<Url>https://blogs.msdn.com/okoboji/default.aspx</Url>
</Blog>
<Blog>
<BlogSite>laurenlavoie.com</BlogSite>
<Blogger OnlineStatus="Offline">Lauren Lavoie </Blogger>
<Url>https://laurenlavoie.com/</Url>
</Blog>
</Blogs>
</XmlDataSource>

   <!--
//
// The BlogDataTemplate is the visual representation of each
// Blog entry and associated data. Notable here is that I use
// Grid SizeSharing which allows layout sizes to be shared across
// named groups. In particular, I have created four columns. Of the
// four columns, three share sizes and the fourth takes the remaining space.
// Note the SharedSizeGroup property on the ColumnDefinitions. This
// will be how the headers and items share their width. Also, notice
// that all of the bindings to particular fields in the Blogs data
// are specified.
//
-->

   <DataTemplate x:Key="BlogDataTemplate">
<Grid TextBlock.FontSize="20">
<ColumnDefinition Width="Auto" SharedSizeGroup="BloggerColumn"/>
<ColumnDefinition Width="Auto" SharedSizeGroup="BlogSiteColumn"/>
<ColumnDefinition Width="Auto" SharedSizeGroup="OnlineStatusColumn"/>
<ColumnDefinition Width="*" SharedSizeGroup="BlogUrlColumn"/>

     <TextBlock Grid.Column="0" Margin="10,0,10,0" TextContent="{Binding XPath=Blogger}"/>
<TextBlock Grid.Column="1" Margin="10,0,10,0" TextContent="{Binding XPath=BlogSite}"/>
<TextBlock Grid.Column="2" Margin="10,0,10,0" TextContent="{Binding XPath=Blogger/@OnlineStatus}"/>

     <TextBlock Grid.Column="3" Margin="10,0,10,0">
<Hyperlink NavigateUri="{Binding XPath=Url}">
<TextBlock TextContent="{Binding XPath=Url}"/>
</Hyperlink>
</TextBlock>
</Grid>
</DataTemplate>
 

   <!--
//
// The following template is for the individual headers which are
// actually Buttons. These are Buttons in the event
// that one would want to allow column sorting by clicking
// on the headers.
//
-->

   <ControlTemplate x:Key="Header" TargetType="{x:Type Button}">
<Border Background="HorizontalGradient navy silver" TextBlock.Foreground="white" TextBlock.FontSize="20" Padding="10,3,10,4">
<ContentPresenter/>
</Border>
</ControlTemplate>
    

   <!--
//
// Here's where a lot of the magic happens. This is a template defined
// for a ListBox. It contains the headers and a content area in a Grid.
// The headers are in a ScrollViewer whose HorizontalOffset is bound to the
// HorizontalOffset of the content area. The content area, defined by
// setting the IsItemsHost property on the Panel that will contain the
// generated visuals for each datum, can be scrolled in both dimensions.
// Notice the headers are defined here, too, and have the same SharedSizeGroup
// names.
//
-->

   <Style x:Key="{x:Type ListBox}" TargetType="{x:Type ListBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid Grid.IsSharedSizeScope="true" >
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>

<Border Grid.Row="1">
<Border Margin="10" BorderThickness="1" BorderBrush="black">
<DockPanel>
<ScrollViewer HorizontalOffset="{Binding Path=HorizontalOffset, ElementName=Master}" HorizontalScrollBarVisibility="hidden" VerticalScrollBarVisibility="disabled" DockPanel.Dock="top">
<Grid DockPanel.Dock="top" Margin="0,0,18,0">
<ColumnDefinition Width="Auto" SharedSizeGroup="BloggerColumn"/>
<ColumnDefinition Width="Auto" SharedSizeGroup="BlogSiteColumn"/>
<ColumnDefinition Width="Auto" SharedSizeGroup="BloggerOnlineStatusColumn"/>
<ColumnDefinition Width="*" SharedSizeGroup="BlogUrlColumn"/>

<Button Grid.Column="0" Template="{StaticResource Header}"> Blogger </Button>
<Button Grid.Column="1" Template="{StaticResource Header}" > Blog Site </Button>
<Button Grid.Column="2" Template="{StaticResource Header}"> Status </Button>
<Button Grid.Column="3" Template="{StaticResource Header}" > Blog Url </Button>
</Grid>
</ScrollViewer>
<ScrollViewer HorizontalScrollBarVisibility="visible" Name="Master">
<StackPanel IsItemsHost="true"/>
</ScrollViewer>

          </DockPanel>
</Border>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

   </Grid.Resources>
  

  <!--
//
// Putting it all together now the data source and data template
// are set on a plain ListBox. The ListBox style itself is applied
// automatically because I defined the key for a type.
//
-->
 

   
<ListBox ItemsSource="{Binding Source={StaticResource BlogData}}" ItemTemplate="{StaticResource BlogDataTemplate}"/>
</Grid>
</Border>