Random behaviour of SelectedItem property of our custom TabItem / TabControl

Marek Sýkorka 0 Reputation points
2025-01-29T07:49:11.0333333+00:00

Issue: Our application is built somewhat similiarly to a common web browsers, meaning that different user controls are grouped to Tabs, which can be created, switched between and closed. To do this we have created custom ClosableTab Item TabControl. It generally works pretty well but every so often the selection of a tab misfires. The selectedIndex property always stays correctly the same but when you interact with any UI in the tab it jumps to some specific Tab seemingly chosen at random. It always jumps to this same tab until user creates new tab and deletes it (somehow resetting the selection process ?!).

Context: We are dealing with this issue for a good part of a 3+ years not beeing able to solve it. The app is written in WPF and C# currently targeting .NET Framework 4.7.2. We don´t really follow MVVM pattern of programming, though we try to keep bussines logic / data structeres / ui separate just not in any standard way.

Code: The main TabControl is located at MainWindow.xaml of the app:

<Window ... 
    <Grid> 
        <TabControl x:Name="tcMain" Margin="10,10,10.286,9.714" SelectionChanged="tcMain_SelectionChanged">
            <TabItem>
                <TabItem.Header>
                    <Tools:AddTabHeader/>
                </TabItem.Header>
            </TabItem>
        </TabControl>
    </Grid>
</Window>

Coresponding code-behind to manipulate TabControl / TabItems MainWindow.xaml.cs:

public void AddItem(Item page)
{
    if (!page.ErrorClose)
    {
        tcMain.Items.Insert(tcMain.Items.Count - 1, page.TabPage);
        tcMain.SelectedItem = page.TabPage;
        page.SetLanguage();
    }
}

        public TabItem GetActual()
        {
            return tcMain.SelectedItem as TabItem;
        }

        public void SetActual(TabItem item)
        {
            tcMain.SelectedItem = item;
        }

        private void tcMain_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            //ak je len jedna zalozka, nerob nic
            if (tcMain.Items.Count <= 1)
                return;

            //ak je aktivna posledna treba zmenit
            if (tcMain.SelectedIndex == (tcMain.Items.Count - 1))
                tcMain.SelectedIndex = tcMain.Items.Count - 2;

            //prejdi zalozky a oznac, ktora je aktivna
            foreach (TabItem tab in tcMain.Items)
            {
                if (tab.Content is Item)
                {
                    Item itemTab = tab.Content as Item;
                    itemTab.Active = tcMain.SelectedContent == itemTab;
                    itemTab.ChangeState();
                }
            }
        }

The custom TabItem is in Item.cs using ClosableTab.cs:

    public class Item : UserControl, INotifyPropertyChanged
    {
        private ClosableTab page = null;
        public DeviceList devices;
        public DispatcherTimer dispatcherTimer;
        public int Communication = 0;
        public bool Run { get; set; } = false;
        public bool Active { get; set; } = true;
        public bool ErrorClose { get; set; } = true;


        public Item(string name, bool isFile = false)
        {
            //vytvor stranku
            page = new ClosableTab();
            page.CloseTab += CloseTab;
            page.DataContext = this;
            devices = new DeviceList();
            if (isFile)
                file = name;
            else
            {
                file = name;
                Title = name;
            }
            //timer
            dispatcherTimer = new DispatcherTimer();
            dispatcherTimer.Tick += new EventHandler(dispatcherTimer_Tick);
            dispatcherTimer.Interval = new TimeSpan(0, 0, 0, 0, 500);
        }

and finally some UI that uses this ClosableTab.cs:

    public class ClosableTab : TabItem
    {
        // Constructor
        public ClosableTab()
        {
            // Create an instance of the usercontrol
            CloseableHeader closableTabHeader = new CloseableHeader();
            // Assign the usercontrol to the tab header
            this.Header = closableTabHeader;

            // Attach to the CloseableHeader events
            // (Mouse Enter/Leave, Button Click, and Label resize)
            closableTabHeader.button_close.MouseEnter +=
               new MouseEventHandler(button_close_MouseEnter);
            closableTabHeader.button_close.MouseLeave +=
               new MouseEventHandler(button_close_MouseLeave);
            closableTabHeader.button_close.Click +=
               new RoutedEventHandler(button_close_Click);
        }

        // Property - Set the Title of the Tab           
		// Override OnSelected - Show the Close Button
        // Override OnUnSelected - Hide the Close Button
        // Override OnMouseEnter - Show the Close Button
        // Override OnMouseLeave - Hide the Close Button (If it is NOT selected) 
        // Button MouseEnter - When the mouse is over the button - change color to Red         	        
		// Button MouseLeave - When mouse is no longer over button - change color back to black
        // Button Close Click - Remove the Tab - (or raise
        // an event indicating a "CloseTab" event has occurred)

Do you have any Idea what could be causing this. We already tried to check if some event is not bubbling up the chain of UI, to forcefully set the selectedItem and so on but no luck. 

.NET
.NET
Microsoft Technologies based on the .NET software framework.
4,088 questions
{count} votes

1 answer

Sort by: Most helpful
  1. Marek Sýkorka 0 Reputation points
    2025-02-06T12:34:42.5766667+00:00

    Hello,

    so just to keep you all updated. I have been working on this issue this past week and it seems to be fixed, or better said .. hacked to work as intended.

    I have not found what exactly is causing this issue, but with the help of third-party tool "Spoon" and Debug.WriteLine(Enviroment.StackTrace) I have been able to compare normal and abnormal behaviour of tabSelectionChanged. Every time the tab misfired it was by part caused by MouseCaptureLost on some part of our UI. Image from WinMerge compare between StackTrace of normal and abnormal tab behaviour.

    To prevent this from happening I have built custom checking logic and sanitized the input of event handlers for our custom class ClosableTab.cs,

    // Internal method to check for tab change validity
        private bool EvaluateTabFocus()
        {
            _mouseOverTabFocus = IsMouseOver;
            bool ret = _newTabFocus || _mouseOverTabFocus || _codeNavigationFocus;
            _codeNavigationFocus = _newTabFocus = false;
            return ret;
            } 
    
        // Override OnSelected - Show the Close Button
        protected override void OnSelected(RoutedEventArgs e)
        {
            // Check if the event was generated by the same type
            if (e.OriginalSource.GetType() != this.GetType())
                return;
        
            // Check if the event is happening whilst the mouse is over top of the header or there is automatic navigation to this tab through code.
            if (EvaluateTabFocus()) 
            {
                _selectionIsValid = true;
                base.OnSelected(e);
                ((CloseableHeader)this.Header).button_close.Visibility = Visibility.Visible;
            }
            else
            {
                _selectionIsValid = false;
                base.OnSelected(e);
            }
        }
    

    I then use this _selectionIsValid inside MainWindow.xaml.cs private void tcMain_SelectionChanged(object sender, SelectionChangedEventArgs e) to check for valid tab changes and repeatadly discard the bad ones.

    It seems to be working now so we will be monitoring the behaviour. If the issue appears again I may return but for now this cutom logic is doing fine. Thanks all to your helpfull insights.

    0 comments No comments

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.