From 0d4220b391ce23f93bae46f7e0b3d90cb7660f08 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Mon, 23 Feb 2026 15:15:18 +0000 Subject: [PATCH 01/15] [ci] Run android on MAUI-Dnceng (#33997) ### Description of Change This pull request updates the CI pipeline configuration to introduce and use a new Linux-based pool for running Android tests, replacing the previous macOS-based pools. The main changes are the addition of the `AndroidPoolLinux` parameter and updating the relevant test stage to use this new pool. **Pipeline configuration updates:** * Added a new `AndroidPoolLinux` parameter to `parameters:` in `eng/pipelines/ci.yml` for specifying a Linux pool (`MAUI-DNCENG`) with the `1ESPT-Ubuntu22.04` image for Android test runs. * Updated the `mac_runandroid_tests` stage to use the new `AndroidPoolLinux` pool instead of the previous macOS-based pools, ensuring Android tests run on Linux infrastructure. --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: rmarinho <1235097+rmarinho@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/arcade/setup-test-env.yml | 2 ++ eng/pipelines/ci.yml | 14 +++++++----- eng/pipelines/common/device-tests-steps.yml | 8 +------ eng/pipelines/common/enable-kvm.yml | 8 +++++++ eng/pipelines/common/ui-tests-steps.yml | 8 +------ src/DotNet/DotNet.csproj | 4 +++- .../AndroidTemplateTests.cs | 22 +++++++++++++++++++ 7 files changed, 46 insertions(+), 20 deletions(-) create mode 100644 eng/pipelines/common/enable-kvm.yml diff --git a/eng/pipelines/arcade/setup-test-env.yml b/eng/pipelines/arcade/setup-test-env.yml index 2941477fb0b7..634e1d4579fa 100644 --- a/eng/pipelines/arcade/setup-test-env.yml +++ b/eng/pipelines/arcade/setup-test-env.yml @@ -12,6 +12,8 @@ steps: fetchDepth: 1 clean: true +- template: /eng/pipelines/common/enable-kvm.yml@self + - template: /eng/pipelines/common/provision.yml@self parameters: checkoutDirectory: '$(System.DefaultWorkingDirectory)' diff --git a/eng/pipelines/ci.yml b/eng/pipelines/ci.yml index ef9fd452abd8..58b1ba5dbb67 100644 --- a/eng/pipelines/ci.yml +++ b/eng/pipelines/ci.yml @@ -108,6 +108,13 @@ parameters: - ImageOverride -equals ACES_VM_SharedPool_Tahoe label: macOS +- name: AndroidPoolLinux + type: object + default: + name: MAUI-DNCENG + demands: + - ImageOverride -equals 1ESPT-Ubuntu22.04 + # Condition for MacOSPool comparison lanes (non-ARM64) # Runs on: (non-PR on main/net*.0/release/*/inflight/*) OR (PR targeting net*.0/release/*/inflight/*) @@ -277,12 +284,9 @@ stages: # TODO: macOSTemplates and AOT template categories - name: mac_runandroid_tests ${{ if eq(variables['Build.DefinitionName'], 'maui-pr') }}: - pool: - name: AcesShared - demands: - - ImageOverride -equals ACES_arm64_Sequoia_Xcode + pool: ${{ parameters.AndroidPoolLinux }} ${{ else }}: - pool: ${{ parameters.MacOSPool.internal }} + pool: ${{ parameters.AndroidPoolLinux }} timeout: 240 testCategory: RunOnAndroid diff --git a/eng/pipelines/common/device-tests-steps.yml b/eng/pipelines/common/device-tests-steps.yml index 8a59c42d73a5..46a7f09f147c 100644 --- a/eng/pipelines/common/device-tests-steps.yml +++ b/eng/pipelines/common/device-tests-steps.yml @@ -35,14 +35,8 @@ steps: continueOnError: true timeoutInMinutes: 60 -# Enable KVM for Android builds on Linux - ${{ if and(ne(parameters.buildType, 'buildOnly'), eq(parameters.platform, 'android')) }}: - - bash: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - displayName: Enable KVM - condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) + - template: enable-kvm.yml # Provision the various SDKs that are needed - template: provision.yml diff --git a/eng/pipelines/common/enable-kvm.yml b/eng/pipelines/common/enable-kvm.yml new file mode 100644 index 000000000000..9bb2050cc14c --- /dev/null +++ b/eng/pipelines/common/enable-kvm.yml @@ -0,0 +1,8 @@ +# Enable KVM for Android tests on Linux +steps: +- bash: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + displayName: Enable KVM + condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) diff --git a/eng/pipelines/common/ui-tests-steps.yml b/eng/pipelines/common/ui-tests-steps.yml index be2d650f4711..e3c1186e329c 100644 --- a/eng/pipelines/common/ui-tests-steps.yml +++ b/eng/pipelines/common/ui-tests-steps.yml @@ -50,14 +50,8 @@ steps: continueOnError: true timeoutInMinutes: 60 -# Enable KVM for Android builds on Linux - ${{ if and(ne(parameters.buildType, 'buildOnly'), eq(parameters.platform, 'android')) }}: - - bash: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - displayName: Enable KVM - condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) + - template: enable-kvm.yml - ${{ if eq(parameters.platform, 'catalyst')}}: - bash: | diff --git a/src/DotNet/DotNet.csproj b/src/DotNet/DotNet.csproj index 5cab15a090ed..4e4e92feec35 100644 --- a/src/DotNet/DotNet.csproj +++ b/src/DotNet/DotNet.csproj @@ -78,7 +78,9 @@ <_WorkloadSource Include="$(NugetArtifactsPath)" /> - <_LocalWorkloadIds Include="maui" /> + + <_LocalWorkloadIds Include="maui-android" Condition="$([MSBuild]::IsOSPlatform('linux'))" /> + <_LocalWorkloadIds Include="maui" Condition="!$([MSBuild]::IsOSPlatform('linux'))" /> <_LocalWorkloadIds Include="tizen" Condition=" '$(IncludeTizenTargetFrameworks)' == 'true' " /> diff --git a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/AndroidTemplateTests.cs b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/AndroidTemplateTests.cs index 2e8027bcfdfd..a4115a3f182d 100644 --- a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/AndroidTemplateTests.cs +++ b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/AndroidTemplateTests.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Runtime.InteropServices; +using System.Text.RegularExpressions; using Microsoft.Maui.IntegrationTests.Android; namespace Microsoft.Maui.IntegrationTests @@ -94,6 +95,12 @@ public void RunOnAndroid(string id, string framework, string config, string? tri Assert.True(DotnetInternal.New(id, projectDir, framework, output: _output), $"Unable to create template {id}. Check test output for errors."); + // On Linux, only the maui-android workload is installed. Previous .NET + // templates may still include iOS/macOS TFMs causing NETSDK1178 errors + // during restore. Strip them so only Android remains. + if (TestEnvironment.IsLinux) + StripNonAndroidTfms(projectFile, framework); + var buildProps = BuildProps; if (!string.IsNullOrEmpty(trimMode)) { @@ -128,5 +135,20 @@ void AddInstrumentation(string projectDir) "MainLauncher = true, Name = \"com.microsoft.mauitemplate.MainActivity\""); } + static void StripNonAndroidTfms(string projectFile, string framework) + { + var content = File.ReadAllText(projectFile); + var androidTfm = $"{framework}-android"; + // Remove conditional TargetFrameworks lines (iOS/macOS/Windows additions) + content = Regex.Replace(content, + @"\s*[^<]*", + ""); + // Set the base TargetFrameworks to Android only + content = Regex.Replace(content, + @"[^<]*", + $"{androidTfm}"); + File.WriteAllText(projectFile, content); + } + } } From 24b8c076bd7b2caed713872484dfd806401621e7 Mon Sep 17 00:00:00 2001 From: TamilarasanSF4853 Date: Tue, 24 Feb 2026 14:25:08 +0530 Subject: [PATCH 02/15] [Android, iOS, Mac] Fixed Entry ClearButton not visible on dark theme Change (#32889) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Issue Details - When TextColor is not specified and the theme is switched between dark and light (and vice versa), the clearButton color in the Entry does not update correctly. ### Root cause - On iOS, The root cause of the issue is that on iOS, when the text color is null, the clearButton.SetImage method reuses the same image created in the light theme. This breaks iOS’s automatic theme handling, causing the clear button color to remain unchanged. - On Android, When entry.TextColor is null (default), the clear button drawable's color filter was being cleared without applying the appropriate theme color, making the button invisible on dark backgrounds. ### Description of Change - On Android, When TextColor is `null`, now properly retrieves the system's `TextColorPrimary` attribute. Applies the correct color filter to the clear button drawable. Respects the app theme by using the enabled state color from the theme's color state list - On iOS, When TextColor is `null`, now sets `clearButton.TintColor = null` to use system default. Allows the clear button to adapt to the current theme automatically. Ensures tinted clear button image uses correct system colors Validated the behaviour in the following platforms - [x] Android - [x] Windows , - [x] iOS, - [x] MacOS ### Issues Fixed Fixes https://github.com/dotnet/maui/issues/32886 ### Output images Android
Before After
https://github.com/user-attachments/assets/cde1145b-1440-442f-9cca-f7a2790500ab https://github.com/user-attachments/assets/b072641b-182f-4189-843f-589261da90cc
--- ...yClearButtonShouldBeVisibleOnDarkTheme.png | Bin 0 -> 20868 bytes ...ClearButtonShouldBeVisibleOnLightTheme.png | Bin 0 -> 21000 bytes .../TestCases.HostApp/Issues/Issue32886.cs | 45 ++++++++++++++ ...yClearButtonShouldBeVisibleOnDarkTheme.png | Bin 0 -> 8261 bytes ...ClearButtonShouldBeVisibleOnLightTheme.png | Bin 0 -> 8792 bytes .../Tests/Issues/Issue32886.cs | 55 ++++++++++++++++++ ...yClearButtonShouldBeVisibleOnDarkTheme.png | Bin 0 -> 7916 bytes ...ClearButtonShouldBeVisibleOnLightTheme.png | Bin 0 -> 7760 bytes ...yClearButtonShouldBeVisibleOnDarkTheme.png | Bin 0 -> 13617 bytes ...ClearButtonShouldBeVisibleOnLightTheme.png | Bin 0 -> 14791 bytes .../Platform/Android/EditTextExtensions.cs | 18 +++++- .../src/Platform/iOS/TextFieldExtensions.cs | 4 +- 12 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 src/Controls/tests/TestCases.Android.Tests/snapshots/android/EntryClearButtonShouldBeVisibleOnDarkTheme.png create mode 100644 src/Controls/tests/TestCases.Android.Tests/snapshots/android/EntryClearButtonShouldBeVisibleOnLightTheme.png create mode 100644 src/Controls/tests/TestCases.HostApp/Issues/Issue32886.cs create mode 100644 src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/EntryClearButtonShouldBeVisibleOnDarkTheme.png create mode 100644 src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/EntryClearButtonShouldBeVisibleOnLightTheme.png create mode 100644 src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32886.cs create mode 100644 src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/EntryClearButtonShouldBeVisibleOnDarkTheme.png create mode 100644 src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/EntryClearButtonShouldBeVisibleOnLightTheme.png create mode 100644 src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/EntryClearButtonShouldBeVisibleOnDarkTheme.png create mode 100644 src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/EntryClearButtonShouldBeVisibleOnLightTheme.png diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/EntryClearButtonShouldBeVisibleOnDarkTheme.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/EntryClearButtonShouldBeVisibleOnDarkTheme.png new file mode 100644 index 0000000000000000000000000000000000000000..6d515899970245cd77ccba6097a4bcbeb2d53800 GIT binary patch literal 20868 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 A(f|Me literal 0 HcmV?d00001 diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/EntryClearButtonShouldBeVisibleOnLightTheme.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/EntryClearButtonShouldBeVisibleOnLightTheme.png new file mode 100644 index 0000000000000000000000000000000000000000..fe4be74dc09a770c9bfdc2df7848217453e81c2f GIT binary patch literal 21000 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 O*j0TKy`tZ5Kl)#AWnfGI literal 0 HcmV?d00001 diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue32886.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue32886.cs new file mode 100644 index 000000000000..b9f5498ed4d3 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue32886.cs @@ -0,0 +1,45 @@ +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 32886, "[Android, iOS, Mac] Entry ClearButton not visible on dark theme", PlatformAffected.Android | PlatformAffected.iOS | PlatformAffected.macOS)] +public class Issue32886 : TestContentPage +{ + protected override void Init() + { + Title = "Issue32886"; + + // Create the UITestEntry with ClearButtonVisibility + var entry = new UITestEntry + { + Text = "Entry Text", + IsCursorVisible = false, + IsSpellCheckEnabled = false, + IsTextPredictionEnabled = false, + AutomationId = "TestEntry", + ClearButtonVisibility = ClearButtonVisibility.WhileEditing + }; + + var button = new Button + { + Text = "Change theme", + AutomationId = "ThemeButton" + }; + button.Clicked += Button_Clicked; + + var layout = new VerticalStackLayout(); + layout.Children.Add(entry); + layout.Children.Add(button); + + Content = layout; + + // Set background color based on app theme + this.SetAppThemeColor(BackgroundColorProperty, Colors.White, Colors.Black); + } + + private void Button_Clicked(object sender, EventArgs e) + { + if (Application.Current is not null) + { + Application.Current.UserAppTheme = Application.Current.UserAppTheme != AppTheme.Dark ? AppTheme.Dark : AppTheme.Light; + } + } +} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/EntryClearButtonShouldBeVisibleOnDarkTheme.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/EntryClearButtonShouldBeVisibleOnDarkTheme.png new file mode 100644 index 0000000000000000000000000000000000000000..12e0dfe930eb13920fd268810c1e8dcbbbcafa87 GIT binary patch literal 8261 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 literal 0 HcmV?d00001 diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/EntryClearButtonShouldBeVisibleOnLightTheme.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/EntryClearButtonShouldBeVisibleOnLightTheme.png new file mode 100644 index 0000000000000000000000000000000000000000..56990c3225f662b57cebaf23ee20c8899a835c0e GIT binary patch literal 8792 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 literal 0 HcmV?d00001 diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32886.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32886.cs new file mode 100644 index 000000000000..9cadff37384f --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32886.cs @@ -0,0 +1,55 @@ +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue32886 : _IssuesUITest +{ + public Issue32886(TestDevice device) : base(device) + { + } + + public override string Issue => "[Android, iOS, Mac] Entry ClearButton not visible on dark theme"; + + [Test, Order(1)] + [Category(UITestCategories.Entry)] + public void EntryClearButtonShouldBeVisibleOnLightTheme() + { + App.WaitForElement("TestEntry"); + App.Tap("TestEntry"); +#if ANDROID // On Android, to address CI flakiness, the keyboard is dismissed. + if (App.WaitForKeyboardToShow(timeout: TimeSpan.FromSeconds(1))) + { + App.DismissKeyboard(); + } +#endif + +#if IOS + // On iOS, the virtual keyboard appears inconsistent with keyboard characters casing, can cause flaky test results. As this test verifying only the entry clear button color, crop the bottom portion of the screenshot to exclude the keyboard. + // Using DismissKeyboard() would unfocus the control in iOS, so we're using cropping instead to maintain focus during testing. + VerifyScreenshot(cropBottom: 1550); +#else + VerifyScreenshot(); +#endif + } + + [Test, Order(2)] + [Category(UITestCategories.Entry)] + public void EntryClearButtonShouldBeVisibleOnDarkTheme() + { + App.WaitForElement("TestEntry"); + App.Tap("ThemeButton"); +#if WINDOWS // On Windows, the clear button isn't visible when Entry loses focus, so manually focused to check its icon color. + App.Tap("TestEntry"); +#endif + +#if IOS + // On iOS, the virtual keyboard appears inconsistent with keyboard characters casing, can cause flaky test results. As this test verifying only the entry clear button color, crop the bottom portion of the screenshot to exclude the keyboard. + // Using DismissKeyboard() would unfocus the control in iOS, so we're using cropping instead to maintain focus during testing. + VerifyScreenshot(cropBottom: 1550); +#else + VerifyScreenshot(); +#endif + } +} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/EntryClearButtonShouldBeVisibleOnDarkTheme.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/EntryClearButtonShouldBeVisibleOnDarkTheme.png new file mode 100644 index 0000000000000000000000000000000000000000..2e31c8a77b08771c2e98c9fbd3887d5349851d7d GIT binary patch literal 7916 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 literal 0 HcmV?d00001 diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/EntryClearButtonShouldBeVisibleOnLightTheme.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/EntryClearButtonShouldBeVisibleOnLightTheme.png new file mode 100644 index 0000000000000000000000000000000000000000..b971b924bc2cffbf5ba4ca6a9ec4d80a7b4de656 GIT binary patch literal 7760 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 literal 0 HcmV?d00001 diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/EntryClearButtonShouldBeVisibleOnLightTheme.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/EntryClearButtonShouldBeVisibleOnLightTheme.png new file mode 100644 index 0000000000000000000000000000000000000000..545e17b916f8c9c8985821fdb01703374bcf129f GIT binary patch literal 14791 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 Date: Tue, 24 Feb 2026 15:54:48 +0100 Subject: [PATCH 03/15] Update MAUI dogfooding scripts to work with new Azure DevOps instance (#33198) Updates the scripts to use the new Azure DevOps instance. After this we can enable the dogfooding comment again so people can test PRs easier. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/dogfood-comment.yml | 31 ++-- eng/scripts/get-maui-pr.ps1 | 165 ++++++++++++-------- eng/scripts/get-maui-pr.sh | 216 ++++++++++++++++---------- 3 files changed, 257 insertions(+), 155 deletions(-) diff --git a/.github/workflows/dogfood-comment.yml b/.github/workflows/dogfood-comment.yml index 92e67a46ebc9..b2a823d75d64 100644 --- a/.github/workflows/dogfood-comment.yml +++ b/.github/workflows/dogfood-comment.yml @@ -1,14 +1,9 @@ name: Add Dogfooding Comment on: - # Use pull_request_target to run in the context of the base branch - # This allows commenting on PRs from forks - pull_request_target: - types: [opened, reopened, synchronize] - branches: - - 'main' - - 'net*' - - 'release/**' + # Trigger when the maui-pr build check completes + check_run: + types: [completed] # Allow manual triggering workflow_dispatch: @@ -20,13 +15,23 @@ on: # Ensure only one instance runs at a time per PR to prevent duplicate comments concurrency: - group: dogfood-comment-${{ github.event.pull_request.number || github.event.inputs.pr_number }} + group: dogfood-comment-${{ github.event.check_run.pull_requests[0].number || github.event.inputs.pr_number || 'unknown' }} cancel-in-progress: true jobs: add-dogfood-comment: - # Only run on the dotnet org to avoid running on forks - if: ${{ github.repository_owner == 'dotnet' }} + # Only run on the dotnet org, for the maui-pr check, when it completes successfully + if: | + github.repository_owner == 'dotnet' && + ( + github.event_name == 'workflow_dispatch' || + ( + github.event_name == 'check_run' && + github.event.check_run.name == 'maui-pr (Pack .NET MAUI Pack Windows)' && + github.event.check_run.conclusion == 'success' && + github.event.check_run.pull_requests[0] != null + ) + ) runs-on: ubuntu-latest permissions: pull-requests: write @@ -36,8 +41,8 @@ jobs: uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: script: | - // Get PR number from either the PR event or manual input - const prNumber = context.payload.number || context.payload.inputs?.pr_number; + // Get PR number from either the check_run event or manual input + const prNumber = context.payload.check_run?.pull_requests?.[0]?.number || context.payload.inputs?.pr_number; const bashScript = 'https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh'; const psScript = 'https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1'; diff --git a/eng/scripts/get-maui-pr.ps1 b/eng/scripts/get-maui-pr.ps1 index edc3ddc3c989..e20f81a86828 100644 --- a/eng/scripts/get-maui-pr.ps1 +++ b/eng/scripts/get-maui-pr.ps1 @@ -44,7 +44,10 @@ param( [int]$PrNumber, [Parameter(Mandatory = $false)] - [string]$ProjectPath = "" + [string]$ProjectPath = "", + + [Parameter(Mandatory = $false)] + [switch]$Yes ) $ErrorActionPreference = "Stop" @@ -52,10 +55,21 @@ $ProgressPreference = "SilentlyContinue" # Configuration - Allow override via environment variable $GitHubRepo = if ($env:MAUI_REPO) { $env:MAUI_REPO } else { "dotnet/maui" } -$AzureDevOpsOrg = "xamarin" +$AzureDevOpsOrg = "dnceng-public" $AzureDevOpsProject = "public" $PackageName = "Microsoft.Maui.Controls" +# Build GitHub auth headers (GITHUB_TOKEN env var or gh CLI) +$GitHubHeaders = @{ "User-Agent" = "MAUI-PR-Script" } +if ($env:GITHUB_TOKEN) { + $GitHubHeaders["Authorization"] = "token $($env:GITHUB_TOKEN)" +} elseif (Get-Command gh -ErrorAction SilentlyContinue) { + try { + $ghToken = gh auth token 2>$null + if ($ghToken) { $GitHubHeaders["Authorization"] = "token $ghToken" } + } catch { } +} + # Color output functions function Write-Info { param([string]$Message) @@ -67,12 +81,12 @@ function Write-Success { Write-Host "✅ $Message" -ForegroundColor Green } -function Write-Warning { +function Write-Warn { param([string]$Message) Write-Host "⚠️ $Message" -ForegroundColor Yellow } -function Write-Error { +function Write-Err { param([string]$Message) Write-Host "❌ $Message" -ForegroundColor Red } @@ -117,7 +131,7 @@ function Get-PullRequestInfo { try { $prUrl = "https://api.github.com/repos/$GitHubRepo/pulls/$PrNumber" - $pr = Invoke-RestMethod -Uri $prUrl -Headers @{ "User-Agent" = "MAUI-PR-Script" } + $pr = Invoke-RestMethod -Uri $prUrl -Headers $GitHubHeaders -TimeoutSec 30 return @{ Number = $pr.number @@ -140,14 +154,13 @@ function Get-BuildInfo { try { $checksUrl = "https://api.github.com/repos/$GitHubRepo/commits/$SHA/check-runs" - $response = Invoke-RestMethod -Uri $checksUrl -Headers @{ - "User-Agent" = "MAUI-PR-Script" + $response = Invoke-RestMethod -Uri $checksUrl -Headers ($GitHubHeaders + @{ "Accept" = "application/vnd.github.v3+json" - } + }) -TimeoutSec 30 - # Look for the main MAUI build check + # Look for the main MAUI build check (not uitests) $buildCheck = $response.check_runs | Where-Object { - $_.name -eq "MAUI-public" -and $_.status -eq "completed" + $_.name -like "maui-pr*" -and $_.name -notlike "*uitests*" -and $_.status -eq "completed" -and $_.details_url -match 'buildId=' } | Select-Object -First 1 if (-not $buildCheck) { @@ -155,18 +168,22 @@ function Get-BuildInfo { } if ($buildCheck.conclusion -ne "success") { - Write-Warning "Build completed with status: $($buildCheck.conclusion)" - $continue = Read-Host "Do you want to continue anyway? (y/N)" - if ($continue -ne "y" -and $continue -ne "Y") { - throw "Build was not successful. Aborting." + Write-Warn "Build completed with status: $($buildCheck.conclusion)" + if (-not $Yes) { + $continue = Read-Host "Do you want to continue anyway? (y/N)" + if ($continue -ne "y" -and $continue -ne "Y") { + throw "Build was not successful. Aborting." + } } } # Extract build ID from details URL if ($buildCheck.details_url -match 'buildId=(\d+)') { - $buildId = $Matches[1] - Write-Success "Found build ID: $buildId" - return $buildId + return @{ + BuildId = $Matches[1] + Status = $buildCheck.conclusion + Url = $buildCheck.details_url + } } throw "Could not extract build ID from check run details." @@ -184,13 +201,13 @@ function Get-BuildArtifacts { try { $artifactsUrl = "https://dev.azure.com/$AzureDevOpsOrg/$AzureDevOpsProject/_apis/build/builds/$BuildId/artifacts?api-version=7.1" - $response = Invoke-RestMethod -Uri $artifactsUrl -Headers @{ "User-Agent" = "MAUI-PR-Script" } + $response = Invoke-RestMethod -Uri $artifactsUrl -Headers @{ "User-Agent" = "MAUI-PR-Script" } -TimeoutSec 30 - # Look for nuget artifact - $artifact = $response.value | Where-Object { $_.name -eq "nuget" } | Select-Object -First 1 + # Look for PackageArtifacts artifact + $artifact = $response.value | Where-Object { $_.name -eq "PackageArtifacts" } | Select-Object -First 1 if (-not $artifact) { - throw "No 'nuget' artifact found in build $BuildId" + throw "No 'PackageArtifacts' artifact found in build $BuildId" } return $artifact.resource.downloadUrl @@ -225,7 +242,19 @@ function Get-Artifacts { Write-Info "Downloading artifacts (this may take a moment)..." try { - Invoke-WebRequest -Uri $DownloadUrl -OutFile $zipFile -UseBasicParsing + # Use curl on non-Windows (Invoke-WebRequest is extremely slow for large files on macOS/Linux) + if ((-not $IsWindows) -and $env:OS -ne "Windows_NT" -and (Get-Command curl -ErrorAction SilentlyContinue)) { + $curlExit = 0 + & curl -sL -o $zipFile $DownloadUrl + $curlExit = $LASTEXITCODE + if ($curlExit -ne 0) { + throw "curl download failed with exit code $curlExit" + } + } else { + $ProgressPreference = 'SilentlyContinue' + Invoke-WebRequest -Uri $DownloadUrl -OutFile $zipFile -UseBasicParsing + $ProgressPreference = 'Continue' + } Write-Success "Downloaded artifacts" Write-Info "Extracting artifacts..." @@ -240,6 +269,9 @@ function Get-Artifacts { throw "Could not find NuGet packages in the extracted artifacts" } + # Clean up zip file to save disk space + Remove-Item $zipFile -Force -ErrorAction SilentlyContinue + return $nupkgDir.FullName } catch { @@ -252,14 +284,14 @@ function Get-PackageVersion { param([string]$PackagesDir) $package = Get-ChildItem -Path $PackagesDir -Filter "$PackageName.*.nupkg" -File | - Where-Object { $_.Name -notmatch '\.symbols\.nupkg$' } | + Where-Object { $_.Name -notmatch '\.symbols\.nupkg$' -and $_.Name -match "$([regex]::Escape($PackageName))\.\d" } | Select-Object -First 1 if (-not $package) { throw "Could not find $PackageName package in artifacts" } - if ($package.Name -match "$PackageName\.(.+)\.nupkg") { + if ($package.Name -match "$([regex]::Escape($PackageName))\.(.+)\.nupkg") { return $Matches[1] } @@ -273,7 +305,7 @@ function Get-TargetFrameworkVersion { $content = Get-Content $ProjectPath -Raw # Look for TargetFramework or TargetFrameworks - if ($content -match '([^<]+)') { + if ($content -match '(.*?)') { $tfms = $Matches[1] # Extract .NET version (e.g., net9.0, net10.0) @@ -326,7 +358,7 @@ function Update-TargetFrameworks { Set-Content -Path $ProjectPath -Value $content -NoNewline Write-Success "Updated target frameworks to .NET $NewNetVersion.0" - Write-Warning "You may need to update other package dependencies to match .NET $NewNetVersion.0" + Write-Warn "You may need to update other package dependencies to match .NET $NewNetVersion.0" } # Create or update NuGet.config @@ -334,7 +366,7 @@ function Update-NuGetConfig { param([string]$ProjectDir, [string]$PackagesDir) $nugetConfigPath = Join-Path $ProjectDir "NuGet.config" - $sourceName = "maui-pr-build" + $sourceName = "maui-pr-$PrNumber" if (Test-Path $nugetConfigPath) { Write-Info "Updating existing NuGet.config..." @@ -428,7 +460,7 @@ try { Write-Info "State: $($prInfo.State)" if ($prInfo.State -ne "open" -and $prInfo.State -ne "closed") { - Write-Warning "PR state is '$($prInfo.State)'. Continuing anyway..." + Write-Warn "PR state is '$($prInfo.State)'. Continuing anyway..." } Write-Step "Detecting target framework" @@ -436,11 +468,11 @@ try { Write-Info "Current target framework: .NET $targetNetVersion.0" Write-Step "Finding build artifacts" - $buildId = Get-BuildInfo -SHA $prInfo.SHA + $buildInfo = Get-BuildInfo -SHA $prInfo.SHA Write-Step "Downloading artifacts" - $downloadUrl = Get-BuildArtifacts -BuildId $buildId - $packagesDir = Get-Artifacts -DownloadUrl $downloadUrl -BuildId $buildId + $downloadUrl = Get-BuildArtifacts -BuildId $buildInfo.BuildId + $packagesDir = Get-Artifacts -DownloadUrl $downloadUrl -BuildId $buildInfo.BuildId Write-Step "Extracting package information" $version = Get-PackageVersion -PackagesDir $packagesDir @@ -453,17 +485,22 @@ try { $compatible = Test-VersionCompatibility -Version $version -TargetNetVersion $targetNetVersion -PackageNetVersion $packageNetVersion $willUpdateTfm = $false if (-not $compatible) { - Write-Warning "This PR build may target a newer .NET version than your project" + Write-Warn "This PR build may target a newer .NET version than your project" Write-Info "Your project targets: .NET $targetNetVersion.0" Write-Info "This PR build targets: .NET $packageNetVersion.0" - $response = Read-Host "`nDo you want to update your project to .NET $packageNetVersion.0? (y/N)" - if ($response -eq "y" -or $response -eq "Y") { + if ($Yes) { $willUpdateTfm = $true - Write-Warning "Note: You may need to manually update other package dependencies to versions compatible with .NET $packageNetVersion.0" + } else { + $response = Read-Host "`nDo you want to update your project to .NET $packageNetVersion.0? (y/N)" + $willUpdateTfm = ($response -eq "y" -or $response -eq "Y") + } + + if ($willUpdateTfm) { + Write-Warn "Note: You may need to manually update other package dependencies to versions compatible with .NET $packageNetVersion.0" } else { - Write-Warning "Continuing without updating target framework. The package may not be compatible." + Write-Warn "Continuing without updating target framework. The package may not be compatible." } } @@ -475,7 +512,7 @@ try { Write-Host "" Write-Host "By continuing, you will apply the PR artifacts to your project." -ForegroundColor Cyan Write-Host "" - Write-Warning "This should NOT be used in production and is for testing purposes only." + Write-Warn "This should NOT be used in production and is for testing purposes only." Write-Host "" Write-Host "TIP: Create a separate Git branch for testing!" -ForegroundColor Cyan Write-Host " git checkout -b test-pr-$PrNumber" -ForegroundColor Gray @@ -487,28 +524,25 @@ try { Write-Host "Changes to be applied:" -ForegroundColor White Write-Host " • Project: $projectName" -ForegroundColor Gray Write-Host " • Package version: $version" -ForegroundColor Gray - - # Extract .NET version from package version (e.g., 10.0.20-ci.main.25607.5 -> 10) - $packageDotNetVersion = $null - if ($version -match '^(\d+)\.') { - $packageDotNetVersion = $Matches[1] - } - if ($willUpdateTfm) { - $targetVersionForDisplay = if ($packageDotNetVersion) { "$packageDotNetVersion.0" } else { "$packageNetVersion.0" } + $targetVersionForDisplay = if ($packageNetVersion) { "$packageNetVersion.0" } else { "10.0" } Write-Host " • Target framework: Will be updated to .NET $targetVersionForDisplay" -ForegroundColor Gray } Write-Host "" - $response = Read-Host "Do you want to continue? (y/N)" - if ($response -ne "y" -and $response -ne "Y") { - Write-Warning "Operation cancelled by user" - exit 0 + if ($Yes) { + Write-Info "Auto-accepting confirmation (-Yes flag)" + } else { + $response = Read-Host "Do you want to continue? (y/N)" + if ($response -ne "y" -and $response -ne "Y") { + Write-Warn "Operation cancelled by user" + return + } } Write-Host "" if ($willUpdateTfm) { - $targetNetVersionToApply = if ($packageDotNetVersion) { [int]$packageDotNetVersion } else { 10 } + $targetNetVersionToApply = if ($packageNetVersion) { [int]$packageNetVersion } else { 10 } Update-TargetFrameworks -ProjectPath $projectPath -NewNetVersion $targetNetVersionToApply $targetNetVersion = $targetNetVersionToApply } @@ -519,16 +553,6 @@ try { Write-Step "Updating package reference" Update-PackageReference -ProjectPath $projectPath -Version $version - # Get latest stable version for revert instructions - try { - $nugetResponse = Invoke-RestMethod -Uri "https://api.nuget.org/v3-flatcontainer/microsoft.maui.controls/index.json" -UseBasicParsing - $stableVersions = $nugetResponse.versions | Where-Object { $_ -notmatch '-' } | Sort-Object -Descending - $latestStable = $stableVersions[0] - } - catch { - $latestStable = "X.Y.Z" - } - Write-Host @" ╔═══════════════════════════════════════════════════════════╗ @@ -548,16 +572,30 @@ try { Write-Info "Local package source: $packagesDir" Write-Host "" + # Get latest stable version for revert instructions + $stableVersion = "X.Y.Z" + try { + $nugetResponse = Invoke-RestMethod -Uri "https://api.nuget.org/v3-flatcontainer/$($PackageName.ToLower())/index.json" -TimeoutSec 30 + if ($nugetResponse -and $nugetResponse.versions) { + $stableVersions = $nugetResponse.versions | Where-Object { $_ -notmatch '-' } + if ($stableVersions) { + $stableVersion = $stableVersions | Sort-Object { [Version]$_ } -Descending | Select-Object -First 1 + } + } + } catch { + # If we can't fetch, just use placeholder + } + Write-Host "═══════════════════════════════════════════════════════════" -ForegroundColor Yellow Write-Host " TO REVERT TO PRODUCTION VERSION" -ForegroundColor Yellow Write-Host "═══════════════════════════════════════════════════════════" -ForegroundColor Yellow Write-Host "" Write-Host "1. Edit $projectName and change the version:" -ForegroundColor White Write-Host " From: Version=`"$version`"" -ForegroundColor Gray - Write-Host " To: Version=`"X.Y.Z`"" -ForegroundColor Gray + Write-Host " To: Version=`"$stableVersion`"" -ForegroundColor Gray Write-Host " (Check https://www.nuget.org/packages/$PackageName for latest)" -ForegroundColor DarkGray Write-Host "" - Write-Host "2. In NuGet.config, remove or comment out the 'maui-pr-build' source" -ForegroundColor White + Write-Host "2. In NuGet.config, remove or comment out the 'maui-pr-$PrNumber' source" -ForegroundColor White Write-Host "" Write-Host "3. Run: dotnet restore --force" -ForegroundColor White Write-Host "" @@ -567,7 +605,7 @@ try { } catch { - Write-Error "Failed to apply PR build: $_" + Write-Err "Failed to apply PR build: $_" Write-Host "" Write-Info "Troubleshooting tips:" Write-Host " • Make sure you're in a directory containing a .NET MAUI project" -ForegroundColor Gray @@ -575,6 +613,5 @@ catch { Write-Host " • Check if there's a completed build for this PR (look for green checkmarks)" -ForegroundColor Gray Write-Host " • Check your internet connection" -ForegroundColor Gray Write-Host " • Visit: https://github.com/dotnet/maui/wiki/Testing-PR-Builds" -ForegroundColor Gray - exit 1 } diff --git a/eng/scripts/get-maui-pr.sh b/eng/scripts/get-maui-pr.sh index d01ad1b7f01c..9541a0556644 100644 --- a/eng/scripts/get-maui-pr.sh +++ b/eng/scripts/get-maui-pr.sh @@ -10,11 +10,12 @@ # # Usage: # curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 33002 -# ./get-maui-pr.sh [PROJECT_PATH] +# ./get-maui-pr.sh [-y|--yes] [PROJECT_PATH] # # Examples: # ./get-maui-pr.sh 33002 # ./get-maui-pr.sh 33002 ./MyApp/MyApp.csproj +# ./get-maui-pr.sh -y 33002 # Skip confirmation prompts # # Requirements: # - .NET SDK installed @@ -51,10 +52,21 @@ handle_error() { # Configuration - Allow override via environment variable GITHUB_REPO="${MAUI_REPO:-dotnet/maui}" -AZURE_DEVOPS_ORG="xamarin" +AZURE_DEVOPS_ORG="dnceng-public" AZURE_DEVOPS_PROJECT="public" PACKAGE_NAME="Microsoft.Maui.Controls" +# Build GitHub auth header if token available (GITHUB_TOKEN or gh CLI) +GITHUB_AUTH_HEADER="" +if [ -n "$GITHUB_TOKEN" ]; then + GITHUB_AUTH_HEADER="Authorization: token $GITHUB_TOKEN" +elif command -v gh &> /dev/null && gh auth status &> /dev/null; then + GITHUB_TOKEN=$(gh auth token 2>/dev/null) + if [ -n "$GITHUB_TOKEN" ]; then + GITHUB_AUTH_HEADER="Authorization: token $GITHUB_TOKEN" + fi +fi + # Colors RED='\033[0;31m' GREEN='\033[0;32m' @@ -69,23 +81,23 @@ NC='\033[0m' # No Color # Output functions info() { - echo -e "${CYAN}ℹ️ $1${NC}" + echo -e "${CYAN}ℹ️ $1${NC}" >&2 } success() { - echo -e "${GREEN}✅ $1${NC}" + echo -e "${GREEN}✅ $1${NC}" >&2 } warning() { - echo -e "${YELLOW}⚠️ $1${NC}" + echo -e "${YELLOW}⚠️ $1${NC}" >&2 } error() { - echo -e "${RED}❌ $1${NC}" + echo -e "${RED}❌ $1${NC}" >&2 } step() { - echo -e "\n${BLUE}▶️ $1${NC}" + echo -e "\n${BLUE}▶️ $1${NC}" >&2 } # Check dependencies @@ -155,7 +167,8 @@ get_pr_info() { info "Fetching PR #$pr_number information from GitHub..." local pr_url="https://api.github.com/repos/$GITHUB_REPO/pulls/$pr_number" - local pr_json=$(curl -s -H "User-Agent: MAUI-PR-Script" "$pr_url") + local pr_json + pr_json=$(curl -s -H "User-Agent: MAUI-PR-Script" ${GITHUB_AUTH_HEADER:+-H "$GITHUB_AUTH_HEADER"} "$pr_url") if [ -z "$pr_json" ] || echo "$pr_json" | jq -e '.message' > /dev/null 2>&1; then error "Failed to fetch PR information. Make sure PR #$pr_number exists." @@ -172,10 +185,11 @@ get_build_info() { info "Looking for build artifacts for commit ${sha:0:7}..." local checks_url="https://api.github.com/repos/$GITHUB_REPO/commits/$sha/check-runs" - local checks_json=$(curl -s -H "User-Agent: MAUI-PR-Script" -H "Accept: application/vnd.github.v3+json" "$checks_url") + local checks_json + checks_json=$(curl -s -H "User-Agent: MAUI-PR-Script" -H "Accept: application/vnd.github.v3+json" ${GITHUB_AUTH_HEADER:+-H "$GITHUB_AUTH_HEADER"} "$checks_url") - # Find the main MAUI build check - local build_check=$(echo "$checks_json" | jq -r '.check_runs[] | select(.name == "MAUI-public" and .status == "completed") | @json' | head -n 1) + # Find the main MAUI build check (not uitests) + local build_check=$(echo "$checks_json" | jq -r '.check_runs[] | select((.name | startswith("maui-pr")) and (.name | contains("uitests") | not) and .status == "completed" and (.details_url | contains("buildId="))) | @json' | head -n 1) if [ -z "$build_check" ] || [ "$build_check" == "null" ]; then error "No completed build found for this PR" @@ -186,11 +200,15 @@ get_build_info() { local conclusion=$(echo "$build_check" | jq -r '.conclusion') if [ "$conclusion" != "success" ]; then warning "Build completed with status: $conclusion" - read -p "Do you want to continue anyway? (y/N) " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - error "Build was not successful. Aborting." - exit 1 + if [ "$YES_FLAG" = true ]; then + info "Auto-accepting non-successful build (-y flag)" + else + read -p "Do you want to continue anyway? (y/N) " -n 1 -r + echo >&2 + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + error "Build was not successful. Aborting." + exit 1 + fi fi fi @@ -216,11 +234,11 @@ get_build_artifacts() { local artifacts_url="https://dev.azure.com/$AZURE_DEVOPS_ORG/$AZURE_DEVOPS_PROJECT/_apis/build/builds/$build_id/artifacts?api-version=7.1" local artifacts_json=$(curl -s -H "User-Agent: MAUI-PR-Script" "$artifacts_url") - # Look for nuget artifact - local download_url=$(echo "$artifacts_json" | jq -r '.value[] | select(.name == "nuget") | .resource.downloadUrl' | head -n 1) + # Look for PackageArtifacts artifact + local download_url=$(echo "$artifacts_json" | jq -r '.value[] | select(.name == "PackageArtifacts") | .resource.downloadUrl' | head -n 1) if [ -z "$download_url" ] || [ "$download_url" == "null" ]; then - error "No 'nuget' artifact found in build $build_id" + error "No 'PackageArtifacts' artifact found in build $build_id" exit 1 fi @@ -262,6 +280,9 @@ get_artifacts() { exit 1 fi + # Clean up zip file to save disk space + rm -f "$zip_file" + echo "$nupkg_dir" } @@ -269,7 +290,7 @@ get_artifacts() { get_package_version() { local packages_dir="$1" - local package_file=$(find "$packages_dir" -type f -name "$PACKAGE_NAME.*.nupkg" -not -name "*.symbols.nupkg" | head -n 1) + local package_file=$(find "$packages_dir" -type f -name "$PACKAGE_NAME.*.nupkg" -not -name "*.symbols.nupkg" | grep -E "$PACKAGE_NAME\.[0-9]" | head -n 1) if [ -z "$package_file" ]; then error "Could not find $PACKAGE_NAME package in artifacts" @@ -343,12 +364,9 @@ update_target_frameworks() { cp "$project_path" "$project_path.bak" # Update all netX.0-* references (including in conditional TargetFrameworks) - sed -i.tmp "s/net[0-9]\+\.0-/net$new_net_version.0-/g" "$project_path" + sed -i.tmp -E "s/net[0-9]+\.0-/net${new_net_version}.0-/g" "$project_path" rm -f "$project_path.tmp" - # Cleanup backup file on success - rm -f "$project_path.bak" - success "Updated target frameworks to .NET $new_net_version.0" warning "You may need to update other package dependencies to match .NET $new_net_version.0" } @@ -359,16 +377,16 @@ update_nuget_config() { local packages_dir="$2" local nuget_config="$project_dir/NuGet.config" - local source_name="maui-pr-build" + local source_name="maui-pr-$pr_number" if [ -f "$nuget_config" ]; then info "Updating existing NuGet.config..." # Remove existing source with same name if it exists - sed -i.tmp "/| \n |" "$nuget_config" + sed -i.tmp "s|| \n |" "$nuget_config" rm -f "$nuget_config.tmp" else @@ -404,32 +422,35 @@ update_package_reference() { fi # Replace the version in PackageReference - sed -i.tmp "s|\(\)|\1$version\2|g" "$project_path" + sed -i.tmp -E "s#( /dev/null 2>&1; then - # Cleanup backup file on success - rm -f "$project_path.bak" - success "Updated $PACKAGE_NAME to version $version" - else - # Restore backup and report error - mv "$project_path.bak" "$project_path" - error "Could not find $PACKAGE_NAME package reference in project file" - exit 1 - fi + success "Updated $PACKAGE_NAME to version $version" } +# Global flag for non-interactive mode (set by -y/--yes) +YES_FLAG=false + # Main execution main() { + # Parse flags + local positional_args=() + for arg in "$@"; do + case "$arg" in + -y|--yes) YES_FLAG=true ;; + *) positional_args+=("$arg") ;; + esac + done + # Check arguments - if [ $# -lt 1 ]; then - error "Usage: $0 [PROJECT_PATH]" + if [ ${#positional_args[@]} -lt 1 ]; then + error "Usage: $0 [-y|--yes] [PROJECT_PATH]" exit 1 fi - pr_number="$1" # Global for error handler - local project_path_arg="${2:-}" + pr_number="${positional_args[0]}" # Global for error handler + local project_path_arg="${positional_args[1]:-}" # Check dependencies check_dependencies @@ -448,54 +469,82 @@ EOF echo -e "${NC}" step "Finding MAUI project" - local project_path=$(find_maui_project "$project_path_arg") - local project_dir=$(dirname "$project_path") - local project_name=$(basename "$project_path") + local project_path + project_path=$(find_maui_project "$project_path_arg") + local project_dir + project_dir=$(dirname "$project_path") + local project_name + project_name=$(basename "$project_path") success "Found project: $project_name" step "Fetching PR information" - local pr_json=$(get_pr_info "$pr_number") - local pr_title=$(echo "$pr_json" | jq -r '.title') - local pr_state=$(echo "$pr_json" | jq -r '.state') - local pr_sha=$(echo "$pr_json" | jq -r '.head.sha') + local pr_json + pr_json=$(get_pr_info "$pr_number") + local pr_title + pr_title=$(echo "$pr_json" | jq -r '.title') + local pr_state + pr_state=$(echo "$pr_json" | jq -r '.state') + local pr_sha + pr_sha=$(echo "$pr_json" | jq -r '.head.sha') info "PR #$pr_number: $pr_title" info "State: $pr_state" step "Detecting target framework" - local target_net_version=$(get_target_framework_version "$project_path") + local target_net_version + target_net_version=$(get_target_framework_version "$project_path") info "Current target framework: .NET $target_net_version.0" step "Finding build artifacts" - local build_id=$(get_build_info "$pr_sha") + local build_id + build_id=$(get_build_info "$pr_sha") step "Downloading artifacts" - local download_url=$(get_build_artifacts "$build_id") - local packages_dir=$(get_artifacts "$download_url" "$build_id") + local download_url + download_url=$(get_build_artifacts "$build_id") + local packages_dir + packages_dir=$(get_artifacts "$download_url" "$build_id") step "Extracting package information" - local version=$(get_package_version "$packages_dir") + local version + version=$(get_package_version "$packages_dir") success "Found package version: $version" # Extract .NET version from package version (e.g., 10.0.20-ci.main.25607.5 -> 10) - local package_dotnet_version - package_dotnet_version=$(get_package_dotnet_version "$version") + local package_dotnet_version="" + if [[ $version =~ ^([0-9]+)\. ]]; then + package_dotnet_version="${BASH_REMATCH[1]}" + fi + + # Get package .NET version + local package_net_version + package_net_version=$(get_package_dotnet_version "$version") # Check compatibility local will_update_tfm=false - local target_version="$package_dotnet_version.0" - if ! test_version_compatibility "$version" "$target_net_version" "$package_dotnet_version"; then + local target_version="$package_net_version.0" + if ! test_version_compatibility "$version" "$target_net_version" "$package_net_version"; then warning "This PR build may target a newer .NET version than your project" info "Your project targets: .NET $target_net_version.0" - info "This PR build targets: .NET $package_dotnet_version.0" + if [[ -n "$package_dotnet_version" ]]; then + info "This PR build targets: .NET $package_dotnet_version.0" + target_version="$package_dotnet_version.0" + else + info "This PR build targets: .NET $package_net_version.0" + fi - read -p "Do you want to update your project to .NET $target_version? (y/N) " -n 1 -r - echo - if [[ $REPLY =~ ^[Yy]$ ]]; then + if [ "$YES_FLAG" = true ]; then will_update_tfm=true warning "Note: You may need to manually update other package dependencies to versions compatible with .NET $target_version" else - warning "Continuing without updating target framework. The package may not be compatible." + read -p "Do you want to update your project to .NET $target_version? (y/N) " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + will_update_tfm=true + warning "Note: You may need to manually update other package dependencies to versions compatible with .NET $target_version" + else + warning "Continuing without updating target framework. The package may not be compatible." + fi fi fi @@ -524,11 +573,15 @@ EOF fi echo "" - read -p "Do you want to continue? (y/N) " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - warning "Operation cancelled by user" - exit 0 + if [ "$YES_FLAG" = true ]; then + info "Auto-accepting confirmation (-y flag)" + else + read -p "Do you want to continue? (y/N) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + warning "Operation cancelled by user" + exit 0 + fi fi echo "" @@ -547,13 +600,6 @@ EOF step "Updating package reference" update_package_reference "$project_path" "$version" - # Get latest stable version for revert instructions - local latest_stable=$(curl -s "https://api.nuget.org/v3-flatcontainer/microsoft.maui.controls/index.json" | \ - jq -r '.versions[]' | grep -v '-' | tail -1) - if [ -z "$latest_stable" ]; then - latest_stable="X.Y.Z" - fi - echo -e "${GREEN}" cat << EOF @@ -575,16 +621,30 @@ EOF info "Local package source: $packages_dir" echo "" + # Get latest stable version for revert instructions + local stable_version="X.Y.Z" + local package_lower=$(echo "$PACKAGE_NAME" | tr '[:upper:]' '[:lower:]') + if command -v curl >/dev/null 2>&1; then + local nuget_response=$(curl -s "https://api.nuget.org/v3-flatcontainer/$package_lower/index.json" 2>/dev/null || echo "") + if [[ -n "$nuget_response" ]]; then + # Extract stable versions (those without -) + stable_version=$(echo "$nuget_response" | grep -o '"[0-9]\+\.[0-9]\+\.[0-9]\+"' | grep -v '-' | tail -1 | tr -d '"') + if [[ -z "$stable_version" ]]; then + stable_version="X.Y.Z" + fi + fi + fi + echo -e "${YELLOW}═══════════════════════════════════════════════════════════${NC}" echo -e "${YELLOW} TO REVERT TO PRODUCTION VERSION${NC}" echo -e "${YELLOW}═══════════════════════════════════════════════════════════${NC}" echo "" echo -e "${WHITE}1. Edit $project_name and change the version:${NC}" echo -e "${GRAY} From: Version=\"$version\"${NC}" - echo -e "${GRAY} To: Version=\"X.Y.Z\"${NC}" + echo -e "${GRAY} To: Version=\"$stable_version\"${NC}" echo -e "${DGRAY} (Check https://www.nuget.org/packages/$PACKAGE_NAME for latest)${NC}" echo "" - echo -e "${WHITE}2. In NuGet.config, remove or comment out the 'maui-pr-build' source${NC}" + echo -e "${WHITE}2. In NuGet.config, remove or comment out the 'maui-pr-$pr_number' source${NC}" echo "" echo -e "${WHITE}3. Run: dotnet restore --force${NC}" echo "" From 49bc594309584f357ba05e09c6d34c9a9317475c Mon Sep 17 00:00:00 2001 From: Stephane Delcroix Date: Tue, 24 Feb 2026 21:13:38 +0100 Subject: [PATCH 04/15] Add MauiXamlHotReload property for IDE communication (#34028) ## Description Introduces the `MauiXamlHotReload` MSBuild property to communicate to the IDE which type of XAML Hot Reload implementation the application expects. ### Values | Value | Description | |-------|-------------| | `Legacy` (default) | Traditional XAML Hot Reload implementation | | `SourceGen` | Source generator-based XAML Hot Reload (experimental) | ### Usage ```xml SourceGen ``` ## IDE Behavior ### When `MauiXamlHotReload=Legacy` (Default) The IDE should behave as it currently does: - XAML Hot Reload enabled - Full page refresh supported - Incremental XAML Hot Reload supported ### When `MauiXamlHotReload=SourceGen` The IDE **MUST**: - Ensure C# Hot Reload is **enabled** - Ensure legacy XAML Hot Reload is **disabled** - Provide a mechanism to trigger the MAUI Update Handler (MUH) The IDE **MUST NOT**: - Trigger full page refresh (i.e., send updated XAML to the app) - Use incremental XAML Hot Reload The IDE **MAY** continue to support: - VisualDiagnostics - BindingDiagnostics ## Warning When `MauiXamlHotReload=SourceGen` is set, warning MAUI1002 is emitted: > MauiXamlHotReload is set to 'SourceGen'. Source generator-based XAML Hot Reload is experimental and not yet fully implemented. Use at your own risk. ## Migration Note This property exists to facilitate migration from legacy XAML Hot Reload to the source generator-based approach. In a future release, `Legacy` may be deprecated with `SourceGen` becoming the only supported mode. Fixes #34027 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Maui.Controls.targets | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.targets b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.targets index 22d79df04b92..43830be2d9aa 100644 --- a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.targets +++ b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.targets @@ -10,6 +10,9 @@ <_MauiXamlInflator Condition="' $(MauiXamlInflator)' != '' ">$(MauiXamlInflator) <_MauiXamlInflator Condition=" '$(MauiXamlInflator)' == '' ">SourceGen + + Legacy + $(EnableMauiDiagnostics) $(EnableDiagnostics) true @@ -73,6 +76,23 @@ Condition="'$(_MauiTargetsImportedAgain)' == 'True'" /> + + + + <_MauiXamlHotReloadUpper>$([System.String]::Copy('$(MauiXamlHotReload)').Trim().ToUpperInvariant()) + + + + + + + Date: Tue, 24 Feb 2026 15:58:48 -0600 Subject: [PATCH 05/15] [build] clean up old `$(DebugType)` MSBuild settings (#34216) These likely came over from the migration from xamarin/Xamarin.Forms -> dotnet/maui. `$(DebugType)=full` seems be causing the error at: * https://github.com/dotnet/maui/pull/34201 ``` src\Controls\tests\Xaml.UnitTests\Issues\Gh2007.rtxc.xaml(3,9): XamlC warning XC0022: Binding could be compiled to improve runtime performance if x:DataType is specified. See https://learn.microsoft.com/dotnet/maui/fundamentals/data-binding/compiled-bindings for more information. [D:\a\_work\1\s\src\Controls\tests\Xaml.UnitTests\Controls.Xaml.UnitTests.csproj] Fatal error. 0xC0000005 at Mono.Cecil.Pdb.ISymUnmanagedWriter2.Close() at Mono.Cecil.Pdb.SymWriter.Close() at Mono.Cecil.Pdb.NativePdbWriter.Write() at Mono.Cecil.ModuleWriter.Write(Mono.Cecil.ModuleDefinition, Mono.Disposable`1, Mono.Cecil.WriterParameters) at Mono.Cecil.ModuleWriter.WriteModule(Mono.Cecil.ModuleDefinition, Mono.Disposable`1, Mono.Cecil.WriterParameters) at Mono.Cecil.ModuleDefinition.Write(System.IO.Stream, Mono.Cecil.WriterParameters) at Mono.Cecil.ModuleDefinition.Write(Mono.Cecil.WriterParameters) at Mono.Cecil.AssemblyDefinition.Write(Mono.Cecil.WriterParameters) at Microsoft.Maui.Controls.Build.Tasks.XamlCTask.Execute(System.Collections.Generic.IList`1 ByRef) at Microsoft.Maui.Controls.Build.Tasks.XamlTask.Execute() at Microsoft.Build.BackEnd.TaskBuilder+d__25.MoveNext() at Microsoft.Build.BackEnd.TaskBuilder+d__23.MoveNext() at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[[Microsoft.Build.BackEnd.TaskBuilder+d__23, Microsoft.Build, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a]](d__23 ByRef) at Microsoft.Build.BackEnd.TaskBuilder+d__19.MoveNext() at Microsoft.Build.BackEnd.TaskBuilder+d__18.MoveNext() at Microsoft.Build.BackEnd.TaskBuilder+d__13.MoveNext() at Microsoft.Build.BackEnd.TargetEntry+d__50.MoveNext() at Microsoft.Build.BackEnd.TargetEntry+d__43.MoveNext() at Microsoft.Build.BackEnd.TargetBuilder+d__24.MoveNext() at System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object) at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib, Version=11.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[Microsoft.Build.BackEnd.TargetBuilder+d__24, Microsoft.Build, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a]].MoveNext() at System.Threading.Tasks.TaskSchedulerAwaitTaskContinuation+<>c.b__2_0(System.Object) at System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef, System.Threading.Thread) at System.Threading.Tasks.Task.ExecuteEntry() at Microsoft.Build.BackEnd.RequestBuilder+DedicatedThreadsTaskScheduler.b__6_0() at System.Threading.Thread+StartHelper.Callback(System.Object) at System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object) at System.Threading.Thread.StartCallback() Build failed with exit code 57005. Check errors above. ``` We shouldn't be using Windows-specific debug settings in a cross-platform project -- the `$(DebugType)` setting is not needed. I went through a couple projects and found old debugging settings that would have been needed in non-SDK-style projects, but are not needed in SDK-style projects. Removing those settings to cleanup the repo. --- .../tests/SourceGen.UnitTests/SourceGen.UnitTests.csproj | 7 ------- .../tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj | 7 ------- src/Essentials/test/UnitTests/Essentials.UnitTests.csproj | 1 - 3 files changed, 15 deletions(-) diff --git a/src/Controls/tests/SourceGen.UnitTests/SourceGen.UnitTests.csproj b/src/Controls/tests/SourceGen.UnitTests/SourceGen.UnitTests.csproj index 17f0b8fc79d4..1767e782eee7 100644 --- a/src/Controls/tests/SourceGen.UnitTests/SourceGen.UnitTests.csproj +++ b/src/Controls/tests/SourceGen.UnitTests/SourceGen.UnitTests.csproj @@ -13,13 +13,6 @@ true - - DEBUG - prompt - full - true - - diff --git a/src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj b/src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj index 78a1f5e72a3a..443c5e8d898d 100644 --- a/src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj +++ b/src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj @@ -21,13 +21,6 @@ True - - $(DefineConstants);DEBUG - prompt - full - true - - diff --git a/src/Essentials/test/UnitTests/Essentials.UnitTests.csproj b/src/Essentials/test/UnitTests/Essentials.UnitTests.csproj index 14e2324edc84..615042d15570 100644 --- a/src/Essentials/test/UnitTests/Essentials.UnitTests.csproj +++ b/src/Essentials/test/UnitTests/Essentials.UnitTests.csproj @@ -4,7 +4,6 @@ $(_MauiDotNetTfm) false Microsoft.Maui.Essentials.UnitTests - portable Debug;Release From 7499d828f9b77d1e709858a28ef9bd6626f720e1 Mon Sep 17 00:00:00 2001 From: Gerald Versluis Date: Wed, 25 Feb 2026 13:44:02 +0100 Subject: [PATCH 06/15] Add MapElement.IsVisible and MapElement.ZIndex properties (#33993) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ## Description Adds `IsVisible` and `ZIndex` properties to all map elements (Polygon, Polyline, Circle), enabling visibility toggling and draw-order control without removing elements from the map. Part of the Maps Control Improvements Epic: #33787 (Phase 4: MapElement Improvements) ## Changes ### Core Interface (`IMapElement`) - `bool IsVisible { get; }` — controls element visibility (default: `true`) - `int ZIndex { get; }` — controls draw order/layering (default: `0`) ### Controls (`MapElement`) - `IsVisibleProperty` BindableProperty (bool, default: true) - `ZIndexProperty` BindableProperty (int, default: 0) ### Platform Implementations **Android:** - `Polygon.Visible`, `Polyline.Visible`, `Circle.Visible` for visibility - `Polygon.ZIndex`, `Polyline.ZIndex`, `Circle.ZIndex` for draw order - Initial values applied after `map.Add*()` calls; runtime changes via `*OnPropertyChanged` **iOS/MacCatalyst:** - Visibility via `MKOverlayRenderer.Alpha` (0 = hidden, 1 = visible) - ZIndex is no-op (MapKit does not support fine-grained overlay ordering) ### Tests - 5 new unit tests (MapElementIsVisibleDefaultIsTrue, MapElementIsVisibleCanBeSet, MapElementZIndexDefaultIsZero, MapElementZIndexCanBeSet, MapElementIsVisibleWorksOnAllTypes) ### Sample - MapElementVisibilityGallery with toggle buttons for each element type and ZIndex controls ## Verified Working - ✅ Android: Toggle Polygon/Polyline/Circle visibility, ZIndex changes (Z=10), Reset - ✅ iOS: Toggle Polygon/Polyline/Circle visibility, ZIndex changes, Reset - ✅ 34 unit tests pass (including 5 new) --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Controls/Maps/src/MapElement.cs | 36 +++++ .../net-android/PublicAPI.Unshipped.txt | 7 + .../PublicAPI/net-ios/PublicAPI.Unshipped.txt | 7 + .../net-maccatalyst/PublicAPI.Unshipped.txt | 7 + .../net-tizen/PublicAPI.Unshipped.txt | 7 + .../net-windows/PublicAPI.Unshipped.txt | 7 + .../src/PublicAPI/net/PublicAPI.Unshipped.txt | 7 + .../netstandard/PublicAPI.Unshipped.txt | 7 + .../MapElementVisibilityGallery.cs | 149 ++++++++++++++++++ .../Controls/MapsGalleries/MapsGallery.cs | 1 + src/Controls/tests/Core.UnitTests/MapTests.cs | 51 ++++++ src/Core/maps/src/Core/IMapElement.cs | 11 ++ .../src/Handlers/Map/MapHandler.Android.cs | 35 ++++ .../MapElement/MapElementHandler.Android.cs | 14 ++ .../MapElement/MapElementHandler.Standard.cs | 2 + .../MapElement/MapElementHandler.Tizen.cs | 2 + .../MapElement/MapElementHandler.Windows.cs | 2 + .../Handlers/MapElement/MapElementHandler.cs | 2 + .../MapElement/MapElementHandler.iOS.cs | 12 ++ .../net-android/PublicAPI.Unshipped.txt | 5 + .../PublicAPI/net-ios/PublicAPI.Unshipped.txt | 5 + .../net-maccatalyst/PublicAPI.Unshipped.txt | 5 + .../net-tizen/PublicAPI.Unshipped.txt | 5 + .../net-windows/PublicAPI.Unshipped.txt | 5 + .../src/PublicAPI/net/PublicAPI.Unshipped.txt | 5 + .../netstandard/PublicAPI.Unshipped.txt | 5 + 26 files changed, 401 insertions(+) create mode 100644 src/Controls/samples/Controls.Sample/Pages/Controls/MapsGalleries/MapElementVisibilityGallery.cs diff --git a/src/Controls/Maps/src/MapElement.cs b/src/Controls/Maps/src/MapElement.cs index 6d469f7de041..31d15554ff10 100644 --- a/src/Controls/Maps/src/MapElement.cs +++ b/src/Controls/Maps/src/MapElement.cs @@ -26,6 +26,20 @@ public partial class MapElement : Element typeof(MapElement), 5f); + /// Bindable property for . + public static readonly BindableProperty IsVisibleProperty = BindableProperty.Create( + nameof(IsVisible), + typeof(bool), + typeof(MapElement), + true); + + /// Bindable property for . + public static readonly BindableProperty ZIndexProperty = BindableProperty.Create( + nameof(ZIndex), + typeof(int), + typeof(MapElement), + 0); + /// /// Gets or sets the stroke color. This is a bindable property. /// @@ -45,6 +59,28 @@ public float StrokeWidth set => SetValue(StrokeWidthProperty, value); } + /// + /// Gets or sets a value indicating whether the map element is visible on the map. + /// The default value is . + /// This is a bindable property. + /// + public bool IsVisible + { + get => (bool)GetValue(IsVisibleProperty); + set => SetValue(IsVisibleProperty, value); + } + + /// + /// Gets or sets the z-index of the map element, which controls its draw order relative to other elements. + /// Higher values are drawn on top of lower values. The default value is 0. + /// This is a bindable property. + /// + public int ZIndex + { + get => (int)GetValue(ZIndexProperty); + set => SetValue(ZIndexProperty, value); + } + /// /// Gets or sets the platform counterpart of this map element. /// diff --git a/src/Controls/Maps/src/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Controls/Maps/src/PublicAPI/net-android/PublicAPI.Unshipped.txt index 31029602ec64..90a0ef71d1ea 100644 --- a/src/Controls/Maps/src/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Controls/Maps/src/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -1,4 +1,11 @@ #nullable enable + +Microsoft.Maui.Controls.Maps.MapElement.IsVisible.get -> bool +Microsoft.Maui.Controls.Maps.MapElement.IsVisible.set -> void +Microsoft.Maui.Controls.Maps.MapElement.ZIndex.get -> int +Microsoft.Maui.Controls.Maps.MapElement.ZIndex.set -> void +static readonly Microsoft.Maui.Controls.Maps.MapElement.IsVisibleProperty -> Microsoft.Maui.Controls.BindableProperty! +static readonly Microsoft.Maui.Controls.Maps.MapElement.ZIndexProperty -> Microsoft.Maui.Controls.BindableProperty! static readonly Microsoft.Maui.Controls.Maps.Map.RegionProperty -> Microsoft.Maui.Controls.BindableProperty! Microsoft.Maui.Controls.Maps.Map.Region.get -> Microsoft.Maui.Maps.MapSpan? Microsoft.Maui.Controls.Maps.Map.Region.set -> void diff --git a/src/Controls/Maps/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Controls/Maps/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt index 31029602ec64..90a0ef71d1ea 100644 --- a/src/Controls/Maps/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Controls/Maps/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -1,4 +1,11 @@ #nullable enable + +Microsoft.Maui.Controls.Maps.MapElement.IsVisible.get -> bool +Microsoft.Maui.Controls.Maps.MapElement.IsVisible.set -> void +Microsoft.Maui.Controls.Maps.MapElement.ZIndex.get -> int +Microsoft.Maui.Controls.Maps.MapElement.ZIndex.set -> void +static readonly Microsoft.Maui.Controls.Maps.MapElement.IsVisibleProperty -> Microsoft.Maui.Controls.BindableProperty! +static readonly Microsoft.Maui.Controls.Maps.MapElement.ZIndexProperty -> Microsoft.Maui.Controls.BindableProperty! static readonly Microsoft.Maui.Controls.Maps.Map.RegionProperty -> Microsoft.Maui.Controls.BindableProperty! Microsoft.Maui.Controls.Maps.Map.Region.get -> Microsoft.Maui.Maps.MapSpan? Microsoft.Maui.Controls.Maps.Map.Region.set -> void diff --git a/src/Controls/Maps/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Controls/Maps/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index 31029602ec64..90a0ef71d1ea 100644 --- a/src/Controls/Maps/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Controls/Maps/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -1,4 +1,11 @@ #nullable enable + +Microsoft.Maui.Controls.Maps.MapElement.IsVisible.get -> bool +Microsoft.Maui.Controls.Maps.MapElement.IsVisible.set -> void +Microsoft.Maui.Controls.Maps.MapElement.ZIndex.get -> int +Microsoft.Maui.Controls.Maps.MapElement.ZIndex.set -> void +static readonly Microsoft.Maui.Controls.Maps.MapElement.IsVisibleProperty -> Microsoft.Maui.Controls.BindableProperty! +static readonly Microsoft.Maui.Controls.Maps.MapElement.ZIndexProperty -> Microsoft.Maui.Controls.BindableProperty! static readonly Microsoft.Maui.Controls.Maps.Map.RegionProperty -> Microsoft.Maui.Controls.BindableProperty! Microsoft.Maui.Controls.Maps.Map.Region.get -> Microsoft.Maui.Maps.MapSpan? Microsoft.Maui.Controls.Maps.Map.Region.set -> void diff --git a/src/Controls/Maps/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt b/src/Controls/Maps/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt index 31029602ec64..90a0ef71d1ea 100644 --- a/src/Controls/Maps/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt +++ b/src/Controls/Maps/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt @@ -1,4 +1,11 @@ #nullable enable + +Microsoft.Maui.Controls.Maps.MapElement.IsVisible.get -> bool +Microsoft.Maui.Controls.Maps.MapElement.IsVisible.set -> void +Microsoft.Maui.Controls.Maps.MapElement.ZIndex.get -> int +Microsoft.Maui.Controls.Maps.MapElement.ZIndex.set -> void +static readonly Microsoft.Maui.Controls.Maps.MapElement.IsVisibleProperty -> Microsoft.Maui.Controls.BindableProperty! +static readonly Microsoft.Maui.Controls.Maps.MapElement.ZIndexProperty -> Microsoft.Maui.Controls.BindableProperty! static readonly Microsoft.Maui.Controls.Maps.Map.RegionProperty -> Microsoft.Maui.Controls.BindableProperty! Microsoft.Maui.Controls.Maps.Map.Region.get -> Microsoft.Maui.Maps.MapSpan? Microsoft.Maui.Controls.Maps.Map.Region.set -> void diff --git a/src/Controls/Maps/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Controls/Maps/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt index 31029602ec64..90a0ef71d1ea 100644 --- a/src/Controls/Maps/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Controls/Maps/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -1,4 +1,11 @@ #nullable enable + +Microsoft.Maui.Controls.Maps.MapElement.IsVisible.get -> bool +Microsoft.Maui.Controls.Maps.MapElement.IsVisible.set -> void +Microsoft.Maui.Controls.Maps.MapElement.ZIndex.get -> int +Microsoft.Maui.Controls.Maps.MapElement.ZIndex.set -> void +static readonly Microsoft.Maui.Controls.Maps.MapElement.IsVisibleProperty -> Microsoft.Maui.Controls.BindableProperty! +static readonly Microsoft.Maui.Controls.Maps.MapElement.ZIndexProperty -> Microsoft.Maui.Controls.BindableProperty! static readonly Microsoft.Maui.Controls.Maps.Map.RegionProperty -> Microsoft.Maui.Controls.BindableProperty! Microsoft.Maui.Controls.Maps.Map.Region.get -> Microsoft.Maui.Maps.MapSpan? Microsoft.Maui.Controls.Maps.Map.Region.set -> void diff --git a/src/Controls/Maps/src/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Controls/Maps/src/PublicAPI/net/PublicAPI.Unshipped.txt index 31029602ec64..90a0ef71d1ea 100644 --- a/src/Controls/Maps/src/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Controls/Maps/src/PublicAPI/net/PublicAPI.Unshipped.txt @@ -1,4 +1,11 @@ #nullable enable + +Microsoft.Maui.Controls.Maps.MapElement.IsVisible.get -> bool +Microsoft.Maui.Controls.Maps.MapElement.IsVisible.set -> void +Microsoft.Maui.Controls.Maps.MapElement.ZIndex.get -> int +Microsoft.Maui.Controls.Maps.MapElement.ZIndex.set -> void +static readonly Microsoft.Maui.Controls.Maps.MapElement.IsVisibleProperty -> Microsoft.Maui.Controls.BindableProperty! +static readonly Microsoft.Maui.Controls.Maps.MapElement.ZIndexProperty -> Microsoft.Maui.Controls.BindableProperty! static readonly Microsoft.Maui.Controls.Maps.Map.RegionProperty -> Microsoft.Maui.Controls.BindableProperty! Microsoft.Maui.Controls.Maps.Map.Region.get -> Microsoft.Maui.Maps.MapSpan? Microsoft.Maui.Controls.Maps.Map.Region.set -> void diff --git a/src/Controls/Maps/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Controls/Maps/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt index 31029602ec64..90a0ef71d1ea 100644 --- a/src/Controls/Maps/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Controls/Maps/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -1,4 +1,11 @@ #nullable enable + +Microsoft.Maui.Controls.Maps.MapElement.IsVisible.get -> bool +Microsoft.Maui.Controls.Maps.MapElement.IsVisible.set -> void +Microsoft.Maui.Controls.Maps.MapElement.ZIndex.get -> int +Microsoft.Maui.Controls.Maps.MapElement.ZIndex.set -> void +static readonly Microsoft.Maui.Controls.Maps.MapElement.IsVisibleProperty -> Microsoft.Maui.Controls.BindableProperty! +static readonly Microsoft.Maui.Controls.Maps.MapElement.ZIndexProperty -> Microsoft.Maui.Controls.BindableProperty! static readonly Microsoft.Maui.Controls.Maps.Map.RegionProperty -> Microsoft.Maui.Controls.BindableProperty! Microsoft.Maui.Controls.Maps.Map.Region.get -> Microsoft.Maui.Maps.MapSpan? Microsoft.Maui.Controls.Maps.Map.Region.set -> void diff --git a/src/Controls/samples/Controls.Sample/Pages/Controls/MapsGalleries/MapElementVisibilityGallery.cs b/src/Controls/samples/Controls.Sample/Pages/Controls/MapsGalleries/MapElementVisibilityGallery.cs new file mode 100644 index 000000000000..33d4bdb8d03b --- /dev/null +++ b/src/Controls/samples/Controls.Sample/Pages/Controls/MapsGalleries/MapElementVisibilityGallery.cs @@ -0,0 +1,149 @@ +using Microsoft.Maui; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Controls.Maps; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Maps; +using GeoLocation = Microsoft.Maui.Devices.Sensors.Location; + +namespace Maui.Controls.Sample.Pages.MapsGalleries +{ + public class MapElementVisibilityGallery : ContentPage + { + readonly Polygon _polygon; + readonly Polyline _polyline; + readonly Circle _circle; + readonly Label _statusLabel; + + public MapElementVisibilityGallery() + { + Title = "Element Visibility & ZIndex"; + + var center = new GeoLocation(47.6062, -122.3321); // Seattle + + _polygon = new Polygon + { + StrokeColor = Colors.Blue, + StrokeWidth = 3, + FillColor = Color.FromRgba(0, 0, 255, 64), + ZIndex = 1, + }; + _polygon.Geopath.Add(new GeoLocation(47.615, -122.345)); + _polygon.Geopath.Add(new GeoLocation(47.615, -122.320)); + _polygon.Geopath.Add(new GeoLocation(47.600, -122.320)); + _polygon.Geopath.Add(new GeoLocation(47.600, -122.345)); + + _polyline = new Polyline + { + StrokeColor = Colors.Red, + StrokeWidth = 5, + ZIndex = 2, + }; + _polyline.Geopath.Add(new GeoLocation(47.610, -122.350)); + _polyline.Geopath.Add(new GeoLocation(47.610, -122.315)); + + _circle = new Circle + { + Center = center, + Radius = new Distance(500), + StrokeColor = Colors.Green, + StrokeWidth = 3, + FillColor = Color.FromRgba(0, 255, 0, 64), + ZIndex = 3, + }; + + var map = new Microsoft.Maui.Controls.Maps.Map(new MapSpan(center, 0.03, 0.03)); + map.MapElements.Add(_polygon); + map.MapElements.Add(_polyline); + map.MapElements.Add(_circle); + + _statusLabel = new Label + { + Text = "All elements visible. ZIndex: Polygon=1, Polyline=2, Circle=3", + HorizontalTextAlignment = TextAlignment.Center, + AutomationId = "StatusLabel" + }; + + var togglePolygonBtn = new Button { Text = "Toggle Polygon", AutomationId = "TogglePolygon" }; + togglePolygonBtn.Clicked += (s, e) => + { + _polygon.IsVisible = !_polygon.IsVisible; + UpdateStatus(); + }; + + var togglePolylineBtn = new Button { Text = "Toggle Polyline", AutomationId = "TogglePolyline" }; + togglePolylineBtn.Clicked += (s, e) => + { + _polyline.IsVisible = !_polyline.IsVisible; + UpdateStatus(); + }; + + var toggleCircleBtn = new Button { Text = "Toggle Circle", AutomationId = "ToggleCircle" }; + toggleCircleBtn.Clicked += (s, e) => + { + _circle.IsVisible = !_circle.IsVisible; + UpdateStatus(); + }; + + var bringPolygonTopBtn = new Button { Text = "Polygon to Top (Z=10)", AutomationId = "PolygonTop" }; + bringPolygonTopBtn.Clicked += (s, e) => + { + _polygon.ZIndex = 10; + UpdateStatus(); + }; + + var resetZIndexBtn = new Button { Text = "Reset ZIndex", AutomationId = "ResetZIndex" }; + resetZIndexBtn.Clicked += (s, e) => + { + _polygon.ZIndex = 1; + _polyline.ZIndex = 2; + _circle.ZIndex = 3; + UpdateStatus(); + }; + + var controls = new VerticalStackLayout + { + Spacing = 4, + Padding = new Thickness(8), + Children = + { + _statusLabel, + new HorizontalStackLayout + { + Spacing = 4, + HorizontalOptions = LayoutOptions.Center, + Children = { togglePolygonBtn, togglePolylineBtn, toggleCircleBtn } + }, + new HorizontalStackLayout + { + Spacing = 4, + HorizontalOptions = LayoutOptions.Center, + Children = { bringPolygonTopBtn, resetZIndexBtn } + } + } + }; + + Content = new Grid + { + RowDefinitions = + { + new RowDefinition(GridLength.Star), + new RowDefinition(GridLength.Auto), + }, + Children = + { + map, + controls + } + }; + + Grid.SetRow(controls, 1); + } + + void UpdateStatus() + { + _statusLabel.Text = $"Polygon:{(_polygon.IsVisible ? "ON" : "OFF")}(Z={_polygon.ZIndex}) " + + $"Polyline:{(_polyline.IsVisible ? "ON" : "OFF")}(Z={_polyline.ZIndex}) " + + $"Circle:{(_circle.IsVisible ? "ON" : "OFF")}(Z={_circle.ZIndex})"; + } + } +} diff --git a/src/Controls/samples/Controls.Sample/Pages/Controls/MapsGalleries/MapsGallery.cs b/src/Controls/samples/Controls.Sample/Pages/Controls/MapsGalleries/MapsGallery.cs index ee6bfdf593e1..594a21829caa 100644 --- a/src/Controls/samples/Controls.Sample/Pages/Controls/MapsGalleries/MapsGallery.cs +++ b/src/Controls/samples/Controls.Sample/Pages/Controls/MapsGalleries/MapsGallery.cs @@ -19,6 +19,7 @@ public MapsGallery() GalleryBuilder.NavButton("Pins ItemsSource", () => new PinItemsSourceGallery(), Navigation), GalleryBuilder.NavButton("Circle", () => new CircleGallery(), Navigation), GalleryBuilder.NavButton("Polygon", () => new PolygonsGallery(), Navigation), + GalleryBuilder.NavButton("Element Visibility & ZIndex", () => new MapElementVisibilityGallery(), Navigation), } } }; diff --git a/src/Controls/tests/Core.UnitTests/MapTests.cs b/src/Controls/tests/Core.UnitTests/MapTests.cs index f938498d57ea..566eef8fc451 100644 --- a/src/Controls/tests/Core.UnitTests/MapTests.cs +++ b/src/Controls/tests/Core.UnitTests/MapTests.cs @@ -410,6 +410,57 @@ public void WorksWithNullItems() Assert.True(IsMapWithItemsSource(itemsSource, map)); } + [Fact] + public void MapElementIsVisibleDefaultIsTrue() + { + var polygon = new Polygon(); + Assert.True(polygon.IsVisible); + } + + [Fact] + public void MapElementIsVisibleCanBeSet() + { + var polygon = new Polygon(); + polygon.IsVisible = false; + Assert.False(polygon.IsVisible); + } + + [Fact] + public void MapElementZIndexDefaultIsZero() + { + var polyline = new Polyline(); + Assert.Equal(0, polyline.ZIndex); + } + + [Fact] + public void MapElementZIndexCanBeSet() + { + var circle = new Circle + { + Center = new Location(0, 0), + Radius = new Distance(100) + }; + circle.ZIndex = 5; + Assert.Equal(5, circle.ZIndex); + } + + [Fact] + public void MapElementIsVisibleWorksOnAllTypes() + { + var polygon = new Polygon { IsVisible = false }; + var polyline = new Polyline { IsVisible = false }; + var circle = new Circle + { + Center = new Location(0, 0), + Radius = new Distance(100), + IsVisible = false + }; + + Assert.False(polygon.IsVisible); + Assert.False(polyline.IsVisible); + Assert.False(circle.IsVisible); + } + // Checks if for every item in the items source there's a corresponding pin static bool IsMapWithItemsSource(IEnumerable itemsSource, Map map) { diff --git a/src/Core/maps/src/Core/IMapElement.cs b/src/Core/maps/src/Core/IMapElement.cs index 410212180df5..c9815403af4f 100644 --- a/src/Core/maps/src/Core/IMapElement.cs +++ b/src/Core/maps/src/Core/IMapElement.cs @@ -9,5 +9,16 @@ public interface IMapElement : IElement, IStroke /// Gets or sets the platform counterpart of this map element. /// object? MapElementId { get; set; } + + /// + /// Gets a value indicating whether the map element is visible on the map. + /// + bool IsVisible { get; } + + /// + /// Gets the z-index of the map element, which controls its draw order relative to other elements. + /// Higher values are drawn on top of lower values. + /// + int ZIndex { get; } } } diff --git a/src/Core/maps/src/Handlers/Map/MapHandler.Android.cs b/src/Core/maps/src/Handlers/Map/MapHandler.Android.cs index 3efed6f29e67..b34fcbfd6dfd 100644 --- a/src/Core/maps/src/Handlers/Map/MapHandler.Android.cs +++ b/src/Core/maps/src/Handlers/Map/MapHandler.Android.cs @@ -154,6 +154,12 @@ void PolygonOnPropertyChanged(IGeoPathMapElement mauiPolygon) nativePolygon.StrokeWidth = (float)mauiPolygon.StrokeThickness; nativePolygon.Points = mauiPolygon.Select(position => new LatLng(position.Latitude, position.Longitude)).ToList(); + + if (mauiPolygon is IMapElement mapElement) + { + nativePolygon.Visible = mapElement.IsVisible; + nativePolygon.ZIndex = mapElement.ZIndex; + } } void PolylineOnPropertyChanged(IGeoPathMapElement mauiPolyline) @@ -168,6 +174,12 @@ void PolylineOnPropertyChanged(IGeoPathMapElement mauiPolyline) nativePolyline.Width = (float)mauiPolyline.StrokeThickness; nativePolyline.Points = mauiPolyline.Select(position => new LatLng(position.Latitude, position.Longitude)).ToList(); + + if (mauiPolyline is IMapElement mapElement) + { + nativePolyline.Visible = mapElement.IsVisible; + nativePolyline.ZIndex = mapElement.ZIndex; + } } @@ -189,6 +201,11 @@ void CircleOnPropertyChanged(ICircleMapElement mauiCircle) nativeCircle.Radius = mauiCircle.Radius.Meters; nativeCircle.StrokeWidth = (float)mauiCircle.StrokeThickness; + if (mauiCircle is IMapElement mapElement) + { + nativeCircle.Visible = mapElement.IsVisible; + nativeCircle.ZIndex = mapElement.ZIndex; + } } protected APolyline? GetNativePolyline(IGeoPathMapElement polyline) @@ -530,6 +547,12 @@ void AddPolyline(IGeoPathMapElement polyline) polyline.MapElementId = nativePolyline.Id; + if (polyline is IMapElement mapElement) + { + nativePolyline.Visible = mapElement.IsVisible; + nativePolyline.ZIndex = mapElement.ZIndex; + } + _polylines.Add(nativePolyline); } } @@ -552,6 +575,12 @@ void AddPolygon(IGeoPathMapElement polygon) polygon.MapElementId = nativePolygon.Id; + if (polygon is IMapElement mapElement) + { + nativePolygon.Visible = mapElement.IsVisible; + nativePolygon.ZIndex = mapElement.ZIndex; + } + _polygons.Add(nativePolygon); } @@ -573,6 +602,12 @@ void AddCircle(ICircleMapElement circle) circle.MapElementId = nativeCircle.Id; + if (circle is IMapElement mapElement) + { + nativeCircle.Visible = mapElement.IsVisible; + nativeCircle.ZIndex = mapElement.ZIndex; + } + _circles.Add(nativeCircle); } } diff --git a/src/Core/maps/src/Handlers/MapElement/MapElementHandler.Android.cs b/src/Core/maps/src/Handlers/MapElement/MapElementHandler.Android.cs index f715a2fdf58c..5bc4c33b3577 100644 --- a/src/Core/maps/src/Handlers/MapElement/MapElementHandler.Android.cs +++ b/src/Core/maps/src/Handlers/MapElement/MapElementHandler.Android.cs @@ -117,5 +117,19 @@ public static void MapCenter(IMapElementHandler handler, IMapElement mapElement) circleOptions.InvokeCenter(new LatLng(circleMapElement.Center.Latitude, circleMapElement.Center.Longitude)); } + + public static void MapIsVisible(IMapElementHandler handler, IMapElement mapElement) + { + // Visibility is applied on the native object after it is added to the map, + // via the UpdateMapElement path in MapHandler.Android.cs. + // PolygonOptions/PolylineOptions/CircleOptions don't expose a Visible setter. + } + + public static void MapZIndex(IMapElementHandler handler, IMapElement mapElement) + { + // ZIndex is applied on the native object after it is added to the map, + // via the UpdateMapElement path in MapHandler.Android.cs. + // PolygonOptions/PolylineOptions/CircleOptions don't expose a ZIndex setter. + } } } diff --git a/src/Core/maps/src/Handlers/MapElement/MapElementHandler.Standard.cs b/src/Core/maps/src/Handlers/MapElement/MapElementHandler.Standard.cs index 234a9f4ca869..b55446d6e93c 100644 --- a/src/Core/maps/src/Handlers/MapElement/MapElementHandler.Standard.cs +++ b/src/Core/maps/src/Handlers/MapElement/MapElementHandler.Standard.cs @@ -7,5 +7,7 @@ public partial class MapElementHandler : ElementHandler public static void MapStroke(IMapElementHandler handler, IMapElement mapElement) => throw new System.NotImplementedException(); public static void MapStrokeThickness(IMapElementHandler handler, IMapElement mapElement) => throw new System.NotImplementedException(); public static void MapFill(IMapElementHandler handler, IMapElement mapElement) => throw new System.NotImplementedException(); + public static void MapIsVisible(IMapElementHandler handler, IMapElement mapElement) => throw new System.NotImplementedException(); + public static void MapZIndex(IMapElementHandler handler, IMapElement mapElement) => throw new System.NotImplementedException(); } } diff --git a/src/Core/maps/src/Handlers/MapElement/MapElementHandler.Tizen.cs b/src/Core/maps/src/Handlers/MapElement/MapElementHandler.Tizen.cs index 234a9f4ca869..b55446d6e93c 100644 --- a/src/Core/maps/src/Handlers/MapElement/MapElementHandler.Tizen.cs +++ b/src/Core/maps/src/Handlers/MapElement/MapElementHandler.Tizen.cs @@ -7,5 +7,7 @@ public partial class MapElementHandler : ElementHandler public static void MapStroke(IMapElementHandler handler, IMapElement mapElement) => throw new System.NotImplementedException(); public static void MapStrokeThickness(IMapElementHandler handler, IMapElement mapElement) => throw new System.NotImplementedException(); public static void MapFill(IMapElementHandler handler, IMapElement mapElement) => throw new System.NotImplementedException(); + public static void MapIsVisible(IMapElementHandler handler, IMapElement mapElement) => throw new System.NotImplementedException(); + public static void MapZIndex(IMapElementHandler handler, IMapElement mapElement) => throw new System.NotImplementedException(); } } diff --git a/src/Core/maps/src/Handlers/MapElement/MapElementHandler.Windows.cs b/src/Core/maps/src/Handlers/MapElement/MapElementHandler.Windows.cs index eaf8d85e3e59..6cb6cff20db7 100644 --- a/src/Core/maps/src/Handlers/MapElement/MapElementHandler.Windows.cs +++ b/src/Core/maps/src/Handlers/MapElement/MapElementHandler.Windows.cs @@ -8,5 +8,7 @@ public partial class MapElementHandler : ElementHandler public static void MapStroke(IMapElementHandler handler, IMapElement mapElement) => throw new System.NotImplementedException(); public static void MapStrokeThickness(IMapElementHandler handler, IMapElement mapElement) => throw new System.NotImplementedException(); public static void MapFill(IMapElementHandler handler, IMapElement mapElement) => throw new System.NotImplementedException(); + public static void MapIsVisible(IMapElementHandler handler, IMapElement mapElement) => throw new System.NotImplementedException(); + public static void MapZIndex(IMapElementHandler handler, IMapElement mapElement) => throw new System.NotImplementedException(); } } diff --git a/src/Core/maps/src/Handlers/MapElement/MapElementHandler.cs b/src/Core/maps/src/Handlers/MapElement/MapElementHandler.cs index 74bc219cca5b..c8cb453477f8 100644 --- a/src/Core/maps/src/Handlers/MapElement/MapElementHandler.cs +++ b/src/Core/maps/src/Handlers/MapElement/MapElementHandler.cs @@ -26,6 +26,8 @@ public partial class MapElementHandler : IMapElementHandler [nameof(IMapElement.Stroke)] = MapStroke, [nameof(IMapElement.StrokeThickness)] = MapStrokeThickness, [nameof(IFilledMapElement.Fill)] = MapFill, + [nameof(IMapElement.IsVisible)] = MapIsVisible, + [nameof(IMapElement.ZIndex)] = MapZIndex, #if MONOANDROID ["Geopath"] = MapGeopath, [nameof(ICircleMapElement.Radius)] = MapRadius, diff --git a/src/Core/maps/src/Handlers/MapElement/MapElementHandler.iOS.cs b/src/Core/maps/src/Handlers/MapElement/MapElementHandler.iOS.cs index ddec558fce9e..0d6ebed492bd 100644 --- a/src/Core/maps/src/Handlers/MapElement/MapElementHandler.iOS.cs +++ b/src/Core/maps/src/Handlers/MapElement/MapElementHandler.iOS.cs @@ -59,5 +59,17 @@ public static void MapFill(IMapElementHandler handler, IMapElement mapElement) if (handler.PlatformView is MKCircleRenderer circleRenderer) circleRenderer.FillColor = platformColor; } + + public static void MapIsVisible(IMapElementHandler handler, IMapElement mapElement) + { + handler.PlatformView.Alpha = mapElement.IsVisible ? 1 : 0; + } + + public static void MapZIndex(IMapElementHandler handler, IMapElement mapElement) + { + // MapKit does not support fine-grained ZIndex on overlays. + // Overlays are drawn in the order they are added to the map. + // The property is accepted but has no visual effect on iOS/MacCatalyst. + } } } diff --git a/src/Core/maps/src/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Core/maps/src/PublicAPI/net-android/PublicAPI.Unshipped.txt index 9bd77ba275b9..0f242ee73f30 100644 --- a/src/Core/maps/src/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Core/maps/src/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -1,4 +1,9 @@ #nullable enable + +Microsoft.Maui.Maps.IMapElement.IsVisible.get -> bool +Microsoft.Maui.Maps.IMapElement.ZIndex.get -> int +static Microsoft.Maui.Maps.Handlers.MapElementHandler.MapIsVisible(Microsoft.Maui.Maps.Handlers.IMapElementHandler! handler, Microsoft.Maui.Maps.IMapElement! mapElement) -> void +static Microsoft.Maui.Maps.Handlers.MapElementHandler.MapZIndex(Microsoft.Maui.Maps.Handlers.IMapElementHandler! handler, Microsoft.Maui.Maps.IMapElement! mapElement) -> void Microsoft.Maui.Maps.MapSpanTypeConverter Microsoft.Maui.Maps.MapSpanTypeConverter.MapSpanTypeConverter() -> void override Microsoft.Maui.Maps.MapSpanTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool diff --git a/src/Core/maps/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Core/maps/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt index 9bd77ba275b9..0f242ee73f30 100644 --- a/src/Core/maps/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Core/maps/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -1,4 +1,9 @@ #nullable enable + +Microsoft.Maui.Maps.IMapElement.IsVisible.get -> bool +Microsoft.Maui.Maps.IMapElement.ZIndex.get -> int +static Microsoft.Maui.Maps.Handlers.MapElementHandler.MapIsVisible(Microsoft.Maui.Maps.Handlers.IMapElementHandler! handler, Microsoft.Maui.Maps.IMapElement! mapElement) -> void +static Microsoft.Maui.Maps.Handlers.MapElementHandler.MapZIndex(Microsoft.Maui.Maps.Handlers.IMapElementHandler! handler, Microsoft.Maui.Maps.IMapElement! mapElement) -> void Microsoft.Maui.Maps.MapSpanTypeConverter Microsoft.Maui.Maps.MapSpanTypeConverter.MapSpanTypeConverter() -> void override Microsoft.Maui.Maps.MapSpanTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool diff --git a/src/Core/maps/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Core/maps/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index 9bd77ba275b9..0f242ee73f30 100644 --- a/src/Core/maps/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Core/maps/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -1,4 +1,9 @@ #nullable enable + +Microsoft.Maui.Maps.IMapElement.IsVisible.get -> bool +Microsoft.Maui.Maps.IMapElement.ZIndex.get -> int +static Microsoft.Maui.Maps.Handlers.MapElementHandler.MapIsVisible(Microsoft.Maui.Maps.Handlers.IMapElementHandler! handler, Microsoft.Maui.Maps.IMapElement! mapElement) -> void +static Microsoft.Maui.Maps.Handlers.MapElementHandler.MapZIndex(Microsoft.Maui.Maps.Handlers.IMapElementHandler! handler, Microsoft.Maui.Maps.IMapElement! mapElement) -> void Microsoft.Maui.Maps.MapSpanTypeConverter Microsoft.Maui.Maps.MapSpanTypeConverter.MapSpanTypeConverter() -> void override Microsoft.Maui.Maps.MapSpanTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool diff --git a/src/Core/maps/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt b/src/Core/maps/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt index 9bd77ba275b9..0f242ee73f30 100644 --- a/src/Core/maps/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt +++ b/src/Core/maps/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt @@ -1,4 +1,9 @@ #nullable enable + +Microsoft.Maui.Maps.IMapElement.IsVisible.get -> bool +Microsoft.Maui.Maps.IMapElement.ZIndex.get -> int +static Microsoft.Maui.Maps.Handlers.MapElementHandler.MapIsVisible(Microsoft.Maui.Maps.Handlers.IMapElementHandler! handler, Microsoft.Maui.Maps.IMapElement! mapElement) -> void +static Microsoft.Maui.Maps.Handlers.MapElementHandler.MapZIndex(Microsoft.Maui.Maps.Handlers.IMapElementHandler! handler, Microsoft.Maui.Maps.IMapElement! mapElement) -> void Microsoft.Maui.Maps.MapSpanTypeConverter Microsoft.Maui.Maps.MapSpanTypeConverter.MapSpanTypeConverter() -> void override Microsoft.Maui.Maps.MapSpanTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool diff --git a/src/Core/maps/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Core/maps/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt index 9bd77ba275b9..0f242ee73f30 100644 --- a/src/Core/maps/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Core/maps/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -1,4 +1,9 @@ #nullable enable + +Microsoft.Maui.Maps.IMapElement.IsVisible.get -> bool +Microsoft.Maui.Maps.IMapElement.ZIndex.get -> int +static Microsoft.Maui.Maps.Handlers.MapElementHandler.MapIsVisible(Microsoft.Maui.Maps.Handlers.IMapElementHandler! handler, Microsoft.Maui.Maps.IMapElement! mapElement) -> void +static Microsoft.Maui.Maps.Handlers.MapElementHandler.MapZIndex(Microsoft.Maui.Maps.Handlers.IMapElementHandler! handler, Microsoft.Maui.Maps.IMapElement! mapElement) -> void Microsoft.Maui.Maps.MapSpanTypeConverter Microsoft.Maui.Maps.MapSpanTypeConverter.MapSpanTypeConverter() -> void override Microsoft.Maui.Maps.MapSpanTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool diff --git a/src/Core/maps/src/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Core/maps/src/PublicAPI/net/PublicAPI.Unshipped.txt index 9bd77ba275b9..0f242ee73f30 100644 --- a/src/Core/maps/src/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Core/maps/src/PublicAPI/net/PublicAPI.Unshipped.txt @@ -1,4 +1,9 @@ #nullable enable + +Microsoft.Maui.Maps.IMapElement.IsVisible.get -> bool +Microsoft.Maui.Maps.IMapElement.ZIndex.get -> int +static Microsoft.Maui.Maps.Handlers.MapElementHandler.MapIsVisible(Microsoft.Maui.Maps.Handlers.IMapElementHandler! handler, Microsoft.Maui.Maps.IMapElement! mapElement) -> void +static Microsoft.Maui.Maps.Handlers.MapElementHandler.MapZIndex(Microsoft.Maui.Maps.Handlers.IMapElementHandler! handler, Microsoft.Maui.Maps.IMapElement! mapElement) -> void Microsoft.Maui.Maps.MapSpanTypeConverter Microsoft.Maui.Maps.MapSpanTypeConverter.MapSpanTypeConverter() -> void override Microsoft.Maui.Maps.MapSpanTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool diff --git a/src/Core/maps/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Core/maps/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt index 9bd77ba275b9..0f242ee73f30 100644 --- a/src/Core/maps/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Core/maps/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -1,4 +1,9 @@ #nullable enable + +Microsoft.Maui.Maps.IMapElement.IsVisible.get -> bool +Microsoft.Maui.Maps.IMapElement.ZIndex.get -> int +static Microsoft.Maui.Maps.Handlers.MapElementHandler.MapIsVisible(Microsoft.Maui.Maps.Handlers.IMapElementHandler! handler, Microsoft.Maui.Maps.IMapElement! mapElement) -> void +static Microsoft.Maui.Maps.Handlers.MapElementHandler.MapZIndex(Microsoft.Maui.Maps.Handlers.IMapElementHandler! handler, Microsoft.Maui.Maps.IMapElement! mapElement) -> void Microsoft.Maui.Maps.MapSpanTypeConverter Microsoft.Maui.Maps.MapSpanTypeConverter.MapSpanTypeConverter() -> void override Microsoft.Maui.Maps.MapSpanTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool From 4cc0dc5af89c13f10ad2ac1e1f9b7dfd1ea0d4e5 Mon Sep 17 00:00:00 2001 From: Stephane Delcroix Date: Wed, 25 Feb 2026 14:04:47 +0100 Subject: [PATCH 07/15] Fix empty string binding to nullable value types (#33536) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ## Description When binding `Entry.Text` to a nullable value type property (e.g., `int?`), clearing the Entry now correctly sets the property to `null` instead of retaining the previous value. ## Issue Fixes #8342 ## Root Cause In `BindingExpressionHelper.TryConvert()`, when converting an empty string to a nullable type like `int?`: 1. The underlying type was extracted (`int`) 2. `Convert.ChangeType("", int)` was called, which throws `FormatException` 3. The catch block returned `false`, keeping the old value ## Solution Added a check before `Convert.ChangeType()` to handle empty/whitespace strings when converting to nullable types. When the target type is nullable and the source is an empty/whitespace string, the value is set to `null` and the conversion succeeds. ## Behavior | Target Type | Empty String Input | Result | |-------------|-------------------|--------| | `int?`, `double?`, etc. (nullable) | `""` | `null` ✅ | | `int`, `double`, etc. (non-nullable) | `""` | Conversion fails, retains last valid value (unchanged) | ## Testing Added two unit tests in `BindingUnitTests.cs`: - `TwoWayBindingToIntPropertyWithEmptyStringRetainsLastValidValue` - Verifies non-nullable int behavior - `TwoWayBindingToNullableIntPropertyWithEmptyStringBecomesNull` - Verifies nullable int now converts to null --- .../src/Core/BindingExpressionHelper.cs | 15 +- .../tests/Core.UnitTests/BindingUnitTests.cs | 165 ++++++++++++++++++ 2 files changed, 178 insertions(+), 2 deletions(-) diff --git a/src/Controls/src/Core/BindingExpressionHelper.cs b/src/Controls/src/Core/BindingExpressionHelper.cs index 03b510b6877e..1781ecb10265 100644 --- a/src/Controls/src/Core/BindingExpressionHelper.cs +++ b/src/Controls/src/Core/BindingExpressionHelper.cs @@ -26,9 +26,20 @@ internal static bool TryConvert(ref object value, BindableProperty targetPropert object original = value; try { - convertTo = Nullable.GetUnderlyingType(convertTo) ?? convertTo; - + var underlyingType = Nullable.GetUnderlyingType(convertTo); var stringValue = value as string ?? string.Empty; + + // Handle empty string conversion to nullable types + // Empty string should convert to null for nullable value types + // Only apply to actual string values to avoid converting non-string inputs + // See: https://github.com/dotnet/maui/issues/8342 + if (underlyingType != null && value is string && string.IsNullOrEmpty(stringValue)) + { + value = null!; + return true; + } + + convertTo = underlyingType ?? convertTo; // see: https://bugzilla.xamarin.com/show_bug.cgi?id=32871 // do not canonicalize "*.[.]"; "1." should not update bound BindableProperty if (stringValue.EndsWith(CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator, StringComparison.Ordinal) && DecimalTypes.Contains(convertTo)) diff --git a/src/Controls/tests/Core.UnitTests/BindingUnitTests.cs b/src/Controls/tests/Core.UnitTests/BindingUnitTests.cs index 0fe13d5ab574..65b565770bf1 100644 --- a/src/Controls/tests/Core.UnitTests/BindingUnitTests.cs +++ b/src/Controls/tests/Core.UnitTests/BindingUnitTests.cs @@ -2532,5 +2532,170 @@ public object Convert(object value, Type targetType, object parameter, CultureIn public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException(); } + + [Fact] + // https://github.com/dotnet/maui/issues/8342 + public void TwoWayBindingToIntPropertyWithEmptyStringRetainsLastValidValue() + { + // This test reproduces the issue where when the user clears an Entry + // that is bound to an int property, the int property retains the first + // digit of the last entered value instead of keeping the last valid value. + // + // The expected behavior is that when an empty string cannot be converted + // to int, the source property should retain its last valid value. + + var vm = new IntViewModel { IntValue = 0 }; + var entry = new Entry { BindingContext = vm }; + entry.SetBinding(Entry.TextProperty, "IntValue", BindingMode.TwoWay); + + // Simulate user entering "456" + entry.SetValueFromRenderer(Entry.TextProperty, "456"); + Assert.Equal(456, vm.IntValue); + + // Simulate user backspacing to "45" + entry.SetValueFromRenderer(Entry.TextProperty, "45"); + Assert.Equal(45, vm.IntValue); + + // Simulate user backspacing to "4" + entry.SetValueFromRenderer(Entry.TextProperty, "4"); + Assert.Equal(4, vm.IntValue); + + // Simulate user backspacing to empty string + // The binding should fail to convert "" to int + // and the source property should retain its last valid value (4) + entry.SetValueFromRenderer(Entry.TextProperty, ""); + + // This is the key assertion - after clearing the Entry, the IntValue + // should still be 4 (the last successfully converted value) + Assert.Equal(4, vm.IntValue); + + // The Entry.Text will be "" because that's what was set from the renderer + // This creates a mismatch between Entry.Text ("") and ViewModel.IntValue (4) + // which is the core of the bug reported in issue #8342 + Assert.Equal("", entry.Text); + } + + [Fact] + // https://github.com/dotnet/maui/issues/8342 + public void TwoWayBindingToNullableIntPropertyWithEmptyStringBecomesNull() + { + // When binding to a nullable int, empty string should be converted to null + var vm = new NullableIntViewModel { IntValue = 123 }; + var entry = new Entry { BindingContext = vm }; + entry.SetBinding(Entry.TextProperty, "IntValue", BindingMode.TwoWay); + + // Verify initial binding + Assert.Equal("123", entry.Text); + + // Clear the entry - for nullable int, empty string should result in null + entry.SetValueFromRenderer(Entry.TextProperty, ""); + + // Nullable int should become null when empty string is entered + Assert.Null(vm.IntValue); + // Entry.Text becomes null because the binding writes back null from vm.IntValue + // This is expected - Entry displays empty for both null and "" text + Assert.Null(entry.Text); + } + + [Fact] + // https://github.com/dotnet/maui/issues/8342 + public void TwoWayBindingToNullableIntPropertyWithWhitespaceRetainsPreviousValue() + { + // Whitespace-only strings should fail conversion, not silently become null + var vm = new NullableIntViewModel { IntValue = 123 }; + var entry = new Entry { BindingContext = vm }; + entry.SetBinding(Entry.TextProperty, "IntValue", BindingMode.TwoWay); + + entry.SetValueFromRenderer(Entry.TextProperty, " "); + + // Whitespace should not convert to null — value should be retained + Assert.Equal(123, vm.IntValue); + } + + [Fact] + // https://github.com/dotnet/maui/issues/8342 + public void TwoWayBindingToNullableDoublePropertyWithEmptyStringBecomesNull() + { + var vm = new NullableDoubleViewModel { Value = 3.14 }; + var entry = new Entry { BindingContext = vm }; + entry.SetBinding(Entry.TextProperty, "Value", BindingMode.TwoWay); + + Assert.Equal("3.14", entry.Text); + + entry.SetValueFromRenderer(Entry.TextProperty, ""); + + Assert.Null(vm.Value); + } + + [Fact] + // https://github.com/dotnet/maui/issues/8342 + public void TwoWayBindingToNullableIntPropertyReentersValueAfterClearing() + { + var vm = new NullableIntViewModel { IntValue = 123 }; + var entry = new Entry { BindingContext = vm }; + entry.SetBinding(Entry.TextProperty, "IntValue", BindingMode.TwoWay); + + // Clear + entry.SetValueFromRenderer(Entry.TextProperty, ""); + Assert.Null(vm.IntValue); + + // Re-enter a value + entry.SetValueFromRenderer(Entry.TextProperty, "456"); + Assert.Equal(456, vm.IntValue); + } + + internal class IntViewModel : INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + + int _intValue; + public int IntValue + { + get => _intValue; + set + { + if (_intValue == value) + return; + _intValue = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IntValue))); + } + } + } + + internal class NullableIntViewModel : INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + + int? _intValue; + public int? IntValue + { + get => _intValue; + set + { + if (_intValue == value) + return; + _intValue = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IntValue))); + } + } + } + + internal class NullableDoubleViewModel : INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + + double? _value; + public double? Value + { + get => _value; + set + { + if (_value == value) + return; + _value = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value))); + } + } + } } } \ No newline at end of file From 65167b349b30fa4075dcbda6623bab0a5b658987 Mon Sep 17 00:00:00 2001 From: Stephane Delcroix Date: Wed, 25 Feb 2026 14:08:01 +0100 Subject: [PATCH 08/15] [XSG] Fix Setter.Value property element lookup for MauiGlobalUri namespace (#34055) > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ## Description Fixes #34039 When `` is used as a property element, `GetValueNode()` in `SetterValueProvider.cs` looked up the `Value` property only by two specific namespace URIs (`""` and `MauiUri`). When the property element resolved to `MauiGlobalUri` instead, the lookup returned null. Since PR #33681 changed the null-return behavior to a skip sentinel, this caused the Setter to be removed from `Variables` entirely, preventing the `.Add()` call from being generated. ### Root Cause `GetValueNode()` checked only two namespace URIs: ```csharp node.Properties.TryGetValue(new XmlName("", "Value"), out valueNode) node.Properties.TryGetValue(new XmlName(XamlParser.MauiUri, "Value"), out valueNode) ``` Property elements like `` inherit their namespace URI from the XML reader, which varies depending on the xmlns declaration used. When the XAML used `MauiGlobalUri`, neither existing check matched, so `GetValueNode()` returned null, triggering the skip sentinel introduced in #33681, which suppressed the `Setters.Add()` call. ### Fix Added `MauiGlobalUri` as a third namespace to check in the `GetValueNode()` lookup chain: ```csharp !node.Properties.TryGetValue(new XmlName("", "Value"), out valueNode) && !node.Properties.TryGetValue(new XmlName(XamlParser.MauiUri, "Value"), out valueNode) && !node.Properties.TryGetValue(new XmlName(XamlParser.MauiGlobalUri, "Value"), out valueNode) && ``` ### Testing - Added XAML unit test (`Maui34039`) verifying Setter with property element value in a Trigger - Added SourceGen unit test (`SetterValueInTrigger`) verifying correct codegen - Tests fail without fix, pass with fix --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/SourceGen/SetterValueProvider.cs | 1 + .../SetterValueInTrigger.cs | 77 +++++++++++++++++++ .../Xaml.UnitTests/Issues/Maui34039.xaml | 20 +++++ .../Xaml.UnitTests/Issues/Maui34039.xaml.cs | 27 +++++++ 4 files changed, 125 insertions(+) create mode 100644 src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SetterValueInTrigger.cs create mode 100644 src/Controls/tests/Xaml.UnitTests/Issues/Maui34039.xaml create mode 100644 src/Controls/tests/Xaml.UnitTests/Issues/Maui34039.xaml.cs diff --git a/src/Controls/src/SourceGen/SetterValueProvider.cs b/src/Controls/src/SourceGen/SetterValueProvider.cs index bff218b7b482..557bb2ed43a5 100644 --- a/src/Controls/src/SourceGen/SetterValueProvider.cs +++ b/src/Controls/src/SourceGen/SetterValueProvider.cs @@ -165,6 +165,7 @@ private static bool TryGetBindablePropertyNameAndType(IFieldSymbol? bpRef, Value INode? valueNode = null; if (!node.Properties.TryGetValue(new XmlName("", "Value"), out valueNode) && !node.Properties.TryGetValue(new XmlName(XamlParser.MauiUri, "Value"), out valueNode) && + !node.Properties.TryGetValue(new XmlName(XamlParser.MauiGlobalUri, "Value"), out valueNode) && node.CollectionItems.Count == 1) valueNode = node.CollectionItems[0]; diff --git a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SetterValueInTrigger.cs b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SetterValueInTrigger.cs new file mode 100644 index 000000000000..9a67536e8cea --- /dev/null +++ b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SetterValueInTrigger.cs @@ -0,0 +1,77 @@ +using System; +using System.Linq; +using Microsoft.CodeAnalysis.CSharp; +using Xunit; + +using static Microsoft.Maui.Controls.Xaml.UnitTests.SourceGen.SourceGeneratorDriver; + +namespace Microsoft.Maui.Controls.SourceGen.UnitTests; + +public class SetterValueInTrigger : SourceGenXamlInitializeComponentTestBase +{ + const string TestXaml = """ + + + + +"""; + + const string TestCode = """ +using Microsoft.Maui.Controls; +using Microsoft.Maui.Controls.Xaml; + +namespace Test; + +[XamlProcessing(XamlInflator.SourceGen)] +public partial class TestPage : ContentPage +{ + public TestPage() + { + InitializeComponent(); + } +} +"""; + + [Fact] + public void SetterWithComplexValueInTriggerIsAdded() + { + // Reproduction from https://github.com/dotnet/maui/issues/34039 + // When is a property element, GetValueNode() must find it + // regardless of the namespace URI on the property element. + var compilation = CreateMauiCompilation() + .AddSyntaxTrees(CSharpSyntaxTree.ParseText(TestCode)) + .AddSyntaxTrees(CSharpSyntaxTree.ParseText("[assembly: global::Microsoft.Maui.Controls.Xaml.Internals.AllowImplicitXmlnsDeclaration]")); + + var workingDirectory = Environment.CurrentDirectory; + var xamlFile = new AdditionalXamlFile( + System.IO.Path.Combine(workingDirectory, "Test.xaml"), TestXaml, + RelativePath: "Test.xaml", + ManifestResourceName: $"{compilation.AssemblyName}.Test.xaml"); + var result = RunGenerator(compilation, xamlFile); + var generated = result.Results.SingleOrDefault().GeneratedSources + .SingleOrDefault(gs => gs.HintName.EndsWith(".xsg.cs")).SourceText?.ToString(); + + Assert.NotNull(generated); + Assert.False(result.Diagnostics.Any(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error), + $"Generator produced errors: {string.Join(", ", result.Diagnostics.Where(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error))}"); + + // The setter must be added to the trigger's Setters collection. + // Without the fix, GetValueNode() fails to find the Value property element, + // causing the setter to be removed from Variables and the .Add() call to be skipped. + Assert.Contains("Setters).Add(", generated, StringComparison.Ordinal); + } +} diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui34039.xaml b/src/Controls/tests/Xaml.UnitTests/Issues/Maui34039.xaml new file mode 100644 index 000000000000..76069d72ca19 --- /dev/null +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui34039.xaml @@ -0,0 +1,20 @@ + + + + diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui34039.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Maui34039.xaml.cs new file mode 100644 index 000000000000..45adffd1f50c --- /dev/null +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui34039.xaml.cs @@ -0,0 +1,27 @@ +using Xunit; + +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public partial class Maui34039 : ContentPage +{ + public Maui34039() => InitializeComponent(); + + [Collection("Issue")] + public class Tests + { + [Theory] + [XamlInflatorData] + internal void SetterWithPropertyElementValueInTriggerIsAdded(XamlInflator inflator) + { + var page = new Maui34039(inflator); + Assert.NotNull(page); + // Verify the trigger has a setter with a FontImageSource value + var style = page.button.Style; + Assert.NotNull(style); + Assert.Single(style.Triggers); + var trigger = (Trigger)style.Triggers[0]; + Assert.Single(trigger.Setters); + Assert.IsType(trigger.Setters[0].Value); + } + } +} From bd8242300ea1ac4692c89871ea28dc2519f36c28 Mon Sep 17 00:00:00 2001 From: Stephane Delcroix Date: Wed, 25 Feb 2026 14:48:56 +0100 Subject: [PATCH 09/15] [XSG] Fix SourceGen OnPlatform default View null handling (#34186) > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ## Summary - fix SourceGen handling for `` when no matching platform/default exists - avoid nullability and null-dereference codegen by using `default!` for reference-type placeholders - skip namescope/source-info operations for OnPlatform default placeholder nodes - add regression coverage for the View OnPlatform missing-platform scenario ## Related issues - Fixes #34074 ## Validation - `dotnet test src/Controls/tests/SourceGen.UnitTests/SourceGen.UnitTests.csproj --filter "SimplifyOnPlatform.OnPlatformWithMissingTargetPlatformShouldUseDefault|SimplifyOnPlatform.OnPlatformViewWithMissingTargetPlatformShouldNotEmitNullabilityWarnings"` --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SourceGen/Visitors/CreateValuesVisitor.cs | 5 +- .../Visitors/SetNamescopesAndRegisterNames.cs | 6 +++ .../OnPlatformAbstractTypes.cs | 10 ++-- .../InitializeComponent/SimplifyOnPlatform.cs | 46 ++++++++++++++++++- .../Xaml.UnitTests/Issues/Maui34074.xaml | 10 ++++ .../Xaml.UnitTests/Issues/Maui34074.xaml.cs | 44 ++++++++++++++++++ 6 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 src/Controls/tests/Xaml.UnitTests/Issues/Maui34074.xaml create mode 100644 src/Controls/tests/Xaml.UnitTests/Issues/Maui34074.xaml.cs diff --git a/src/Controls/src/SourceGen/Visitors/CreateValuesVisitor.cs b/src/Controls/src/SourceGen/Visitors/CreateValuesVisitor.cs index 53aed8c94373..c971bdd1d48d 100644 --- a/src/Controls/src/SourceGen/Visitors/CreateValuesVisitor.cs +++ b/src/Controls/src/SourceGen/Visitors/CreateValuesVisitor.cs @@ -43,9 +43,10 @@ public static void CreateValue(ElementNode node, IndentedTextWriter writer, IDic if (node.IsOnPlatformDefaultValue) { var variableName = NamingHelpers.CreateUniqueVariableName(Context, type); - writer.WriteLine($"{type.ToFQDisplayString()} {variableName} = default;"); + // Reference-type defaults are null; use default! so generated code does not emit nullable warnings. + var defaultValue = type.IsReferenceType ? "default!" : "default"; + writer.WriteLine($"{type.ToFQDisplayString()} {variableName} = {defaultValue};"); variables[node] = new LocalVariable(type, variableName); - node.RegisterSourceInfo(Context, writer); return; } diff --git a/src/Controls/src/SourceGen/Visitors/SetNamescopesAndRegisterNames.cs b/src/Controls/src/SourceGen/Visitors/SetNamescopesAndRegisterNames.cs index c8b949660cda..d7447da28625 100644 --- a/src/Controls/src/SourceGen/Visitors/SetNamescopesAndRegisterNames.cs +++ b/src/Controls/src/SourceGen/Visitors/SetNamescopesAndRegisterNames.cs @@ -62,6 +62,12 @@ public void Visit(ElementNode node, INode parentNode) namesInNamescope = Context.Scopes[parentNode].namesInScope; } + if (node.IsOnPlatformDefaultValue) + { + Context.Scopes[node] = (namescope, namesInNamescope); + return; + } + if (setNameScope && Context.Variables[node].Type.InheritsFrom(Context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Controls.BindableObject")!, Context)) using (PrePost.NewConditional(Writer, "!_MAUIXAML_SG_NAMESCOPE_DISABLE")) { diff --git a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/OnPlatformAbstractTypes.cs b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/OnPlatformAbstractTypes.cs index 18085e2189f6..acd21cb0663b 100644 --- a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/OnPlatformAbstractTypes.cs +++ b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/OnPlatformAbstractTypes.cs @@ -284,10 +284,10 @@ public TestPage() // Should NOT contain "new Brush()" - that would be a compiler error Assert.DoesNotContain("new global::Microsoft.Maui.Controls.Brush()", generated, StringComparison.Ordinal); - // Should generate default instead of trying to instantiate the type - // The pattern is: Brush brush0 = default; + // Should generate default! instead of trying to instantiate the type (reference types use default!) + // The pattern is: Brush brush0 = default!; Assert.Contains("Brush", generated, StringComparison.Ordinal); - Assert.Contains("= default;", generated, StringComparison.Ordinal); + Assert.Contains("= default!;", generated, StringComparison.Ordinal); // There should be no compilation errors Assert.False(result.Diagnostics.Any(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error), @@ -339,9 +339,9 @@ public TestPage() // Should NOT contain "new View()" - that would be a compiler error due to protected ctor Assert.DoesNotContain("new global::Microsoft.Maui.Controls.View()", generated, StringComparison.Ordinal); - // Should generate default instead of trying to instantiate the type + // Should generate default! instead of trying to instantiate the type (reference types use default!) Assert.Contains("View", generated, StringComparison.Ordinal); - Assert.Contains("= default;", generated, StringComparison.Ordinal); + Assert.Contains("= default!;", generated, StringComparison.Ordinal); // There should be no compilation errors Assert.False(result.Diagnostics.Any(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error), diff --git a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SimplifyOnPlatform.cs b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SimplifyOnPlatform.cs index 53961b12c06d..7f3c7bbf6648 100644 --- a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SimplifyOnPlatform.cs +++ b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SimplifyOnPlatform.cs @@ -391,4 +391,48 @@ public TestPage() // The generated code should include: double double0 = default; Assert.Contains("double double0 = default;", generated, StringComparison.Ordinal); } -} \ No newline at end of file + + [Fact] + public void OnPlatformViewWithMissingTargetPlatformShouldNotEmitNullabilityWarnings() + { + var xaml = +""" + + + + + + + +"""; + + var code = +""" +using System; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Controls.Xaml; + +namespace Test; + +[XamlProcessing(XamlInflator.SourceGen)] +public partial class TestPage : ContentPage +{ + public TestPage() + { + InitializeComponent(); + } +} +"""; + + var (result, generated) = RunGenerator(xaml, code, targetFramework: "net10.0-android"); + + Assert.DoesNotContain(result.Diagnostics, d => d.Id == "CS8600" || d.Id == "CS8602"); + Assert.Contains("global::Microsoft.Maui.Controls.View", generated, StringComparison.Ordinal); + Assert.Contains("default!;", generated, StringComparison.Ordinal); + Assert.DoesNotContain(".transientNamescope", generated, StringComparison.Ordinal); + } +} diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui34074.xaml b/src/Controls/tests/Xaml.UnitTests/Issues/Maui34074.xaml new file mode 100644 index 000000000000..68d081eea748 --- /dev/null +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui34074.xaml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui34074.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Maui34074.xaml.cs new file mode 100644 index 000000000000..abde19e99ed3 --- /dev/null +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui34074.xaml.cs @@ -0,0 +1,44 @@ +using System; +using Microsoft.Maui.Controls.Core.UnitTests; +using Microsoft.Maui.Devices; +using Xunit; + +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public partial class Maui34074 : ContentPage +{ + public Maui34074() => InitializeComponent(); + + [Collection("Issue")] + public class Tests : IDisposable + { + readonly MockDeviceInfo _mockDeviceInfo; + + public Tests() + { + Application.SetCurrentApplication(new MockApplication()); + DeviceInfo.SetCurrent(_mockDeviceInfo = new MockDeviceInfo()); + } + + public void Dispose() => DeviceInfo.SetCurrent(null); + + [Theory] + [XamlInflatorData] + internal void OnPlatformViewMissingTargetUsesNullDefault(XamlInflator inflator) + { + _mockDeviceInfo.Platform = DevicePlatform.MacCatalyst; + var page = new Maui34074(inflator); + Assert.Null(page.Content); + } + + [Theory] + [XamlInflatorData] + internal void OnPlatformViewMatchingTargetStillWorks(XamlInflator inflator) + { + _mockDeviceInfo.Platform = DevicePlatform.WinUI; + var page = new Maui34074(inflator); + var label = Assert.IsType