Search code examples
c#wpfxamltabcontrol

Random behaviour of SelectedItem property of our custom TabItem / TabControl


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 programming pattern, 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:

<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);
        }
    
        ...
}
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)

and finally some UI that uses this ClosableTab.cs:

private void cm_Connect_Click(object sender, RoutedEventArgs e)
{
    MainWindow.AppWindow.AddItem(new Search());
}

/// \<summary\>
/// Interaction logic for Search.xaml
/// \</summary\>
public partial class Search
{
public ObservableCollection\<ComPort\> AvailablePorts { get; set; }

    public Search()
    {
        InitializeComponent();
    
        //daj ze mozem otvorit
        ErrorClose = false;
    
        //nastav priradenie
        AvailablePorts = new ObservableCollection<ComPort>();
        dispatcherTimer = new DispatcherTimer();
        dispatcherTimer.Tick += new EventHandler(dispatcherTimer_Tick);
        dispatcherTimer.Interval = new TimeSpan(0, 0, 0, 5);
        Run = true;
        dispatcherTimer.Start();
        dispatcherTimer_Tick(null, null);
    
        Image = ToolFuncs.GetIcon("IcoConnect");
        comboBox_ConnectionType.SelectedIndex = 0;
        comboBox_Port.ItemsSource = AvailablePorts;
    }
    ...

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.


Solution

  • 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.