Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #3675. Adds ObjectDisposedExceptions #3676

Draft
wants to merge 20 commits into
base: v2_develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Terminal.Gui/Application/Application.Keyboard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ public static bool OnKeyDown (Key keyEvent)
{
foreach (Toplevel topLevel in TopLevels.ToList ())
{
#if DEBUG_IDISPOSABLE
ObjectDisposedException.ThrowIf (topLevel is { WasDisposed: true }, typeof (View));
#endif
if (topLevel.NewKeyDownEvent (keyEvent))
{
return true;
Expand All @@ -160,6 +163,9 @@ public static bool OnKeyDown (Key keyEvent)
}
else
{
#if DEBUG_IDISPOSABLE
ObjectDisposedException.ThrowIf (Current is { WasDisposed: true }, typeof (View));
#endif
if (Current.NewKeyDownEvent (keyEvent))
{
return true;
Expand Down
34 changes: 34 additions & 0 deletions Terminal.Gui/Application/Application.Mouse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ private static bool OnGrabbingMouse (View? view)
return false;
}

#if DEBUG_IDISPOSABLE
ObjectDisposedException.ThrowIf (view.WasDisposed, typeof (View));
#endif
var evArgs = new GrabMouseEventArgs (view);
GrabbingMouse?.Invoke (view, evArgs);

Expand All @@ -88,6 +91,10 @@ private static bool OnUnGrabbingMouse (View? view)
return false;
}

#if DEBUG_IDISPOSABLE
ObjectDisposedException.ThrowIf (view.WasDisposed, typeof (View));
#endif

var evArgs = new GrabMouseEventArgs (view);
UnGrabbingMouse?.Invoke (view, evArgs);

Expand All @@ -102,6 +109,10 @@ private static void OnGrabbedMouse (View? view)
return;
}

#if DEBUG_IDISPOSABLE
ObjectDisposedException.ThrowIf (view.WasDisposed, typeof (View));
#endif

GrabbedMouse?.Invoke (view, new (view));
}

Expand All @@ -113,6 +124,10 @@ private static void OnUnGrabbedMouse (View? view)
return;
}

#if DEBUG_IDISPOSABLE
ObjectDisposedException.ThrowIf (view.WasDisposed, typeof (View));
#endif

UnGrabbedMouse?.Invoke (view, new (view));
}

Expand All @@ -139,10 +154,21 @@ internal static void OnMouseEvent (MouseEvent mouseEvent)
return;
}

#if DEBUG_IDISPOSABLE
if (Current is { })
{
ObjectDisposedException.ThrowIf (Current.WasDisposed, typeof (View));
}
#endif

var view = View.FindDeepestView (Current, mouseEvent.Position);

if (view is { })
{
#if DEBUG_IDISPOSABLE
ObjectDisposedException.ThrowIf (view.WasDisposed, typeof (View));
#endif

mouseEvent.View = view;
}

Expand Down Expand Up @@ -250,6 +276,10 @@ internal static void OnMouseEvent (MouseEvent mouseEvent)
}
else if (MouseEnteredView != view)
{
#if DEBUG_IDISPOSABLE
ObjectDisposedException.ThrowIf (MouseEnteredView is { WasDisposed: true }, typeof (View));
#endif

MouseEnteredView.NewMouseLeaveEvent (me);
view.NewMouseEnterEvent (me);
MouseEnteredView = view;
Expand All @@ -262,6 +292,10 @@ internal static void OnMouseEvent (MouseEvent mouseEvent)

WantContinuousButtonPressedView = view.WantContinuousButtonPressed ? view : null;

#if DEBUG_IDISPOSABLE
ObjectDisposedException.ThrowIf (WantContinuousButtonPressedView is { WasDisposed: true }, typeof (View));
#endif

//Debug.WriteLine ($"OnMouseEvent: ({a.MouseEvent.X},{a.MouseEvent.Y}) - {a.MouseEvent.Flags}");

while (view.NewMouseEvent (me) is not true && MouseGrabView is not { })
Expand Down
19 changes: 15 additions & 4 deletions Terminal.Gui/Application/Application.Run.cs
Original file line number Diff line number Diff line change
Expand Up @@ -436,8 +436,6 @@
#if DEBUG_IDISPOSABLE
Debug.Assert (TopLevels.Count == 0);
#endif
runState.Dispose ();

return;
}

