【编辑】解决 Wpf TabControl 在所有选项卡上仅创建一个视图 的问题
原标题:Wpf TabControl create only one view at all tabs
一、问题
TabControl’s ItemsSource property binded to collection in the ViewModel. ContentTemplate is ListView – UserControl. All the tabs use only one ListView control (the constructor of ListView is called only once). The problem is that all tabs have a common visual state – for example, if you change the size of any item in the one tab, this change will be on all tabs. How to create a separate ListView for each tab, but at the same time use ItemsSource property?
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>
二、Rachel(初版)
There’s no easy way of doing this.
没有简单的方法可以做到这一点。
The problem is you have a WPF Template, which is meant to be the same regardless of what data you put behind it. So one copy of the template is created, and anytime WPF encounters a ListViewModel
in your UI tree it draws it using that template. Properties of that control which are not bound to the DataContext will retain their state between changing DataSources.
问题是你有一个 WPF 模板,无论你在它后面放什么数据,它都应该是相同的。因此,将创建模板的一个副本,每当 WPF 在 UI 树中遇到 ListViewModel
时,它都会使用该模板绘制它。未绑定到 DataContext 的该控件的属性将在更改 DataSources 之间保持其状态。
You could use x:Shared="False"
(example here), however this creates a new copy of your template anytime WPF requests it, which includes when you switch tabs.
您可以使用 x:Shared="False"
(此处为示例),但是,这会在 WPF 请求模板时创建模板的新副本,包括切换选项卡时。
When [
x:Shared
is] set to false, modifies Windows Presentation Foundation (WPF) resource retrieval behavior such that requests for a resource will create a new instance for each request, rather than sharing the same instance for all requests.
当 [x:Shared
is] 设置为 false 时,将修改 Windows Presentation Foundation (WPF) 资源检索行为,以便对资源的请求将为每个请求创建一个新实例,而不是为所有请求共享同一实例。
What you really need is for the TabControl.Items
to each generate a new copy of your control for each item, but that doesn’t happen when you use the ItemsSource
property (this is by design).
您真正需要的是让 TabControl.Items
为每个项生成控件的新副本,但在使用 ItemsSource
属性时不会发生这种情况(这是设计使然)。
One possible alternative which might work would be to create a custom DependencyProperty that binds to your collection of items, and generates the TabItem
and UserControl
objects for each item in the collection. This custom DP would also need to handle the collection change events to make sure the TabItems stay in sync with your collection.
一种可能的替代方法可能是创建一个自定义 DependencyProperty,该属性绑定到您的项集合,并为集合中的每个项生成 TabItem
和 UserControl
对象。此自定义 DP 还需要处理集合更改事件,以确保 TabItems 与集合保持同步。
Here’s one I was playing around with. It was working for simple cases, such as binding to an ObservableCollection, and adding/removing items.
这是我正在玩的一个。它适用于简单的情况,例如绑定到 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); } } }
Its used from the XAML like this :
它在 XAML 中使用,如下所示:
<TabControl local:TabControlHelpers.CachedItemsSource="{Binding Values}"> <TabControl.Resources> <Style TargetType="{x:Type TabItem}"> <Setter Property="Header" Value="{Binding SomeString}" /> </Style> </TabControl.Resources> </TabControl>
A few things to note :
需要注意的几点:
TabItem.Header
is not set, so you’ll have to setup a binding for it inTabControl.Resources
未设置TabItem.Header
,因此必须在TabControl.Resources
中为其设置绑定- DependencyProperty implementation currently hardcodes the creation of the new UserControl. May want to do that some other way, such as trying to use a template property or perhaps a different DP to tell it what UserControl to create
当前实现中的(相关) DependencyProperty 对新 UserControl 的创建进行硬编码。可能希望以其他方式执行此操作,例如尝试使用模板属性或不同的 DP 来告诉它要创建哪个 UserControl - Would probably need more testing… not sure if there’s any issues with memory leaks due to change handler, etc
可能需要更多的测试…… 不确定是否存在由于更改处理程序等原因导致的内存泄漏问题,等等。
三、Maël Pedretti(完善版)
Based on @Rachel answer I made a few modifications.
基于 @Rachel 的回答,我做了一些修改。
First of all, you now have to specify a user control type as content template which is dynamically created.
首先,您现在必须为动态创建的内容模板指定一个用户控件类型。
I have also corrected a mistake in collectionChanged handler removal.
我还更正了移除 collectionChanged 处理方法时的一个错误。
The code is the following:
代码如下:
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); } } }
This one is used the following way:
此方法的使用方式如下:
<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>
四、DLGCY(调整版)
帮助类重命名为 “TabControlAttached”,“界面元素类型” 附加属性重命名为 “ContentTemplateType”,使用设置的 ItemTemplate 载入 Header,其它代码调整等:
using System; using System.Collections; using System.Collections.Specialized; using System.Windows; using System.Windows.Controls; /* * 源码已托管:https://gitee.com/dlgcy/WPFTemplateLib * 版本:2024年8月19日 */ namespace WPFTemplateLib.Attached { /// <summary> /// TabControl 附加属性帮助类 /// </summary> public class TabControlAttached { #region TabControl 绑定模式下,让每一个标签页有自己单独的界面实例 //https://stackoverflow.com/questions/43347266/wpf-tabcontrol-create-only-one-view-at-all-tabs #region [附加属性] 每一项能创建单独界面实例的 ItemsSource public static IList GetCachedItemsSource(DependencyObject obj) { return (IList)obj.GetValue(CachedItemsSourceProperty); } /// <summary> /// 每一项能创建单独界面实例的 ItemsSource /// </summary> public static void SetCachedItemsSource(DependencyObject obj, IList value) { obj.SetValue(CachedItemsSourceProperty, value); } /// <summary> /// [附加属性] 每一项能创建单独界面实例的 ItemsSource /// </summary> public static readonly DependencyProperty CachedItemsSourceProperty = DependencyProperty.RegisterAttached("CachedItemsSource", typeof(IList), typeof(TabControlAttached), new PropertyMetadata(null, CachedItemsSource_Changed)); #endregion #region [附加属性] ContentTemplate 中指定的界面元素的类型 public static Type GetContentTemplateType(DependencyObject obj) { return (Type)obj.GetValue(ContentTemplateTypeProperty); } /// <summary> /// ContentTemplate 中指定的界面元素的类型 /// </summary> public static void SetContentTemplateType(DependencyObject obj, Type value) { obj.SetValue(ContentTemplateTypeProperty, value); } /// <summary> /// [附加属性] ContentTemplate 中指定的界面元素的类型 /// </summary> public static readonly DependencyProperty ContentTemplateTypeProperty = DependencyProperty.RegisterAttached("ContentTemplateType", typeof(Type), typeof(TabControlAttached), new PropertyMetadata(null, CachedItemsSource_Changed)); #endregion /// <summary> /// 附加属性改变事件 /// </summary> public static void CachedItemsSource_Changed(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) { TabControl control = dependencyObject as TabControl; if(control == null) return; var changeAction = new NotifyCollectionChangedEventHandler((o, args) => { if(dependencyObject is TabControl tabControl && GetContentTemplateType(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(GetContentTemplateType(dependencyObject) != null && GetCachedItemsSource(control) != null) UpdateTabItems(control); } /// <summary> /// 更新 TabItems /// </summary> /// <param name="tabControl"></param> 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(GetContentTemplateType(tabControl)), }; try { if(tabControl.ItemTemplate != null) { //使用设置的 ItemTemplate 载入 Header; tabItem.Header = tabControl.ItemTemplate.LoadContent(); } } catch(Exception ex) { Console.WriteLine(ex); } tabControl.Items.Add(tabItem); continue; } TabItem current = tabControl.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 = tabControl.Items.Count; i > itemsSource.Count; i--) { tabControl.Items.RemoveAt(i - 1); } } #endregion } }
使用:
<TabControl SelectedItem="{Binding SelectedLoadPort}" attached:TabControlAttached.CachedItemsSource="{Binding LoadPortViewModels}" attached:TabControlAttached.ContentTemplateType="{x:Type main:MainLoadPort}"> <TabControl.ItemTemplate> <DataTemplate> <Border Cursor="Hand"> <TextBlock Text="{Binding Name}"/> </Border> </DataTemplate> </TabControl.ItemTemplate> </TabControl>
4.1、2024 年 8 月 20 日 更新
以上方式会导致两个绑定错误:
错误 1:ItemTemplate and ItemTemplateSelector are ignored for items already of the ItemsControl’s container type; Type=’TabItem’
错误 2:……Cannot convert ‘System.Windows.Controls.TabItem Header: Content:’ from type ‘TabItem’ to type ‘xxxViewModel’ for ‘en-US’ culture with default conversions; consider using Converter property of Binding. NotSupportedException:’System.NotSupportedException: TypeConverter 无法从 System.Windows.Controls.TabItem 转换。
先来解决 错误 2,也就是因为现在相当于是直接设置 TabControl 的 Items 了,所以 SelectedItem 也就变成了 TabItem,此时还使用之前的绑定(SelectedItem="{Binding SelectedLoadPort}"
),就会无法正确转换。解决方法也很简单,只需要使用 SelectedValue 绑定,配合上 SelectedValuePath="DataContext"
,以此来替代之前的 SelectedItem 绑定即可:
再来看看 错误 1,实际上说的就是,给 TabControl(ItemsControl 类型)设置了 Items 的话,ItemTemplate 或 ItemTemplateSelector 会被忽略。这个其实只是个警告,并不影响功能,不过会出现在 “Xaml 绑定失败” 窗口中,还是挺烦人的,所以最好还是解决一下。
解决方法也很简单,我们不用它自带的 ItemTemplate,而是自己加一个相关附加属性即可,所以更新后的代码如下:
using System; using System.Collections; using System.Collections.Specialized; using System.Windows; using System.Windows.Controls; /* * 源码已托管:https://gitee.com/dlgcy/WPFTemplateLib * 版本:2024年8月20日 */ namespace WPFTemplateLib.Attached { /// <summary> /// TabControl 附加属性帮助类 /// </summary> public class TabControlAttached { #region TabControl 绑定模式下,让每一个标签页有自己单独的界面实例 //https://stackoverflow.com/questions/43347266/wpf-tabcontrol-create-only-one-view-at-all-tabs #region [附加属性] 每一项能创建单独界面实例的 ItemsSource public static IList GetCachedItemsSource(DependencyObject obj) { return (IList)obj.GetValue(CachedItemsSourceProperty); } /// <summary> /// 每一项能创建单独界面实例的 ItemsSource /// </summary> public static void SetCachedItemsSource(DependencyObject obj, IList value) { obj.SetValue(CachedItemsSourceProperty, value); } /// <summary> /// [附加属性] 每一项能创建单独界面实例的 ItemsSource /// </summary> public static readonly DependencyProperty CachedItemsSourceProperty = DependencyProperty.RegisterAttached("CachedItemsSource", typeof(IList), typeof(TabControlAttached), new PropertyMetadata(null, CachedItemsSource_Changed)); #endregion #region [附加属性] ContentTemplate 中指定的界面元素的类型 public static Type GetContentTemplateType(DependencyObject obj) { return (Type)obj.GetValue(ContentTemplateTypeProperty); } /// <summary> /// ContentTemplate 中指定的界面元素的类型 /// </summary> public static void SetContentTemplateType(DependencyObject obj, Type value) { obj.SetValue(ContentTemplateTypeProperty, value); } /// <summary> /// [附加属性] ContentTemplate 中指定的界面元素的类型 /// </summary> public static readonly DependencyProperty ContentTemplateTypeProperty = DependencyProperty.RegisterAttached("ContentTemplateType", typeof(Type), typeof(TabControlAttached), new PropertyMetadata(null, CachedItemsSource_Changed)); #endregion #region [附加属性] ItemTemplate public static DataTemplate GetItemTemplate(DependencyObject obj) { return (DataTemplate)obj.GetValue(ItemTemplateProperty); } /// <summary> /// ItemTemplate /// </summary> public static void SetItemTemplate(DependencyObject obj, DataTemplate value) { obj.SetValue(ItemTemplateProperty, value); } /// <summary> /// [附加属性] ItemTemplate /// </summary> public static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.RegisterAttached("ItemTemplate", typeof(DataTemplate), typeof(TabControlAttached), new PropertyMetadata(null, CachedItemsSource_Changed)); #endregion /// <summary> /// CachedItemsSource 及相关的附加属性改变事件 /// </summary> public static void CachedItemsSource_Changed(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) { TabControl control = dependencyObject as TabControl; if(control == null) return; var changeAction = new NotifyCollectionChangedEventHandler((o, args) => { if(dependencyObject is TabControl tabControl && GetContentTemplateType(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(GetContentTemplateType(dependencyObject) != null && GetCachedItemsSource(control) != null) UpdateTabItems(control); } /// <summary> /// 更新 TabItems /// </summary> /// <param name="tabControl"></param> 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(i >= tabControl.Items.Count) { TabItem tabItem = new TabItem { DataContext = itemsSource[i], Content = Activator.CreateInstance(GetContentTemplateType(tabControl)), }; SetTabItemHeader(tabControl, tabItem); tabControl.Items.Add(tabItem); continue; } TabItem current = tabControl.Items[i] as TabItem; if(current == null) continue; if(current.DataContext != itemsSource[i]) { current.DataContext = itemsSource[i]; } SetTabItemHeader(tabControl, current); } //移除多余的标签页 for(int i = tabControl.Items.Count; i > itemsSource.Count; i--) { tabControl.Items.RemoveAt(i - 1); } } /// <summary> /// 设置 TabItem 的 Header /// </summary> /// <param name="tabControl"></param> /// <param name="tabItem"></param> private static void SetTabItemHeader(TabControl tabControl, TabItem tabItem) { try { //使用设置的 ItemTemplate 载入 Header DataTemplate itemTemplate = GetItemTemplate(tabControl); if(itemTemplate == null) { //如果未设置 ItemTemplate 附加属性,则尝试使用原生的 ItemTemplate(这种情况可能会有 Xaml 绑定失败提示,不过不影响功能) itemTemplate = tabControl.ItemTemplate; } if(itemTemplate != null) { tabItem.Header = itemTemplate.LoadContent(); } } catch(Exception ex) { Console.WriteLine(ex); } } #endregion } }
对应的使用方法为:
<TabControl SelectedValuePath="DataContext" SelectedValue="{Binding SelectedLoadPort}" attached:TabControlAttached.CachedItemsSource="{Binding LoadPortViewModels}" attached:TabControlAttached.ContentTemplateType="{x:Type main:MainLoadPort}"> <attached:TabControlAttached.ItemTemplate> <DataTemplate> <Border Cursor="Hand"> <TextBlock Text="{Binding Name}"/> </Border> </DataTemplate> </attached:TabControlAttached.ItemTemplate> </TabControl>
也可直接通过 NuGet 包 “WPFTemplateLib” 进行使用:
发表评论