Make some things observable

This commit is contained in:
Mike Griese
2025-09-13 06:14:06 -05:00
parent 2671a9d4f9
commit e970c18de6
7 changed files with 192 additions and 23 deletions

View File

@@ -0,0 +1,10 @@
// 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.
namespace Microsoft.CmdPal.Common.Messages;
public partial class GetHwndMessage
{
public nint Hwnd { get; set; } = 0;
}

View File

@@ -4,6 +4,7 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Common.Messages;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
@@ -17,15 +18,51 @@ namespace Microsoft.CmdPal.Core.ViewModels;
public abstract partial class ParameterRunViewModel : ExtensionObjectViewModel
{
private ExtensionObject<IParameterRun> _model;
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<IPageContext> context)
internal ParameterRunViewModel(IParameterRun model, WeakReference<IPageContext> context)
: base(context)
{
_model = new(model);
}
public override void InitializeProperties()
{
if (IsInitialized)
{
return;
}
var model = _model.Unsafe;
if (model == null)
{
return;
}
model.PropChanged += Model_PropChanged;
}
private void Model_PropChanged(object sender, IPropChangedEventArgs args)
{
try
{
FetchProperty(args.PropertyName);
}
catch (Exception ex)
{
ShowException(ex);
}
}
protected virtual void FetchProperty(string propertyName)
{
// Override in derived classes
}
}
@@ -36,7 +73,7 @@ public partial class LabelRunViewModel : ParameterRunViewModel
public string Text { get; set; } = string.Empty;
public LabelRunViewModel(ILabelRun labelRun, WeakReference<IPageContext> context)
: base(context)
: base(labelRun, context)
{
_model = new(labelRun);
}
@@ -70,7 +107,7 @@ public partial class ParameterValueRunViewModel : ParameterRunViewModel
public bool NeedsValue { get; set; }
public ParameterValueRunViewModel(IParameterValueRun valueRun, WeakReference<IPageContext> context)
: base(context)
: base(valueRun, context)
{
_model = new(valueRun);
}
@@ -147,18 +184,31 @@ public partial class StringParameterRunViewModel : ParameterValueRunViewModel
}
}
public partial class CommandParameterRunViewModel : ParameterValueRunViewModel
public partial class CommandParameterRunViewModel : ParameterValueRunViewModel, IDisposable
{
private ExtensionObject<ICommandParameterRun> _model;
private ListViewModel? _listViewModel;
private CommandViewModel? _commandViewModel;
private AppExtensionHost _extensionHost;
public string DisplayText { get; set; } = string.Empty;
public IconInfoViewModel Icon { get; set; } = new(null);
public CommandParameterRunViewModel(ICommandParameterRun commandRun, WeakReference<IPageContext> context)
public string ButtonLabel => !string.IsNullOrEmpty(DisplayText) ? DisplayText : string.Empty;
public string SearchBoxText
{
get => GetSearchText();
set => SetSearchText(value);
}
public CommandParameterRunViewModel(ICommandParameterRun commandRun, WeakReference<IPageContext> context, AppExtensionHost extensionHost)
: base(commandRun, context)
{
_model = new(commandRun);
_extensionHost = extensionHost;
}
public override void InitializeProperties()
@@ -182,9 +232,71 @@ public partial class CommandParameterRunViewModel : ParameterValueRunViewModel
Icon.InitializeProperties();
}
GetHwndMessage msg = new();
WeakReferenceMessenger.Default.Send(msg);
var command = commandRun.GetSelectValueCommand((ulong)msg.Hwnd);
if (command == null)
{
}
else if (command is IListPage list)
{
if (PageContext.TryGetTarget(out var pageContext))
{
_listViewModel = new ListViewModel(list, pageContext.Scheduler, _extensionHost);
_listViewModel.InitializeProperties();
}
}
else if (command is IInvokableCommand invokable)
{
_commandViewModel = new CommandViewModel(invokable, this.PageContext);
_commandViewModel.InitializeProperties();
}
UpdateProperty(nameof(DisplayText));
UpdateProperty(nameof(Icon));
}
protected override void FetchProperty(string propertyName)
{
var model = this._model.Unsafe;
if (model is null)
{
return; // throw?
}
switch (propertyName)
{
case nameof(ICommandParameterRun.DisplayText):
DisplayText = model.DisplayText;
break;
case nameof(ICommandParameterRun.Icon):
Icon = new(model.Icon);
if (Icon is not null)
{
Icon.InitializeProperties();
}
break;
}
UpdateProperty(propertyName);
}
private string GetSearchText()
{
return _listViewModel?.SearchText ?? string.Empty;
}
private void SetSearchText(string value)
{
_listViewModel?.SearchTextBox = value;
}
public void Dispose()
{
GC.SuppressFinalize(this);
_listViewModel?.Dispose();
}
}
public partial class ParametersPageViewModel : PageViewModel, IDisposable
@@ -257,7 +369,7 @@ public partial class ParametersPageViewModel : PageViewModel, IDisposable
{
ILabelRun labelRun => new LabelRunViewModel(labelRun, PageContext),
IStringParameterRun stringRun => new StringParameterRunViewModel(stringRun, PageContext),
ICommandParameterRun commandRun => new CommandParameterRunViewModel(commandRun, PageContext),
ICommandParameterRun commandRun => new CommandParameterRunViewModel(commandRun, PageContext, this.ExtensionHost),
_ => null,
};
if (itemVm != null)

