Fixes #4495 - Adds SuperViewChanging event to View following Cancellable Work Pattern (#4503)

* Initial plan

* Add SuperViewChanging event infrastructure and tests

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Refactor SuperViewChanging to follow Cancellable Work Pattern with cancellation support

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Remove SuperViewChangingEventArgs class and use CancelEventArgs<View?> directly

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Simplified SuperViewChanged

* Refactor SuperView change to use CWP and event args

Refactored the handling of SuperView changes in the View hierarchy to use the Cancellable Work Pattern (CWP) and standardized event argument types. The SuperView property is now settable only via an internal SetSuperView method, which leverages CWP for property changes and cancellation.

- Updated OnSuperViewChanging to accept ValueChangingEventArgs<View?>.
- SuperViewChanging event now uses EventHandler<ValueChangingEventArgs<View?>>; cancellation is via e.Handled = true.
- OnSuperViewChanged and SuperViewChanged now use ValueChangedEventArgs<View?>.
- All SuperView assignments in Add, Remove, and RemoveAll now use SetSuperView, which returns a bool for cancellation.
- CWPPropertyHelper.ChangeProperty now accepts a sender parameter, passed to event handlers.
- All property changes in View now pass this as sender to ChangeProperty.
- Updated event handler signatures and overrides in MenuBar, StatusBar, TextField, and TextView.
- Updated unit tests to use new event args and cancellation pattern.
- Minor code cleanups and improved comments.

These changes modernize and standardize property change handling, improving API consistency, extensibility, and testability.

* Refactor subview removal and event arg types

Removed redundant index check when removing subviews in View.cs, simplifying the cleanup loop. Updated TextField.OnSuperViewChanged to use a non-nullable ValueChangedEventArgs<View> parameter, enforcing stricter type safety.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tig <585482+tig@users.noreply.github.com>
Co-authored-by: Tig <tig@users.noreply.github.com>
This commit is contained in:
Copilot
2025-12-18 06:32:01 -07:00
committed by GitHub
parent 993487d0a1
commit f4b10511c0
10 changed files with 433 additions and 61 deletions

View File

