views:

67

answers:

3

I need a single scrollable surface that contains two bound lists. At first, I used a ScrollViewer with two ListBox inside, each having their scrolling disabled, so I could still have item selection. Seeing the poor loading time performance, I changed my ListBoxes to ItemsControl, but the performance is still terrible. In total, my two lists have only 110 items.

<ScrollViewer Grid.Row="1">
    <StackPanel>
        <Button Style="{StaticResource EmptyNonSelectButtonStyle}" BorderThickness="0" HorizontalContentAlignment="Left" Click="AnyCityButton_Click">
            <TextBlock Text="{Binding Resources.CurrentLocationItem, Source={StaticResource LocalizedResources}}" FontFamily="{StaticResource PhoneFontFamilyNormal}" FontSize="{StaticResource PhoneFontSizeLarge}" />
        </Button>
        <TextBlock Text="{Binding Resources.TopTenCitiesHeader, Source={StaticResource LocalizedResources}}" Style="{StaticResource PhoneTextSubtleStyle}" Margin="12,12,12,8" />
        <ItemsControl ItemsSource="{Binding TopTenCities}" ItemTemplate="{StaticResource CityDataTemplate}" HorizontalContentAlignment="Stretch" />
        <TextBlock Text="{Binding Resources.TopHundredCitiesHeader, Source={StaticResource LocalizedResources}}" Style="{StaticResource PhoneTextSubtleStyle}" Margin="12,12,12,8" />
        <ItemsControl ItemsSource="{Binding TopHundredCities}" ItemTemplate="{StaticResource CityDataTemplate}" HorizontalContentAlignment="Stretch" />
    </StackPanel>
</ScrollViewer>

What can I do to improve performance? I've tried setting the ItemsSource after the page loading, but it still ugly (empty lists for a few seconds), doesn't make more sense.

Thank you.

A: 

Could you use one list/itemscontrol, but different datatemplates to get the same effect?

Or you could use a pivot control instead, putting the top 10 sitties in one pivot, top 100 in another pivot..

John Gardner
I'd prefer having a databound solution, and I have text fields in between the sections (like grouping). We did evaluate a pivot, but it's for a popup window and feel it would be overdesigning the view just to circumvent a perf. problem.
Martin Plante
ah, in a popup it wouldn't make sense. But you could still use a list with different data templates, couldn't you? a grouped collectionviewsource would work great, but it isn't in silverlight yet, is it? What is the "first-party" solution they use to do grouping in stuff like the contacts hub? they have the letters of the alphabet styled differently in a huge list to break up contacts by letter...
John Gardner
+1  A: 

This answer has turned into a monster but slog through it and I think you'll find an answer.

We need in some way to use the VirtualizingStackPanel as ListBox. We need to collect all the items to display (the button, the two textblocks and two sets of city data) into a single enumerable of some type. The the real trick and would be to determine one of three templates to use to render the items.

Bottom line is we need to create a new type of ItemsControl. Now we can gain a little advantage by simply accepting we want to create a very specific ItemsControl that supports only this task. First here is a "starter for 10" (a UK media reference).

A really dumb example of creating a specific items control:-

public class SwitchingItemsControl : ItemsControl
{
    public DataTemplate AlternativeItemTemplate { get; set; }

    protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
    {
        ContentPresenter cp = (ContentPresenter)element;
        if (AlternativeItemTemplate != null && (((int)item) & 1) == 1)
            cp.ContentTemplate = AlternativeItemTemplate;
        else
            cp.ContentTemplate = ItemTemplate;

        cp.Content = item;
    }
}

This control assumes its items are a set of integers. It has an AlternativeItemTemplate which if supplied it toggles between on an odd/even basis (note that is a facet of the item).

Now lets put that use with a VirtualizingStackPanel:-

<UserControl x:Class="CustomVirtualizingPanelInSL.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:local="clr-namespace:SilverlightApplication1"
    Width="400" Height="300">
    <Grid x:Name="LayoutRoot" Background="White">
        <local:SwitchingItemsControl  x:Name="itemsControl" >
            <local:SwitchingItemsControl.Template>
                <ControlTemplate TargetType="local:SwitchingItemsControl">
                    <ScrollViewer VerticalScrollBarVisibility="Visible">
                        <ItemsPresenter />
                    </ScrollViewer>
                </ControlTemplate>
            </local:SwitchingItemsControl.Template>
            <local:SwitchingItemsControl.ItemTemplate>
                <DataTemplate>
                    <Border CornerRadius="2" BorderBrush="Blue" BorderThickness="1" Margin="2">
                        <TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Text="{Binding}" />
                    </Border>
                </DataTemplate>
            </local:SwitchingItemsControl.ItemTemplate>
            <local:SwitchingItemsControl.AlternativeItemTemplate>
                <DataTemplate>
                    <Border CornerRadius="2" BorderBrush="Red" BorderThickness="1" Margin="2">
                        <TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Text="{Binding}" />
                    </Border>
                </DataTemplate>
            </local:SwitchingItemsControl.AlternativeItemTemplate>
            <local:SwitchingItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <VirtualizingStackPanel />
                </ItemsPanelTemplate>
            </local:SwitchingItemsControl.ItemsPanel>
        </local:SwitchingItemsControl>
    </Grid>
</UserControl>

Note the ItemsPanel is using the VirtualizingStackPanel and that gets presented in a ScrollViewer.

Now we can give it lot of content:-

namespace SilverlightApplication1
{
    public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();
            itemsControl.ItemsSource = Enumerable.Range(0, 10000);
        }
    }

}

If you switch to a standard StackPanel this takes ages to load, whereas it appears instant with virtualizing.

Armed with this info you should be able to create a special ItemsControl which has the properties:-

  • ButtonTemplate (DataTemplate)
  • HeaderTemplate (DataTemplate)
  • TopTenHeaderText (String)
  • TopHundredHeaderText (String)
  • TopTenSource (IEnumerable<City>)
  • TipHunderedSource (IEnumerable<City>)

Now you can create a single enumerable with some Linq extension methods:-

itemsControl.ItemsSource =  Enumerable.Repeat((object)null, 1)
   .Concat(Enumerable.Repeat((object)TopTenHeadeText))
   .Concat(TopTenSource.Cast<object>())
   .Concat(Enumerable.Repeat((object)TopHundredText))
   .Concat(TopHundredSource.Cast<object>())

Now you just need to override PrepareContainerForItemOverride and choose between ButtonTemplate (for the first null item), the HeaderTemplate for item of type string or the ItemTemplate for an item of type City.

AnthonyWJones
Great solution. I guessed there was some way to do this as a single list with different templates. Thanks for showing how.
Matt Lacey
Thank you very much. It did help me a lot in making my own hybrid ListBox control. Thanks!
Martin Plante
A: 

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.

Martin Plante