As an opening word, let's check on the background of the Prerendering tab controls.
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 wpf, Tabcontrol 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.