@@ -18,7 +18,7 @@ public class SubViewTests
};
sub.SuperViewChanged += (s, e) =>
{
if (e.SuperView is { })
if (sub.SuperView is { })
{
subRaisedCount++;
}
@@ -46,7 +46,7 @@ public class SubViewTests
};
sub.SuperViewChanged += (s, e) =>
{
if (e.SuperView is null)
if (sub.SuperView is null)
{
subRaisedCount++;
}
@@ -393,23 +393,23 @@ public class SubViewTests
var superView = new View ();
int superViewChangedCount = 0;
//int superViewChangingCount = 0;
int superViewChangingCount = 0;
view.SuperViewChanged += (s, e) =>
{
superViewChangedCount++;
};
//view.SuperViewChanging += (s, e) =>
//{
// superViewChangingCount++;
//};
view.SuperViewChanging += (s, e) =>
{
superViewChangingCount++;
};
// Act
superView.Add (view);
// Assert
//Assert.Equal (1, superViewChangingCount);
Assert.Equal (1, superViewChangingCount);
Assert.Equal (1, superViewChangedCount);
}
@@ -692,4 +692,295 @@ public class SubViewTests
Assert.Single (removedViews);
Assert.Contains (subView2, removedViews);
}
[Fact]
public void SuperViewChanging_Raised_Before_SuperViewChanged ()
{
// Arrange
var superView = new View ();
var subView = new View ();
var events = new List<string> ();
subView.SuperViewChanging += (s, e) => { events.Add ("SuperViewChanging"); };
subView.SuperViewChanged += (s, e) => { events.Add ("SuperViewChanged"); };
// Act
superView.Add (subView);
// Assert
Assert.Equal (2, events.Count);
Assert.Equal ("SuperViewChanging", events [0]);
Assert.Equal ("SuperViewChanged", events [1]);
}
[Fact]
public void SuperViewChanging_Provides_OldSuperView_On_Add ()
{
// Arrange
var superView = new View ();
var subView = new View ();
View? currentValueInEvent = new View (); // Set to non-null to ensure it gets updated
View? newValueInEvent = null;
subView.SuperViewChanging += (s, e) =>
{
currentValueInEvent = e.CurrentValue;
newValueInEvent = e.NewValue;
};
// Act
superView.Add (subView);
// Assert
Assert.Null (currentValueInEvent); // Was null before add
Assert.Equal (superView, newValueInEvent); // Will be superView after add
}
[Fact]
public void SuperViewChanging_Provides_OldSuperView_On_Remove ()
{
// Arrange
var superView = new View ();
var subView = new View ();
superView.Add (subView);
View? currentValueInEvent = null;
View? newValueInEvent = new View (); // Set to non-null to ensure it gets updated
subView.SuperViewChanging += (s, e) =>
{
currentValueInEvent = e.CurrentValue;
newValueInEvent = e.NewValue;
};
// Act
superView.Remove (subView);
// Assert
Assert.Equal (superView, currentValueInEvent); // Was superView before remove
Assert.Null (newValueInEvent); // Will be null after remove
}
[Fact]
public void SuperViewChanging_Allows_Access_To_App_Before_Remove ()
{
// Arrange
using IApplication app = Application.Create ();
var runnable = new Runnable<bool> ();
var subView = new View ();
runnable.Add (subView);
SessionToken? token = app.Begin (runnable);
IApplication? appInEvent = null;
subView.SuperViewChanging += (s, e) =>
{
Assert.NotNull (s);
// At this point, SuperView is still set, so App should be accessible
appInEvent = (s as View)?.App;
};
Assert.NotNull (runnable.App);
// Act
runnable.Remove (subView);
// Assert
Assert.NotNull (appInEvent);
Assert.Equal (app, appInEvent);
app.End (token!);
runnable.Dispose ();
}
[Fact]
public void OnSuperViewChanging_Called_Before_OnSuperViewChanged ()
{
// Arrange
var superView = new View ();
var events = new List<string> ();
var subView = new TestViewWithSuperViewEvents (events);
// Act
superView.Add (subView);
// Assert
Assert.Equal (2, events.Count);
Assert.Equal ("OnSuperViewChanging", events [0]);
Assert.Equal ("OnSuperViewChanged", events [1]);
}
[Fact]
public void SuperViewChanging_Raised_When_Changing_Between_SuperViews ()
{
// Arrange
var superView1 = new View ();
var superView2 = new View ();
var subView = new View ();
superView1.Add (subView);
View? currentValueInEvent = null;
View? newValueInEvent = null;
subView.SuperViewChanging += (s, e) =>
{
currentValueInEvent = e.CurrentValue;
newValueInEvent = e.NewValue;
};
// Act
superView2.Add (subView);
// Assert
Assert.Equal (superView1, currentValueInEvent);
Assert.Equal (superView2, newValueInEvent);
}
// Helper class for testing virtual method calls
private class TestViewWithSuperViewEvents : View
{
private readonly List<string> _events;
public TestViewWithSuperViewEvents (List<string> events) { _events = events; }
protected override bool OnSuperViewChanging (ValueChangingEventArgs<View?> args)
{
_events.Add ("OnSuperViewChanging");
return base.OnSuperViewChanging (args);
}
protected override void OnSuperViewChanged (ValueChangedEventArgs<View?> args)
{
_events.Add ("OnSuperViewChanged");
base.OnSuperViewChanged (args);
}
}
[Fact]
public void SuperViewChanging_Can_Be_Cancelled_Via_Event ()
{
// Arrange
var superView = new View ();
var subView = new View ();
subView.SuperViewChanging += (s, e) =>
{
e.Handled = true; // Cancel the change
};
// Act
superView.Add (subView);
// Assert - SuperView should not be set because the change was cancelled
Assert.Null (subView.SuperView);
Assert.Empty (superView.SubViews);
}
[Fact]
public void SuperViewChanging_Can_Be_Cancelled_Via_Virtual_Method ()
{
// Arrange
var superView = new View ();
var subView = new TestViewThatCancelsChange ();
// Act
superView.Add (subView);
// Assert - SuperView should not be set because the change was cancelled
Assert.Null (subView.SuperView);
Assert.Empty (superView.SubViews);
}
[Fact]
public void SuperViewChanging_Virtual_Method_Cancellation_Prevents_Event ()
{
// Arrange
var superView = new View ();
var subView = new TestViewThatCancelsChange ();
var eventRaised = false;
subView.SuperViewChanging += (s, e) =>
{
eventRaised = true;
};
// Act
superView.Add (subView);
// Assert - Event should not be raised because virtual method cancelled first
Assert.False (eventRaised);
Assert.Null (subView.SuperView);
}
[Fact]
public void SuperViewChanging_Cancellation_On_Remove ()
{
// Arrange
var superView = new View ();
var subView = new View ();
superView.Add (subView);
Assert.Equal (superView, subView.SuperView);
subView.SuperViewChanging += (s, e) =>
{
// Cancel removal if trying to set to null
if (e.NewValue is null)
{
e.Handled = true;
}
};
// Act
superView.Remove (subView);
// Assert - SuperView should still be set because removal was cancelled
Assert.Equal (superView, subView.SuperView);
Assert.Single (superView.SubViews);
}
[Fact]
public void SuperViewChanging_Cancellation_When_Changing_Between_SuperViews ()
{
// Arrange
var superView1 = new View ();
var superView2 = new View ();
var subView = new View ();
superView1.Add (subView);
subView.SuperViewChanging += (s, e) =>
{
// Cancel if trying to move to superView2
if (e.NewValue == superView2)
{
e.Handled = true;
}
};
// Act
superView2.Add (subView);
// Assert - Should still be in superView1 because change was cancelled
Assert.Equal (superView1, subView.SuperView);
Assert.Single (superView1.SubViews);
Assert.Empty (superView2.SubViews);
}
// Helper class for testing cancellation
private class TestViewThatCancelsChange : View
{
protected override bool OnSuperViewChanging (ValueChangingEventArgs<View?> args)
{
return true; // Always cancel the change
}
}
}