Expand Down Expand Up @@ -571,15 +569,17 @@
{
if (MainLoop!.Running && MainLoop.EventsPending ())
{
// Notify Toplevel it's ready
#if DEBUG_IDISPOSABLE
ObjectDisposedException.ThrowIf (state.Toplevel.WasDisposed, typeof (View));

Check warning on line 573 in Terminal.Gui/Application/Application.Run.cs

View workflow job for this annotation

GitHub Actions / build_and_test_debug (ubuntu-latest)

Dereference of a possibly null reference.

Check warning on line 573 in Terminal.Gui/Application/Application.Run.cs

View workflow job for this annotation

GitHub Actions / build_and_test_debug (ubuntu-latest)

Dereference of a possibly null reference.

Check warning on line 573 in Terminal.Gui/Application/Application.Run.cs

View workflow job for this annotation

GitHub Actions / build_and_test_debug (windows-latest)

Dereference of a possibly null reference.

Check warning on line 573 in Terminal.Gui/Application/Application.Run.cs

View workflow job for this annotation

GitHub Actions / build_and_test_debug (windows-latest)

Dereference of a possibly null reference.

Check warning on line 573 in Terminal.Gui/Application/Application.Run.cs

View workflow job for this annotation

GitHub Actions / build_and_test_debug (macos-latest)

Dereference of a possibly null reference.

Check warning on line 573 in Terminal.Gui/Application/Application.Run.cs

View workflow job for this annotation

GitHub Actions / build_and_test_debug (macos-latest)

Dereference of a possibly null reference.
#endif // Notify Toplevel it's ready
if (firstIteration)
{
state.Toplevel.OnReady ();

Check warning on line 577 in Terminal.Gui/Application/Application.Run.cs

View workflow job for this annotation

GitHub Actions / build_release

Dereference of a possibly null reference.

Check warning on line 577 in Terminal.Gui/Application/Application.Run.cs

View workflow job for this annotation

GitHub Actions / build_release

Dereference of a possibly null reference.
}

MainLoop.RunIteration ();
Iteration?.Invoke (null, new ());
EnsureModalOrVisibleAlwaysOnTop (state.Toplevel);

Check warning on line 582 in Terminal.Gui/Application/Application.Run.cs

View workflow job for this annotation

GitHub Actions / build_release

Possible null reference argument for parameter 'topLevel' in 'void Application.EnsureModalOrVisibleAlwaysOnTop(Toplevel topLevel)'.

Check warning on line 582 in Terminal.Gui/Application/Application.Run.cs

View workflow job for this annotation

GitHub Actions / build_release

Possible null reference argument for parameter 'topLevel' in 'void Application.EnsureModalOrVisibleAlwaysOnTop(Toplevel topLevel)'.

// TODO: Overlapped - Move elsewhere
if (state.Toplevel != Current)
Expand All @@ -592,6 +592,10 @@
}
}

#if DEBUG_IDISPOSABLE
ObjectDisposedException.ThrowIf (state.Toplevel is { WasDisposed: true }, typeof (View));
#endif

firstIteration = false;

if (Current == null)
Expand Down Expand Up @@ -671,6 +675,10 @@
top = Current;
}

#if DEBUG_IDISPOSABLE
ObjectDisposedException.ThrowIf (top is { WasDisposed: true }, typeof (View));
#endif

