Jim
Jim

Reputation: 2828

Performance when using TabControl in Catel MVVM WPF

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

Answers (2)

Anatoliy Nikolaev
Anatoliy Nikolaev

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

Geert van Horrik
Geert van Horrik

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

Related Questions