Wpf TabControl создает только один вид на всех вкладках

Свойство ItemsCource элемента TabControl, привязанное к коллекции в ViewModel. ContentTemplate - это ListView - UserControl. Все вкладки используют только один элемент управления ListView (конструктор ListView вызывается только один раз). Проблема заключается в том, что все вкладки имеют общее визуальное состояние - например, если вы измените размер какого-либо элемента на одной вкладке, это изменение будет на всех вкладках. Как создать отдельный ListView для каждой вкладки, но при этом использовать свойство ItemsSource?

<TabControl Grid.Row="1" Grid.Column="2" TabStripPlacement="Bottom" >    

    <TabControl.ContentTemplate>
        <DataTemplate DataType="viewModel:ListViewModel" >
            <view:ListView />
        </DataTemplate>
    </TabControl.ContentTemplate>

    <TabControl.ItemsSource>
        <Binding Path="Lists"/>
    </TabControl.ItemsSource>
</TabControl>

1 ответ

Там нет простого способа сделать это.

Проблема в том, что у вас есть шаблон WPF, который должен быть одним и тем же независимо от того, какие данные вы положили за ним. Таким образом, создается одна копия шаблона, и каждый раз, когда WPF встречает ListViewModel в вашем дереве пользовательского интерфейса он рисует его, используя этот шаблон. Свойства этого элемента управления, которые не связаны с DataContext, сохраняют свое состояние при изменении источника данных.

Вы могли бы использовать x:Shared="False" (пример здесь), однако это создает новую копию вашего шаблона каждый раз, когда WPF запрашивает его, в том числе при переключении вкладок.

Когда [x:Shared is] установлен в false, изменяет поведение поиска ресурса в Windows Presentation Foundation (WPF) так, что запросы на ресурс будут создавать новый экземпляр для каждого запроса, а не совместно использовать один и тот же экземпляр для всех запросов.

Что вам действительно нужно для TabControl.Items для каждого генерировать новую копию вашего элемента управления для каждого элемента, но это не происходит, когда вы используете ItemsSource собственность (это по замыслу).

Одна из возможных альтернатив, которая может сработать, заключается в создании пользовательского свойства DependencyProperty, которое связывается с вашей коллекцией элементов и генерирует TabItem а также UserControl объекты для каждого элемента в коллекции. Этот пользовательский DP также должен обрабатывать события изменения коллекции, чтобы обеспечить синхронизацию TabI tems с вашей коллекцией.

Вот тот, с которым я играл. Это работало для простых случаев, таких как привязка к ObservableCollection и добавление / удаление элементов.

public class TabControlHelpers
{
    // Custom DependencyProperty for a CachedItemsSource
    public static readonly DependencyProperty CachedItemsSourceProperty =
        DependencyProperty.RegisterAttached("CachedItemsSource", typeof(IList), typeof(TabControlHelpers), new PropertyMetadata(null, CachedItemsSource_Changed));

    // Get
    public static IList GetCachedItemsSource(DependencyObject obj)
    {
        if (obj == null)
            return null;

        return obj.GetValue(CachedItemsSourceProperty) as IList;
    }

    // Set
    public static void SetCachedItemsSource(DependencyObject obj, IEnumerable value)
    {
        if (obj != null)
            obj.SetValue(CachedItemsSourceProperty, value);
    }

    // Change Event
    public static void CachedItemsSource_Changed(
        DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        if (!(obj is TabControl))
            return;

        var changeAction = new NotifyCollectionChangedEventHandler(
            (o, args) =>
            {
                var tabControl = obj as TabControl;

                if (tabControl != null)
                    UpdateTabItems(tabControl);
            });


        // if the bound property is an ObservableCollection, attach change events
        INotifyCollectionChanged newValue = e.NewValue as INotifyCollectionChanged;
        INotifyCollectionChanged oldValue = e.OldValue as INotifyCollectionChanged;

        if (oldValue != null)
            newValue.CollectionChanged -= changeAction;

        if (newValue != null)
            newValue.CollectionChanged += changeAction;

        UpdateTabItems(obj as TabControl);
    }

    static void UpdateTabItems(TabControl tc)
    {
        if (tc == null)
            return;

        IList itemsSource = GetCachedItemsSource(tc);

        if (itemsSource == null || itemsSource.Count == null)
        {
            if (tc.Items.Count > 0)
                tc.Items.Clear();

            return;
        }

        // loop through items source and make sure datacontext is correct for each one
        for(int i = 0; i < itemsSource.Count; i++)
        {
            if (tc.Items.Count <= i)
            {
                TabItem t = new TabItem();
                t.DataContext = itemsSource[i];
                t.Content = new UserControl1(); // Should be Dynamic...
                tc.Items.Add(t);
                continue;
            }

            TabItem current = tc.Items[i] as TabItem;
            if (current == null)
                continue;

            if (current.DataContext == itemsSource[i])
                continue;

            current.DataContext = itemsSource[i];
        }

        // loop backwards and cleanup extra tabs
        for (int i = tc.Items.Count; i > itemsSource.Count; i--)
        {
            tc.Items.RemoveAt(i - 1);
        }
    }
}

Используется из XAML следующим образом:

