Add PublicAPI.Unshipped.txt BOM sort warning to Copilot instructions#34327
Add PublicAPI.Unshipped.txt BOM sort warning to Copilot instructions#34327jfversluis merged 1 commit intomainfrom
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34327Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34327" |
There was a problem hiding this comment.
Pull request overview
Updates Copilot guidance to avoid PublicAPI.Unshipped.txt BOM sorting issues (RS0017), and includes a broad set of Controls fixes across handlers/platforms plus additional device test coverage.
Changes:
- Document safe conflict-resolution/sorting guidance for
PublicAPI.Unshipped.txtfiles while preserving#nullable enableon line 1. - Add/adjust platform fixes across Controls (e.g., Android toolbar/menu item handling, iOS selection/scroll behaviors, Windows enabled-state propagation).
- Add/expand device tests related to navigation/handler lifecycle and memory/leak regressions.
Reviewed changes
Copilot reviewed 44 out of 673 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Controls/tests/DeviceTests/Memory/MemoryTests.cs | Updates leak test coverage for NavigationPage on Android. |
| src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.Android.cs | Adds Android device test validating StackNavigationManager clears references on disconnect. |
| src/Controls/src/Core/TabbedPage/TabbedPage.Windows.cs | Propagates tab IsEnabled into Windows tab view model. |
| src/Controls/src/Core/SwipeView/SwipeView.cs | Closes open SwipeView on iOS/MacCatalyst when binding context changes. |
| src/Controls/src/Core/Shapes/Shape.cs | Adds shape versioning via IVersionedShape and increments version on property changes. |
| src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt | Tracks new/changed API surface for Shape.OnPropertyChanged. |
| src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt | Tracks new/changed API surface for Shape.OnPropertyChanged. |
| src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt | Tracks new/changed API surface for Shape.OnPropertyChanged. |
| src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt | Tracks new/changed API surface for Shape.OnPropertyChanged. |
| src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt | Tracks new/changed API surface for Shape.OnPropertyChanged. |
| src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt | Tracks new/changed API surface for Shape.OnPropertyChanged. |
| src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt | Tracks new/changed API surface (Shape.OnPropertyChanged, RecyclerView touch overrides). |
| src/Controls/src/Core/Platform/iOS/Extensions/FormattedStringExtensions.cs | Refactors span font resolution via GetEffectiveFont. |
| src/Controls/src/Core/Platform/Windows/TabbedPage/TabbedPageStyle.xaml | Binds IsEnabled for Windows tab items. |
| src/Controls/src/Core/Platform/Windows/Extensions/FormattedStringExtensions.cs | Refactors span font resolution via GetEffectiveFont. |
| src/Controls/src/Core/Platform/Android/Extensions/ToolbarExtensions.cs | Adds menu-item mapping to guard async icon updates + requests insets apply. |
| src/Controls/src/Core/Platform/Android/Extensions/FormattedStringExtensions.cs | Refactors span font resolution via GetEffectiveFont. |
| src/Controls/src/Core/NavigationPage/NavigationPageToolbar.cs | Re-applies toolbar changes for FlyoutLayoutBehavior updates. |
| src/Controls/src/Core/NavigationPage/NavigationPage.cs | Removes duplicate RemoveFromInnerChildren call. |
| src/Controls/src/Core/Label/Label.cs | Removes handler attribute (handler registration now centralized). |
| src/Controls/src/Core/Label/Label.Android.cs | Improves handler change lifecycle for both MauiTextView and MauiMaterialTextView. |
| src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs | Conditionally registers Material3 handler variants on Android. |
| src/Controls/src/Core/Handlers/Shell/ShellItemHandler.Windows.cs | Updates Windows Shell item mapping and enables state propagation. |
| src/Controls/src/Core/Handlers/Items2/iOS/SelectableItemsViewController2.cs | Adjusts selection timing based on CollectionView.IsLoaded(). |
| src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewDelegator2.cs | Ensures offsets are updated even when no visible items are detected. |
| src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.cs | Uses actual scroll direction when mapping scroll-to position. |
| src/Controls/src/Core/Handlers/Items/iOS/SelectableItemsViewController.cs | Mirrors selection timing fix in non-Items2 handler. |
| src/Controls/src/Core/Handlers/Items/iOS/ItemsViewDelegator.cs | Mirrors offset update behavior in non-Items2 handler. |
| src/Controls/src/Core/Handlers/Items/Android/RecyclerViewScrollListener.cs | Changes first-layout scroll behavior + refines threshold reached logic. |
| src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs | Adds disabled-state touch interception/handling for ItemsView. |
| src/Controls/src/Core/FontExtensions.cs | Adds Span.GetEffectiveFont helper. |
| src/Controls/src/Core/FlyoutPage/FlyoutPage.cs | Triggers toolbar reevaluation on Android/Windows orientation changes. |
| src/Controls/src/Core/Editor/Editor.Mapper.cs | Extends mapper updates for EditorHandler2 under Material3 on Android. |
| src/Controls/src/Core/Editor/Editor.Android.cs | Adds MapText overload for EditorHandler2. |
| src/Controls/src/Core/ContentPresenter.cs | Restores inherited binding context propagation for hosted content. |
| src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cs | Disposes Shell pages when fragments are removed. |
| src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutTemplatedContentRenderer.cs | Ensures vertical scroll mode is updated when flyout content is rebuilt. |
| src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutRenderer.cs | Updates flow direction handling + null-guard ordering. |
| src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutRecyclerAdapter.cs | Adjusts flyout item measuring for non-enabled vertical scroll mode. |
| src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs | Adds explicit page disposal/disconnect path. |
| src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs | Multiple iOS navigation bar updates (iOS 26 tinting, flyout icon updates, stage manager resize). |
| src/Controls/src/Core/Compatibility/Handlers/FlyoutPage/iOS/PhoneFlyoutPageRenderer.cs | Applies safe-area top inset to FlyoutPage container. |
| src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/maui-sc.aotprofile.txt | Removes AOT profile entries for removed/changed interop methods. |
| .github/copilot-instructions.md | Adds warned/safe guidance for PublicAPI.Unshipped.txt sorting with BOM and #nullable enable. |
Comments suppressed due to low confidence (1)
src/Controls/src/Core/Handlers/Items/Android/RecyclerViewScrollListener.cs:1
- The header/footer bounds check and threshold math appear to mix “count” and “adapter index”. With a footer and no header (headerValue=0), a footer position can slip through (e.g.,
Last == actualItemCount) and incorrectly triggerRemainingItemsThresholdReached. A safer approach is to compute the last data adapter index (e.g.,lastDataIndex = headerValue + dataCount - 1) and compareLastagainst[headerValue, headerValue + dataCount - 1], then compute distance-to-end usingLast - headerValue.
#nullable disable
| foreach (var item in shellItemController.GetItems()) | ||
| { | ||
| item.PropertyChanged -= OnShellItemPropertyChanged; | ||
| } |
There was a problem hiding this comment.
MapMenuItems() unconditionally adds PropertyChanged += OnShellItemPropertyChanged each time it runs. If MapMenuItems() is invoked multiple times, this can lead to duplicate subscriptions (and -= in DisconnectHandler will remove only one instance per call). Consider tracking subscribed items (e.g., a HashSet<BaseShellItem>), unsubscribing before re-subscribing, or ensuring MapMenuItems() first detaches existing handlers for the current item set.
|
|
||
| foreach (var item in shellItemController.GetItems()) | ||
| { | ||
| item.PropertyChanged += OnShellItemPropertyChanged; |
There was a problem hiding this comment.
MapMenuItems() unconditionally adds PropertyChanged += OnShellItemPropertyChanged each time it runs. If MapMenuItems() is invoked multiple times, this can lead to duplicate subscriptions (and -= in DisconnectHandler will remove only one instance per call). Consider tracking subscribed items (e.g., a HashSet<BaseShellItem>), unsubscribing before re-subscribing, or ensuring MapMenuItems() first detaches existing handlers for the current item set.
| } | ||
|
|
||
| // Check if we're at or within threshold distance from the last data item | ||
| bool isThresholdReached = (Last == actualItemCount - 1) || (actualItemCount - 1 - Last <= _itemsView.RemainingItemsThreshold); |
There was a problem hiding this comment.
The header/footer bounds check and threshold math appear to mix “count” and “adapter index”. With a footer and no header (headerValue=0), a footer position can slip through (e.g., Last == actualItemCount) and incorrectly trigger RemainingItemsThresholdReached. A safer approach is to compute the last data adapter index (e.g., lastDataIndex = headerValue + dataCount - 1) and compare Last against [headerValue, headerValue + dataCount - 1], then compute distance-to-end using Last - headerValue.
| public override bool OnTouchEvent(MotionEvent e) | ||
| { | ||
| // If ItemsView is disabled, don't handle touch events | ||
| if (ItemsView?.IsEnabled == false) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| return base.OnTouchEvent(e); | ||
| } | ||
|
|
||
| public override bool OnInterceptTouchEvent(MotionEvent e) | ||
| { | ||
| // If ItemsView is disabled, intercept all touch events to prevent interactions | ||
| if (ItemsView?.IsEnabled == false) | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| return base.OnInterceptTouchEvent(e); | ||
| } |
There was a problem hiding this comment.
The disabled-path logic is inconsistent: OnInterceptTouchEvent returns true (intercept), but OnTouchEvent returns false (not handled). This can lead to unpredictable gesture behavior since the view claims the stream but then doesn’t consume it. Consider making both methods consistently “consume and do nothing” when disabled (return true in both), or consistently “don’t participate” (return false in both) depending on the intended UX.
| MauiTextView _mauiTextView; | ||
| MauiMaterialTextView _mauiMaterialTextView; |
There was a problem hiding this comment.
_mauiTextView and _mauiMaterialTextView are declared as non-nullable but are assigned null in DetachLayoutChanged(). In builds where nullable warnings are treated as errors, this can break compilation. These fields should be nullable (e.g., MauiTextView? / MauiMaterialTextView?) or otherwise structured so they’re never assigned null.
| MauiTextView _mauiTextView; | |
| MauiMaterialTextView _mauiMaterialTextView; | |
| MauiTextView? _mauiTextView; | |
| MauiMaterialTextView? _mauiMaterialTextView; |
| void DetachLayoutChanged() | ||
| { | ||
| if (_mauiTextView is not null) | ||
| { | ||
| if (_mauiTextView != null) | ||
| { | ||
| _mauiTextView.LayoutChanged -= OnLayoutChanged; | ||
| _mauiTextView = null; | ||
| } | ||
| _mauiTextView.LayoutChanged -= OnLayoutChanged; | ||
| _mauiTextView = null; | ||
| } | ||
|
|
||
| if (_mauiMaterialTextView is not null) | ||
| { | ||
| _mauiMaterialTextView.LayoutChanged -= OnLayoutChanged; | ||
| _mauiMaterialTextView = null; | ||
| } | ||
| } |
There was a problem hiding this comment.
_mauiTextView and _mauiMaterialTextView are declared as non-nullable but are assigned null in DetachLayoutChanged(). In builds where nullable warnings are treated as errors, this can break compilation. These fields should be nullable (e.g., MauiTextView? / MauiMaterialTextView?) or otherwise structured so they’re never assigned null.
| SetInheritedBindingContext(BottomItems, BindingContext); | ||
|
|
||
| // In Windows and Android, new cells are created when rebinding, so _isOpen is false. | ||
| // In iOS, CollectionView reuses cells, so SwipeView remain open (_isOpen = true). |
There was a problem hiding this comment.
Correct grammar: “SwipeView remain open” → “SwipeView remains open”.
| // In iOS, CollectionView reuses cells, so SwipeView remain open (_isOpen = true). | |
| // In iOS, CollectionView reuses cells, so SwipeView remains open (_isOpen = true). |
| } | ||
| else | ||
| { | ||
| handlersCollection.AddHandler<Label, LabelHandler>(); | ||
| } | ||
| #else | ||
| handlersCollection.AddHandler<Label, LabelHandler>(); | ||
| #endif | ||
| #if ANDROID | ||
| if (RuntimeFeature.IsMaterial3Enabled) | ||
| { | ||
| handlersCollection.AddHandler<Editor, EditorHandler2>(); | ||
| } | ||
| else | ||
| { | ||
| handlersCollection.AddHandler<Editor, EditorHandler>(); | ||
| } | ||
| #else | ||
| handlersCollection.AddHandler<Editor, EditorHandler>(); | ||
| #endif | ||
| #if ANDROID | ||
| if (RuntimeFeature.IsMaterial3Enabled) | ||
| { | ||
| handlersCollection.AddHandler<Picker, PickerHandler2>(); | ||
| } | ||
| else | ||
| { | ||
| handlersCollection.AddHandler<Picker, PickerHandler>(); | ||
| } | ||
| #else |
There was a problem hiding this comment.
The Material3 handler registration pattern is repeated for multiple controls, which increases the chance of future drift (e.g., one control updated but others missed). Consider consolidating this into a small helper method/local function (or a single if (IsMaterial3Enabled) block registering all “2” handlers) to reduce duplication while keeping the preprocessor conditions readable.
| } | |
| else | |
| { | |
| handlersCollection.AddHandler<Label, LabelHandler>(); | |
| } | |
| #else | |
| handlersCollection.AddHandler<Label, LabelHandler>(); | |
| #endif | |
| #if ANDROID | |
| if (RuntimeFeature.IsMaterial3Enabled) | |
| { | |
| handlersCollection.AddHandler<Editor, EditorHandler2>(); | |
| } | |
| else | |
| { | |
| handlersCollection.AddHandler<Editor, EditorHandler>(); | |
| } | |
| #else | |
| handlersCollection.AddHandler<Editor, EditorHandler>(); | |
| #endif | |
| #if ANDROID | |
| if (RuntimeFeature.IsMaterial3Enabled) | |
| { | |
| handlersCollection.AddHandler<Picker, PickerHandler2>(); | |
| } | |
| else | |
| { | |
| handlersCollection.AddHandler<Picker, PickerHandler>(); | |
| } | |
| #else | |
| handlersCollection.AddHandler<Editor, EditorHandler2>(); | |
| handlersCollection.AddHandler<Picker, PickerHandler2>(); | |
| } | |
| else | |
| { | |
| handlersCollection.AddHandler<Label, LabelHandler>(); | |
| handlersCollection.AddHandler<Editor, EditorHandler>(); | |
| handlersCollection.AddHandler<Picker, PickerHandler>(); | |
| } | |
| #else | |
| handlersCollection.AddHandler<Label, LabelHandler>(); | |
| handlersCollection.AddHandler<Editor, EditorHandler>(); |
| public override bool OnTouchEvent(MotionEvent e) | ||
| { | ||
| // If ItemsView is disabled, don't handle touch events | ||
| if (ItemsView?.IsEnabled == false) | ||
| { | ||
| return false; | ||
| } |
There was a problem hiding this comment.
This introduces new interaction behavior for disabled ItemsView on Android (touch handling/interception). Please add a corresponding automated test across TestCases.HostApp (UI surface) and TestCases.Shared.Tests (assertion) to verify disabled ItemsViews do not scroll/click and that re-enabling restores interactions, per repository device/UI testing guidelines.
The prior agent session used LC_ALL=C sort to resolve merge conflicts in PublicAPI.Unshipped.txt files, which pushed the BOM-prefixed #nullable enable line to the bottom. This caused RS0017 analyzer errors fixed by PR #34320. Add explicit guidance to prevent this: explain why plain sort breaks these files and provide a safe conflict resolution script. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
34c0e26 to
224a93f
Compare
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Description
PR #34320 fixed RS0017 analyzer errors caused by
#nullable enablebeing sorted to the bottom of 14 MapsPublicAPI.Unshipped.txtfiles. The root cause was a prior Copilot agent session that usedLC_ALL=C sort -uto resolve merge conflicts — the BOM bytes (0xEF 0xBB 0xBF) sort after all ASCII characters, pushing the directive below the API entries.This updates the Copilot instructions to prevent this from recurring:
#nullable enablemust remain on line 1sorton these files (BOM sort ordering)