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