WPF - Enhanced TabControl - TabControlEx aka Prerendering TabControl

46 篇文章 0 订阅

As an opening word, let's check on the background  of the Prerendering tab controls. 

TabControlEx 写道
There is a known behavior of TabControl that it will "virtualize" its tabs when they are created via data binding. Only the visible tab actually exists and is bound to the selected data item. When selection changes, existing controls are reused and bound to new data context:
http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/e1d95d22-ce08-4a9d-a244-31e69ac7c064

So in the case when the ContentTemplate of a TabControl is complicated and data binding takes time, its performance will be worse than the non-MVVM ways that programmatically creating TabItem.

To solve this while still keeping the MVVM pattern, we found a way to create a subclass named “TabControlEx” to change the behavior from “virtualize” to keeping the TabItem:
http://stackoverflow.com/questions/2193166/how-do-i-prerender-the-controls-on-a-tabitem-in-wpf

Hao has tried this solution with some modifications and it works fine. Also I have used the similar solution before of a application in some firm..

As the scenario of TabControl data-binding is very common during our work, so I think we can consider putting it in GuiToolKit or other shared lib.

 
The root case to this is that 


1. TabControl as the container has virutalization applied when the ControlTemplate is used. the optimization is basically tabItem will be reused and you might get delay when you switch tabs. and beside We have to take care of the styling and templating.

 

 

The solution

And the solution to this problem is TabControlEx, prerendering TabControl which will render all tabs and hide those which is not selected. (by setting the SeelctedItem and others)

 

As for the root cause of the issue, you can check on the reference page - How do I prerender the controls on a tabitem in wpfTabcontrol reuses contained controls;

 

And the page How do I prerender the controls on a tabitem in wpf tells you how to do the prerending of the tab items.


