Reputation: 2828
I am using a Catel
as my primary MVVM framework. In current application I have splited my UI in several Tabs. Each Tab is loading the appropriate View
. While I am getting expected results I've noticed that by changing each Tab it takes almost 1~3
sec for a View to be shown. Is there a way to speed up this process?
<TabItem Header="Tools">
<catel:StackGrid>
<TabControl>
<TabItem Header="Worker">
<Grid>
<Views:WorkerReportView />
</Grid>
</TabItem>
<TabItem Header="Business">
<Grid>
<Views:BusinessReportView />
</Grid>
</TabItem>
</TabControl>
</catel:StackGrid>
</TabItem>
Upvotes: 1
Views: 1261
Reputation: 22702
I think, in this case the problem covered not in Catel
. A standard TabControl
unloading and reloading the VisualTree of Content
every time when you switching tabs. This means that every time you go to the tab, View is rendered anew, therefore affecting the performance, even more so if View is weighty.
You can see this if you create for the content Loaded
event handler, like this:
<TabControl>
<TabItem Header="Worker">
<ContentControl Name="Content1"
Loaded="Content1_Loaded" />
</TabItem>
<TabItem Header="Business">
<ContentControl Name="Content2"
Loaded="Content2_Loaded"/>
</TabItem>
</TabControl>
These events will be triggered every time when you switch between tabs.
As an alternative, you can try the implementation of TabControl
that loads the content for tabs only once, when the program starts. I found one in this answer:
How to preserve the full state of the View when navigating between Views in an MVVM application?
Here is listing:
Class TabControlEx
[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;
}
}
Style
I have it a bit modified for better display:
<Style TargetType="{x:Type this:TabControlEx}">
<Setter Property="Background" Value="Transparent" />
<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="Left"
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>
Some notes
Personally, I always use a DataTemplate
as View. As it turns more dynamic and less impact on performance because UserControl is a "weighty" control, and I realize it is extremely rare - when it is necessary to create a separate control, eg DatePicker
.
Therefore, as one more piece of advice: if possible, remake your View under DataTemplate and load them into ContentControl
. I think using a DataTemplate
and TabControlEx
can significantly improve the performance of the entire application.
Update
Mr.@Geert van Horrik
in comment mentioned that Catel
does provide this implementation as well as Catel.Windows.Controls.TabControl
. I think you should to check it out first.
Upvotes: 2
Reputation: 5724
There are a lot of ways to improve performance in Catel. You should definitely take a look at the Performance Considerations.
In the latest version (nightly build, is the upcoming 4.0 version) the team spent a lot of time tweaking performance and introducing a new feature called ApiCop. This feature will generate an advisory report when a debugger is attached, especially when loading views and features are enabled (by default) which aren't being used.
Upvotes: 3