Silverlight: Байндинг коллекции элементов на TabControl.ItemsSource

    • Silverlight
    • XAML
    • Binding
    • TabControl
    • Converter
  • modified:
  • reading: 4 minutes

Раньше я как-то обходился без подобного в Silverlight. Всегда размещал TabItem в XAML коде, а не байндил коллекцию объектов, и при помощи DataTemplate настраивал вид того, что находится в TabItem.Content. Просто не было необходимости байндить коллекцию моих объектов (неких BindingModel) на TabControl.ItemsSource, а тут, буквально недавно, захотелось немного отрефакторить код, так как коллекция табов все росла, и управлять ею уже было сложно, и как раз придумал как это возможно сделать через описанный выше способ. Сказано – сделано. Потратил пару часов, переписал код, запускаю, и обнаруживаю такой вот exception:

System.ArgumentException: Unable to cast object of type 'SilverlightTabControl.Foo' to type 'System.Windows.Controls.TabItem'.

Быстро гуглю, нахожу на форумах Silverlight тему Databinding a TabControl (Я не одинок! Как показало более глубокое угугление, я совсем не одинок), а там

This is because currently TabControl doesn't override PrepareContainerForItemOverride, so it won't automatically wrap your data source in TabItems.

Ну и в качестве решения предлагается написать свой TabConverter. Microsoft, ну я точно помню, что в WPF байндинг на ItemsSource у TabControl’а работает прекрасно. Я это делал. Ладно, терпим, что в Silverlight контролах достаточно много багов, но тут-то просто ребята немного не доделали, а контрол зарелизили, да и сколько версий он уже живет? В реальности на первый взгляд нужно сделать 2 вещи:

  1. Переопределить метод ItemsControl.GetContainerForItemOverride, чтобы он возвращал TabItem.
  2. Переопределить метод ItemsControl.PrepareContainerForItemOverride, чтобы он тому созданному контейнеру из шага 1 выставлял нужный Header из DisplayMemberPath (там простая строка, путь до свойства), а так же выставил в Content элемент полученный из DataTemplate, указанный в ItemTemplate, а если не указан, то просто выставить туда элемент вашей байндинг модели.

И это все. И я даже подумал, что напишу сейчас TabControlEx, который бы наследовался от TabControl и выполнил два этих действия. Но ребята из Microsoft написали обработчик на изменение ItemsSource, который и ломает все мечты. К сожалению, код посмотреть не удалось, .NET Reflector почему-то не может дизасемблировать код TabControl.

В общем, ничего не оставалось, и я тоже написал конвертер из коллекции объектов в коллекцию TabItem.

/// <summary>
/// Convert collection ob objects to List of <see cref="TabItem"/>. 
///  </summary>
public class CollectionToTabItemsConverter : IValueConverter
{    /// <summary>
    /// Set <see cref="ControlTemplate"/> object to parameter
    /// to change view of <see cref="TabItem"/>'s <see cref="TabItem.Content"/>
    /// </summary>
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {        IEnumerable source = value as IEnumerable;
        if (source != null)
        {            var controlTemplate = parameter as ControlTemplate;
             List<TabItem> result = new List<TabItem>();
             foreach (object item in source)
            {
                PropertyInfo[] propertyInfos = item.GetType().GetProperties();
                 // Reflection Magic: trying to get possible header properties
                PropertyInfo propertyInfo = propertyInfos.First(x => x.Name == "Header" || x.Name == "Name");
                 string headerText = null;
                if (propertyInfo != null)
                {                    object propValue = propertyInfo.GetValue(item, null);
                    headerText = (propValue ?? string.Empty).ToString();
                }
                 var tabItem = new TabItem
                {
                    DataContext = item,
                    Header = headerText,                    Content = controlTemplate == null ? item : new ContentControl { Template = controlTemplate }
                };
 
                result.Add(tabItem);
            }
             return result;
        }        return null;
    }
     /// <summary>
    /// ConvertBack method is not supported
    /// </summary>
    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {        throw new NotSupportedException("ConvertBack method is not supported");
    }
}

Только в своем конвертере я захотел сделать так, чтобы мог его использовать по всему проекту, а не писать отдельно конвертер на каждый TabControl, потому я сделал его, на сколько это возможно, универсальным. Не обошлось и без магии, вид контента я еще могу передать через parameter, а вот что подставить в Header пришлось брать таким вот магическим образом: смотрю, есть ли у объекта свойства Header или Name (обычно они применяются). Есть и другой способ, можно просто переопределить ToString и просто подставлять сам объект в Header.

Ну и пример, как использую. Тестовая ViewModel:

public class Foo
{    public string Header { get; set; }
     public string SomeContent { get; set; }
}
 public class ViewModel
{    public ViewModel()
    {        Collection = new ObservableCollection<Foo>
                         {                             new Foo {Header = "Foo 1", SomeContent = "Some Content 1"},
                             new Foo {Header = "Foo 2", SomeContent = "Some Content 2"},
                             new Foo {Header = "Foo 3", SomeContent = "Some Content 3"}
                         };
    }
     public ObservableCollection<Foo> Collection { get; set; }
}

И разметка:

<UserControl x:Class="SilverlightTabControl.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:SilverlightTabControl="clr-namespace:SilverlightTabControl" 
    xmlns:Controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls" >
    <UserControl.Resources>
        <SilverlightTabControl:CollectionToTabItemsConverter x:Key="CollectionToTabItemsConverter" />
         <ControlTemplate x:Key="MyTabItemContentTemplate">
            <StackPanel>
                <TextBlock Text="{Binding Path=SomeContent}" />
            </StackPanel>
        </ControlTemplate>
    </UserControl.Resources>
    <UserControl.DataContext>
        <SilverlightTabControl:ViewModel />
    </UserControl.DataContext>
    <Grid x:Name="LayoutRoot" Background="White">
         <Controls:TabControl Grid.Row="1" 
                             ItemsSource="{Binding Path=Collection, Converter={StaticResource CollectionToTabItemsConverter}, 
                                                            ConverterParameter={StaticResource MyTabItemContentTemplate}}" />
    </Grid>
</UserControl>

Достаточно все просто. Устанавливаю байндинг коллекции на ItemsSource, устанавливаю Converter, а так же выставляю в параметр необходимый мне ControlTemplate. Исходный код можно взять с assembla.

Еще у меня была идея передать в Converter сам элемент TabControl, из него выдрать ItemTemplate и DisplayMemberPath, но вот такой вот байндинг у меня не заработал:

{Binding Path=Collection, Converter={StaticResource CollectionToTabItemsConverter}, ConverterParameter={RelativeSource Self}}

Может знает кто-нибудь еще способ попроще и получше? Можно, конечно же, еще сделать все-таки свой TabControlEx со своей коллекцией MyItemsSource, в которую устанавливать коллекцию объектов, и на ее основе выставлять в TabControl.Items элементы TabItem, но не вижу плюсов по сравнению с конвертером.

See Also