Мы используем стандартный WPF TabControl, и проблема в том, что он очищает VisualTree каждый раз, когда вы меняете SelectedItem.
В итоге мы создали специальный TabControl (я назвал его TabControlEx), который сохраняет все элементы отображаемыми, но выбирает просто показать / скрыть ContentPresenters для TabItems.
Вот соответствующий код
using System;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
namespace MVVM.Demo
{
/// <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 += ItemContainerGenerator_StatusChanged;
this.Loaded += TabControlEx_Loaded;
}
#endregion
#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;
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();
break;
case NotifyCollectionChangedAction.Add:
case NotifyCollectionChangedAction.Remove:
if (e.OldItems != null)
{
foreach (var item in e.OldItems)
{
ContentPresenter cp = FindChildContentPresenter(item);
if (cp != null)
{
itemsHolder.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");
}
}
/// <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 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;
}
#endregion
#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>
private 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>
private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
if (this.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
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 = GetSelectedTabItem();
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(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));
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
}
}
Где бы вы шаблонировали его примерно так (вам может понадобиться развернуть его для левой / правой вкладки TabStripLocation)
<ControlTemplate x:Key="MainTabControlTemplateEx"
TargetType="{x:Type controls:TabControlEx}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition x:Name="row0" Height="Auto"/>
<RowDefinition x:Name="row1" Height="4"/>
<RowDefinition x:Name="row2" Height="*"/>
</Grid.RowDefinitions>
<TabPanel x:Name="tabpanel"
Background="{StaticResource OutlookButtonHighlight}"
Margin="0"
Grid.Row="0"
IsItemsHost="True" />
<Grid x:Name="divider"
Grid.Row="1" Background="Black"
HorizontalAlignment="Stretch"/>
<Grid x:Name="PART_ItemsHolder"
Grid.Row="2"/>
</Grid>
<!-- no content presenter -->
<ControlTemplate.Triggers>
<Trigger Property="TabStripPlacement" Value="Top">
<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>
Который вы могли бы использовать вот так
<local:TabControlEx
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Path=Workspaces}"
Template="{StaticResource MainTabControlTemplateEx}">
</local:TabControlEx>
Он работает очень хорошо, и мы уже давно используем его для получения эффекта.