From f0b584f31509f927664f108a0ea8ef0bd356c3ea Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Thu, 25 Jun 2026 17:48:53 +0200 Subject: [PATCH] Fix CmdPal gallery crash when extension has no homepage The gallery item detail page bound HyperlinkButton.NavigateUri (a Uri) directly to the raw Homepage string. x:Bind evaluates all bindings regardless of element visibility, so for extensions without a homepage the generated string-to-Uri conversion ran new Uri(null), throwing ArgumentNullException and crashing the page. Bind NavigateUri to a new validated Uri? HomepageUri property (backed by the existing _homepageHttpUri) so null is passed through without conversion. Add unit tests covering the populated and missing homepage cases. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Gallery/ExtensionGalleryItemViewModel.cs | 7 +++++ .../Settings/ExtensionGalleryItemPage.xaml | 2 +- .../ExtensionGalleryItemViewModelTests.cs | 27 +++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Gallery/ExtensionGalleryItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Gallery/ExtensionGalleryItemViewModel.cs index 4f643fc61ce3..c5ba9c9d8cf8 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Gallery/ExtensionGalleryItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Gallery/ExtensionGalleryItemViewModel.cs @@ -105,6 +105,13 @@ public ExtensionGalleryItemViewModel( public string? Homepage => _entry.Homepage; + // Validated, browser-openable homepage uri. Null when the entry has no + // homepage or it is not a web uri. NavigateUri bindings must use this + // (a Uri) rather than the raw Homepage string: x:Bind evaluates bindings + // regardless of element visibility, and converting a null/invalid string + // to Uri throws and crashes the page. + public Uri? HomepageUri => _homepageHttpUri; + public Uri IconUri { get; } public ImageSource IconSource diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionGalleryItemPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionGalleryItemPage.xaml index 3ed9a4f6b405..cc9585cd4fee 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionGalleryItemPage.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionGalleryItemPage.xaml @@ -232,7 +232,7 @@ Grid.Row="3" Padding="0" AutomationProperties.AutomationId="CmdPal_GalleryItemPage_ViewRepository" - NavigateUri="{x:Bind ViewModel.Homepage, Mode=OneWay}" + NavigateUri="{x:Bind ViewModel.HomepageUri, Mode=OneWay}" ToolTipService.ToolTip="{x:Bind ViewModel.Homepage, Mode=OneWay}" Visibility="{x:Bind help:BindTransformers.BoolToVisibility(ViewModel.HasHomepage), Mode=OneWay, FallbackValue=Collapsed}"> diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.UI.ViewModels.UnitTests/ExtensionGalleryItemViewModelTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.UI.ViewModels.UnitTests/ExtensionGalleryItemViewModelTests.cs index fc7469b32094..563f2ead1cb3 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.UI.ViewModels.UnitTests/ExtensionGalleryItemViewModelTests.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.UI.ViewModels.UnitTests/ExtensionGalleryItemViewModelTests.cs @@ -119,6 +119,7 @@ public void Constructor_IgnoresNonWebGalleryLinks() var viewModel = CreateViewModel(entry); Assert.IsFalse(viewModel.HasHomepage); + Assert.IsNull(viewModel.HomepageUri); Assert.IsFalse(viewModel.HasAuthorUrl); Assert.IsFalse(viewModel.HasUrlSource); Assert.IsFalse(viewModel.HasActionableSourceDetails); @@ -131,6 +132,32 @@ public void Constructor_IgnoresNonWebGalleryLinks() Assert.IsFalse(viewModel.OpenInstallUrlCommand.CanExecute(null)); } + [TestMethod] + public void Constructor_SetsHomepageUri_WhenHomepageIsWebUri() + { + var entry = CreateEntry(iconUrl: null); + entry.Homepage = "https://example.com/extension"; + + var viewModel = CreateViewModel(entry); + + Assert.IsTrue(viewModel.HasHomepage); + Assert.AreEqual(new Uri("https://example.com/extension"), viewModel.HomepageUri); + Assert.IsTrue(viewModel.OpenHomepageCommand.CanExecute(null)); + } + + [TestMethod] + public void Constructor_LeavesHomepageUriNull_WhenHomepageIsMissing() + { + var entry = CreateEntry(iconUrl: null); + entry.Homepage = null; + + var viewModel = CreateViewModel(entry); + + Assert.IsFalse(viewModel.HasHomepage); + Assert.IsNull(viewModel.HomepageUri); + Assert.IsFalse(viewModel.OpenHomepageCommand.CanExecute(null)); + } + [TestMethod] public void Constructor_EnablesCopyCommand_WhenWinGetIdIsAvailable() {