imageEs ist nicht besonders selten, das wir Entwickler Listen mit Gruppierungen in irgendeiner Form darstellen müssen. Leider ist das auf dem Windows Phone nicht out-of-the-box möglich. Meine ersten Ansätze mit CollectionViewSource zu arbeiten wie von Bea Stollnitz beschrieben, schlugen fehl weil Silverlight auf dem Phone die GroupDescription nichtunterstützt. PagedCollectionViewSource geht auch nicht, so was tun?

Schlussendlich folgte die Lösung dem Motto ‘Back to the Basics’.

Zuerst brauchen wir eine gruppierte Liste als Basis. Hier verwende ich eine einfache ViewModelBase Klasse als Basisklasse, aber im Grunde ginge auch INotifyPropertyChanged (hab ich aber nicht getestet). Die Gruppierung erzeuge ich über LINQ on the fly aus einer flachen Liste:

       /// <summary>
        /// A collection for ItemViewModel objects.
        /// </summary>
        public ObservableCollection<ItemViewModel> Items { get; private set; }

       /// <summary>
        /// A collection for Category and ItemViewModel objects.
        /// </summary>
        public ObservableCollection<ViewModelBase> CategoriesAndItems { get; private set; }

        /// <summary>
        /// Creates and adds a few ItemViewModel objects into the Items collection.
        /// </summary>
        public void LoadData()
        {
            // Sample data; replace with real data
            this.Items.Add(new ItemViewModel() { 
                                  Id=Items.Count, 
                                  Group="Group 1", 
                                  LineOne = "runtime one",
                                  LineTwo = "Maecenas ... bibendum" });
                // ....
            this.Items.Add(new ItemViewModel() { 
                                 Id = Items.Count,
                                 Group = "Group 4", 
                                 LineOne = "runtime sixteen", 
                                 LineTwo = "Nascetur....t pulvinar" });

            var categories = from i in Items
                                     group i by i.Group into category
                                     select new CategoryViewModel(category.Key, category);
            foreach (var category in categories)
            {
               CategoriesAndItems.Add(category);
                foreach (var item in category.Items)
                {
                    CategoriesAndItems.Add(item);
                }
            }
            this.IsDataLoaded = true;

            NotifyPropertyChanged("ItemsView");
        }

Jetzt brauchen wir nur noch eine Listbox und ein Datatemplate pro Datatype. Dummerweise gibt es implizite Styles erst ab Silverlight 4, also helfen wir uns mit TemplateSelector … welchen es auf dem Phone nicht gibt. Also kramen wir noch mal unser Basiswissen raus. TemplateSelector ist ein Control welches Content mit passemdem Template darstellt. Also nur ein ContentControl mit zusätzlicher Logik zur Auswahl des Templates. ContentControl bietet eine Property ContentTemplate und eine Methode, die aufgerufen wird, wenn sich der Content geändert hat. Also führen wir eine kleine Konvention ein, dass das Datatemplate so heißen soll, wie der Type des Contents.

<Application.Resources>
        <DataTemplate x:Key="TestGroupedList.CategoryViewModel">
            <TextBlock Text="{Binding CategoryText}"
                       Style="{StaticResource PhoneTextGroupHeaderStyle}"
                       Margin="0,18,0,0"/>
        </DataTemplate>
        <DataTemplate x:Key="TestGroupedList.ItemViewModel">
            <StackPanel Margin="32,0,0,17"
                        Width="432">
                <TextBlock Text="{Binding LineOne}"
                           TextWrapping="Wrap"
                           Style="{StaticResource PhoneTextExtraLargeStyle}" />
                <TextBlock Text="{Binding LineTwo}"
                           TextWrapping="Wrap"
                           Margin="12,-6,12,0"
                           Style="{StaticResource PhoneTextSubtleStyle}" />
            </StackPanel>
        </DataTemplate>
    </Application.Resources>

Die Selector Klasse selbst ist dann sehr einfach:

public class DataTemplateSelector : ContentControl
{
    protected override void OnContentChanged(object oldContent, object newContent)
    {
        if (newContent == null)
       {
           ContentTemplate = null;
        } else {
            var type = newContent.GetType();
            if (Application.Current.Resources.Contains(type.FullName))
            {
                ContentTemplate = App.Current.Resources[type.FullName] as DataTemplate;
            }
         }
    }
}

Unser XAML für die Listbox sieht so aus:

<ListBox x:Name="MainListBox"
		 Margin="0,0,-12,0"
	                 ItemsSource="{Binding CategoriesAndItems}"
		SelectionChanged="MainListBox_SelectionChanged">
	<ListBox.ItemTemplate>
		<DataTemplate>
			<TestGroupedList:DataTemplateSelector Content="{Binding}" />
		</DataTemplate>
	</ListBox.ItemTemplate>
</ListBox>

In diesem Fall möchte ich nicht das ein Klick auf einen Gruppenheader irgendwas bewirkt, also navigiere ich nur wenn das ausgewählte Item ein ItemViewModel ist. Je nach Anwendungsfall kann ich hier natürlich auch zu Gruppeneinstellungen navigieren o.ä.

 var item = MainListBox.SelectedItem as ItemViewModel;
 if (item != null)
 {
  // Navigate to the new page
  NavigationService.Navigate(new Uri("/DetailsPage.xaml?Id=" + item.Id,
                                                   UriKind.Relative));

   // Reset selected index to -1 (no selection)
    MainListBox.SelectedIndex = -1;
}

Der größte Nachteil dieses Ansatzes soll nicht verschwiegen werden. Da die Auflösung des Templates erst zur Laufzeit stattfindet, kann weder Visual Studio noch Expression Blend eine Vorschau zeigen.

In diesem Fall hatte ich mich entschieden, das die Gruppierung rein für die Darstellung dient, die Logik sollte davon weitgehend unbeeinflusst bleiben. Alternativ hätte ich eine reine Kategorieliste nehmen können und die Items  über HeaderedItemsControl darzustellen. In dem Fall sind Features wie auf- und zuklappen usw. viel einfacher zu implementieren.

Beispielprojekt

Advertisements