2

I have read the posts @ How to stop Wpf Tabcontrol to unload Visual tree on Tab change, but I could not get it work, I must have missed something. Pls help. thanks

I am using class TabControlEx from sample project @ http://www.pluralsight-training.net/community/blogs/eburke/archive/2009/04/30/keeping-the-wpf-tab-control-from-destroying-its-children.aspx

<Window x:Class="MyMainwindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:sharedC="clr-namespace:myShared.Converter;assembly=myShared"
    xmlns:mainTab="clr-namespace:MyWPFLib.View" Title="{Binding Title}"        
    Height="600" Width="850" Top="223" Left="164" ResizeMode="CanResize" Closing="WindowClosing"
    WindowStyle="ToolWindow">
<Grid>
 <mainTab:TabControlEx IsSynchronizedWithCurrentItem="True"
                        Grid.Row="0"  
                        Margin="10 0 5 5"
                        x:Name="MainTabRegion"  
                        TabStripPlacement="Top" 
                        SelectedIndex="{Binding Tabs.SelectedIndex}" 
                        ItemsSource="{Binding Tabs.TabItems}"                         
                        >
                        <TabControl.ItemContainerStyle>
                            <Style TargetType="{x:Type TabItem}">
                                <Setter Property="Header">
                                    <Setter.Value>
                                        <Binding Path="Header"/>
                                    </Setter.Value>
                                </Setter>
                                <Setter Property="Visibility" 
                                Value="{Binding IsVisible, Mode=OneWay, Converter={StaticResource boolToVis}}"/>
                            </Style>
                        </TabControl.ItemContainerStyle>
                    </mainTab:TabControlEx>
</Grid>
</Window>

Datasource for the maintab is Tabs.TabItems

    public ObservableCollection<ITabControl> TabItems
    {
        get
        {
            return _items;
        }
    }

each TabItem is built with

<DataTemplate DataType="{x:Type vm:Item1ViewModel}">
    <vw:View1 />
</DataTemplate>
Community
  • 1
  • 1
toosensitive
  • 2,335
  • 7
  • 46
  • 88
  • I use the code that was posted in that link all the time and it works fine for me. What are you using to determine that the tab control is destorying and re-creating it's children? – Rachel Oct 20 '11 at 15:14
  • I have a treeview on one tab, when I switch away and back to the tab, treeview always collapses, no selection, no expansion. Another tab has a datagrid, I highligh a few items, when I switch away and back, the selection is gone. I do not understand sample code and simply use it so I must miss something. Can you share you code on how to use it correctly? thanks – toosensitive Oct 20 '11 at 15:45
  • I debug in VS and see view on tab is created (constructor is called) every time when I switch tabs. So a new control is created each time when tab changes – toosensitive Oct 20 '11 at 16:14
  • I added the code as an answer below. The link you posted is the same link I originally got the code from, but the website has changed since I was last there. I have not checked to see if the code has changed at all, so you might want to look for code changes between the two. – Rachel Oct 20 '11 at 16:14

2 Answers2

5

I use the code from that link all the time without a problem, although I notice that the website has changed since when I first got the code. You may want to check for any code differences between what I have and what the website has.

Here's the code I got before they changed the site:

// 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 did make some changes to the original code because I was dragging/dropping tabs and didn't want to re-create the tabs when removing an item and re-adding it, but honestly it's been so long that I forget what part is mine and what isn't.

I did a quick test to double-check, and yes it does keep your non-bound values such as selected or expanded values.

EDIT

In response to your comments below, put DataTemplates in your Resources somewhere that define which UserControl to use for each Tab. When the TabControl tries to draw each Tab's Content, it will use whatever DataTemplate you defined for that data type

<Window.Resources>
    <DataTemplate TargetType="{x:Type local:TabAViewModel}">
        <local:TabAView />
    </DataTemplate>
    <DataTemplate TargetType="{x:Type local:TabBViewModel}">
        <local:TabBView />
    </DataTemplate>
</Window.Resources>

<TabControl ItemsSource="{Binding TabItems}">
    <TabControl.ItemContainerStyle> 
        <Style TargetType="{x:Type TabItem}"> 
            <Setter Property="Header" Value="{Binding Header}" />
        </Style> 
    </TabControl.ItemContainerStyle>