And the code is as below. 

    // check on : http://stackoverflow.com/questions/2193166/how-do-i-prerender-the-controls-on-a-tabitem-in-wpf

    /// <summary>
    /// The standard WPF TabControl is quite bad in the fact that it only
    /// even contains the current TabItem in the VisualTree, so if you
    /// have complex views it takes a while to re-create the view each tab
    /// selection change.Which makes the standard TabControl very sticky to
    /// work with. This class along with its associated ControlTemplate
    /// allow all TabItems to remain in the VisualTree without it being Sticky.
    /// It does this by keeping all TabItem content in the VisualTree but
    /// hides all inactive TabItem content, and only keeps the active TabItem
    /// content shown.
    /// </summary>

    [TemplatePart(Name = "PART_ItemsHolder", Type = typeof(Panel))]
    public class TabControlEx : TabControl
    {
        #region Data
        private Panel itemsHolder = null;
        #endregion 

        #region Ctor
        public TabControlEx() : base()
        {
            // this is necessary so that we get the initial databound selected item
            this.ItemContainerGenerator.StatusChanged += new EventHandler(ItemContainerGenerator_StatusChanged);
            this.Loaded += new RoutedEventHandler(TabControlEx_Loaded);
        }

        #endregion Ctor

        #region Public/Protected Methods
        /// <summary>
        /// get the ItemsHolder and generate any children
        /// </summary>
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            itemsHolder = GetTemplateChild("PART_ItemsHolder") as Panel; // get Part as specified in the Control conract

            UpdateSelectedItem();
        }

        /// <summary>
        /// when the items change we remove any generated panel children and add any new ones as necessary
        /// </summary>
        /// <param name="e"></param>
        protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e) // NOTE where the NotifyCollectionChangedEventArgs belongs to (the namespace here)
        {
            base.OnItemsChanged(e);
            if (itemsHolder == null)
                return;
            switch (e.Action)
            {
                case System.Collections.Specialized.NotifyCollectionChangedAction.Reset:
                    itemsHolder.Children.Clear();
                    break;
                case System.Collections.Specialized.NotifyCollectionChangedAction.Add:
                case System.Collections.Specialized.NotifyCollectionChangedAction.Remove:
                    if (e.OldItems != null)
                    {
                        foreach (var item in e.OldItems)
                        {
                            ContentPresenter cp = FindChildContentPresenter(item);
                            if (cp != null)
                            {
                                itemsHolder.Children.Remove(cp); // remove the current switched out item and we wil take care of the items that come in
                            }
                        }
                    }
                    // don't do anything with new items because we don't want to
                    // create visuals that aren't being shown

                    UpdateSelectedItem();

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

        /// <summary>
        /// update the visible child in the ItemsHolder
        /// </summary>
        /// <param name="e"></param>
        protected override void OnSelectionChanged(SelectionChangedEventArgs e)
        {
            base.OnSelectionChanged(e);
            UpdateSelectedItem();
        }

        /// <summary>
        /// copied from TabControl; wish it were protected in that class instead of private
        /// </summary>
        /// <returns></returns>
        protected TabItem GetSelectedTablItem()
        {
            object selectedItem = base.SelectedItem;
            if (selectedItem == null)
            {
                return null;
            }
            TabItem item = selectedItem as TabItem;
            if (item == null)
            {
                item = base.ItemContainerGenerator.ContainerFromIndex(base.SelectedIndex) as TabItem;
            }
            return item;
        }

       

        #endregion Public/Protected Methods

        #region Private Methods
        /// <summary>
        /// in some scenarios we need to update when loaded in case the 
        /// ApplyTemplate happens before the databind.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void TabControlEx_Loaded(object sender, RoutedEventArgs e)
        {
            UpdateSelectedItem();
        }

        /// <summary>
        /// if containers are done, generate the selected item
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
        {
            if (this.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated) // The namespace of GeneratorStatus is from System.Windows.Control.Primitives
            {
                this.ItemContainerGenerator.StatusChanged -= ItemContainerGenerator_StatusChanged;
                UpdateSelectedItem();
            }
        }

        /// <summary>
        /// generate a ContentPresenter for the selected item
        /// </summary>
        private void UpdateSelectedItem()
        {
            if (itemsHolder == null)
            {
                return;
            }

            // Generate a ContentPresenter if necessary
            TabItem item = GetSelectedTablItem();
            if (item != null)
            {
                CreateChildContentPresenter(item);
            }
            // Show the right child
            foreach (ContentPresenter child in itemsHolder.Children)
            {
                child.Visibility = ((child.Tag as TabItem).IsSelected) ? Visibility.Visible : Visibility.Collapsed;
            }
        }

        /// <summary>
        /// create the child ContentPresenter for the given item (could be data or a TabItem)
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        private ContentPresenter CreateChildContentPresenter(TabItem item)
        {

            if (item == null)
            {
                return null;
            }
            ContentPresenter cp = FindChildContentPresenter(item);

            if (cp != null)
            {
                return cp;
            }
            // the actual child to be added, cp.Tag is a reference to the TabItem.
            cp = new ContentPresenter();
            cp.Content = (item is TabItem) ? (item as TabItem).Content : item;
            cp.ContentTemplate = this.SelectedContentTemplate;
            cp.ContentTemplateSelector = this.SelectedContentTemplateSelector;
            cp.ContentStringFormat = this.SelectedContentStringFormat;
            cp.Visibility = System.Windows.Visibility.Collapsed;
            cp.Tag = (item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item));
            itemsHolder.Children.Add(cp);
            return cp;
        }

        /// <summary>
        /// Find the CP for the given object.  data could be a TabItem or a piece of data
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        private ContentPresenter FindChildContentPresenter(object data)
        {
            if (data is TabItem)
            {
                data = (data as TabItem).Content;
            }
            if (data == null)
            {
                return null;
            }
            if (itemsHolder == null)
            {
                return null;
            }

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

        #endregion Private Methods


    }

 

So basically the TabControlEx is done by subclassing the TabControl and Override/extends the methods that is pertaining to the ItemContainerGenerator methods/events. this is what you might have seen related to the 

 

this.ItemContainerGenerator.StatusChanged += new EventHandler(ItemContainerGenerator_StatusChanged);

 and 

            if (item == null)
            {
                item = base.ItemContainerGenerator.ContainerFromIndex(base.SelectedIndex) as TabItem;
            }

and the code depends on the successfully manipulation on the ChildContentPresenter. Which may include the following.

 

        private ContentPresenter CreateChildContentPresenter(TabItem item)
        {

            if (item == null)
            {
                return null;
            }
            ContentPresenter cp = FindChildContentPresenter(item);

            if (cp != null)
            {
                return cp;
            }
            // the actual child to be added, cp.Tag is a reference to the TabItem.
            cp = new ContentPresenter();
            cp.Content = (item is TabItem) ? (item as TabItem).Content : item;
            cp.ContentTemplate = this.SelectedContentTemplate;
            cp.ContentTemplateSelector = this.SelectedContentTemplateSelector;
            cp.ContentStringFormat = this.SelectedContentStringFormat;
            cp.Visibility = System.Windows.Visibility.Collapsed;
            cp.Tag = (item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item));
            itemsHolder.Children.Add(cp);
            return cp;
        }

 Well, for the rest of the code, you can reason out most of the logics. 

