WPF 扩展 TabControl 可保存显示的标签页

一 功能描述:

扩展的 TabControl 可保存显示的项目,这样切换选项卡时就不会因卸载和重新加载 VisualTree 而影响性能

csharp 复制代码
 [TemplatePart(Name = "PART_ItemsHolder", Type = typeof(Panel))]
 public class TabControlEx : HandyControl.Controls.TabControl
 {
     private Panel _itemsHolderPanel;

     public TabControlEx()
     {
         // This is necessary so that we get the initial databound selected item
         ItemContainerGenerator.StatusChanged += ItemContainerGeneratorStatusChanged;
     }

     /// <summary>
     /// If containers are done, generate the selected item
     /// </summary>
     /// <param name="sender">The sender.</param>
     /// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
     private void ItemContainerGeneratorStatusChanged(object sender, EventArgs e)
     {
         if (ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
         {
             ItemContainerGenerator.StatusChanged -= ItemContainerGeneratorStatusChanged;
             UpdateSelectedItem();
         }
     }

     /// <summary>
     /// Get the ItemsHolder and generate any children
     /// </summary>
     public override void OnApplyTemplate()
     {
         base.OnApplyTemplate();
         _itemsHolderPanel = GetTemplateChild("PART_ItemsHolder") as Panel;
         UpdateSelectedItem();
     }

     /// <summary>
     /// When the items change we remove any generated panel children and add any new ones as necessary
     /// </summary>
     /// <param name="e">The <see cref="NotifyCollectionChangedEventArgs"/> instance containing the event data.</param>
     protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
     {
         base.OnItemsChanged(e);

         if (_itemsHolderPanel == null)
         {
             return;
         }

         switch (e.Action)
         {
             case NotifyCollectionChangedAction.Reset:
                 _itemsHolderPanel.Children.Clear();
                 break;

             case NotifyCollectionChangedAction.Add:
             case NotifyCollectionChangedAction.Remove:
                 if (e.OldItems != null)
                 {
                     foreach (var item in e.OldItems)
                     {
                         var cp = FindChildContentPresenter(item);
                         if (cp != null)
                         {
                             _itemsHolderPanel.Children.Remove(cp);
                         }
                     }
                 }

                 // Don't do anything with new items because we don't want to
                 // create visuals that aren't being shown

                 UpdateSelectedItem();
                 break;

             case NotifyCollectionChangedAction.Replace:
                 throw new NotImplementedException("Replace not implemented yet");
         }
     }

     protected override void OnSelectionChanged(SelectionChangedEventArgs e)
     {
         base.OnSelectionChanged(e);
         UpdateSelectedItem();
     }

     private void UpdateSelectedItem()
     {
         if (_itemsHolderPanel == null)
         {
             return;
         }

         // Generate a ContentPresenter if necessary
         var item = GetSelectedTabItem();
         if (item != null)
         {
             CreateChildContentPresenter(item);
         }

         // show the right child
         foreach (ContentPresenter child in _itemsHolderPanel.Children)
         {
             child.Visibility = ((child.Tag as HandyControl.Controls.TabItem).IsSelected) ? Visibility.Visible : Visibility.Collapsed;
         }
     }

     private ContentPresenter CreateChildContentPresenter(object item)
     {
         if (item == null)
         {
             return null;
         }

         var cp = FindChildContentPresenter(item);

         if (cp != null)
         {
             return cp;
         }

         var tabItem = item as HandyControl.Controls.TabItem;
         cp = new ContentPresenter
         {
             Content = (tabItem != null) ? tabItem.Content : item,
             ContentTemplate = this.SelectedContentTemplate,
             ContentTemplateSelector = this.SelectedContentTemplateSelector,
             ContentStringFormat = this.SelectedContentStringFormat,
             Visibility = Visibility.Collapsed,
             Tag = tabItem ?? (this.ItemContainerGenerator.ContainerFromItem(item))
         };
         _itemsHolderPanel.Children.Add(cp);
         return cp;
     }

     private ContentPresenter FindChildContentPresenter(object data)
     {
         if (data is HandyControl.Controls.TabItem)
         {
             data = (data as HandyControl.Controls.TabItem).Content;
         }

         if (data == null)
         {
             return null;
         }

         if (_itemsHolderPanel == null)
         {
             return null;
         }

         foreach (ContentPresenter cp in _itemsHolderPanel.Children)
         {
             if (cp.Content == data)
             {
                 return cp;
             }
         }

         return null;
     }

     protected HandyControl.Controls.TabItem GetSelectedTabItem()
     {
         var selectedItem = SelectedItem;
         if (selectedItem == null)
             return null;

         return selectedItem as HandyControl.Controls.TabItem ?? ItemContainerGenerator.ContainerFromIndex(SelectedIndex) as HandyControl.Controls.TabItem;
     }

     protected override AutomationPeer OnCreateAutomationPeer()
     {
         return new TabControlAutomationPeer(this);
     }
 }

二 使用示例:

注意注释地方和系统样式不一样!
修改内容:系统样式使用ContentPresenter 作为承载容器,切换标签页使会重新加载并且标签页内容会互相影响。这里使用Grid x:Name="PART_ItemsHolder"作为承载容器,内容通过Visibility控制显示!

csharp 复制代码
 <control:TabControlEx ItemsSource="{Binding TabItems, Mode=TwoWay}"
                       SelectedItem="{Binding SelectedTab, Mode=TwoWay}">
	<control:TabControlEx.Style>
		<Style TargetType="control:TabControlEx">
			<Setter Property="Template">
				<Setter.Value>
					<ControlTemplate TargetType="control:TabControlEx">
						<Border BorderBrush="{TemplateBinding BorderBrush}"
								CornerRadius="{TemplateBinding hc:BorderElement.CornerRadius}"
								BorderThickness="{TemplateBinding BorderThickness}"
								Background="{DynamicResource Background2}">
							<Grid Name="templateRoot"
								  ClipToBounds="true"
								  SnapsToDevicePixels="true">
								<Grid.ColumnDefinitions>
									<ColumnDefinition Width="*"
													  x:Name="column1" />
									<ColumnDefinition Width="auto" />
								</Grid.ColumnDefinitions>
								<Grid.RowDefinitions>
									<RowDefinition Height="60" />
									<RowDefinition Height="*" />
								</Grid.RowDefinitions>
								
								<TabPanel Name="PART_HeaderPanel"
										 IsItemsHost="true"
										 ZIndex="1" />
							   
								<Border x:Name="contentPanel"
										Background="{DynamicResource WhiteBackground}"
										Grid.Column="0"
										Grid.ColumnSpan="2"
										Grid.Row="1">
								
									<Grid x:Name="PART_ItemsHolder"
										  SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}">	
										<!--<ContentPresenter Name="PART_SelectedContentHost"
														      ContentSource="SelectedContent"
														      Margin="{TemplateBinding Padding}"
														      SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />-->
									</Grid>
								</Border>
							</Grid>
						</Border>
					</ControlTemplate>
				</Setter.Value>
			</Setter>
		</Style>
	</control:TabControlEx.Style>
