ListBox in TabControl scrolling issue - wpf

I have an issue with WPF ListBox in TabControl. ListBox resets it's scrollbar position to 0 when I change tabs. Here is repro code:
<TabControl x:Name="_tabs">
<TabItem Header="1">
<ListBox ItemsSource="{Binding}" ScrollViewer.VerticalScrollBarVisibility="Auto"/>
</TabItem>
<TabItem Header="2">
<ListBox ItemsSource="{Binding}" ScrollViewer.VerticalScrollBarVisibility="Auto"/>
</TabItem>
</TabControl>
_tabs.DataContext = Enumerable.Range(1, 300).ToArray();
When the window opens I open the second tab, scroll list somewhere to the middle, return to the first tab and then open the second again. For some reason the list is scrolled to the top.
Why this happens? Have I made some stupid mistake?

The default behavior of WPF is to unload items which are not visible, which includes unloading TabItems which are not visible. This means when you go back to the tab, the TabItem gets re-loaded, and anything not bound (such as a scroll position) will get reset.
There was is a good site here which contains code to extend the TabControl and stop it from destroying it's TabItems when switching tabs, however the site appears to be down atm.
Here's the code I use. It initially was from that site, although I've made some changes to it. It preserves the ContentPresenter of TabItems when switching tabs, and uses it to redraw the TabItem when you go back to the page. It takes up a bit more memory, however I find it better on performance since the TabItem no longer has to re-create all the controls that were on it.
// 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)
{
base.OnSelectionChanged(e);
UpdateSelectedItem();
}
/// <summary>
/// generate a ContentPresenter for the selected item
/// </summary>
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>
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>
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;
}
/// <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;
}
if (_deletedObject == selectedItem)
{
}
TabItem item = selectedItem as TabItem;
if (item == null)
{
item = base.ItemContainerGenerator.ContainerFromIndex(base.SelectedIndex) as TabItem;
}
return item;
}
}
The TabControl template I usually use looks something like this:
<Style x:Key="TabControlEx_NoHeadersStyle" TargetType="{x:Type local:TabControlEx}">
<Setter Property="SnapsToDevicePixels" Value="true"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type localControls:TabControlEx}">
<DockPanel>
<!-- This is needed to draw TabControls with Bound items -->
<StackPanel IsItemsHost="True" Height="0" Width="0" />
<Grid x:Name="PART_ItemsHolder" />
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