Well, getting the class extended is not yet done the job, we have to as well to define some template to use.  E.g basically you will need to set up the content holder and the rest. 

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:TabControlExLib"
    xmlns:ViewModels="clr-namespace:TabControlExLib.ViewModels"
    >

    
    
    <ControlTemplate x:Key="MainTabControlTemplateEx" TargetType="{x:Type local:TabControlEx}" >
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition x:Name="row0" Height="Auto" />
                <RowDefinition x:Name="row1" Height="4" />
                <RowDefinition x:Name="row2" Height="*" />
            </Grid.RowDefinitions>
            
            <!-- 
            
            Background definition is as follow
            
            Background="{StaticResource OutLookButtonHighlight}"
            -->
            <TabPanel x:Name="tabpanel"
                      Margin="0"
                      Grid.Row="0"
                      IsItemsHost="True" /> <!-- what does the isItemHost mean? does it mean ItemContainerGenerator -->
            <Grid x:Name="divider"
                  Grid.Row="1"
                  Background="Black"
                  HorizontalAlignment="Stretch"
                  />
            <Grid x:Name="PART_ItemsHolder"
                  Grid.Row="2" /> <!--  Grid layout control is a subclass of Panel? See the code for definition and Control contract-->
        </Grid>
        <!-- No Content Presenter -->        <!-- Content Presenter should be managed by the code, so we can create or delete Child from the ItemsHost explicitly-->
        <ControlTemplate.Triggers>
            <Trigger Property="TabStripPlacement" Value="Top"> <!-- Tabstrip is the strip where the tab's label is placed -->
                <Setter TargetName="tabpanel" Property="Grid.Row" Value="0" />
                <Setter TargetName="divider" Property="Grid.Row" Value="1" />
                <Setter TargetName="PART_ItemsHolder" Property="Grid.Row" Value="2" />
                <Setter TargetName="row0" Property="Height" Value="Auto" />
                <Setter TargetName="row1" Property="Height" Value="4" />
                <Setter TargetName="row2" Property="Height" Value="*" />
            </Trigger>
            <Trigger Property="TabStripPlacement" Value="Bottom">
                <Setter TargetName="tabpanel" Property="Grid.Row" Value="2" />
                <Setter TargetName="divider" Property="Grid.Row" Value="1" />
                <Setter TargetName="PART_ItemsHolder" Property="Grid.Row" Value="0" />
                <Setter TargetName="row0" Property="Height" Value="*" />
                <Setter TargetName="row1" Property="Height" Value="4" />
                <Setter TargetName="row2" Property="Height" Value="Auto" />
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>
</ResourceDictionary?

 

With this, you might as well define the Header template, so that each tab control can display some meaning information...

 

well, to do that, you first need to get the viewmodel right, suppose that each tab will be modelded in such an class called TabControlExViewModel.

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Collections.ObjectModel;

namespace TabControlExLib.ViewModels
{
    public class TabControlExViewModel : INotifyPropertyChanged
    {

        public TabControlExViewModel()
        {

        }

        public string Name { get; set; }
        private ObservableCollection<string> _associatedNames = new ObservableCollection<string>();
        public ObservableCollection<string> AssociatedNames { get { return _associatedNames; } set { _associatedNames = value; } }


        #region INotifyPropertyChanged Implementation
        public event PropertyChangedEventHandler PropertyChanged;
        #endregion INotifyPropertyChanged Implementation
    }
}

 So, get back to our Header template, we can write as such .

    <!-- DataTemplate definition -->
    <!-- For details on the ItemsControl 
    Please check on this: http://msdn.microsoft.com/en-us/library/system.windows.controls.itemscontrol.aspx
    -->
    <DataTemplate DataType="{x:Type ViewModels:TabControlExViewModel}" x:Key="HeaderTemplate">
        <Grid>
            <TextBlock Text="{Binding Path=Name}" />
        </Grid>

    </DataTemplate>
    

 
