Stop Tabcontrol from Recreating Its Children

Stop TabControl from recreating its children

By default, the TabControl shares a panel to render it's content. To do what you want (and many other WPF developers), you need to extend TabControl like so:

TabControlEx.cs

[TemplatePart(Name = "PART_ItemsHolder", Type = typeof(Panel))]
public class TabControlEx : TabControl
{
private Panel ItemsHolderPanel = null;

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

/// <summary>
/// If containers are done, generate the selected item
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
if (this.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
this.ItemContainerGenerator.StatusChanged -= ItemContainerGenerator_StatusChanged;
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"></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)
{
ContentPresenter 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
TabItem item = GetSelectedTabItem();
if (item != null)
CreateChildContentPresenter(item);

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

private ContentPresenter CreateChildContentPresenter(object 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 = Visibility.Collapsed;
cp.Tag = (item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item));
ItemsHolderPanel.Children.Add(cp);
return cp;
}

private ContentPresenter FindChildContentPresenter(object data)
{
if (data is TabItem)
data = (data as 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 TabItem GetSelectedTabItem()
{
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;
}
}

XAML

<Style TargetType="{x:Type controls:TabControlEx}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabControl}">
<Grid Background="{TemplateBinding Background}" ClipToBounds="True" KeyboardNavigation.TabNavigation="Local" SnapsToDevicePixels="True">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="ColumnDefinition0" />
<ColumnDefinition x:Name="ColumnDefinition1" Width="0" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition x:Name="RowDefinition0" Height="Auto" />
<RowDefinition x:Name="RowDefinition1" Height="*" />
</Grid.RowDefinitions>
<DockPanel Margin="2,2,0,0" LastChildFill="False">
<TabPanel x:Name="HeaderPanel" Margin="0,0,0,-1" VerticalAlignment="Bottom" Panel.ZIndex="1" DockPanel.Dock="Right"
IsItemsHost="True" KeyboardNavigation.TabIndex="1" />
</DockPanel>
<Border x:Name="ContentPanel" Grid.Row="1" Grid.Column="0"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
KeyboardNavigation.DirectionalNavigation="Contained" KeyboardNavigation.TabIndex="2" KeyboardNavigation.TabNavigation="Local">
<Grid x:Name="PART_ItemsHolder" Margin="{TemplateBinding Padding}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

Note: I did not come up with this solution. It has been shared in programming forums for several years and believe that it is in now one of those WPF recipes books. The oldest or original source for I believe was PluralSight .NET blog post and this answer on StackOverflow.

HTH,

How to fix this behavior in a WPF TabControl?

A simplified example.

Collection item:

using Simplified;

namespace AddTabItem
{
public class TabVm : BaseInpc
{
string _header;
bool _isPlaceholder;
private string _text;

public string Header { get => _header; set => Set(ref _header, value); }

public bool IsPlaceholder { get => _isPlaceholder; set => Set(ref _isPlaceholder, value); }

public string Text { get => _text; set => Set(ref _text, value); }
}
}

ViewModel:

using Simplified;
using System.Collections.ObjectModel;

namespace AddTabItem
{
public class TabsCollectionViewModel : BaseInpc
{
private TabVm _selectedTab;
private RelayCommand _addNewTabCommand;
private RelayCommand _removeTabCommand;

public ObservableCollection<TabVm> Tabs { get; } = new ObservableCollection<TabVm>();

public TabVm SelectedTab { get => _selectedTab; set => Set(ref _selectedTab, value); }

public RelayCommand AddNewTabCommand => _addNewTabCommand
?? (_addNewTabCommand = new RelayCommand(
() =>
{
TabVm tab = new TabVm() { Header = $"Tab{Tabs.Count}" };
Tabs.Add(tab);
SelectedTab = tab;
}));

public RelayCommand RemoveTabCommand => _removeTabCommand
?? (_removeTabCommand = new RelayCommand<TabVm>(
tab =>
{
int index = Tabs.IndexOf(tab);
if (index >= 0)
{
Tabs.RemoveAt(index);
if (index >= Tabs.Count)
index = Tabs.Count - 1;
if (index < 0)
SelectedTab = null;
else
SelectedTab = Tabs[index];
}
},
tab => Tabs.Contains(tab)));
}
}

Window XAML:

<Window x:Class="AddTabItem.AddTabExamleWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:AddTabItem"
mc:Ignorable="d"
Title="AddTabExamleWindow" Height="450" Width="800"
DataContext="{DynamicResource viewModel}">
<FrameworkElement.Resources>
<local:TabsCollectionViewModel x:Key="viewModel"/>
<local:TabVm x:Key="newTab"/>
<CollectionViewSource x:Key="tabsCollectionView"
Source="{Binding Tabs}"/>
<CompositeCollection x:Key="tabs">
<CollectionContainer Collection="{Binding Mode=OneWay, Source={StaticResource tabsCollectionView}}"/>
<StaticResource ResourceKey="newTab"/>
</CompositeCollection>
<DataTemplate x:Key="TabItem.HeaderTemplate"
DataType="{x:Type local:TabVm}">
<Grid>
<StackPanel Orientation="Horizontal">
<Panel.Style>
<Style TargetType="StackPanel">
<Style.Triggers>
<DataTrigger Binding="{Binding}" Value="{StaticResource newTab}">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Panel.Style>
<TextBlock Text="{Binding Header}"
Margin="2"/>
<Button Content="❌" FontWeight="Bold" Foreground="Red"
Command="{Binding RemoveTabCommand, Mode=OneWay, Source={StaticResource viewModel}}"
CommandParameter="{Binding Mode=OneWay}"/>
</StackPanel>
<Button Content="✚" FontWeight="Bold" Foreground="Green"
Command="{Binding AddNewTabCommand, Mode=OneWay, Source={StaticResource viewModel}}">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding}" Value="{StaticResource newTab}">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</Grid>
</DataTemplate>
<DataTemplate x:Key="TabItem.ContentTemplate"
DataType="{x:Type local:TabVm}">
<TextBox Text="{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<TextBox.Style>
<Style TargetType="TextBox">
<Style.Triggers>
<DataTrigger Binding="{Binding}" Value="{StaticResource newTab}">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
</DataTemplate>
</FrameworkElement.Resources>
<Grid>
<TabControl ItemsSource="{DynamicResource tabs}"
ItemTemplate="{DynamicResource TabItem.HeaderTemplate}"
ContentTemplate="{DynamicResource TabItem.ContentTemplate}"
SelectedItem="{Binding SelectedTab, Mode=TwoWay}"/>
</Grid>
</Window>

To eliminate ambiguities, I give the codes of the classes used in the example:
BaseInpc:

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace Simplified
{
/// <summary>Base class with implementation of the <see cref="INotifyPropertyChanged"/> interface.</summary>
public abstract class BaseInpc : INotifyPropertyChanged
{
/// <inheritdoc cref="INotifyPropertyChanged"/>
public event PropertyChangedEventHandler PropertyChanged;

/// <summary>The protected method for raising the event <see cref = "PropertyChanged"/>.</summary>
/// <param name="propertyName">The name of the changed property.
/// If the value is not specified, the name of the method in which the call was made is used.</param>
protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

/// <summary> Protected method for assigning a value to a field and raising
/// an event <see cref = "PropertyChanged" />. </summary>
/// <typeparam name = "T"> The type of the field and assigned value. </typeparam>
/// <param name = "propertyFiled"> Field reference. </param>
/// <param name = "newValue"> The value to assign. </param>
/// <param name = "propertyName"> The name of the changed property.
/// If no value is specified, then the name of the method
/// in which the call was made is used. </param>
/// <returns>Returns <see langword="true"/> if the value being assigned
/// was not equal to the value of the field and
/// therefore the value of the field was changed.</returns>
/// <remarks> The method is intended for use in the property setter. <br/>
/// To check for changes,
/// used the <see cref = "object.Equals (object, object)" /> method.
/// If the assigned value is not equivalent to the field value,
/// then it is assigned to the field. <br/>
/// After the assignment, an event is created <see cref = "PropertyChanged" />
/// by calling the method <see cref = "RaisePropertyChanged (string)" />
/// passing the parameter <paramref name = "propertyName" />. <br/>
/// After the event is created,
/// the <see cref = "OnPropertyChanged (string, object, object)" />
/// method is called. </remarks>
protected bool Set<T>(ref T propertyFiled, T newValue, [CallerMemberName] string propertyName = null)
{
bool notEquals = !object.Equals(propertyFiled, newValue);
if (notEquals)
{
T oldValue = propertyFiled;
propertyFiled = newValue;
RaisePropertyChanged(propertyName);

OnPropertyChanged(propertyName, oldValue, newValue);
}
return notEquals;
}

/// <summary> The protected virtual method is called after the property has been assigned a value and after the event is raised <see cref = "PropertyChanged" />. </summary>
/// <param name = "propertyName"> The name of the changed property. </param>
/// <param name = "oldValue"> The old value of the property. </param>
/// <param name = "newValue"> The new value of the property. </param>
/// <remarks> Can be overridden in derived classes to respond to property value changes. <br/>
/// It is recommended to call the base method as the first operator in the overridden method. <br/>
/// If the overridden method does not call the base class, then an unwanted change in the base class logic is possible. </remarks>
protected virtual void OnPropertyChanged(string propertyName, object oldValue, object newValue) { }
}
}

RelayCommand:

using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;

namespace Simplified
{
#region Delegates for WPF Command Methods
public delegate void ExecuteHandler(object parameter);
public delegate bool CanExecuteHandler(object parameter);
#endregion

#region Класс команд - RelayCommand
/// <summary> A class that implements <see cref = "ICommand" />. <br/>
/// Implementation taken from <see href = "https://www.cyberforum.ru/wpf-silverlight/thread2390714-page4.html#post13535649" />
/// and added a constructor for methods without a parameter.</summary>
public class RelayCommand : ICommand
{
private readonly CanExecuteHandler canExecute;
private readonly ExecuteHandler execute;
private readonly EventHandler requerySuggested;

/// <inheritdoc cref="ICommand.CanExecuteChanged"/>
public event EventHandler CanExecuteChanged;

/// <summary> Command constructor. </summary>
/// <param name = "execute"> Command method to execute. </param>
/// <param name = "canExecute"> Method that returns the state of the command. </param>
public RelayCommand(ExecuteHandler execute, CanExecuteHandler canExecute = null)
: this()
{
this.execute = execute ?? throw new ArgumentNullException(nameof(execute));
this.canExecute = canExecute;

requerySuggested = (o, e) => Invalidate();
CommandManager.RequerySuggested += requerySuggested;
}

/// <inheritdoc cref="RelayCommand(ExecuteHandler, CanExecuteHandler)"/>
public RelayCommand(Action execute, Func<bool> canExecute = null)
: this
(
p => execute(),
p => canExecute?.Invoke() ?? true
)
{ }

private RelayCommand()
=> dispatcher = Application.Current.Dispatcher;

private readonly Dispatcher dispatcher;

/// <summary> The method that raises the event <see cref = "CanExecuteChanged" />. </summary>
public void RaiseCanExecuteChanged()
{
if (dispatcher.CheckAccess())
Invalidate();
else
dispatcher.BeginInvoke((Action)Invalidate);
}
private void Invalidate()
=> CanExecuteChanged?.Invoke(this, EventArgs.Empty);

/// <inheritdoc cref="ICommand.CanExecute(object)"/>
public bool CanExecute(object parameter) => canExecute?.Invoke(parameter) ?? true;

/// <inheritdoc cref="ICommand.Execute(object)"/>
public void Execute(object parameter) => execute?.Invoke(parameter);
}
#endregion
}

RelayCommand<T>:

using System;
using System.Windows.Input;
namespace Simplified
{
#region Delegates for WPF Command Methods
public delegate void ExecuteHandler<T>(T parameter);
public delegate bool CanExecuteHandler<T>(T parameter);
#endregion

/// <summary> RelayCommand implementation for generic parameter methods. </summary>
/// <typeparam name = "T"> Method parameter type. </typeparam>
public class RelayCommand<T> : RelayCommand
{
/// <inheritdoc cref="RelayCommand(ExecuteHandler, CanExecuteHandler)"/>
public RelayCommand(ExecuteHandler<T> execute, CanExecuteHandler<T> canExecute = null)
: base
(
p =>
{
if (p is T t)
execute(t);
},
p => (p is T t) && (canExecute?.Invoke(t) ?? true)
)
{ }
}
}

WPF TabControl when changed selected item MediaUriElement hides

I suppose that because normal TabConrol has only one contentPresenter it dicards the previous content.
If so, this should work:
Stop TabControl from recreating its children

How to preserve control state within tab items in a TabControl

The Writer sample application of the WPF Application Framework (WAF) shows how to solve your issue. It creates a new UserControl for every TabItem. So the state is preserved when the user changes the active Tab.

WPF TabControl MVVM ViewModel get instantiated every time i toggle between tabs

Solution to problem is extending TabControl and replacing default behaviour so it will not unload old tabs.
The final solution (with include's both control & control template) is @
Stop TabControl from recreating its children

Thanks for Shoe for pointing me in right direction that lead to final solution :)

WPF TabControl - Preventing Unload on Tab Change?

I found a workaround here: https://web.archive.org/web/20120429044747/http://eric.burke.name/dotnetmania/2009/04/26/22.09.28

Edit: This is the corrected link:
http://web.archive.org/web/20110825185059/http://eric.burke.name/dotnetmania/2009/04/26/22.09.28

It basically stores the ContentPresenter of the tab and loads that up when switching tabs instead of redrawing it. It was still causing the delay when dragging/dropping tabs since that was an remove/add operation, however with some modifications I got that to go away as well (ran the Remove code at a lower dispatcher priority then the Add code, so the add operation had a chance to cancel the Remove operation and use the old ContentPresenter instead of drawing a new one)

Edit: The link above appears to no longer work, so I'll paste a copy of the code here. It's been modified a bit to allow dragging/dropping, but it should still work the same way.

using System;
using System.Windows;
using System.Windows.Threading;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Collections.Specialized;

// Extended TabControl which saves the displayed item so you don't get the performance hit of
// unloading and reloading the VisualTree when switching tabs

// Obtained from http://eric.burke.name/dotnetmania/2009/04/26/22.09.28
// and made a some modifications so it reuses a TabItem's ContentPresenter when doing drag/drop operations

[TemplatePart(Name = "PART_ItemsHolder", Type = typeof(Panel))]
public class TabControlEx : System.Windows.Controls.TabControl
{
// Holds all items, but only marks the current tab's item as visible
private Panel _itemsHolder = null;

// Temporaily holds deleted item in case this was a drag/drop operation
private object _deletedObject = null;

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

/// <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)
{
this.ItemContainerGenerator.StatusChanged -= ItemContainerGenerator_StatusChanged;
UpdateSelectedItem();
}
}

/// <summary>
/// get the ItemsHolder and generate any children
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_itemsHolder = 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"></param>
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);

if (_itemsHolder == null)
{
return;
}

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

if (base.Items.Count > 0)
{
base.SelectedItem = base.Items[0];
UpdateSelectedItem();
}

break;

case NotifyCollectionChangedAction.Add:
case NotifyCollectionChangedAction.Remove:

// Search for recently deleted items caused by a Drag/Drop operation
if (e.NewItems != null && _deletedObject != null)
{
foreach (var item in e.NewItems)
{
if (_deletedObject == item)
{
// If the new item is the same as the recently deleted one (i.e. a drag/drop event)
// then cancel the deletion and reuse the ContentPresenter so it doesn't have to be
// redrawn. We do need to link the presenter to the new item though (using the Tag)
ContentPresenter cp = FindChildContentPresenter(_deletedObject);
if (cp != null)
{
int index = _itemsHolder.Children.IndexOf(cp);

(_itemsHolder.Children[index] as ContentPresenter).Tag =
(item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item));
}
_deletedObject = null;
}
}
}

if (e.OldItems != null)
{
foreach (var item in e.OldItems)
{

_deletedObject = item;

// We want to run this at a slightly later priority in case this
// is a drag/drop operation so that we can reuse the template
this.Dispatcher.BeginInvoke(DispatcherPriority.DataBind,
new Action(delegate()
{
if (_deletedObject != null)
{
ContentPresenter cp = FindChildContentPresenter(_deletedObject);
if (cp != null)
{
this._itemsHolder.Children.Remove(cp);
}
}
}
));
}
}

UpdateSelectedItem();
break;

case 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)


Related Topics



Leave a reply



Submit