v16.0.0 Bug Fix-A-Palooza 🎉
We have FINALLY implemented Device Tests! These are platform-specific unit tests that run on iOS, Android, MacCatalyst and Windows. This allows us to unit test all platform-specific code and has MASSIVELY increased our Unit Test Code Coverage:
- 90.8% Code Coverage on
CommunityToolkit.Maui - 96.7% Code Coverage on
CommunityToolkit.Maui.Camera - 95% Code Coverage on
CommunityToolkit.Maui.Core - 91.8% Code Coverage on
CommunityToolkti.Maui.MediaElement
And thanks to the new Device Tests (along with excellent contributions from the community), this Release contains fixes for a whopping 58 Issues. 💪
Breaking Changes
To fix certain bugs, and re-align features to ensure they behave the same across all platforms, we did have to modify some APIs and the associated underlying behavior. Please find the features you use, below, and update your apps accordingly.
New APIs In This Release
All packages
ArgumentNullException at the call. Public and protected members of the five libraries now guard the non-nullable reference parameters they cannot handle (about 275 guards).
NullReferenceException later (or was stored silently) fails at the call, with a ParamName. What to do: fix the caller.
IProgress<double> or a Paint background of null.
BasePlatformBehavior guards only bindable. A null platform view passes through, because the Android IconTintColorBehavior accepts it.
<queries> entry for the launcher (MAIN/HOME) to the app's merged manifest through the package's library manifest (src/CommunityToolkit.Maui.Core/AndroidManifest.xml). Badge needs it on Android 11 and later. If your app or Play listing reviews <queries>, check the merged manifest.
System.Speech 10.0.11 (it was 10.0.1). No action is needed unless you pin the package. This comes from #3288 on main, not from this PR.
Popup now declares IPaddingElement, which it already inherited from ContentView
GravatarImageSource declares IStreamImageSource, which it already inherited from StreamImageSource, and overrides Cancel() (see GravatarImageSource, after that ApiCompat run);
[SupportedOSPlatform("android23.0")] on the Android StatusBarBehavior (see StatusBar and StatusBarBehavior).
DefaultPopupSettings.BackgroundColorDark and OnInitializing (#3345)
NumericValidationBehavior.Interval (#3329)
AvatarView
Behavior
HorizontalOptions, VerticalOptions, HeightRequest, WidthRequest, Padding, Stroke, StrokeThickness and StrokeShape are no longer local values, so an implicit AvatarView Style applies (and a Border Style with ApplyToDerivedTypes).
Text on every platform; iOS, MacCatalyst and Windows had previously showed an empty circle. Set Text to string.Empty or "" to display a blank circle.
GravatarImageSource asks for its picture only after its image has a size. So:
Email changes or a new load succeeds.
Badge
Setup
<queries> entry for the launcher (MAIN/HOME) to Android apps; Badge needs it on Android 11 and later (see All packages).
Behavior
Badge.SetCount writes one trace line and returns, instead of throwing a COMException that ended the app. No badge appears, so package the app if you need the taskbar badge.
Camera
ColorAnimationExtensions
Behavior
BackgroundColorTo and TextColorTo with a rate of 0 throw ArgumentOutOfRangeException from the call. Before, the error came later on the animation ticker and ended the app. Pass a rate above 0.
DialogFragment (Android)
Behavior
DialogFragmentService lays the dialog of a modal page out like the activity's window, for every status bar color.
DrawingView
Behavior
GetImageStream with no lines returns Stream.Null instead of throwing InvalidOperationException, and a NaN image size, or one that is infinite on both sides, throws ArgumentException. Handle Stream.Null where you caught the exception.
ImageOptions.DesiredSize has its pixel size rounded half away from zero on Android, iOS and Mac Catalyst, as Windows already did. A 1920 x 1080 target of a 411 DIP canvas gives 1920 x 934 on Android, where it gave 1919 x 934.
DrawingLineCompleted, a lost capture raises DrawingLineCancelled, and a second pointer no longer starts or changes a line. No action is needed unless you worked around the old behavior.
Smaller changes
DrawAction draws in a canvas state of its own.
Expander
Setup
UseMauiCommunityToolkit registers the Expander layout handler (see Behavior below), so keep calling it at startup. That handler keeps its own platform view even when the app sets LayoutHandler.PlatformViewFactory.
Behavior
ExpanderLayoutHandler, so a handler an app registers for Grid no longer applies to the Expander's internal Grid; LayoutHandler.Mapper changes still apply.
LayoutHandler.PlatformViewFactory no longer creates the platform view of that layout, so the layout keeps the platform view that gives the header this support. The factory still creates the platform views of the app's other layouts, including the ones in the Expander's Header and Content, and on Android also the platform view of that layout.
Smaller changes
Direction can be changed again by bindings and Styles.
FileSaver and FolderPicker
API
ArgumentNullException at the call; FileSaver and FolderPicker still return the exception in the result. (see All packages)
Behavior
progress reports percentages from 0 to 100. It reported fractions from 0 to 1 during the copy, then a final 100, so remove any * 100 you applied.
FilePath is its content:// URI. It used to fail with FileSaveException. Open such a path with the ContentResolver.
fileName or stream (and a null initialPath for FolderPicker) comes back as an ArgumentNullException in the result, without showing the picker.
GravatarImageSource
Behavior
HttpClient.Timeout limits the whole download, and a body that fails halfway gives Stream.Null.
CancellationToken.None never throws. Only a token that you pass and cancel makes GetStreamAsync and the default Stream function throw OperationCanceledException, instead of returning Stream.Null. .NET MAUI catches it for the loads that it cancels itself, so a view keeps the picture of the load that replaced it.
Cancel() ends only the loads started without a cancelable token, which then return Stream.Null. Its task always completes with true.
Stream or GetStreamAsync with your own token.
WidthRequest and HeightRequest that the Button sets, or at Gravatar's default 80 pixels, and again when those requests change. It was requested at the Button's laid-out size, so on iOS and Mac Catalyst each picture made the Button larger until it was as wide as the screen (older than this PR).
Image, ImageButton and AvatarView still request the size they are laid out at.
Button without size requests now gets an 80-pixel picture instead of one the size of the laid-out Button: on a 420 dpi Android screen, a 30 dp icon instead of 15 dp. Set HeightRequest on the Button for a smaller picture.
ContentLayout Top or Bottom) needs a HeightRequest that leaves room for the picture.
Text needs a HeightRequest or MinimumHeightRequest. The .NET MAUI app template's Button style sets MinimumHeightRequest to 44.
ImageOpened event, which a picture decoded from a stream has already raised.
KeyboardExtensions
Smaller changes
IsSoftKeyboardShowing called from another thread blocks until the UI thread answers. The tasks of HideKeyboardAsync and ShowKeyboardAsync complete once the UI thread has done the work.
Maps
- See Maps v5.0.0: https://github.com/CommunityToolkit/Maui/releases#release-5.0.0-maps
MaxLengthReachedBehavior
API
MaxLengthReachedEventArgs rejects null for its non-nullable members.
Behavior
MaxLengthReachedBehavior hides the keyboard only while its own InputView has the focus, and through the view's own window. So Android no longer hides the keyboard of the view the user moved to, and the keyboard goes away in a modal page or a Popup too. No action is needed.
MediaElement
NavigationBar (Android)
Behavior
Color is still transparent, but it is now a plain default value, so reading the color no longer makes IsSet true. As a result:
Color or Style gives the window back its original color.
Popup
API
Options.SetPopupDefaults(null) and SetPopupOptionsDefaults(null) no longer store null.
async void PopupExtensions.ShowPopup overloads throw to the caller instead of posting the exception to the SynchronizationContext.
Behavior
BackgroundColor is null by default, and Styles now apply. Margin, Padding, HorizontalOptions, VerticalOptions, CanBeDismissedByTappingOutsideOfPopup and BackgroundColor are now default values. 15.0.1 set them as local values in the constructor.
Popup.BackgroundColor reads null until something sets it.
DefaultPopupSettings.BackgroundColor, or in the dark theme BackgroundColorDark: black with #3345 in this release, where 15.0.1 showed white.
BackgroundColor where a fixed color is needed, and check popups in the dark theme.
ShowPopupAsync. When navigation removes the popup page without CloseAsync, ShowPopupAsync completes and Closed is raised exactly once. Before, neither happened, and code that awaited it stayed suspended.
PopModalAsync or PopAsync called by other code, Window.Page being replaced, or Shell popping the popup out of a modal NavigationPage.
Closed handler can show another popup.
WasDismissedByTappingOutsideOfPopup is true. It now means "closed without a result: by tapping outside the popup, or because navigation removed it"; it is false only after CloseAsync.
ShowPopupAsync<TResult>, Result is null for a reference or nullable type. Reading it for a value type such as int throws PopupResultException, as after a tap outside.
WasDismissedByTappingOutsideOfPopup first. Subscribe to Closed if you must tell a tap from a navigation removal.
Closed is raised without a preceding Opened.
CancellationToken used as a timeout keeps working.
CloseAsync and ClosePopupAsync of a popup shown on a modal NavigationPage close only the popup. CloseAsync used to pop the whole page and never complete, and ClosePopupAsync threw PopupBlockedException.
PopupBlockedException and pops nothing:
CloseAsync, Popup.CloseAsync and ClosePopupAsync (also through IPopupService) wait for earlier popup operations. When the close finally runs, they check again that the popup is still the page on top.
ShowPopupAsync was showing, or a modal page your code pushed. They already threw PopupBlockedException and popped nothing when that page was on top at the call.
ClosePopupAsync still targets the popup that was the most recent when you called it, not the newer one. Call it again to close the newer one.
async void) when an earlier tap, Back or navigation has already closed the popup;
NavigationPage cancels a PopAsync that waited behind a navigation that failed). The popup stays open, and a later tap closes it.
ShowPopupAsync, CloseAsync and ClosePopupAsync throw OperationCanceledException when the app cancels the navigation.
Window.ModalPopping or Application.ModalPopping handler that sets e.Cancel, or a Shell.Navigating handler that cancels), Popup.CloseAsync, Popup<T>.CloseAsync, ClosePopupAsync and IPopupService.ClosePopupAsync throw OperationCanceledException. The popup stays open and usable, Closed is not raised, and the navigation lock is released.
Shell.Navigating handler cancels the navigation that shows a popup (ShowPopupAsync of a Shell, or of a page or INavigation under a Shell root), every ShowPopupAsync overload, also through IPopupService, throws OperationCanceledException and nothing is shown.
ShowPopup and the tap outside or Android Back show nothing, or do nothing, and throw nothing.
OperationCanceledException when (!token.IsCancellationRequested).
ModalPopping handler that cancels makes .NET MAUI retry the pop in a loop, so CloseAsync cannot return. Cancel in Shell.Navigating there instead.
Smaller changes
OnDisappearing of a modal page that is closing appears on the page below.
WidthRequest or HeightRequest larger than the window is limited to the window.
Opened is raised once per show.
PopupClosed and Closed are raised after the navigation lock is released, in the same order as before.
RatingView
Smaller changes
FillOption changes.
SelectAllTextBehavior
API
SelectAllTextBehavior on Windows throws ArgumentNullException instead of InvalidOperationException for a null platform view (only a derived class can reach it).
SemanticOrderView
Behavior
ViewOrder that the container holds are listed first in its tab focus children, followed by the rest of its content.
TabIndex that the app set is no longer overwritten. The exception: views of ViewOrder that a control within the container (a ScrollView, for example) holds, or that are outside the container, still get a TabIndex from the order.
Smaller changes
ContentViewHandler.PlatformViewFactory no longer creates the SemanticOrderView's platform view, so ViewOrder applies.
CreatePlatformView in a handler derived from SemanticOrderViewHandler, and register it after UseMauiCommunityToolkit.
Snackbar and Toast
Behavior
Toast.Show or Toast.Dismiss after an earlier toast, and a Snackbar.Show or Snackbar.Dismiss while a snackbar was shown, removed every notification of the app (AppNotificationManager.RemoveAllAsync). That included the other alert and the notifications the app shows itself, on screen and in the Action Center.
Show no longer clears the app's other notifications, a Toast or Snackbar shown while another notification banner of the app is on screen waits behind that banner, as Windows queues banners.
Show now returns once Windows lists the new notification. That takes up to 1 s longer when Windows lists nothing for the app, for example while its notifications are off. A Snackbar whose Duration is zero or less does not wait.
Show and Dismiss calls now take effect in call order. A call whose token is cancelled before its turn removes nothing and throws OperationCanceledException.
Show or Dismiss Task inside a Shown or Dismissed handler: .Wait() and .Result deadlock there.
Smaller changes
Duration, raises Dismissed once and runs its action on the UI thread.
Show can take up to 1 s longer while the app's notifications are off (see Behavior above).
Show no longer throws for a Duration such as TimeSpan.MaxValue or TimeSpan.MinValue.
Duration below 1 ms shows it for 1 ms, and Dismissed is raised once.
Duration.
SpeechToText
Setup
System.Speech 10.0.11 on Windows (see All packages).
Smaller changes
StartListenAsync with null options throws ArgumentNullException from the call (see All packages: null arguments). With valid options, a canceled token still returns a canceled Task, so awaiting it throws OperationCanceledException.
StartListenAsync completes without an exception and without listening, even if its token is canceled afterwards.
StopListenAsync or DisposeAsync (on Windows, also a newer StartListenAsync) is called while the start waits for the platform.
StateContainer
Behavior
StateKey that is not defined throws StateContainerException with its own message and no inner exception. It used to throw the "assigned to multiple StateViews" message, so change any code that matches the message text.
StatusBar and StatusBarBehavior
API
StatusBarBehavior requires Android 6.0 (API 23). The Android type is now [SupportedOSPlatform("android23.0")], like StatusBar already was.
if (OperatingSystem.IsIOS() || OperatingSystem.IsAndroidVersionAtLeast(23)). Not IsAndroidVersionAtLeast(23) alone: it is false on iOS.
StatusBarBehavior declared in XAML gets no warning.
Setup
UIViewControllerBasedStatusBarAppearance from Info.plist if you set it to false for StatusBar.SetStyle (see Behavior below).
StatusBarBehavior needs a minimum Android version of 23 or a guard (see API above). CommunityToolkit.Maui's minimum Android version stays 5.0 (API 21), the same as .NET MAUI 10's.
Behavior
StatusBar.SetStyle and StatusBarBehavior. SetStyle no longer needs UIViewControllerBasedStatusBarAppearance set to false in Info.plist.
LightContent or DarkContent replaces preferredStatusBarStyle until the app ends, for every view controller that does not choose its own style (the system pickers included). Default gives each view controller its own style back.
SetStyle called off the main thread throws UIKitThreadAccessException, in Release builds too, so call it on the main thread.
Smaller changes
StringToListConverter
Smaller changes
Separators list, as documented; before, it split at Separator.
TouchBehavior
Smaller changes
ShouldMakeChildrenInputTransparent takes effect when it changes.
DefaultOpacity, HoveredOpacity or PressedOpacity throws ArgumentOutOfRangeException, where 15.0.1 accepted it.
CurrentHoverStatus says, a release over the element completes the tap (Command, TouchGestureCompleted), and a release outside it cancels.
Entered. So a release outside the element could complete, and one over it could leave the press running.
UserStoppedTypingBehavior
API
async void overrides of UserStoppedTypingBehavior throw to the caller instead of posting the exception to the SynchronizationContext.
Behavior
UserStoppedTypingBehavior hides the keyboard only while its own InputView has the focus, and through the view's own window. So Android no longer hides the keyboard of the view the user moved to, and the keyboard goes away in a modal page or a Popup too. No action is needed.
UserStoppedTypingBehavior.StoppedTypingTimeThreshold rejects a negative value. A negative value is ignored and the previous value is kept (MAUI logs a warning). Before, it failed inside Task.Delay. Use 0 or more.
Validation behaviors
API
async void overrides of ValidationBehavior throw to the caller instead of posting the exception to the SynchronizationContext.
Smaller changes
Errors is set on the UI thread, and IsRunning is reset after a cancel.
What's Changed
- Add Device Tests, Apply Fixes for Bugs Found by @TheCodeTraveler in #3344
- Handle missing Windows notification runtime module by @jfversluis in #3301
- Restore Windows notification registration compatibility by @jfversluis in #3303
- Fix iOS video recording delegate lifetime by @austinbhale in #3309
- Add video capture and image capture to Android manifest in sample app by @matt-goldman in
- Add interval validation to NumericValidationBehavior by @meyerdominik in #3329
#3330 - Fix AppThemeResource updates in trimmed Release builds by @jfversluis in #3334
- Clarify required Windows Toast notification configuration by @jfversluis in #3333
- Fix Android FileSaver null initialPath handling by @snydcol in #3340
- Fix UseCommunityToolkitInitializationAnalyzer code fix inserting UseMauiCommunityToolkit() after Build() by @alexh30486-ui in #3343
- Add
Popupsupport forAppTheme, IntroduceDefaultPopupSettings.OnInitializingby @stephenquan in #3345 - Fix: [BUG] StatusBarBehavior is causing Android layouts to go behind status bar and navigation bar buttons on launch #2351 by @Pastajello in #3189
- Add SoftInputKeyboardPopup and integrate keyboard handling for popups by @FabriBertani in #3117
- Await native video recording start acknowledgment by @austinbhale in #3311
- Fix: Popup unlabeled outer element using android talkback by @IeuanWalker in #3367
- [iOS] StatusBar: apply the color only to the app windows by @marcelomoresco in #3373
Housekeeping
- Update Unit Tests Packages by @dependabot[bot] in #3288
- [housekeeping] Automated PR to fix formatting errors by @github-actions[bot] in #3370
- [housekeeping] Automated PR to fix formatting errors by @github-actions[bot] in #3350
- Bump System.Security.Cryptography.Pkcs from 9.0.21 to 10.0.13 by @dependabot[bot] in #3352
- Bump actions/setup-java from 6.0.1 to 6.1.0 by @dependabot[bot] in #3357
- Bump System.Security.Cryptography.Pkcs from 10.0.13 to 10.0.14 by @dependabot[bot] in #3358
- Bump Microsoft.Testing.Extensions.TrxReport from 2.5.1 to 2.5.2 by @dependabot[bot] in #3362
- Bump Microsoft.Testing.Extensions.GitHubActionsReport from 2.5.1 to 2.5.2 by @dependabot[bot] in #3361
- Bump xunit.v3 from 4.0.1 to 4.0.2 by @dependabot[bot] in #3346
- Bump dependabot/fetch-metadata from 2 to 3 by @dependabot[bot] in #3348
- Bump actions/upload-artifact from 7.0.1 to 7.0.2 by @dependabot[bot] in #3347
- Bump actions/download-artifact from 8.0.1 to 8.0.2 by @dependabot[bot] in #3349
- Bump actions/setup-java from 5.7.0 to 6.0.0 by @dependabot[bot] in #3298
- Bump Microsoft.Testing.Extensions.CodeCoverage from 18.10.0 to 18.11.0 by @dependabot[bot] in #3302
- Bump actions/setup-java from 6.0.0 to 6.0.1 by @dependabot[bot] in #3314
- Bump Microsoft.Testing.Extensions.GitHubActionsReport from 2.4.0 to 2.4.1 by @dependabot[bot] in #3318
- Bump xunit.v3 from 4.0.0 to 4.0.1 by @dependabot[bot] in #3316
- Bump Microsoft.Testing.Extensions.TrxReport from 2.4.0 to 2.4.1 by @dependabot[bot] in #3319
- [housekeeping] Automated PR to fix formatting errors by @github-actions[bot] in #3335
- Bump Microsoft.Testing.Extensions.CodeCoverage from 18.11.0 to 18.12.0 by @dependabot[bot] in #3336
- Bump Microsoft.Testing.Extensions.GitHubActionsReport from 2.4.1 to 2.5.0 by @dependabot[bot] in #3337
- Bump Microsoft.Testing.Extensions.TrxReport from 2.4.1 to 2.5.0 by @dependabot[bot] in #3338
- Bump Microsoft.Testing.Extensions.TrxReport from 2.5.0 to 2.5.1 by @dependabot[bot] in #3342
- Bump Microsoft.Testing.Extensions.GitHubActionsReport from 2.5.0 to 2.5.1 by @dependabot[bot] in #3341
- [housekeeping] Automated PR to fix formatting errors by @github-actions[bot] in #3320
- [housekeeping] Automated PR to fix formatting errors by @github-actions[bot] in #3332
New Contributors
- @austinbhale made their first contribution in #3309
- @meyerdominik made their first contribution in #3329
- @snydcol made their first contribution in #3340
- @alexh30486-ui made their first contribution in #3343
- @marcelomoresco made their first contribution in #3373
Full Changelog: 15.0.1...16.0.0