And we might as well write a DataTemplate so that each Tab can have meaningful representation on the ViewModel.

    <!-- 
    we can also define implicit Data Template for the ViewModels:TabControlExViewModel 
    -->
    <DataTemplate DataType="{x:Type ViewModels:TabControlExViewModel}" >
        <Grid>
            <Grid.RowDefinitions>
                <!-- RowDefinition MaxHeight 
                MaxHeight="{DynamicResource {x:Static SystemParameters.WindowCaptionHeight}}"
                MaxHeight="{DynamicResource {x:Static SystemParameters.VerticalScrollBarButtonHeightKey}}"
                Height="0.00001*" 
                -->
                <RowDefinition MaxHeight="{DynamicResource {x:Static SystemParameters.ThickHorizontalBorderHeightKey}}"/>
                <RowDefinition Height="0.0001*"/>
            </Grid.RowDefinitions>
            <!-- this will be handled at the tabHeader -->
            <!--<TextBlock Text="{Binding Path=Name}" />-->
            <Line Grid.Row="0" />
            <!--<ItemsControl ItemsSource="{Binding Path=AssociatedNames}"
                          Grid.Row="2"
            />-->
            <ListView
            ItemsSource="{Binding Path=AssociatedNames}"
                Grid.Row="1"/>
        </Grid>
    </DataTemplate>

 

All those templates are defined in a resource file called ResourceDictionary.xaml file.

 

 To make for a demo, I have created the demo viewmodel, which composite a Collection of TabControlExViewModel. The code of which is as such.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
// use Microsoft.Practises.Prism.ViewModel for the class "NotificationObject"
using Microsoft.Practices.Prism.ViewModel;

namespace TabControlExLib.ViewModels
{
    public class ExampleTabControlExViewModel : NotificationObject
    {
        #region Ctor
        public ExampleTabControlExViewModel()
        {
            Initialize();
        }
        #endregion Ctor


        #region Properties
        private ObservableCollection<TabControlExViewModel> _availableViewModels = new ObservableCollection<TabControlExViewModel>();
        public ObservableCollection<TabControlExViewModel> AvailableViewModels
        {
            get
            {
                return _availableViewModels;
            }
            set
            {
                if (Equals(value, _availableViewModels))
                {
                    return;
                }
                _availableViewModels = value;
            }

        }
        #endregion Properties


        #region Private Instance Methods
        // Mock the creation of the TablControlExViewModel collections
        private void Initialize()
        {
            List<TabControlExViewModel> viewmodels = new List<TabControlExViewModel>();

            viewmodels.Add(new TabControlExViewModel
            {
                Name = "Name1",
                AssociatedNames =
                {
                    "Associated Name1",
                    "Associated Name2",
                    "Associated Name3",
                }
            });

            viewmodels.Add(new TabControlExViewModel
            {
                Name = "Name2",
                AssociatedNames =
                {
                    "Associated Name3",
                    "Associated Name4",
                    "Associated Name5",
                }
            });

            AvailableViewModels = new ObservableCollection<TabControlExViewModel>(viewmodels);
            RaisePropertyChanged(() => AvailableViewModels); // Remembered that we have several ways to do NotifyPropertyChanged things.

        }
        #endregion Instance Methods
    }
}

 

and in the MainWindow.xaml file, we can have this:

<Window x:Class="TabControlExLib.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:TabControlExLib"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <!-- 
        
        Define and use MainTabControlTemplateEx to specify how does the control template looks like
        Deine the ItemTemplate to tell how to render the ViewModel, in this case the TabControlExViewModel
        
        Define the ItemsSource so that correct data binding is setup
        -->
        <local:TabControlEx
            IsSynchronizedWithCurrentItem="True"
            ItemsSource="{Binding Path=AvailableViewModels}"
            Template="{StaticResource MainTabControlTemplateEx}"
            ItemTemplate="{DynamicResource HeaderTemplate}"
            >
            
        </local:TabControlEx>
    </Grid>
</Window>

 and then in the constructor of the MainWindow, we can do proper initialization.

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InstallThemes();
            InitializeComponent();
            Initialize();

        }

        private void Initialize()
        {
            
            this.DataContext = new ExampleTabControlExViewModel();
        }


        private void InstallThemes()
        {
            Resources.MergedDictionaries.Add(new ResourceDictionary() { Source = new Uri("/Themes/Generic.xaml", UriKind.RelativeOrAbsolute) } );
        }
    }

 Thus, if you run the code, you might see the following result.


 

You can find the zipped file - Download zip file.

References:

How do I prerender the controls on a tabitem in wpf

Tabcontrol reuses contained controls

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值