Skip to content

Fix bug in TabPageCollection.RemoveAt (#7837)#7911

Merged
lonitra merged 6 commits intodotnet:mainfrom
albahari:issue-7837
Dec 21, 2022
Merged

Fix bug in TabPageCollection.RemoveAt (#7837)#7911
lonitra merged 6 commits intodotnet:mainfrom
albahari:issue-7837

Conversation

@albahari
Copy link
Contributor

@albahari albahari commented Oct 10, 2022

Fixes #7837

Proposed changes

  • Fix logic in TabPageCollection.RemoveAt to avoid removing incorrect TabPage when TabControl.Controls collection is out of sync with TabControls._tabPages. This can happen when Control.WmWindowPosChanged calls _parent.UpdateChildControlIndex, which calls Controls.SetChildIndex.

Customer Impact

  • None

Regression?

  • No

Risk

  • Low

Test methodology

  • Adhoc testing. There's just one of code, and the new implementation calls only public methods.
Microsoft Reviewers: Open in CodeFlow

Copy link
Contributor

@RussKie RussKie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks sensible.

Do you think it's possible to come up with a regression test that would simulate the original issue, so that we can verify the fix?

@RussKie RussKie added the waiting-author-feedback The team requires more information from the author label Oct 10, 2022
@albahari
Copy link
Contributor Author

  • Do you think it's possible to come up with a regression test that would simulate the original issue, so that we can verify the fix?

It's tricky - the sequence of events to get the collections out of sync is complicated.

@ghost ghost removed the waiting-author-feedback The team requires more information from the author label Oct 10, 2022
@RussKie
Copy link
Contributor

RussKie commented Oct 11, 2022

How did you initially observe the issue? How can we be sure we've fixed it?

@RussKie RussKie added the waiting-author-feedback The team requires more information from the author label Oct 11, 2022
@ghost ghost added the no-recent-activity label Nov 16, 2022
@ghost
Copy link

ghost commented Nov 16, 2022

This submission has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for 14 days.

It will be closed if no further activity occurs within 7 days of this comment.

@dreddy-work
Copy link
Member

@albahari , Can you respond to the questions above? PRs with inactivity will be closed automatically after 14days.

@ghost ghost removed the no-recent-activity label Nov 18, 2022
@albahari
Copy link
Contributor Author

albahari commented Nov 20, 2022

Sorry for the delay - it took a while to get to a minimum repro. It's not until you subclass TabControl that the condition becomes apparent (or at least easy to reproduce). This discovery in itself has lead to finding a second bug. This second bug may in fact be a root cause (rendering the first bug fix redundant).

Control.UpdateChildControlIndex starts with the following code:

if (GetType().IsAssignableFrom(typeof(TabControl)))
{
   return;
}

This check is the wrong way around. It should be:

if (typeof(TabControl).IsAssignableFrom (GetType()))
{
   return;
}

Fixing this bug means that the tabControl.Controls collection (appears to) stay in sync with TabControls._tabPages. Assuming that the two collections are intended to always stay in sync (and there are no other conditions that could result in these collections being ordered differently), the first bug fix becomes redundant.

Here is a minimum repro. Note that it requires a running message loop, so TabPage can receive a WM_WINDOWPOSCHANGED message:

// Instantiate a subclassed TabControl with 5 pages:
var tc = new MyTabControl ();
var pages = Enumerable.Range (0, 5).Select (i => new TabPage ($"Page {i}")).ToArray();
tc.TabPages.AddRange (pages);	

// Select the last page:
tc.SelectedIndex = tc.TabPages.Count - 1;

// Show the TabControl on a form:
var form = new Form();
form.Controls.Add (tc);
form.Show();

// The change we made to SelectedIndex will result in a WM_WINDOWPOSCHANGED message which will 
// fire TabPage.WmWindowPosChanged, which will call TabControl.UpdateChildControlIndex, which will
// call Controls.SetChildIndex. This will reorder Controls without reordering TabPages.

// Notice that the TabPages and Controls collections are out of sync:
for (int i = 0; i < tc.TabCount; i++)
    Console.WriteLine ($"TabPage {tc.TabPages [i].Text} => {tc.Controls [i].Text}");
    
// I don't know whether having these collections out of sync in itself violates an invariant,
// or whether these collections are allowed to be differently-ordered. In any case, calling
// RemoveAt(int index) fails when the collections are not in sync:

// Remove the first page by position:
tc.TabPages.RemoveAt (0);

// We should be left with pages[1] and pages[2]
Debug.Assert (tc.TabPages[0] == pages [1]);      // Fails
Debug.Assert (tc.TabPages[1] == pages [2]);      // Fails

class MyTabControl : TabControl
{	
}

How should I proceed with this? Create a new PR for the second bug fix?

Note that while the original bug fix has virtually zero risk, the second bug fix could conceivably result in changed behavior in an (arguably obscure) scenario. Consider that someone instantiates the Control class directly: right now, the bug inappropriately short-circuits UpdateChildControlIndex when WM_WINDOWPOSCHANGED events are received. It might be prudent to mitigate that risk with this:

if (typeof(TabControl).IsAssignableFrom(GetType()) || GetType() == typeof(Control))
{
   return;
}

@ghost ghost removed the waiting-author-feedback The team requires more information from the author label Nov 20, 2022
@dreddy-work
Copy link
Member

Thank you @albahari for making time and sharing the findings here. We will review this and update you on next steps.

@dreddy-work
Copy link
Member

@albahari , lets go ahead and update this PR with the new fix given current one is redundant after that. Also, make sure you add some unit tests / integration tests to cover the code path.

@lonitra lonitra added the waiting-author-feedback The team requires more information from the author label Dec 9, 2022
@ghost ghost removed the waiting-author-feedback The team requires more information from the author label Dec 16, 2022
@albahari
Copy link
Contributor Author

I've updated the PR and added an integration test that fails without the bugfix.

It doesn't make sense that the CI test would fail for X86 release on ActiveXFontMarshaler_RoundTripManagedFont() - perhaps this test needs to be repeated?

Copy link
Member

@lonitra lonitra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test looks great! Just a few comments.

@lonitra lonitra added the waiting-author-feedback The team requires more information from the author label Dec 16, 2022
@ghost ghost removed the waiting-author-feedback The team requires more information from the author label Dec 19, 2022
@lonitra
Copy link
Member

lonitra commented Dec 19, 2022

Looks like there is some trailing whitespace that is causing build to complain. It is difficult for me to see where on web, but if you build on command line (cd to winforms repo and type 'build'), the same errors should show and help point you to where the trailing whitespace is.

@lonitra lonitra added the waiting-author-feedback The team requires more information from the author label Dec 19, 2022
@ghost ghost removed the waiting-author-feedback The team requires more information from the author label Dec 20, 2022
@lonitra lonitra added the waiting-author-feedback The team requires more information from the author label Dec 20, 2022
@ghost ghost removed the waiting-author-feedback The team requires more information from the author label Dec 21, 2022
Copy link
Member

@lonitra lonitra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

@lonitra lonitra merged commit aeaa188 into dotnet:main Dec 21, 2022
@ghost ghost added this to the 8.0 Preview1 milestone Dec 21, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Jan 20, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TabControl.TabPageCollection.RemoveAt(int) removes wrong item!

4 participants