</TabControl>

ViewModel...

public MyViewModelConstructor()
{
    TabItems.Add(New TabAViewModel { Header = "Tab A" });
    TabItems.Add(New TabBViewModel { Header = "Tab B" });
}

EDIT #2

Text XAML, as you requested

<local:TabControlEx>
    <TabItem Header="Test1">
        <DataGrid ItemsSource="{Binding Test}">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding TestValue}" Header="Test" />
            </DataGrid.Columns>
        </DataGrid>
    </TabItem>
    <TabItem Header="Test2">
        <DataGrid ItemsSource="{Binding Test}">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding TestValue}" Header="Test" />
            </DataGrid.Columns>
        </DataGrid>
    </TabItem>
</local:TabControlEx>
Rachel
  • 130,264
  • 66
  • 304
  • 490
  • thank you, Rachel. hmm interesting, still not work for me. I will double check see what I am missing – toosensitive Oct 20 '11 at 18:08
  • what tabItem do you have in tabcontrol? I have variable # of tabItems. Each item is defined by – toosensitive Oct 20 '11 at 19:35
  • For the recent test, I simply put a hardcoded ``, although I have used this code in the past the same way you are, binding the TabControl to an Collection of TabViewModels and using DataTemplates to define how the different Tabs should look based on what type of TabViewModel it is. Do you have anything custom that runs when the selected tab switches? – Rachel Oct 20 '11 at 20:05
  • Yes, remember last selectedIndex, clear out data in a panel (not in tabcontrol), that's it. I notice _itemHolder == null all the time – toosensitive Oct 20 '11 at 20:41
  • What is your Visibility Converter based off of? Making an item invisible, than visible again will also reload the control. – Rachel Oct 20 '11 at 23:25
  • It used to control hide/show a tab and later it was not used any more. So it is always true. I just remove it from xaml, but still does not work. – toosensitive Oct 21 '11 at 15:45
  • I think I did something wrong in xaml but not sure how to change. each tabitem is a usercontrol in my case. so I am not sure how to set TabControl.ItemTemplate. I tried this, not work – toosensitive Oct 25 '11 at 15:40
  • @toosensitive Overwritting the `TabControl.Template` is replacing the default TabControl template entirely. Instead, you want to set `TabControl.ItemTemplate` to a `DataTemplate` containing your `UserControl` – Rachel Oct 25 '11 at 15:48
  • thanks. not sure how to set dataTemplate since each tab has different usercontrol. I tried this, not work. – toosensitive Oct 25 '11 at 18:48
  • Also tried this , not work either – toosensitive Oct 25 '11 at 18:49
  • @toosensitive See my edit... Use DataTemplates to tell WPF which User Controls to use to display the different ViewModels, and the only thing you need the style for is the Tab Header binding – Rachel Oct 25 '11 at 18:59
  • Yes, I have defined the resouces the same way as you edited and use the ItemContainerStyle but only Headers show up for the tabcontrol and no body at all. I changed Template a couple times, tabcontrol just does not display correct. I think if it displays correctly then it will work correctly(I mean preserve tab item states). But not sure how to make it display correctly first? – toosensitive Oct 25 '11 at 19:21
  • @toosensitive Are you overwriting the Tab Control's Template? It sounds like you have, otherwise the default content should be the `.ToString()` of your ViewModels – Rachel Oct 25 '11 at 19:23
  • Yes, I did. I put this – toosensitive Oct 25 '11 at 19:33
  • But if I remove , tabcontrol displays correctly but it does NOT work, i.e. does not preserve control state at all. I tracked and saw _itemHolder is null all the time. Can you post your sample xaml that works? thanks – toosensitive Oct 25 '11 at 19:34
  • @toosensitive You need `` in your Template somewhere – Rachel Oct 25 '11 at 19:38
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/4518/discussion-between-rachel-and-toosensitive) – Rachel Oct 25 '11 at 19:44
2

I gave up with the workaround, it just did not work in my case. I purchase a third party WPF library (DevExpress WPF) which solves the issue. thanks

toosensitive
  • 2,335
  • 7
  • 46
  • 88