Sunday, February 20, 2011

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

Раньше я как-то обходился без подобного в 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();
               }

Read more: outcoldman