-1

We have our own TabControl class which does an override on OnSelectionChanged.

The relevant code is:

protected override void OnSelectionChanged(SelectionChangedEventArgs e)
    {
        base.OnSelectionChanged(e);
        UpdateSelectedItem();
    }


    internal Grid ItemsHolder { get; set; }

    public TabControl()
        : base()
    {
        ItemsHolder = new Grid();
        // 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();
        }
    }

    private bool _isTemplateApplied = false;
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        var itemsHolderParent = GetTemplateChild("PART_ItemsHolderParent") as Panel;
        if (itemsHolderParent != null)
        {
            var parent = ItemsHolder.Parent as Panel;
            if (parent != null)
                parent.Children.Remove(ItemsHolder);
            itemsHolderParent.Children.Add(ItemsHolder);
            if (parent != null)
                ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
            UpdateSelectedItem();
        }
        _isTemplateApplied = true;
    }


    internal void ClearChildren()
    {
        foreach (var cp in ItemsHolder.Children.OfType<ContentPresenter>())
            cp.Content = null;
        ItemsHolder.Children.Clear();
    }

    /// <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);

        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Reset:
                var removeList = new List<ContentPresenter>();
                foreach (var presenter in ItemsHolder.Children.OfType<ContentPresenter>())
                    removeList.Add(presenter);
                var oldItemsCount = removeList.Count;

                foreach (var item in Items)
                {
                    var itemPresenter = FindChildContentPresenter(item);
                    if (removeList.Contains(itemPresenter))
                        removeList.Remove(itemPresenter);
                }

                foreach (var removePresenter in removeList)
                    ItemsHolder.Children.Remove(removePresenter);

                //If there were old items, the SelectionChanged in the Selector will force a new selected item
                //If there are no items we can't update the selected item (there is nothing to select)
                //If the tempalte is not yet applied, applying the template will select the tabitem
                if (oldItemsCount == 0 && Items != null && Items.Count > 0 && _isTemplateApplied)
                    UpdateSelectedItem();
                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>
    /// generate a ContentPresenter for the selected item
    /// </summary>
    internal void UpdateSelectedItem()
    {
        if (SelectedIndex == -1 && Items != null && Items.Count > 0)
            SelectedIndex = 0;
        if (SelectedIndex == -1)
            return;

        // generate a ContentPresenter if necessary
        TabItem item = GetSelectedTabItem();
        if (item != null)
        {
            FindOrElseCreateChildContentPresenter(item);
        }

        // show the right child
        foreach (ContentPresenter child in ItemsHolder.Children.OfType<ContentPresenter>())
        {
            if ((child.Tag as TabItem).IsSelected)
                SelectedTabItem = child.Tag as TabItem;
            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>
    internal ContentPresenter FindOrElseCreateChildContentPresenter(object item)
    {
        if (item == null)
        {
            return null;
        }

        ContentPresenter cp = FindChildContentPresenter(item);

        if (cp != null)
        {
            cp.Tag = (item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item));
            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;

        Dispatcher.BeginInvoke(new Action(() =>
        {
            cp.ContentTemplate = this.SelectedContentTemplate;
            cp.ContentTemplateSelector = this.SelectedContentTemplateSelector;
            cp.ContentStringFormat = this.SelectedContentStringFormat;
        }), System.Windows.Threading.DispatcherPriority.Send);


        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>
    public ContentPresenter FindChildContentPresenter(object data)
    {
        if (data is TabItem)
        {
            data = (data as TabItem).Content;
        }

        if (data == null)
        {
            return null;
        }

        foreach (ContentPresenter cp in ItemsHolder.Children.OfType<ContentPresenter>())
        {
            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;
        }
        TabItem item = selectedItem as TabItem;
        if (item == null)
        {
            item = base.ItemContainerGenerator.ContainerFromIndex(base.SelectedIndex) as TabItem;
        }
        return item;


    }



    internal TabItem SelectedTabItem
    {
        get { return (TabItem)GetValue(SelectedTabItemProperty); }
        set { SetValue(SelectedTabItemProperty, value); }
    }

    // Using a DependencyProperty as the backing store for SelectedTabItem.  This enables animation, styling, binding, etc...
    internal static readonly DependencyProperty SelectedTabItemProperty =
        DependencyProperty.Register("SelectedTabItem", typeof(TabItem), typeof(TabControl), new UIPropertyMetadata(null));

When I use the debugger I see that sometimes the OnSelectionChanged is fired multiple times for one tab switch. Is this a bug? How can I fix this? Or is it intended behaviour and can I use another event to detect tab switches?

Sybren
  • 1,071
  • 3
  • 17
  • 51
  • When you change from one tab to another then the event is fired for both. – Nawed Nabi Zada Sep 29 '16 at 09:48
  • @NawedNabiZada No, it isn't. At least that's not the default behavior. However, from the question it's unclear what `UpdateSelectedItem()` is doing. – Clemens Sep 29 '16 at 09:50
  • @NawedNabiZada Why is that the behaviour? How would I detect tabswitches (which event)? – Sybren Sep 29 '16 at 09:59
  • @Clemens look at the link in my question and look at Rachel's post, you will find the method there. The rest of my TabControl is similar to the code in her post. – Sybren Sep 29 '16 at 10:01
  • 2
    Please take a look at [How to create a Minimal, Complete, and Verifiable example](http://stackoverflow.com/help/mcve). – Clemens Sep 29 '16 at 10:06
  • [Related?](http://stackoverflow.com/questions/3659858/in-c-sharp-wpf-why-is-my-tabcontrols-selectionchanged-event-firing-too-often) – grek40 Sep 29 '16 at 10:07
  • @Clemens if I put no code in the override, the method is still fired multiple times – Sybren Sep 29 '16 at 11:00
  • @grek40 `e.Handled`doesnt work – Sybren Sep 29 '16 at 11:13
  • Full code added so that people can test it – Sybren Sep 29 '16 at 11:45
  • Did you already inspect the call stack in debugger to see where each call originates from? If the change is triggered by the UI, the call stack should be pretty much empty (external calls ignored), but if the selection is touched anywhere in code, it should show up in the call stack. – grek40 Sep 29 '16 at 11:48
  • @grek40 yes, every call is from the same tabcontrol but from different tabpages – Sybren Sep 29 '16 at 11:50

1 Answers1

-1

If you set selectedValue of the TabControl in your UpdateSelectedItem method, It is going to enter the code block more than once.For instance, if you set

    private void UpdateSelectedItem()
    {
        this.SelectedValue = 0; // set to a value
    }

like this code block you will see the debugger enters OnSelectionChanged method twice.

FreeMan
  • 1,417
  • 14
  • 20
  • `SelectedValue` is never set in `UpdateSelectedItem()`. What do u mean? – Sybren Sep 29 '16 at 11:12
  • @Sybren :) I cant know that you wrote in UpdateSelectedItem() without any information , do I ? – FreeMan Sep 29 '16 at 11:38
  • @Sybren But it works well for me without UpdateSelectedItem() method. I mean that you set something related to your tabcontrol in that method my friend.Does that clear? – FreeMan Sep 29 '16 at 11:39
  • @Sybren Interesting, when I copied and paste your code into my class, and created a simple tab control which contains two tab item, it works well.Just one selection changed event fired.I saw your code that you call UpdateSelectedItem() method more than once.I think we need to check your xaml structure.(Your container structure as well) – FreeMan Sep 29 '16 at 11:59
  • what happens with more test tabitems? What do you need from the xaml? – Sybren Sep 29 '16 at 12:09
  • @Sybren It works well with more test tabItems(at least I created 5 tabitems). Actually, I wanted learn how you created TabControl in xaml.I mean "what is parent container etc"... – FreeMan Sep 29 '16 at 12:13