Wpf TabControl在所有标签页上仅创建一个视图

4
TabControl的ItemsSource属性绑定到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>

@mm8 我认为那是一个错误的重复链接。这个问题是关于为每个选项卡创建单独的ContentTemplate来显示,而不是默认创建一个项目并仅在它们后面交换DataContext。我认为可能有一个模板属性来强制执行此操作,但我不确定。就我个人而言,我会小心这样的设计...即使它不可见,您也将创建/存储多个控件副本。如果大小很重要,您可以在您的DataContext上创建一个属性,并绑定它,以便它随着每个选项卡更改而更改。 - Rachel
我想到的属性是 x:Shared="False"(示例在这里)。但这并不是理想的解决方案,因为每次选择选项卡时都会创建一个UserControl的新副本,所以像大小更改之类的东西不会被保留。如果您正在使用模板构建TabControl项目,则建议仅存储/绑定所有关心的属性,以便当用户切换选项卡时,使用相同的模板但DataContext不同,因此所有绑定都将更新。 - Rachel
也许可以研究一下自定义的TabControl DependencyProperty,为每个项目构建.TabItems,而不是使用ItemsSource? - Rachel
我使用的解决方法是将可视属性(例如您示例中的列宽度)绑定到ViewModel中的属性。然后,它们共享一个模板就没关系了。 - Robin Bennett
2个回答

4
没有简单的方法来完成这个任务。
问题在于您有一个 WPF 模板,无论放置什么数据,它都应该是相同的。因此,创建了模板的一个副本,并且每当 WPF 在您的 UI 树中遇到一个 ListViewModel 时,它就会使用该模板进行绘制。未绑定到 DataContext 的控件属性将保留其在更改数据源之间的状态。
您可以使用 x:Shared="False"(示例 here),但这会在每次 WPF 请求它时创建模板的新副本,包括切换选项卡时。
当 [x:Shared] 设置为 false 时,修改 Windows Presentation Foundation (WPF) 资源检索行为,以便请求资源将为每个请求创建一个新实例,而不是为所有请求共享相同的实例。
您真正需要的是 TabControl.Items 为每个项目生成您控件的新副本,但是当您使用 ItemsSource 属性时,这不会发生(这是设计如此)。
一种可能的替代方案是创建一个自定义DependencyProperty,绑定到您的项目集合,并为集合中的每个项目生成TabItem和UserControl对象。这个自定义DP还需要处理集合变化事件,以确保TabItems与您的集合保持同步。
这是我正在尝试的一个方案。它适用于简单情况,比如绑定到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,谢谢你的答案,你救了我的一天。我在下面完成了你的答案,如果需要请随时纠正我。干杯 - Maël Pedretti

1

根据@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>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接