if (ApplicationOverlapped.OverlappedTop != null
&& top!.IsOverlappedContainer
&& top?.Running == true
Expand Down Expand Up @@ -791,13 +799,17 @@
{
ArgumentNullException.ThrowIfNull (runState);

#if DEBUG_IDISPOSABLE
ObjectDisposedException.ThrowIf (runState.Toplevel is { WasDisposed: true }, typeof (View));
#endif

if (ApplicationOverlapped.OverlappedTop is { })
{
ApplicationOverlapped.OverlappedTop.OnChildUnloaded (runState.Toplevel);

Check warning on line 808 in Terminal.Gui/Application/Application.Run.cs

View workflow job for this annotation

GitHub Actions / build_and_test_debug (ubuntu-latest)

Possible null reference argument for parameter 'top' in 'void Toplevel.OnChildUnloaded(Toplevel top)'.

Check warning on line 808 in Terminal.Gui/Application/Application.Run.cs

View workflow job for this annotation

GitHub Actions / build_and_test_debug (ubuntu-latest)

Possible null reference argument for parameter 'top' in 'void Toplevel.OnChildUnloaded(Toplevel top)'.

Check warning on line 808 in Terminal.Gui/Application/Application.Run.cs

View workflow job for this annotation

GitHub Actions / build_release

Possible null reference argument for parameter 'top' in 'void Toplevel.OnChildUnloaded(Toplevel top)'.

Check warning on line 808 in Terminal.Gui/Application/Application.Run.cs

View workflow job for this annotation

GitHub Actions / build_release

Possible null reference argument for parameter 'top' in 'void Toplevel.OnChildUnloaded(Toplevel top)'.

Check warning on line 808 in Terminal.Gui/Application/Application.Run.cs

View workflow job for this annotation

GitHub Actions / build_and_test_debug (windows-latest)

Possible null reference argument for parameter 'top' in 'void Toplevel.OnChildUnloaded(Toplevel top)'.

Check warning on line 808 in Terminal.Gui/Application/Application.Run.cs

View workflow job for this annotation

GitHub Actions / build_and_test_debug (windows-latest)

Possible null reference argument for parameter 'top' in 'void Toplevel.OnChildUnloaded(Toplevel top)'.

Check warning on line 808 in Terminal.Gui/Application/Application.Run.cs

View workflow job for this annotation

GitHub Actions / build_and_test_debug (macos-latest)

Possible null reference argument for parameter 'top' in 'void Toplevel.OnChildUnloaded(Toplevel top)'.

Check warning on line 808 in Terminal.Gui/Application/Application.Run.cs

View workflow job for this annotation

GitHub Actions / build_and_test_debug (macos-latest)

Possible null reference argument for parameter 'top' in 'void Toplevel.OnChildUnloaded(Toplevel top)'.
}
else
{
runState.Toplevel.OnUnloaded ();

Check warning on line 812 in Terminal.Gui/Application/Application.Run.cs

View workflow job for this annotation

GitHub Actions / build_and_test_debug (ubuntu-latest)

Dereference of a possibly null reference.

Check warning on line 812 in Terminal.Gui/Application/Application.Run.cs

View workflow job for this annotation

GitHub Actions / build_and_test_debug (ubuntu-latest)

Dereference of a possibly null reference.

Check warning on line 812 in Terminal.Gui/Application/Application.Run.cs

View workflow job for this annotation

GitHub Actions / build_release

Dereference of a possibly null reference.

Check warning on line 812 in Terminal.Gui/Application/Application.Run.cs

View workflow job for this annotation

GitHub Actions / build_release

Dereference of a possibly null reference.

Check warning on line 812 in Terminal.Gui/Application/Application.Run.cs

View workflow job for this annotation

GitHub Actions / build_and_test_debug (windows-latest)

Dereference of a possibly null reference.

Check warning on line 812 in Terminal.Gui/Application/Application.Run.cs

View workflow job for this annotation

GitHub Actions / build_and_test_debug (windows-latest)

Dereference of a possibly null reference.

Check warning on line 812 in Terminal.Gui/Application/Application.Run.cs

View workflow job for this annotation

GitHub Actions / build_and_test_debug (macos-latest)

Dereference of a possibly null reference.

Check warning on line 812 in Terminal.Gui/Application/Application.Run.cs

View workflow job for this annotation

GitHub Actions / build_and_test_debug (macos-latest)

Dereference of a possibly null reference.
}

// End the RunState.Toplevel
Expand Down Expand Up @@ -878,6 +890,5 @@
}

runState.Toplevel = null;
runState.Dispose ();
}
}
6 changes: 6 additions & 0 deletions Terminal.Gui/Application/ApplicationNavigation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,14 @@ public static bool IsInHierarchy (View start, View? view)
return null;
}