Great answer! Cheers Rachel! I can't comment on existing posts so have added my own answer.
I found the above managed to solve the problem. However I also found it necessary to add:
public TabControlEx()
{
Loaded += delegate { UpdateSelectedItem(); };
}
to get the initial selected tab to load correctly - the ItemContainerGenerator had no contents and so GetSelectedTabItem failed to return a TabItem. Presumably this is something to do with rendering which has yet to happen when the template is applied.
I found this problem manifested itself only when binding the ItemsSource and SelectedItem of a tab control - in an application I am working on we have recently switched to using this so we could programmatically switch tabs to do a bit of custom navigation.
The Tab Item contents were originally specified by hand eg:
<TabItem Header="blah">
<someControl/>
</TabItem>
With that setup changing the selected item was fine (didn't reload tabs on selection) although it was only ever done by wpf. This seems to suggest that this behaviour can be turned off in the default control but only when not binding to a list of items (although I didn't confirm this to be the case).
Another solutions I attempted were changing the ItemSource binding Mode to OneTime but this did not solve the problem (we aren't using an INotifyCollectionChanged collection here anyway).

Related

Tab create new row and focus new row in DataGrid

I have some problems with my data grid. My project is transforming a Delphi project to .Net. The product owner want the same behaviour for the datagrids.
When positioned on the last cell and tab or enter is hit, the following should happen:
A new row is added
The first cell in the new row is selected
Other demands for the datagrid is:
The focus should remain inside the datagrid once it has the focus (ALT + key combinations is the way to leave the datagrid again).
The datagrid is databound
The datagrid is used in MVVM
We use the .net4.0 full profile
This article had the best solution I could find.
I preferred to use an attached property rather than a behavior, since this enabled me to set it easily in the default style for DataGrid. Here's the code:
namespace SampleDataGridApp
{
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
/// <summary>
/// An attached behavior that modifies the tab behavior for a <see cref="DataGrid"/>.
/// </summary>
public static class DataGridBehavior
{
/// <summary>
/// Identifies the <c>NewLineOnTab</c> attached property.
/// </summary>
public static readonly DependencyProperty NewLineOnTabProperty = DependencyProperty.RegisterAttached(
"NewLineOnTab",
typeof(bool),
typeof(DataGridBehavior),
new PropertyMetadata(default(bool), OnNewLineOnTabChanged));
/// <summary>
/// Sets the value of the <c>NewLineOnTab</c> attached property.
/// </summary>
/// <param name="element">The <see cref="DataGrid"/>.</param>
/// <param name="value">A value indicating whether to apply the behavior.</param>
public static void SetNewLineOnTab(DataGrid element, bool value)
{
element.SetValue(NewLineOnTabProperty, value);
}
/// <summary>
/// Gets the value of the <c>NewLineOnTab</c> attached property.
/// </summary>
/// <param name="element">The <see cref="DataGrid"/>.</param>
/// <returns>A value indicating whether to apply the behavior.</returns>
public static bool GetNewLineOnTab(DataGrid element)
{
return (bool)element.GetValue(NewLineOnTabProperty);
}
/// <summary>
/// Called when the value of the <c>NewLineOnTab</c> property changes.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private static void OnNewLineOnTabChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
DataGrid d = sender as DataGrid;
if (d == null)
{
return;
}
bool newValue = (bool)e.NewValue;
bool oldValue = (bool)e.OldValue;
if (oldValue == newValue)
{
return;
}
if (oldValue)
{
d.PreviewKeyDown -= AssociatedObjectKeyDown;
}
else
{
d.PreviewKeyDown += AssociatedObjectKeyDown;
KeyboardNavigation.SetTabNavigation(d, KeyboardNavigationMode.Contained);
}
}
/// <summary>
/// Handles the <see cref="UIElement.KeyDown"/> event for a <see cref="DataGridCell"/>.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private static void AssociatedObjectKeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Tab)
{
return;
}
DataGrid dg = e.Source as DataGrid;
if (dg == null)
{
return;
}
if (dg.CurrentColumn.DisplayIndex == dg.Columns.Count - 1)
{
var icg = dg.ItemContainerGenerator;
if (dg.SelectedIndex == icg.Items.Count - 2)
{
dg.CommitEdit(DataGridEditingUnit.Row, false);
}
}
}
}
}
My default style looks like this:
<Style TargetType="DataGrid">
<Setter Property="GridLinesVisibility" Value="None" />
<Setter Property="KeyboardNavigation.TabNavigation" Value="Contained" />
<Setter Property="sampleDataGridApp:DataGridBehavior.NewLineOnTab" Value="True" />
<Setter Property="IsSynchronizedWithCurrentItem" Value="True" />
</Style>
If the last column's DataGridCell has it's IsTabStop set to false like in this example the above will not work.
Here is a buggy workaround:
private static void AssociatedObjectKeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Tab)
{
return;
}
DataGrid dg = e.Source as DataGrid;
if (dg == null)
{
return;
}
int offSet = 1;
var columnsReversed = dg.Columns.Reverse();
foreach (var dataGridColumn in columnsReversed)
{
// Bug: This makes the grand assumption that a readonly column's "DataGridCell" has IsTabStop == false;
if (dataGridColumn.IsReadOnly)
{
offSet++;
}
else
{
break;
}
}
if (dg.CurrentColumn.DisplayIndex == (dg.Columns.Count - offSet))
{
var icg = dg.ItemContainerGenerator;
if (dg.SelectedIndex == icg.Items.Count - 2)
{
dg.CommitEdit(DataGridEditingUnit.Row, false);
}
}
}
Ok, I have been fighting this problem for many hours now. I have tried nearly every proposed solution out there and here is what I found that works for me...
private void grid_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Tab)
{
if (grid.SelectedIndex == grid.Items.Count - 2 && grid.CurrentColumn.DisplayIndex == grid.Columns.Count - 1)
{
grid.CommitEdit(DataGridEditingUnit.Row, false);
e.Handled = true;
}
}
}
private void DataGrid_RowEditEnding(object sender, DataGridRowEditEndingEventArgs e)
{
if (grid.SelectedIndex == grid.Items.Count - 2)
{
grid.SelectedIndex = grid.Items.Count - 1;
grid.CurrentCell = new DataGridCellInfo(grid.Items[grid.Items.Count - 1], grid.Columns[0]);
}
}
What this code does is when you tab to the last cell of the last row and press tab it will move focus to the first cell of the new row. Which is what you would expect but this is not default behavior. The default behavior is to move focus to the next control and not commit the current row edit. This is clearly a bug in the DataGrid I believe which is why all the proposed solutions have a whiff of kluge. My solution doesn't smell that good I admit, but if you agree that this is bug, I prefer this to the ridiculous default behavior.
This solution works even if the grid is sorted. The newly entered row will sort to the proper place but the focus will be put on the first column of the new row.
The only unsolved issue is that when tabbing down from the top to the last cell before the new row, tab must be entered twice before focus is moved to the new row. I looked into this quirk for a bit and finally gave up on it.