View File

@@ -42,6 +42,19 @@
Style="{StaticResource SearchTextBoxStyle}"
Text="{x:Bind Text, Mode=TwoWay}" />
</DataTemplate>
<DataTemplate x:Key="ListParamTemplate" x:DataType="coreVm:CommandParameterRunViewModel">
<TextBox
MinHeight="32"
Padding="4,2"
VerticalAlignment="Center"
VerticalContentAlignment="Stretch"
BorderBrush="{ThemeResource AccentTextFillColorPrimaryBrush}"
BorderThickness="1"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
PlaceholderText="{x:Bind PlaceholderText, Mode=OneWay}"
Style="{StaticResource SearchTextBoxStyle}"
Text="{x:Bind SearchBoxText, Mode=TwoWay}" />
</DataTemplate>
<cpcontrols:ParameterRunTemplateSelector
x:Key="ParameterRunTemplateSelector"
ButtonParamTemplate="{StaticResource ButtonParamTemplate}"

View File

@@ -46,6 +46,7 @@ public sealed partial class MainWindow : WindowEx,
IRecipient<ShowWindowMessage>,
IRecipient<HideWindowMessage>,
IRecipient<QuitMessage>,
IRecipient<GetHwndMessage>,
IDisposable
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Stylistically, window messages are WM_")]
@@ -87,6 +88,7 @@ public sealed partial class MainWindow : WindowEx,
WeakReferenceMessenger.Default.Register<QuitMessage>(this);
WeakReferenceMessenger.Default.Register<ShowWindowMessage>(this);
WeakReferenceMessenger.Default.Register<HideWindowMessage>(this);
WeakReferenceMessenger.Default.Register<GetHwndMessage>(this);
// Hide our titlebar.
// We need to both ExtendsContentIntoTitleBar, then set the height to Collapsed
@@ -747,4 +749,9 @@ public sealed partial class MainWindow : WindowEx,
_localKeyboardListener.Dispose();
DisposeAcrylic();
}
public void Receive(GetHwndMessage message)
{
message.Hwnd = this.GetWindowHandle();
}
}

View File

@@ -48,6 +48,38 @@ public sealed partial class SimpleParameterTest : ParametersPage
public override IListItem Command => new ListItem(_command);
}
public sealed partial class ButtonParameterTest : ParametersPage
{
private readonly CommandParameterRun _stringParameter;
private readonly InvokableCommand _command;
public ButtonParameterTest()
{
Name = "Open";
_stringParameter = new FilePickerParameterRun();
_command = new AnonymousCommand(() =>
{
var input = _stringParameter.Value?.ToString();
var toast = new ToastStatusMessage(new StatusMessage() { Message = $"You entered: '{input}'" });
toast.Show();
})
{
Name = "Submit",
Icon = new IconInfo("\uE724"), // Send
Result = CommandResult.KeepOpen(),
};
}
public override IParameterRun[] Parameters => new IParameterRun[]
{
new LabelRun("Enter a value:"),
_stringParameter,
};
public override IListItem Command => new ListItem(_command);
}
public sealed partial class CreateNoteParametersPage : ParametersPage
{
private readonly SelectFolderPage _selectFolderPage = new();

View File

@@ -95,7 +95,7 @@ public partial class StringParameterRun : ParameterValueRun, IStringParameterRun
}
}
public partial class CommandParameterRun : ParameterValueRun
public partial class CommandParameterRun : ParameterValueRun, ICommandParameterRun
{
private string? _displayText;
@@ -164,35 +164,25 @@ internal sealed partial class FilePickerParameterRun : CommandParameterRun
{
public StorageFile? File { get; private set; }
public override string? DisplayText { get => File != null ? File.DisplayName : "Select a file"; }
public FilePickerParameterRun()
{
var command = new FilePickerCommand();
command.FileSelected += (s, file) =>
{
File = file;
if (file != null)
{
Value = file;
DisplayText = file.Name;
// Icon = new IconInfo("File");
}
else
{
Value = null;
DisplayText = null;
// Icon = new IconInfo("File");
}
Value = file != null ? file : (object?)null;
OnPropertyChanged(nameof(DisplayText));
};
PlaceholderText = "Select a file";
Icon = new IconInfo("File");
Icon = new IconInfo("\uE710"); // Add
Command = command;
}
private sealed partial class FilePickerCommand : InvokableCommand, IRequiresHostHwnd
{
public override IconInfo Icon => new("File");
public override IconInfo Icon => new("\uE710"); // Add
public override string Name => "Pick a file";

View File

@@ -95,6 +95,11 @@ public partial class SamplesListPage : ListPage
Title = "Sample parameters page",
Subtitle = "A demo of a command that takes simple parameters",
},
new ListItem(new ButtonParameterTest())
{
Title = "Button parameters page",
Subtitle = "A demo of a command that takes simple parameters",
},
// Evil edge cases
// Anything weird that might break the palette - put that in here.