#if DEBUG_IDISPOSABLE
ObjectDisposedException.ThrowIf (view is { WasDisposed: true }, typeof (View));
#endif
foreach (View v in view.Subviews)
{
#if DEBUG_IDISPOSABLE
ObjectDisposedException.ThrowIf (v is { WasDisposed: true }, typeof (View));
#endif
if (v.HasFocus)
{
return GetDeepestFocusedSubview (v);
Expand Down
59 changes: 7 additions & 52 deletions Terminal.Gui/Application/RunState.cs
Original file line number Diff line number Diff line change
@@ -1,57 +1,12 @@
namespace Terminal.Gui;
#nullable enable

namespace Terminal.Gui;

using System.Numerics;

/// <summary>The execution state for a <see cref="Toplevel"/> view.</summary>
public class RunState : IDisposable
public sealed record RunState (Toplevel Toplevel) : IEqualityOperators<RunState, RunState, bool>
{
/// <summary>Initializes a new <see cref="RunState"/> class.</summary>
/// <param name="view"></param>
public RunState (Toplevel view) { Toplevel = view; }

/// <summary>The <see cref="Toplevel"/> belonging to this <see cref="RunState"/>.</summary>
public Toplevel Toplevel { get; internal set; }

/// <summary>Releases all resource used by the <see cref="RunState"/> object.</summary>
/// <remarks>Call <see cref="Dispose()"/> when you are finished using the <see cref="RunState"/>.</remarks>
/// <remarks>
/// <see cref="Dispose()"/> method leaves the <see cref="RunState"/> in an unusable state. After calling
/// <see cref="Dispose()"/>, you must release all references to the <see cref="RunState"/> so the garbage collector can
/// reclaim the memory that the <see cref="RunState"/> was occupying.
/// </remarks>
public void Dispose ()
{
Dispose (true);
GC.SuppressFinalize (this);
#if DEBUG_IDISPOSABLE
WasDisposed = true;
#endif
}

/// <summary>Releases all resource used by the <see cref="RunState"/> object.</summary>
/// <param name="disposing">If set to <see langword="true"/> we are disposing and should dispose held objects.</param>
protected virtual void Dispose (bool disposing)
{
if (Toplevel is { } && disposing)
{
// Previously we were requiring Toplevel be disposed here.
// But that is not correct becaue `Begin` didn't create the TopLevel, `Init` did; thus
// disposing should be done by `Shutdown`, not `End`.
throw new InvalidOperationException (
"Toplevel must be null before calling Application.RunState.Dispose"
);
}
}

#if DEBUG_IDISPOSABLE
/// <summary>For debug (see DEBUG_IDISPOSABLE define) purposes to verify objects are being disposed properly</summary>
public bool WasDisposed;

/// <summary>For debug (see DEBUG_IDISPOSABLE define) purposes to verify objects are being disposed properly</summary>
public int DisposedCount = 0;

/// <summary>For debug (see DEBUG_IDISPOSABLE define) purposes; the runstate instances that have been created</summary>
public static List<RunState> Instances = new ();

/// <summary>Creates a new RunState object.</summary>
public RunState () { Instances.Add (this); }
#endif
public Toplevel? Toplevel { get; internal set; } = Toplevel;
}
8 changes: 8 additions & 0 deletions Terminal.Gui/View/Adornment/Adornment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ public Thickness Thickness
/// <summary>Called whenever the <see cref="Thickness"/> property changes.</summary>
public void OnThicknessChanged ()
{
#if DEBUG_IDISPOSABLE
ObjectDisposedException.ThrowIf (WasDisposed, typeof (Adornment));
#endif

ThicknessChanged?.Invoke (this, EventArgs.Empty);
}

Expand Down Expand Up @@ -147,6 +151,10 @@ public override Point ScreenToFrame (in Point location)
/// <summary>Redraws the Adornments that comprise the <see cref="Adornment"/>.</summary>
public override void OnDrawContent (Rectangle viewport)
{
#if DEBUG_IDISPOSABLE
ObjectDisposedException.ThrowIf (WasDisposed, typeof (Adornment));
#endif

if (Thickness == Thickness.Empty)
{
return;
Expand Down
4 changes: 4 additions & 0 deletions Terminal.Gui/View/View.Layout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ public partial class View // Layout APIs

View? subview = null;

#if DEBUG_IDISPOSABLE
ObjectDisposedException.ThrowIf (start.WasDisposed, typeof (View));
#endif

for (int i = start.InternalSubviews.Count - 1; i >= 0; i--)
{
if (start.InternalSubviews [i].Visible
Expand Down
20 changes: 8 additions & 12 deletions Terminal.Gui/View/View.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,10 @@ public View ()
/// </remarks>
public virtual void BeginInit ()
{
#if DEBUG_IDISPOSABLE
ObjectDisposedException.ThrowIf (WasDisposed, typeof (View));
#endif

if (IsInitialized)
{
throw new InvalidOperationException ("The view is already initialized.");
Expand Down Expand Up @@ -432,22 +436,10 @@ public string Title
{
get
{
#if DEBUG_IDISPOSABLE
if (WasDisposed)
{
throw new ObjectDisposedException (GetType ().FullName);
}
#endif
return _title;
}
set
{
#if DEBUG_IDISPOSABLE
if (WasDisposed)
{
throw new ObjectDisposedException (GetType ().FullName);
}
#endif
if (value == _title)
{
return;
Expand Down Expand Up @@ -486,6 +478,10 @@ private void SetTitleTextFormatterSize ()
/// <summary>Called when the <see cref="View.Title"/> has been changed. Invokes the <see cref="TitleChanged"/> event.</summary>
protected void OnTitleChanged ()
{
#if DEBUG_IDISPOSABLE
ObjectDisposedException.ThrowIf (WasDisposed, typeof (View));
#endif

TitleChanged?.Invoke (this, new (in _title));
}

Expand Down
10 changes: 0 additions & 10 deletions UICatalog/UICatalog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -364,16 +364,6 @@ private static void VerifyObjectsWereDisposed ()
}

Responder.Instances.Clear ();

// Validate there are no outstanding Application.RunState-based instances
// after a scenario was selected to run. This proves the main UI Catalog
// 'app' closed cleanly.
foreach (RunState? inst in RunState.Instances)
{
Debug.Assert (inst.WasDisposed);
}

RunState.Instances.Clear ();
#endif
}

Expand Down
4 changes: 0 additions & 4 deletions UnitTests/Application/ApplicationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

#if DEBUG_IDISPOSABLE
Responder.Instances.Clear ();
RunState.Instances.Clear ();
#endif
}

Expand Down Expand Up @@ -177,7 +176,6 @@
Application.End (rs);

#if DEBUG_IDISPOSABLE
Assert.True (rs.WasDisposed);
Assert.False (Application.Top.WasDisposed); // Is true because the rs.Toplevel is the same as Application.Top
#endif

Expand Down Expand Up @@ -1069,8 +1067,6 @@
w)); // Invalid - w has been disposed. Run it in debug mode will throw, otherwise the user may want to run it again
Assert.NotNull (exception);

exception = Record.Exception (() => Assert.Equal (string.Empty, w.Title)); // Invalid - w has been disposed and cannot be accessed
Assert.NotNull (exception);
exception = Record.Exception (() => w.Title = "NewTitle"); // Invalid - w has been disposed and cannot be accessed
Assert.NotNull (exception);
#endif
Expand Down Expand Up @@ -1190,7 +1186,7 @@
#region ShutdownTests

[Fact]
public async void Shutdown_Allows_Async ()

Check warning on line 1189 in UnitTests/Application/ApplicationTests.cs

View workflow job for this annotation

GitHub Actions / build_and_test_debug (ubuntu-latest)

Support for 'async void' unit tests is being removed from xUnit.net v3. To simplify upgrading, convert the test to 'async Task' instead. (https://xunit.net/xunit.analyzers/rules/xUnit1048)

Check warning on line 1189 in UnitTests/Application/ApplicationTests.cs

View workflow job for this annotation

GitHub Actions / build_and_test_debug (ubuntu-latest)

Support for 'async void' unit tests is being removed from xUnit.net v3. To simplify upgrading, convert the test to 'async Task' instead. (https://xunit.net/xunit.analyzers/rules/xUnit1048)

Check warning on line 1189 in UnitTests/Application/ApplicationTests.cs

View workflow job for this annotation

GitHub Actions / build_release

Support for 'async void' unit tests is being removed from xUnit.net v3. To simplify upgrading, convert the test to 'async Task' instead. (https://xunit.net/xunit.analyzers/rules/xUnit1048)

Check warning on line 1189 in UnitTests/Application/ApplicationTests.cs

View workflow job for this annotation

GitHub Actions / build_release

Support for 'async void' unit tests is being removed from xUnit.net v3. To simplify upgrading, convert the test to 'async Task' instead. (https://xunit.net/xunit.analyzers/rules/xUnit1048)

Check warning on line 1189 in UnitTests/Application/ApplicationTests.cs

View workflow job for this annotation

GitHub Actions / build_and_test_debug (windows-latest)

Support for 'async void' unit tests is being removed from xUnit.net v3. To simplify upgrading, convert the test to 'async Task' instead. (https://xunit.net/xunit.analyzers/rules/xUnit1048)

Check warning on line 1189 in UnitTests/Application/ApplicationTests.cs

View workflow job for this annotation

GitHub Actions / build_and_test_debug (windows-latest)

Support for 'async void' unit tests is being removed from xUnit.net v3. To simplify upgrading, convert the test to 'async Task' instead. (https://xunit.net/xunit.analyzers/rules/xUnit1048)

Check warning on line 1189 in UnitTests/Application/ApplicationTests.cs

View workflow job for this annotation

GitHub Actions / build_and_test_debug (macos-latest)

Support for 'async void' unit tests is being removed from xUnit.net v3. To simplify upgrading, convert the test to 'async Task' instead. (https://xunit.net/xunit.analyzers/rules/xUnit1048)

Check warning on line 1189 in UnitTests/Application/ApplicationTests.cs

View workflow job for this annotation

GitHub Actions / build_and_test_debug (macos-latest)

Support for 'async void' unit tests is being removed from xUnit.net v3. To simplify upgrading, convert the test to 'async Task' instead. (https://xunit.net/xunit.analyzers/rules/xUnit1048)
{
var isCompletedSuccessfully = false;

Expand Down
1 change: 0 additions & 1 deletion UnitTests/Application/KeyboardTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ public KeyboardTests (ITestOutputHelper output)
_output = output;
#if DEBUG_IDISPOSABLE
Responder.Instances.Clear ();
RunState.Instances.Clear ();
#endif
}

Expand Down
1 change: 0 additions & 1 deletion UnitTests/Application/MouseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ public MouseTests (ITestOutputHelper output)
_output = output;
#if DEBUG_IDISPOSABLE
Responder.Instances.Clear ();
RunState.Instances.Clear ();
#endif
}

Expand Down
Loading