How to preserve the full state of the View when navigating between Views in an MVVM application?

I have an MVVM application that requires basic backward/forward navigation between screens. Currently, I have implemented this using a WorkspaceHostViewModel that tracks the current workspace and exposes the necessary navigation commands as follows.
public class WorkspaceHostViewModel : ViewModelBase
{
private WorkspaceViewModel _currentWorkspace;
public WorkspaceViewModel CurrentWorkspace
{
get { return this._currentWorkspace; }
set
{
if (this._currentWorkspace == null
|| !this._currentWorkspace.Equals(value))
{
this._currentWorkspace = value;
this.OnPropertyChanged(() => this.CurrentWorkspace);
}
}
}
private LinkedList<WorkspaceViewModel> _navigationHistory;
public ICommand NavigateBackwardCommand { get; set; }
public ICommand NavigateForwardCommand { get; set; }
}
I also have a WorkspaceHostView that binds to the WorkspaceHostViewModel as follows.
<Window x:Class="MyNavigator.WorkspaceHostViewModel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Window.Resources>
<ResourceDictionary Source="../../Resources/WorkspaceHostResources.xaml" />
</Window.Resources>
<Grid>
<!-- Current Workspace -->
<ContentControl Content="{Binding Path=CurrentWorkspace}"/>
</Grid>
</Window>
In the WorkspaceHostResources.xaml file, I associate the View that WPF should use to render each WorkspaceViewModel using DataTemplates.
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyNavigator">
<DataTemplate DataType="{x:Type local:WorkspaceViewModel1}">
<local:WorkspaceView1/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:WorkspaceViewModel2}">
<local:WorkspaceView2/>
</DataTemplate>
</ResourceDictionary>
This works pretty well, but one disadvantage is that the Views are recreated between each navigation due to the mechanics of DataTemplates. If the view contains complex controls, like DataGrids or TreeViews, their internal state is lost. For example if I have a DataGrid with expandable and sortable rows, the expand/collapse state and sort order is lost when the user navigates to the next screen and then back to the DataGrid screen. In most cases it would be possible to track each piece of state information that needs to be preserved between navigations, but it seems like a very inelegant approach.
Is there a better way to preserve the entire state of a view between navigation events that change the entire screen?
I had the same issue, and I ended up using some code I found online that extends a TabControl to stop it from destorying it's children when switching tabs. I usually overwrite the TabControl template to hide the tabs, and I'll just use the SelectedItem to define what "workspace" should be currently visible.
The idea behind it is that the ContentPresenter of each TabItem gets cached when switching to a new item, then when you switch back it re-loads the cached item instead of re-creating it
<local:TabControlEx ItemsSource="{Binding AvailableWorkspaces}"
SelectedItem="{Binding CurrentWorkspace}"
Template="{StaticResource BlankTabControlTemplate}" />
The site the code was on seems to have been taken down, however here's the code I use. It's been modified a little bit from the original.
// 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://www.pluralsight-training.net/community/blogs/eburke/archive/2009/04/30/keeping-the-wpf-tab-control-from-destroying-its-children.aspx
// 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)
{
base.OnSelectionChanged(e);
UpdateSelectedItem();
}
/// <summary>
/// generate a ContentPresenter for the selected item
/// </summary>
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>
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>
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;
}
/// <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;
}
if (_deletedObject == selectedItem)
{
}
TabItem item = selectedItem as TabItem;
if (item == null)
{
item = base.ItemContainerGenerator.ContainerFromIndex(base.SelectedIndex) as TabItem;
}
return item;
}
}
I ended up adding an ActiveWorkspaces ObservableCollection property to the WorkspaceHostViewModel and binding an ItemsControl to it as follows.
<!-- Workspace -->
<ItemsControl ItemsSource="{Binding Path=ActiveWorkspaces}">
<ItemsControl.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="{x:Type ContentPresenter}">
<Setter Property="Visibility" Value="{Binding Visible, Converter={StaticResource BooleanToVisibilityConverter}}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
The ActiveWorkspaces property contains all of the workspaces in the navigation history. They all get rendered on top of one another in the UI, but by binding the Visibility of their respective ContentPresenter I am able to show only one at a time.
The logic that manipulates the Visible property (which is a new property in the Workspace itself) exists in the navigate forward/backward commands.
This is a very similar approach to the solution proposed by Rachel and is in part based on the ItemsControl tutorial found on her web site; however, I opted to write the show/hide logic myself rather than rely on a subclassed TabControl to do it for me. I still feel that it would be possible to improve the show/hide logic. Specifically I would like to eliminate the Visible property from the Workspace class, but for now this works well enough.
UPDATE:
After using the above solution successfully for several months, I opted to replace it with the view-based navigation functionality provided by Prism. Although this approach requires much more overhead, the advantages greatly outweigh the effort involved. The general idea is to define Region's in your Views and then navigate by calling regionManager.RequestNavigate("RegionName", "navigationUri") in your ViewModel. Prism handles the legwork of instantiating, initializing and displaying your View in the specified Region. Additionally, you can control the lifetime of your View, whether or not it should be re-used upon subsequent navigation requests, what logic should be performed on navigation to and on navigation from events, and whether or not navigation should be aborted (due to unsaved changes in current View, etc.) Note that Prism view-based navigation requires a Dependency Injection Container (such as Unity or MEF) so you will likely need to incorporate this into your application architecture, but even without Prism navigation, adopting a DI container is well worth the investment.
For the TabControlEx to work you must also apply the control template, which was not presented in answer here.
You can find it # Stop TabControl from recreating its children
I have managed to fix it without using TabControlEx (cause it didn't work for me either).
I've used Datatemplates and templateselector in order to switch between tabs.
Xaml:
<Window.Resources>
<local:MainTabViewDataTemplateSelector x:Key="myMainContentTemplateSelector" />
<DataTemplate x:Key="Dashboard">
<views:DashboardView />
</DataTemplate>
<DataTemplate x:Key="SystemHealth">
<views:SystemHealthView />
</DataTemplate>
</Window.Resources>
<TabControl ItemsSource="{Binding MainTabs}"
Margin="0,33,0,0"
Grid.RowSpan="2"
SelectedIndex="0"
Width="auto"
Style="{DynamicResource TabControlStyleMain}"
ContentTemplateSelector="{StaticResource myMainContentTemplateSelector}"
Padding="20" Grid.ColumnSpan="2"
VerticalAlignment="Stretch">
<TabControl.Background>
<ImageBrush ImageSource="/SystemHealthAndDashboard;component/Images/innerBackground.png"/>
</TabControl.Background>
<TabControl.ItemTemplate>
<DataTemplate >
<TextBlock Grid.Column="0" Text="{Binding Name}" VerticalAlignment="Center" HorizontalAlignment="Left"/>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
The DataTemplateSelector:
public class MainTabViewDataTemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
FrameworkElement element = container as FrameworkElement;
switch ((item as TabInfoEntity).TabIndex)
{
case 1:
{
return element.FindResource("Dashboard") as DataTemplate;
}
case 2:
{
return element.FindResource("SystemHealth") as DataTemplate;
}
}
return null;
}
}
TabInfoEntity class (list of objects of this type are the itemsource of the TabControl):
public class TabInfoEntity
{
public TabInfoEntity()
{
}
private string name;
public string Name
{
get { return name; }
set { name = value; }
}
private int tabindex;
public int TabIndex
{
get { return tabindex; }
set { tabindex = value; }
}
}
I may be missing the point, but any important view state could (or maybe even should) be stored in the ViewModel. It kind of depends on how much there is, and how dirty you are willing to get.
If that's not palatable (from a purist perspective it may not sit with what you're doing), you could bind those not-quite-VM-able parts of the view it to a separate class containing the state (call them ViewState classes perhaps?).
If they truly are view-only properties and you don't want to take either of those routes, then they are where they belong, in the view. You should instead work out a way of not recreating the view each time: use a factory rather than built-in datatemplating, for example. If you go the DataTemplateSelector you get to return a template I believe, perhaps there's a way to re-use instances of the view there? (I would have to check..)

