How do I prerender the controls on a TabItem in WPF?
C# XBap Application
I have a TabControl with four TabItems in it. Two of these TabItems simply contain a DataGrid from the WPFToolkit which pulls a fairly small datatable (100 rows by 4 columns) from a SQL Server database. My problem is that when I load my application and I click on 开发者_高级运维one of the TabItems containing a datagrid. There seems to me a 2-3 second pause before it brings that tab into focus. This only happens the very first time the tab is clicked. It seems to be the rendering of the datagrid.
How can I make those tabs prerender when the application loads so that when a user clicks a tab, there is not that initial pause of 2-3 seconds before the tab shows up.
Thanks
We use the standard WPF TabControl and the problem is that is trashes the VisualTree each time you change the SelectedItem.
What we ended up doing was creating a special TabControl (I called it TabControlEx) which keeps all items rendered but chooses to simply show/hide the ContentPresenters for the TabItems.
Here is the relevant code
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
}
}
Where you would template it something like this (you may need to expand it for Left/Right 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>
Which you could use like this
<local:TabControlEx
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Path=Workspaces}"
Template="{StaticResource MainTabControlTemplateEx}">
</local:TabControlEx>
It works very well and we have been using it to great effect for a long time now
The problem is not getting the data (you could preload that in a seperate thread) but actually the building of the visual items in the datagrid.
If you check with Snoop you can see that there are a lot of visual items, if you don't need all the datagrid functionality you could go with a simpler representation (ListView/ItemsControl/Custom)
Load the Tab content dynamically on selection, for UI to be responsive, use code similar to below:
private void tab_Selected(object sender, EventArgs e)
{
//Get the selected tab
Action loadTab = delegate
{
LoadSelectedTab(tabItem);
}
Dispatcher.BeginInvoke(DispatcherPriority.Background, loadTab);
}
public void LoadSelectedTab(TabItem item)
{
item.Content = new EmployeeTab();
.....
}
The UI repsonse will be very fast, UI start loading very fast and you don't see pause for any delays
Can't add comments but want to thank sacha for the answer and widen it a bit. There was a problem with loading of that TabControl, so first tab was not displayed (not present in a visual tree). Adding following code to "Public/Protected Methods" region solves the issue.
/// <summary>
/// There was a flaky issue when first tab was uninitialized
/// </summary>
/// <param name="oldValue"></param>
/// <param name="newValue"></param>
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
base.OnItemsSourceChanged(oldValue, newValue);
UpdateSelectedItem();
}
精彩评论