Thank you @AnthonyWJones, your answer was (almost) exactly what I was looking for. I've decided to provide my own answer here so that other readers know how I've adapted his answer to my needs.
First, as suggested, I'm deriving from ItemsControl, and providing a second "Template" property, called HeaderTemplate
:
#region HeaderTemplate PROPERTY
public static readonly DependencyProperty HeaderTemplateProperty = DependencyProperty.Register(
"HeaderTemplate",
typeof( DataTemplate ),
typeof( ItemsControlWithHeaders ),
new PropertyMetadata( null, new PropertyChangedCallback( OnHeaderTemplateChanged ) ) );
public DataTemplate HeaderTemplate
{
get { return ( DataTemplate )this.GetValue( HeaderTemplateProperty ); }
set { this.SetValue( HeaderTemplateProperty, value ); }
}
private static void OnHeaderTemplateChanged( DependencyObject obj, DependencyPropertyChangedEventArgs args )
{
ItemsControlWithHeaders control = obj as ItemsControlWithHeaders;
control.InvalidateArrange();
}
#endregion
Second, I'm overriding PrepareContainerForItemOverride
to provide my own template selection logic. What I'm doing is simply redirecting any "string" item to the HeaderTemplate
, and other items to the usual ItemTemplate
:
protected override void PrepareContainerForItemOverride( DependencyObject element, object item )
{
base.PrepareContainerForItemOverride( element, item );
ContentPresenter presenter = element as ContentPresenter;
if( presenter != null )
{
if( item is string )
{
presenter.ContentTemplate = this.HeaderTemplate;
}
else
{
presenter.ContentTemplate = this.ItemTemplate;
}
}
}
This control can now be used like this:
<local:ItemsControlWithHeaders Grid.Row="1" ItemsSource="{Binding GroupedCities}" ScrollViewer.VerticalScrollBarVisibility="Auto">
<local:ItemsControlWithHeaders.Template>
<ControlTemplate TargetType="local:ItemsControlWithHeaders">
<ScrollViewer>
<ItemsPresenter />
</ScrollViewer>
</ControlTemplate>
</local:ItemsControlWithHeaders.Template>
<local:ItemsControlWithHeaders.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</local:ItemsControlWithHeaders.ItemsPanel>
<local:ItemsControlWithHeaders.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" Style="{StaticResource PhoneTextSubtleStyle}" Foreground="{StaticResource PhoneAccentBrush}" Margin="12,12,12,8" />
</DataTemplate>
</local:ItemsControlWithHeaders.HeaderTemplate>
<local:ItemsControlWithHeaders.ItemTemplate>
<DataTemplate>
<Button Style="{StaticResource EmptyNonSelectButtonStyle}" BorderThickness="0" HorizontalContentAlignment="Left" Click="AnyCityButton_Click">
<TextBlock Text="{Binding Name, Mode=OneWay}" FontFamily="{StaticResource PhoneFontFamilyNormal}" FontSize="{StaticResource PhoneFontSizeLarge}" />
</Button>
</DataTemplate>
</local:ItemsControlWithHeaders.ItemTemplate>
</local:ItemsControlWithHeaders>
To build the data source you must pass to this special hybrid control, LINQ is fine, but I've chosen a much more explicit solution, implemented in my view-model:
public IEnumerable<object> GroupedCities
{
get
{
yield return new CurrentLocationCityViewModel();
yield return Localized.TopTenCitiesHeader; // string resource
foreach( CityViewModel city in this.TopTenCities )
{
yield return city;
}
yield return Localized.TopHundredCitiesHeader; // string resource
foreach( CityViewModel city in this.TopHundredCities )
{
yield return city;
}
}
}
I now have a generic ItemsControlWithHeaders
I can reuse in more than just this scenario. Performance is great. The only problem remaining for a purist like me is that the base ItemsControl complains in DEBUG, since an "object" type does not have a "Name" property. It generates a System.Windows.Data Error: BindingExpression path error: 'Name' property not found
message in the debug output, which can be ignored.