<TabControl local:TabControlHelpers.CachedItemsSource="{Binding Values}">
    <TabControl.Resources>
        <Style TargetType="{x:Type TabItem}">
            <Setter Property="Header" Value="{Binding SomeString}" />
        </Style>
    </TabControl.Resources>
</TabControl>

Несколько вещей, на которые стоит обратить внимание:

  • TabItem.Header не установлен, поэтому вам нужно установить привязку для него в TabControl.Resources
  • Реализация DependencyProperty в настоящее время жестко кодирует создание нового UserControl. Возможно, вы захотите сделать это другим способом, например, попытаться использовать свойство шаблона или, возможно, другой DP, чтобы сообщить ему, что создать UserControl.
  • Возможно, потребуется больше тестирования... не уверен, есть ли какие-либо проблемы с утечками памяти из-за изменения обработчика и т. Д.

На основании ответа @Rachel я сделал несколько модификаций.

Прежде всего, теперь вы должны указать тип пользовательского элемента управления в качестве шаблона контента, который создается динамически.

Я также исправил ошибку в удалении обработчика collectionChanged.

Код следующий:

public static class TabControlExtension
{
    // Custom DependencyProperty for a CachedItemsSource
    public static readonly DependencyProperty CachedItemsSourceProperty =
        DependencyProperty.RegisterAttached("CachedItemsSource", typeof(IList), typeof(TabControlExtension), new PropertyMetadata(null, CachedItemsSource_Changed));

    // Custom DependencyProperty for a ItemsContentTemplate
    public static readonly DependencyProperty ItemsContentTemplateProperty =
        DependencyProperty.RegisterAttached("ItemsContentTemplate", typeof(Type), typeof(TabControlExtension), new PropertyMetadata(null, CachedItemsSource_Changed));

    // Get items
    public static IList GetCachedItemsSource(DependencyObject dependencyObject)
    {
        if (dependencyObject == null)
            return null;

        return dependencyObject.GetValue(CachedItemsSourceProperty) as IList;
    }

    // Set items
    public static void SetCachedItemsSource(DependencyObject dependencyObject, IEnumerable value)
    {
        if (dependencyObject != null)
            dependencyObject.SetValue(CachedItemsSourceProperty, value);
    }

    // Get ItemsContentTemplate
    public static Type GetItemsContentTemplate(DependencyObject dependencyObject)
    {
        if (dependencyObject == null)
            return null;

        return dependencyObject.GetValue(ItemsContentTemplateProperty) as Type;
    }

    // Set ItemsContentTemplate
    public static void SetItemsContentTemplate(DependencyObject dependencyObject, IEnumerable value)
    {
        if (dependencyObject != null)
            dependencyObject.SetValue(ItemsContentTemplateProperty, value);
    }

    // Change Event
    public static void CachedItemsSource_Changed(
        DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        if (!(dependencyObject is TabControl))
            return;

        var changeAction = new NotifyCollectionChangedEventHandler(
            (o, args) =>
            {

                if (dependencyObject is TabControl tabControl && GetItemsContentTemplate(tabControl) != null && GetCachedItemsSource(tabControl) != null)
                    UpdateTabItems(tabControl);
            });

        // if the bound property is an ObservableCollection, attach change events
        if (e.OldValue is INotifyCollectionChanged oldValue)
            oldValue.CollectionChanged -= changeAction;

        if (e.NewValue is INotifyCollectionChanged newValue)
            newValue.CollectionChanged += changeAction;

        if (GetItemsContentTemplate(dependencyObject) != null && GetCachedItemsSource(dependencyObject) != null)
            UpdateTabItems(dependencyObject as TabControl);
    }

    private static void UpdateTabItems(TabControl tabControl)
    {
        if (tabControl == null)
            return;

        IList itemsSource = GetCachedItemsSource(tabControl);

        if (itemsSource == null || itemsSource.Count == 0)
        {
            if (tabControl.Items.Count > 0)
                tabControl.Items.Clear();

            return;
        }

        // loop through items source and make sure datacontext is correct for each one
        for (int i = 0; i < itemsSource.Count; i++)
        {
            if (tabControl.Items.Count <= i)
            {
                TabItem tabItem = new TabItem
                {
                    DataContext = itemsSource[i],
                    Content = Activator.CreateInstance(GetItemsContentTemplate(tabControl))
                };
                tabControl.Items.Add(tabItem);
                continue;
            }

            TabItem current = tabControl.Items[i] as TabItem;
            if (!(tabControl.Items[i] is TabItem))
                continue;

            if (current.DataContext == itemsSource[i])
                continue;

            current.DataContext = itemsSource[i];
        }

        // loop backwards and cleanup extra tabs
        for (int i = tabControl.Items.Count; i > itemsSource.Count; i--)
        {
            tabControl.Items.RemoveAt(i - 1);
        }
    }
}

Этот используется следующим образом:

<TabControl main:TabControlExtension.CachedItemsSource="{Binding Channels}" main:TabControlExtension.ItemsContentTemplate="{x:Type YOURUSERCONTROLTYPE}">
    <TabControl.Resources>
        <Style BasedOn="{StaticResource {x:Type TabItem}}" TargetType="{x:Type TabItem}">
            <Setter Property="Header" Value="{Binding Name}" />
        </Style>
    </TabControl.Resources>
</TabControl>
Другие вопросы по тегам