</control:TabControlEx>

三 原文连接:stackoverflow

四 TabControl 扩展(模板选择器)

本教程基于Prism + HandyControl + .Net Framework 4.8

要手动添加标签页,并且每个标签页不同,可使用模板选择器实现,示例:

这里每个标签页有自己的后台处理逻辑,即FrontPageView.xaml的后台是FrontPageViewModel.cs

注意点:由于标签页有自己的处理逻辑,并且是嵌入在TabControl中,所以
不能在标签页中开启Prism的自动查找功能 即xmlns:prism="http://prismlibrary.com/" prism:ViewModelLocator.AutoWireViewModel="False" 或者 不写这两行代码

csharp 复制代码
<UserControl.Resources>
    <!-- 首页模板 -->
    <DataTemplate x:Key="FrontPageTemplate"
                  DataType="{x:Type pageview:FrontPageViewModel}">
        <pageview:FrontPageView />
    </DataTemplate>

    <!-- 其他页面模板 -->
    <DataTemplate x:Key="WebPageTemplate"
                  DataType="{x:Type pageview:WebPageViewModel}">
        <pageview:WebPageView />
    </DataTemplate>

    <!-- 模板选择器 -->
    <helper:TabContentTemplateSelector x:Key="TabContentTemplateSelector"
                                       FrontPageTemplate="{StaticResource FrontPageTemplate}"
                                       WebPageTemplate="{StaticResource WebPageTemplate}" />
</UserControl.Resources>


<control:TabControlEx ItemsSource="{Binding TabItems, Mode=TwoWay}"
                       SelectedItem="{Binding SelectedTab, Mode=TwoWay}"
                       ContentTemplateSelector="{StaticResource TabContentTemplateSelector}">
</control:TabControlEx>
csharp 复制代码
  internal class TabContentTemplateSelector : DataTemplateSelector
  {
      public DataTemplate FrontPageTemplate { get; set; }
      public DataTemplate WebPageTemplate { get; set; }

      public override DataTemplate SelectTemplate(object item, DependencyObject container)
      {
          if (item is FrontPageViewModel)
              return FrontPageTemplate;

          if (item is WebPageViewModel)
              return WebPageTemplate;

          return base.SelectTemplate(item, container);
      }
  }

TabControlEx 的后台代码:

csharp 复制代码
private ObservableCollection<object> _allTabItems;
public ObservableCollection<object> TabItems
{
    get { return _allTabItems; }
    set { SetProperty(ref _allTabItems, value); }
}

private object _selectedTab;
public object SelectedTab
{
    get { return _selectedTab; }
    set { SetProperty(ref _selectedTab, value); }
}

//添加标签页
private void OnAddTabCommand()
{
	WebPageViewModel webPage = new WebPageViewModel();
	//TODO:
	TabItems.Add(webPage);
}
相关推荐
wangnaisheng7 小时前
【WPF】使用BitmapImage给Image的Source赋值,并释放原占用资源,避免删除原文件时导致程序崩溃
c#·wpf
还是叫明9 小时前
WPF中RenderTargetBitmap问题解决
wpf
雾削木9 小时前
C# WPF Material DesignThemes 5.0 命名规则改变后导致找不到资源
开发语言·c#·wpf
流浪阿丁10 小时前
WPF页面中将一个控件的宽度绑定到其父级用户控件的实际宽度
wpf
weixin_464078071 天前
开源Material Design WPF UI 控件库简单上手
ui·wpf
军训猫猫头1 天前
34.键盘1 C#例子 WPF例子
开发语言·c#·wpf
hao_wujing2 天前
分布式 L2 网关下的 OVS 未知单播泛洪
分布式·wpf
Crazy Struggle2 天前
C#+ WPF 实现蓝牙转WIFI计步上位机
c#·wpf·上位机软件
CV大法好3 天前
WPF通过反射机制动态加载控件
visualstudio·c#·wpf