From e557e052f83a970e1ac7ff9ef334aff0bd73b525 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Fri, 12 Sep 2025 15:09:17 -0500 Subject: [PATCH] Wire it up to the UI, painfully, but it works --- .../ParametersViewModels.cs | 307 ++++++++++++++++++ .../CommandPalettePageViewModelFactory.cs | 1 + .../Controls/SearchBar.xaml | 86 ++++- .../Controls/SearchBar.xaml.cs | 51 +++ .../ExtViews/ParametersPage.xaml | 29 ++ .../ExtViews/ParametersPage.xaml.cs | 85 +++++ .../Pages/ShellPage.xaml.cs | 1 + .../IconPathConverter.cpp | 5 - .../Pages/ParameterSamples.cs | 34 ++ .../SamplePagesExtension/SamplesListPage.cs | 7 + 10 files changed, 587 insertions(+), 19 deletions(-) create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ParametersViewModels.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ParametersPage.xaml create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ParametersPage.xaml.cs diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ParametersViewModels.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ParametersViewModels.cs new file mode 100644 index 0000000000..32ce799a87 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ParametersViewModels.cs @@ -0,0 +1,307 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.ObjectModel; +using Microsoft.CmdPal.Core.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Foundation; + +namespace Microsoft.CmdPal.Core.ViewModels; + +#pragma warning disable SA1402 // File may only contain a single type +#pragma warning disable SA1649 // File name should match first type name + +public abstract partial class ParameterRunViewModel : ExtensionObjectViewModel +{ + internal InitializedState Initialized { get; set; } = InitializedState.Uninitialized; + + protected bool IsInitialized => IsInErrorState || Initialized.HasFlag(InitializedState.Initialized); + + public bool IsInErrorState => Initialized.HasFlag(InitializedState.Error); + + internal ParameterRunViewModel(WeakReference context) + : base(context) + { + } +} + +public partial class LabelRunViewModel : ParameterRunViewModel +{ + private ExtensionObject _model; + + public string Text { get; set; } = string.Empty; + + public LabelRunViewModel(ILabelRun labelRun, WeakReference context) + : base(context) + { + _model = new(labelRun); + } + + public override void InitializeProperties() + { + if (IsInitialized) + { + return; + } + + var labelRun = _model.Unsafe; + if (labelRun == null) + { + return; + } + + Text = labelRun.Text; + UpdateProperty(nameof(Text)); + + Initialized = InitializedState.Initialized; + } +} + +public partial class ParameterValueRunViewModel : ParameterRunViewModel +{ + private ExtensionObject _model; + + public string PlaceholderText { get; set; } = string.Empty; + + public bool NeedsValue { get; set; } + + public ParameterValueRunViewModel(IParameterValueRun valueRun, WeakReference context) + : base(context) + { + _model = new(valueRun); + } + + public override void InitializeProperties() + { + if (IsInitialized) + { + return; + } + + var valueRun = _model.Unsafe; + if (valueRun == null) + { + return; + } + + PlaceholderText = valueRun.PlaceholderText; + NeedsValue = valueRun.NeedsValue; + UpdateProperty(nameof(PlaceholderText)); + UpdateProperty(nameof(NeedsValue)); + + Initialized = InitializedState.Initialized; + } +} + +public partial class StringParameterRunViewModel : ParameterValueRunViewModel +{ + private ExtensionObject _model; + + public string Text { get; set; } = string.Empty; + + public StringParameterRunViewModel(IStringParameterRun stringRun, WeakReference context) + : base(stringRun, context) + { + _model = new(stringRun); + } + + public override void InitializeProperties() + { + if (IsInitialized) + { + return; + } + + base.InitializeProperties(); + var stringRun = _model.Unsafe; + if (stringRun == null) + { + return; + } + + Text = stringRun.Text; + UpdateProperty(nameof(Text)); + } +} + +public partial class CommandParameterRunViewModel : ParameterValueRunViewModel +{ + private ExtensionObject _model; + + public string DisplayText { get; set; } = string.Empty; + + public IconInfoViewModel Icon { get; set; } = new(null); + + public CommandParameterRunViewModel(ICommandParameterRun commandRun, WeakReference context) + : base(commandRun, context) + { + _model = new(commandRun); + } + + public override void InitializeProperties() + { + if (IsInitialized) + { + return; + } + + base.InitializeProperties(); + var commandRun = _model.Unsafe; + if (commandRun == null) + { + return; + } + + DisplayText = commandRun.DisplayText; + Icon = new(commandRun.Icon); + if (Icon is not null) + { + Icon.InitializeProperties(); + } + + UpdateProperty(nameof(DisplayText)); + UpdateProperty(nameof(Icon)); + } +} + +public partial class ParametersPageViewModel : PageViewModel, IDisposable +{ + private ExtensionObject _model; + + public override bool IsInitialized + { + get => base.IsInitialized; protected set + { + base.IsInitialized = value; + } + } + + public ObservableCollection Items { get; set; } = []; + + private readonly Lock _listLock = new(); + + public event TypedEventHandler? ItemsUpdated; + + public ParametersPageViewModel(IParametersPage model, TaskScheduler scheduler, AppExtensionHost host) + : base(model, scheduler, host) + { + _model = new(model); + } + + private void Model_ItemsChanged(object sender, IItemsChangedEventArgs args) => FetchItems(); + + //// Run on background thread, from InitializeAsync + public override void InitializeProperties() + { + base.InitializeProperties(); + + var model = _model.Unsafe; + if (model is null) + { + return; // throw? + } + + FetchItems(); + + // model.ItemsChanged += Model_ItemsChanged; // TODO! + } + + //// Run on background thread, from InitializeAsync or Model_ItemsChanged + private void FetchItems() + { + // Collect all the items into new viewmodels + Collection newViewModels = []; + try + { + var newItems = _model.Unsafe!.Parameters; + foreach (var item in newItems) + { + ParameterRunViewModel? itemVm = item switch + { + ILabelRun labelRun => new LabelRunViewModel(labelRun, PageContext), + IStringParameterRun stringRun => new StringParameterRunViewModel(stringRun, PageContext), + ICommandParameterRun commandRun => new CommandParameterRunViewModel(commandRun, PageContext), + _ => null, + }; + if (itemVm != null) + { + itemVm.InitializeProperties(); + newViewModels.Add(itemVm); + } + } + + // Update the Items collection on the UI thread + List removedItems = []; + lock (_listLock) + { + // Now that we have new ViewModels for everything from the + // extension, smartly update our list of VMs + ListHelpers.InPlaceUpdateList(Items, newViewModels, out removedItems); + + // DO NOT ThrowIfCancellationRequested AFTER THIS! If you do, + // you'll clean up list items that we've now transferred into + // .Items + } + + // If we removed items, we need to clean them up, to remove our event handlers + foreach (var removedItem in removedItems) + { + removedItem.SafeCleanup(); + } + } + catch (Exception) + { + // Handle exceptions (e.g., log them) + } + + DoOnUiThread( + () => + { + ItemsUpdated?.Invoke(this, EventArgs.Empty); + OnPropertyChanged(nameof(Items)); // TODO! hack + }); + } + + public void Dispose() + { + GC.SuppressFinalize(this); + + // TODO! + + // _cancellationTokenSource?.Cancel(); + // _cancellationTokenSource?.Dispose(); + // _cancellationTokenSource = null; + + // _fetchItemsCancellationTokenSource?.Cancel(); + // _fetchItemsCancellationTokenSource?.Dispose(); + // _fetchItemsCancellationTokenSource = null; + } + + protected override void UnsafeCleanup() + { + base.UnsafeCleanup(); + + // _cancellationTokenSource?.Cancel(); + // _fetchItemsCancellationTokenSource?.Cancel(); + var model = _model.Unsafe; + if (model is not null) + { + // model.ItemsChanged -= Model_ItemsChanged; + } + + lock (_listLock) + { + foreach (var item in Items) + { + item.SafeCleanup(); + } + + Items.Clear(); + } + } +} + +#pragma warning restore SA1649 // File name should match first type name +#pragma warning restore SA1402 // File may only contain a single type diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPalettePageViewModelFactory.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPalettePageViewModelFactory.cs index 90e70d7fef..a33a92fc77 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPalettePageViewModelFactory.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPalettePageViewModelFactory.cs @@ -23,6 +23,7 @@ public class CommandPalettePageViewModelFactory { IListPage listPage => new ListViewModel(listPage, _scheduler, host) { IsNested = nested }, IContentPage contentPage => new CommandPaletteContentPageViewModel(contentPage, _scheduler, host), + IParametersPage paramsPage => new ParametersPageViewModel(paramsPage, _scheduler, host), _ => null, }; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml index 80eb1a3ad6..b08e118613 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml @@ -4,7 +4,9 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:cmdpalUi="using:Microsoft.CmdPal.UI" + xmlns:controls="using:CommunityToolkit.WinUI.Controls" xmlns:converters="using:CommunityToolkit.WinUI.Converters" + xmlns:coreVm="using:Microsoft.CmdPal.Core.ViewModels" xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" @@ -13,21 +15,77 @@ + + + + + +