WPF TabControl - Preventing Unload on Tab Change?

Is there a way to prevent the Tab Unload/Reload when a tab changes in a WPF tab control? Or if that is not possible, is there a recommended method for caching the tabs contents so they don't have to be regenerated with each tab change?
For example, one tab's UI is completely customizable and stored in the database. When the user selects an object to work on, the items in the customized layout get populated with that object's data. Users expect a minor delay on initial load or when retrieving data, but not when changing back and forth between tabs, and the delay when changing tabs is very noticeable.
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)
{
base.OnSelectionChanged(e);
UpdateSelectedItem();
}
/// <summary>
/// generate a ContentPresenter for the selected item
/// </summary>
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>
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>
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;
}
/// <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;
}
if (_deletedObject == selectedItem)
{
}
TabItem item = selectedItem as TabItem;
if (item == null)
{
item = base.ItemContainerGenerator.ContainerFromIndex(base.SelectedIndex) as TabItem;
}
return item;
}
}
Just to add to this, I had a similar problem and managed to solve it by caching a user control that represented the contents of a tab item in the code behind.
In my project I have a tab control that is bound to a collection (MVVM). However the first tab is an overview that shows a summary of all of the other tabs in a list view. The problem I was having was that whenever a user moves their selection from an item tab to the overview tab, the overview is redrawn with all of the summary data, which can take 10-15 seconds depending on the number of items in the collection. (note their is no reloading of actual data from a db or anything, it is purely the drawing of the summary view that was taking the time).
What I wanted was for this loading of the summary view to only occur once when the data context is first loaded and any subsequent switching between tabs to be instantaneous.
Solution:
Classes involved:
MainWindow.xaml - The main page containing the tab control.
MainWindow.xaml.cs - Code behind for above.
MainWindowViewModel.cs - View model for the above view, contains the collection.
Overview.xaml - User control that draws the overview tab item content.
OverviewViewModel.cs - View model for the above view.
Steps:
Replace the datatemplate in 'MainWindow.xaml' that draws the overview tab item with a blank user control named 'OverviewPlaceholder'
Make the reference to 'OverviewViewModel' public within 'MainWindowViewModel.cs'
Add a static reference to 'Overview' in 'MainWindow.xaml.cs'
Add an event handler to the loaded event of user control 'OverviewPlaceholder', within this method instantiate the static reference to 'Overview' only if it is null, set the datacontext of this reference to the 'OverviewViewModel' reference within the current datacontext (that is 'MainWindowViewModel') and set the place holder's content to be the static reference to 'Overview'.
Now the overview page is only drawn once because each time it is loaded (i.e. the user clicks onto the overview tab), it puts the already rendered, static user control back onto the page.
I have a really simple solution to avoid the tab reload on tab change,
use a contentPresenter in the tabItem instead of the content property.
e.g.(in MVVM style)
replace
<TabItem Header="Tab1" Content="{Binding Tab1ViewModel}" />
by
<TabItem Header="Tab1">
<ContentPresenter Content="{Binding Tab1ViewModel}" />
</TabItem>

