Conversation
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs
Outdated
Show resolved
Hide resolved
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
There was a problem hiding this comment.
Pull Request Overview
This PR refactors the GlobalWindowInsetListener architecture from a single activity-level instance to a local listener pattern attached to specific views. This improves isolation in complex navigation scenarios (modals, flyouts, shell navigation) and eliminates cross-contamination of inset tracking between different UI contexts.
Key changes:
- Introduces per-view local listeners with static registry for better isolation
- Removes activity-level GlobalWindowInsetListener singleton pattern
- Adds registration/unregistration methods for view lifecycle management
- Simplifies code with null-conditional operators and improved formatting
- Converts indentation from spaces to tabs in several files
Reviewed Changes
Copilot reviewed 21 out of 24 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| GlobalWindowInsetListener.cs | Major refactor: adds static registry, view registration/unregistration, local listener setup methods |
| SafeAreaExtensions.cs | Converts spacing to tabs, updates to use FindListenerForView |
| NavigationRootManager.cs | Manages CoordinatorLayout listener lifecycle, uses new local listener pattern |
| MauiAppCompatActivity.cs | Removes activity-level GlobalWindowInsetListener singleton |
| ViewHandler.Android.cs | Updates to use FindListenerForView instead of context extension |
| FlyoutViewHandler.Android.cs | Adds local listener setup/cleanup for flyout views |
| ModalNavigationManager.Android.cs | Removes modal-specific listener, relies on local pattern |
| Shell renderers | Updates to use SetupViewWithLocalListener for CoordinatorLayouts |
| Multiple extension files | Code cleanup: null-conditional operators, whitespace fixes |
src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellSectionRenderer.cs
Show resolved
Hide resolved
...ontrols/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutTemplatedContentRenderer.cs
Show resolved
Hide resolved
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
This reverts commit 534c071.
c036163 to
c004621
Compare
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
|
/backport to release/10.0.1xx-sr1 |
|
Started backporting to |
Performance Analysis: FindListenerForView in PR #32278Problem: Nested Loop O(n*m) ComplexityThe current implementation has nested loops creating O(n*m) time complexity: internal static MauiWindowInsetListener? FindListenerForView(AView view)
{
// Walk up the view hierarchy looking for a registered view
var parent = view.Parent;
while (parent is not null) // ← Outer loop: n = hierarchy depth
{
// Skip setting listener on views inside nested scroll containers or AppBarLayout (except MaterialToolbar)
if (view is not MaterialToolbar &&
(parent is AppBarLayout || parent is MauiScrollView || parent is IMauiRecyclerView))
{
return null;
}
if (parent is AView parentView)
{
// Check if this parent view is registered
// Clean up dead references while searching
for (int i = _registeredViews.Count - 1; i >= 0; i--) // ← Inner loop: m = registry size
{
var entry = _registeredViews[i];
if (!entry.View.TryGetTarget(out var registeredView))
{
_registeredViews.RemoveAt(i);
}
else if (ReferenceEquals(registeredView, parentView))
{
return entry.Listener;
}
}
}
parent = parent.Parent;
}
return null;
}Complexity Analysis:
Why This Matters:
Option 1: ConditionalWeakTable (RECOMMENDED)Best choice: O(n) lookup, automatic cleanup, thread-safe, idiomatic .NET static readonly ConditionalWeakTable<AView, MauiWindowInsetListener> _registeredViews = new();
internal static void SetupViewWithLocalListener(AView view)
{
var listener = new MauiWindowInsetListener(view);
_registeredViews.AddOrUpdate(view, listener);
ViewCompat.SetOnApplyWindowInsetsListener(view, listener);
}
internal static MauiWindowInsetListener? FindListenerForView(AView view)
{
var parent = view.Parent;
while (parent is not null)
{
// Skip setting listener on views inside nested scroll containers or AppBarLayout (except MaterialToolbar)
if (view is not MaterialToolbar &&
(parent is AppBarLayout || parent is MauiScrollView || parent is IMauiRecyclerView))
{
return null;
}
if (parent is AView parentView &&
_registeredViews.TryGetValue(parentView, out var listener))
{
return listener;
}
parent = parent.Parent;
}
return null;
}
internal static void RemoveViewWithLocalListener(AView view)
{
if (_registeredViews.TryGetValue(view, out var listener))
{
listener.Dispose();
_registeredViews.Remove(view);
}
}
// Remove the old ViewEntry record - no longer neededBenefits:
Drawbacks:
Option 2: Dictionary with RuntimeHelpers.GetHashCodeGood choice: O(n) lookup, manual cleanup, lightweight static readonly Dictionary<int, WeakReference<MauiWindowInsetListener>> _registeredViews = new();
static readonly List<(int hash, WeakReference<AView> view)> _cleanupTracker = new();
internal static void SetupViewWithLocalListener(AView view)
{
var listener = new MauiWindowInsetListener(view);
int viewHash = System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(view);
_registeredViews[viewHash] = new WeakReference<MauiWindowInsetListener>(listener);
_cleanupTracker.Add((viewHash, new WeakReference<AView>(view)));
ViewCompat.SetOnApplyWindowInsetsListener(view, listener);
}
internal static MauiWindowInsetListener? FindListenerForView(AView view)
{
var parent = view.Parent;
while (parent is not null)
{
if (view is not MaterialToolbar &&
(parent is AppBarLayout || parent is MauiScrollView || parent is IMauiRecyclerView))
{
return null;
}
if (parent is AView parentView)
{
int parentHash = System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(parentView);
if (_registeredViews.TryGetValue(parentHash, out var listenerRef) &&
listenerRef.TryGetTarget(out var listener))
{
return listener;
}
}
parent = parent.Parent;
}
return null;
}
internal static void RemoveViewWithLocalListener(AView view)
{
int viewHash = System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(view);
if (_registeredViews.TryGetValue(viewHash, out var listenerRef) &&
listenerRef.TryGetTarget(out var listener))
{
listener.Dispose();
}
_registeredViews.Remove(viewHash);
// Periodic cleanup
CleanupDeadReferences();
}
static void CleanupDeadReferences()
{
// Only cleanup periodically (e.g., every 10th removal) to avoid overhead
if (_cleanupTracker.Count < 10 || _cleanupTracker.Count % 10 != 0)
return;
for (int i = _cleanupTracker.Count - 1; i >= 0; i--)
{
var (hash, viewRef) = _cleanupTracker[i];
if (!viewRef.TryGetTarget(out _))
{
_cleanupTracker.RemoveAt(i);
_registeredViews.Remove(hash);
}
}
}Benefits:
Drawbacks:
Option 3: Simple Cache for Last ResultMinimal change: Adds O(1) fast path for common case, keeps existing code static AView? _lastQueriedView;
static MauiWindowInsetListener? _lastFoundListener;
internal static MauiWindowInsetListener? FindListenerForView(AView view)
{
// Quick cache check for repeated lookups on same branch
if (_lastQueriedView != null && IsViewContainedIn(view, _lastQueriedView))
return _lastFoundListener;
var parent = view.Parent;
while (parent is not null)
{
if (view is not MaterialToolbar &&
(parent is AppBarLayout || parent is MauiScrollView || parent is IMauiRecyclerView))
{
_lastQueriedView = null;
_lastFoundListener = null;
return null;
}
if (parent is AView parentView)
{
for (int i = _registeredViews.Count - 1; i >= 0; i--)
{
var entry = _registeredViews[i];
if (!entry.View.TryGetTarget(out var registeredView))
{
_registeredViews.RemoveAt(i);
}
else if (ReferenceEquals(registeredView, parentView))
{
// Cache the result
_lastQueriedView = parentView;
_lastFoundListener = entry.Listener;
return entry.Listener;
}
}
}
parent = parent.Parent;
}
_lastQueriedView = null;
_lastFoundListener = null;
return null;
}
static bool IsViewContainedIn(AView child, AView potentialAncestor)
{
var parent = child.Parent;
while (parent is not null)
{
if (ReferenceEquals(parent, potentialAncestor))
return true;
parent = parent.Parent;
}
return false;
}Benefits:
Drawbacks:
Option 4: Separate Cleanup PassHybrid approach: Keep list structure, decouple cleanup from lookup static readonly List<ViewEntry> _registeredViews = new();
static int _lookupsSinceCleanup = 0;
const int CLEANUP_THRESHOLD = 100;
internal static MauiWindowInsetListener? FindListenerForView(AView view)
{
var parent = view.Parent;
while (parent is not null)
{
if (view is not MaterialToolbar &&
(parent is AppBarLayout || parent is MauiScrollView || parent is IMauiRecyclerView))
{
return null;
}
if (parent is AView parentView)
{
// Fast lookup without cleanup
foreach (var entry in _registeredViews)
{
if (entry.View.TryGetTarget(out var registeredView) &&
ReferenceEquals(registeredView, parentView))
{
return entry.Listener;
}
}
}
parent = parent.Parent;
}
// Periodic cleanup after many lookups
if (++_lookupsSinceCleanup >= CLEANUP_THRESHOLD)
{
CleanupDeadReferences();
_lookupsSinceCleanup = 0;
}
return null;
}
static void CleanupDeadReferences()
{
for (int i = _registeredViews.Count - 1; i >= 0; i--)
{
if (!_registeredViews[i].View.TryGetTarget(out _))
{
_registeredViews.RemoveAt(i);
}
}
}
internal static void RemoveViewWithLocalListener(AView view)
{
for (int i = _registeredViews.Count - 1; i >= 0; i--)
{
if (_registeredViews[i].View.TryGetTarget(out var registeredView) &&
ReferenceEquals(registeredView, view))
{
_registeredViews[i].Listener?.Dispose();
_registeredViews.RemoveAt(i);
break;
}
}
// Explicit cleanup on removal
CleanupDeadReferences();
}Benefits:
Drawbacks:
Performance Comparison
RecommendationUse Option 1: ConditionalWeakTable This is the best solution because:
The current implementation likely works fine with small registry sizes (< 10 views), but for foundational infrastructure that runs on every layout pass, the optimization is worthwhile and eliminates a potential performance cliff as apps scale. Migration NotesIf choosing Option 1 (ConditionalWeakTable):
Breaking Changes: None - API surface remains identical |
mattleibow
left a comment
There was a problem hiding this comment.
Overall I think it is good.
Improved implementation of WindowInsetListener for per-view layout based on PR #31898
Comparison with PR #31898
After analyzing PR #31898, I've developed my own implementation approach and then compared it with the existing PR. Here's a detailed comparison:
Original PR Approach:
My Improvements Over Original:
?) on fields in Core project where nullable is enabled#nullable disabledirective (Shell fragments)IsViewContainedInwith early termination when encountering another CoordinatorLayout - this prevents incorrect listener lookup when layouts are nestedIsImeAnimationhelper method - reduces code duplication and improves readability.ToArray()instead ofnew List<>()for better performance in ResetAllViews - eliminates unnecessary list allocationWhy Not Use the Existing PR
While PR #31898 provides a solid foundation, my implementation offers several key improvements:
Key Changes
Renamed Class and Methods
New API
Test Fixes
Implementation Plan
Security Summary
✅ No security vulnerabilities detected by CodeQL analysis
Build Status
✅ All builds successful
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.
Fixes
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.