Encountering lag in WPF Tab Control when switching between tabs (new instance of view is created each time I switch tabs)

StackOverflow https://stackoverflow.com/questions/23252643

  •  08-07-2023
  •  | 
  •  

Question

I am a beginner with WPF, and I am having to do a lot of learning while coding, so please bear with me. I have searched the archives, and couldn't really find any answer that fits my question.

I currently have a view that contains a tab control, which is initially loaded with 2 default tabs, and then additional tabs are added and removed programmatically based on user actions.

The way I do this is by having an observable collection of ViewModels for the different types of Views that I can add and remove.

In my Xaml for the view, I had a few different DataTemplates that are used to determine what kind of view to load.

The issue I'm facing is that when I click from one tab to another, I am seeing a lag - close to one second. I put a break point in the code behind for the view, and noticed that the InitializeComponent() method is being called for a view every time I click on that tab.

The collection of ViewModels is called ActiveContents, which holds items of type ActiveContent. The ActiveContent class is one I created, and it contains an object named ContentItem which will hold the ViewModel.

Here are is some of the code from my project:

One of the data templates from the xaml:

<UserControl.Resources>
    <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
        .
        .
        .
            </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>

        <DataTemplate DataType="{x:Type viewmodels:InstallQueueViewModel}">
            <local:InstallQueueView DataContext="{Binding Path=DataContext.InstallQueueVM, 
                RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" />
        </DataTemplate>
    .
    .
    .
        <DataTemplate x:Key="DataTemplateTabControlContent">
            <ContentControl Content="{Binding ContentItem}"></ContentControl>
        </DataTemplate>
</UserControl.Resources>

<Grid>
    <TabControl ItemsSource="{Binding ActiveContents}"
                SelectedIndex="{Binding SelectedTab, Mode=TwoWay}"
                IsSynchronizedWithCurrentItem="True"
                ItemContainerStyle="{DynamicResource TabItemContainerStyleSpacedMedium}"
                ItemTemplate="{StaticResource DataTemplateTabControlHeader}"
                ContentTemplate="{StaticResource DataTemplateTabControlContent}">
    </TabControl>

Is there a way to prevent it from loading a new instance of the view every time? Like I said, this is my first time, so it's entirely possible I am just going about this the wrong way - if I should change how I'm doing something, please do let me know.

One thing I guess I should mention is that I'm using MahApps to get a Metro feel for the application. I'm not sure if this is contributing at all to the lag I'm seeing.

I am seeing a lag primarily when I switch to a view that contains a DataGrid. I am considering changing it to a ListView since the data is read-only, and I am hoping that will have give me some improvement, but the root cause will still be unresolved.

Any help would be greatly appreciated.

Was it helpful?

Solution

This was a huge problem for me with one project, and I ended up creating an extended TabControl that would save the ContentPresenter of each TabItem when switching tabs, and reloading it when you go back to the tab.

The original version of the code is from here, or you can find my version of the code in this related question about the problem a while back: WPF TabControl - Preventing Unload on Tab Change?

// 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;
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top