TabControl.Items.Remove - Modest Delay + Memory Leak?

I have a TabControl in WPF.
When I remove a certain TabItem from the collection, depending on the content displayed in the TabItem, there can be a noticeable two or three second delay, before the TabItem is removed.
Any suggestions on how to improve this so that the delay is not noticeable?
Also - there appears to be a memory leak - as the memory retained by the WPF application does not decrease when the TabItem is removed. Anybody experience similar behavior?
Thanks!
Chris
Do you get the same delay when using blank tab items? And do you get the same delay when switching tabs?
WPF unloads the VisualTree of the tabs that are not currently being displayed. If you switch tabs, it unloads the one you just had and reloads the new tab. Depending on how complex your TabItems are, this could be the cause of your delay.
I ran into the same problem and found an alternative here. It basically stores the ContextPresenter of tab items in memory, and loads those up instead of redrawing the VisualTree
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.
// 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)
{
base.OnSelectionChanged(e);
UpdateSelectedItem();
}
/// <summary>
/// generate a ContentPresenter for the selected item
/// </summary>
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>
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>
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;
}
/// <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;
}
if (_deletedObject == selectedItem)
{
}
TabItem item = selectedItem as TabItem;
if (item == null)
{
item = base.ItemContainerGenerator.ContainerFromIndex(base.SelectedIndex) as TabItem;
}
return item;
}
}

How to preserve control state within tab items in a TabControl

I am a newcomer to WPF, attempting to build a project that follows the recommendations of Josh Smith's excellent article describing The Model-View-ViewModel Design Pattern.
Using Josh's sample code as a base, I have created a simple application that contains a number of "workspaces", each represented by a tab in a TabControl. In my application, a workspace is a document editor that allows a hierarchical document to be manipulated via a TreeView control.
Although I have succeeded in opening multiple workspaces and viewing their document content in the bound TreeView control, I find that the TreeView "forgets" its state when switching between tabs. For example, if the TreeView in Tab1 is partially expanded, it will be shown as fully collapsed after switching to Tab2 and returning to Tab1. This behaviour appears to apply to all aspects of control state for all controls.
After some experimentation, I have realized that I can preserve state within a TabItem by explicitly binding each control state property to a dedicated property on the underlying ViewModel. However, this seems like a lot of additional work, when I simply want all my controls to remember their state when switching between workspaces.
I assume I am missing something simple, but I am not sure where to look for the answer. Any guidance would be much appreciated.
Thanks,
Tim
Update:
As requested, I will attempt to post some code that demonstrates this problem. However, since the data that underlies the TreeView is complex, I will post a simplified example that exhibits the same symtoms. Here is the XAML from the main window:
<TabControl IsSynchronizedWithCurrentItem="True" ItemsSource="{Binding Path=Docs}">
<TabControl.ItemTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding Path=Name}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<view:DocumentView />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
The above XAML correctly binds to an ObservableCollection of DocumentViewModel, whereby each member is presented via a DocumentView.
For the simplicity of this example, I have removed the TreeView (mentioned above) from the DocumentView and replaced it with a TabControl containing 3 fixed tabs:
<TabControl>
<TabItem Header="A" />
<TabItem Header="B" />
<TabItem Header="C" />
</TabControl>
In this scenario, there is no binding between the DocumentView and the DocumentViewModel. When the code is run, the inner TabControl is unable to remember its selection when the outer TabControl is switched.
However, if I explicitly bind the inner TabControl's SelectedIndex property ...
<TabControl SelectedIndex="{Binding Path=SelectedDocumentIndex}">
<TabItem Header="A" />
<TabItem Header="B" />
<TabItem Header="C" />
</TabControl>
... to a corresponding dummy property on the DocumentViewModel ...
public int SelecteDocumentIndex { get; set; }
... the inner tab is able to remember its selection.
I understand that I can effectively solve my problem by applying this technique to every visual property of every control, but I am hoping there is a more elegant solution.
I got resolve it with this tip WPF TabControl: Turning Off Tab Virtualization at http://www.codeproject.com/Articles/460989/WPF-TabControl-Turning-Off-Tab-Virtualization this a class of TabContent with property IsCached.
i had the same problem and found a nice solution you can use it like a normal TabControl as far as i tested it. In case it it important for you here the Current License
Here the Code on case the Link goes down:
using System;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
namespace CefSharp.Wpf.Example.Controls
{
/// <summary>
/// Extended TabControl which saves the displayed item so you don't get the performance hit of
/// unloading and reloading the VisualTree when switching tabs
/// </summary>
/// <remarks>
/// Based on example from http://stackoverflow.com/a/9802346, which in turn is based on
/// http://www.pluralsight-training.net/community/blogs/eburke/archive/2009/04/30/keeping-the-wpf-tab-control-from-destroying-its-children.aspx
/// with some modifications so it reuses a TabItem's ContentPresenter when doing drag/drop operations
/// </remarks>
[TemplatePart(Name = "PART_ItemsHolder", Type = typeof(Panel))]
public class NonReloadingTabControl : TabControl
{
private Panel itemsHolderPanel;
public NonReloadingTabControl()
{
// 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 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 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 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()
{
var selectedItem = SelectedItem;
if (selectedItem == null)
return null;
var item = selectedItem as TabItem ?? ItemContainerGenerator.ContainerFromIndex(SelectedIndex) as TabItem;
return item;
}
}
}
License at Copietime
// Copyright © 2010-2016 The CefSharp Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//
// * Neither the name of Google Inc. nor the name Chromium Embedded
// Framework nor the name CefSharp nor the names of its contributors
// may be used to endorse or promote products derived from this software
// without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Based on #Arsen's answer above, here is another behavior, that:
Doesn't need any additional references. (unless you put the code in an external library)
It doesn't use a base class.
It handles both Reset and Add collection changes.
To use it
Declare the namespace in the xaml:
<ResourceDictionary
...
xmlns:behaviors="clr-namespace:My.Behaviors;assembly=My.Wpf.Assembly"
...
>
Update the style:
<Style TargetType="TabControl" x:Key="TabControl">
...
<Setter Property="behaviors:TabControlBehavior.DoNotCacheControls" Value="True" />
...
</Style>
Or update the TabControl directly:
<TabControl behaviors:TabControlBehavior.DoNotCacheControls="True" ItemsSource="{Binding Tabs}" SelectedItem="{Binding SelectedTab}">
And here is the code for the behavior:
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
namespace My.Behaviors
{
/// <summary>
/// Wraps tab item contents in UserControl to prevent TabControl from re-using its content
/// </summary>
public class TabControlBehavior
{
private static readonly HashSet<TabControl> _tabControls = new HashSet<TabControl>();
private static readonly Dictionary<ItemCollection, TabControl> _tabControlItemCollections = new Dictionary<ItemCollection, TabControl>();
public static bool GetDoNotCacheControls(TabControl tabControl)
{
return (bool)tabControl.GetValue(DoNotCacheControlsProperty);
}
public static void SetDoNotCacheControls(TabControl tabControl, bool value)
{
tabControl.SetValue(DoNotCacheControlsProperty, value);
}
public static readonly DependencyProperty DoNotCacheControlsProperty = DependencyProperty.RegisterAttached(
"DoNotCacheControls",
typeof(bool),
typeof(TabControlBehavior),
new UIPropertyMetadata(false, OnDoNotCacheControlsChanged));
private static void OnDoNotCacheControlsChanged(
DependencyObject depObj,
DependencyPropertyChangedEventArgs e)
{
var tabControl = depObj as TabControl;
if (null == tabControl)
return;
if (e.NewValue is bool == false)
return;
if ((bool)e.NewValue)
Attach(tabControl);
else
Detach(tabControl);
}
private static void Attach(TabControl tabControl)
{
if (!_tabControls.Add(tabControl))
return;
_tabControlItemCollections.Add(tabControl.Items, tabControl);
((INotifyCollectionChanged)tabControl.Items).CollectionChanged += TabControlUcWrapperBehavior_CollectionChanged;
}
private static void Detach(TabControl tabControl)
{
if (!_tabControls.Remove(tabControl))
return;
_tabControlItemCollections.Remove(tabControl.Items);
((INotifyCollectionChanged)tabControl.Items).CollectionChanged -= TabControlUcWrapperBehavior_CollectionChanged;
}
private static void TabControlUcWrapperBehavior_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
var itemCollection = (ItemCollection)sender;
var tabControl = _tabControlItemCollections[itemCollection];
IList items;
if (e.Action == NotifyCollectionChangedAction.Reset)
{ /* our ObservableArray<T> swops out the whole collection */
items = (ItemCollection)sender;
}
else
{
if (e.Action != NotifyCollectionChangedAction.Add)
return;
items = e.NewItems;
}
foreach (var newItem in items)
{
var ti = tabControl.ItemContainerGenerator.ContainerFromItem(newItem) as TabItem;
if (ti != null)
{
var userControl = ti.Content as UserControl;
if (null == userControl)
ti.Content = new UserControl { Content = ti.Content };
}
}
}
}
}
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.
Using the idea of WAF I come to this simple solution which seems to solve the issue.
I use Interactivity Behavior, but the same can be done with attached property if Interactivity library is not referenced
/// <summary>
/// Wraps tab item contents in UserControl to prevent TabControl from re-using its content
/// </summary>
public class TabControlUcWrapperBehavior
: Behavior<UIElement>
{
private TabControl AssociatedTabControl { get { return (TabControl) AssociatedObject; } }
protected override void OnAttached()
{
((INotifyCollectionChanged)AssociatedTabControl.Items).CollectionChanged += TabControlUcWrapperBehavior_CollectionChanged;
base.OnAttached();
}
protected override void OnDetaching()
{
((INotifyCollectionChanged)AssociatedTabControl.Items).CollectionChanged -= TabControlUcWrapperBehavior_CollectionChanged;
base.OnDetaching();
}
void TabControlUcWrapperBehavior_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action != NotifyCollectionChangedAction.Add)
return;
foreach (var newItem in e.NewItems)
{
var ti = AssociatedTabControl.ItemContainerGenerator.ContainerFromItem(newItem) as TabItem;
if (ti != null && !(ti.Content is UserControl))
ti.Content = new UserControl { Content = ti.Content };
}
}
}
And usage
<TabControl ItemsSource="...">
<i:Interaction.Behaviors>
<controls:TabControlUcWrapperBehavior/>
</i:Interaction.Behaviors>
</TabControl>
I have posted an answer for similar question. In my case manually creating the TabItems have solved the problem of creating the View again and again. Check here

Resources