Add support for commands on content pages (#495)

Basically just abstracts what we already had for list items, and uses that abstraction for contentpageviewmodel too


Closes #476
This commit is contained in:
Mike Griese
2025-03-05 16:08:14 -06:00
committed by GitHub
parent 5accdc636f
commit abdd298c3c
11 changed files with 210 additions and 25 deletions

View File

@@ -18,8 +18,24 @@ internal sealed partial class SampleContentPage : ContentPage
public SampleContentPage()
{
Name = "Sample Content";
Name = "Open";
Title = "Sample Content";
Icon = new IconInfo("\uECA5"); // Tiles
Commands = [
new CommandContextItem(
title: "Do thing",
name: "Do thing",
subtitle: "Pops a toast",
result: CommandResult.ShowToast(new ToastArgs() { Message = "what's up doc", Result = CommandResult.KeepOpen() }),
action: () => { Title = Title + "+1"; }),
new CommandContextItem(
title: "Something else",
name: "Something else",
subtitle: "Something else",
result: CommandResult.ShowToast(new ToastArgs() { Message = "turn down for what?", Result = CommandResult.KeepOpen() }),
action: () => { Title = Title + "-1"; }),
];
}
}

View File

@@ -13,7 +13,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class CommandBarViewModel : ObservableObject,
IRecipient<UpdateCommandBarMessage>
{
public ListItemViewModel? SelectedItem
public ICommandBarContext? SelectedItem
{
get => field;
set
@@ -51,11 +51,11 @@ public partial class CommandBarViewModel : ObservableObject,
public void Receive(UpdateCommandBarMessage message) => SelectedItem = message.ViewModel;
private void SetSelectedItem(ListItemViewModel? value)
private void SetSelectedItem(ICommandBarContext? value)
{
if (value != null)
{
PrimaryCommand = value;
PrimaryCommand = value.PrimaryCommand;
value.PropertyChanged += SelectedItemPropertyChanged;
}
else
@@ -92,7 +92,7 @@ public partial class CommandBarViewModel : ObservableObject,
SecondaryCommand = SelectedItem.SecondaryCommand;
if (SelectedItem.MoreCommands.Count > 1)
if (SelectedItem.MoreCommands.Count() > 1)
{
ShouldShowContextMenu = true;
ContextCommands = [.. SelectedItem.AllCommands];
@@ -104,7 +104,26 @@ public partial class CommandBarViewModel : ObservableObject,
}
// InvokeItemCommand is what this will be in Xaml due to source generator
// this comes in when an item in the list is tapped
[RelayCommand]
private void InvokeItem(CommandContextItemViewModel item) =>
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.Command.Model, item.Model));
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.Command.Model, item.Model));
// this comes in when the primary button is tapped
public void InvokePrimaryCommand()
{
if (PrimaryCommand != null)
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(PrimaryCommand.Command.Model, PrimaryCommand.Model));
}
}
// this comes in when the secondary button is tapped
public void InvokeSecondaryCommand()
{
if (SecondaryCommand != null)
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(SecondaryCommand.Command.Model, SecondaryCommand.Model));
}
}
}

View File

@@ -9,7 +9,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class CommandContextItemViewModel(ICommandContextItem contextItem, IPageContext context) : CommandItemViewModel(new(contextItem), context)
{
public ExtensionObject<ICommandContextItem> Model { get; } = new(contextItem);
public new ExtensionObject<ICommandContextItem> Model { get; } = new(contextItem);
public bool IsCritical { get; private set; }

View File

@@ -2,14 +2,17 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class CommandItemViewModel : ExtensionObjectViewModel
public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBarContext
{
public ExtensionObject<ICommandItem> Model => _commandItemModel;
private readonly ExtensionObject<ICommandItem> _commandItemModel = new(null);
private CommandContextItemViewModel? _defaultCommandContextItem;
@@ -37,10 +40,14 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel
public List<CommandContextItemViewModel> MoreCommands { get; private set; } = [];
IEnumerable<CommandContextItemViewModel> ICommandBarContext.MoreCommands => MoreCommands;
public bool HasMoreCommands => MoreCommands.Count > 0;
public string SecondaryCommandName => SecondaryCommand?.Name ?? string.Empty;
public CommandItemViewModel? PrimaryCommand => this;
public CommandItemViewModel? SecondaryCommand => HasMoreCommands ? MoreCommands[0] : null;
public bool ShouldBeVisible => !string.IsNullOrEmpty(Name);

View File

@@ -5,6 +5,7 @@
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Models;
@@ -13,7 +14,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ContentPageViewModel : PageViewModel
public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
{
private readonly ExtensionObject<IContentPage> _model;
@@ -29,6 +30,20 @@ public partial class ContentPageViewModel : PageViewModel
[MemberNotNullWhen(true, nameof(Details))]
public bool HasDetails => Details != null;
/////// ICommandBarContext ///////
public IEnumerable<CommandContextItemViewModel> MoreCommands => Commands.Skip(1);
public bool HasMoreCommands => Commands.Count > 1;
public string SecondaryCommandName => SecondaryCommand?.Name ?? string.Empty;
public CommandItemViewModel? PrimaryCommand => HasCommands ? Commands[0] : null;
public CommandItemViewModel? SecondaryCommand => HasMoreCommands ? Commands[1] : null;
public List<CommandContextItemViewModel> AllCommands => Commands;
/////// /ICommandBarContext ///////
// Remember - "observable" properties from the model (via PropChanged)
// cannot be marked [ObservableProperty]
public ContentPageViewModel(IContentPage model, TaskScheduler scheduler, CommandPaletteHost host)
@@ -102,6 +117,10 @@ public partial class ContentPageViewModel : PageViewModel
.Select(contextItem => (contextItem as ICommandContextItem)!)
.Select(contextItem => new CommandContextItemViewModel(contextItem, PageContext))
.ToList();
Commands.ForEach(contextItem =>
{
contextItem.InitializeProperties();
});
var extensionDetails = model.Details;
if (extensionDetails != null)
@@ -114,6 +133,15 @@ public partial class ContentPageViewModel : PageViewModel
FetchContent();
model.ItemsChanged += Model_ItemsChanged;
Task.Factory.StartNew(
() =>
{
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(this));
},
CancellationToken.None,
TaskCreationOptions.None,
PageContext.Scheduler);
}
protected override void FetchProperty(string propertyName)
@@ -128,10 +156,47 @@ public partial class ContentPageViewModel : PageViewModel
switch (propertyName)
{
// case nameof(Commands):
// TODO GH #360 - make MoreCommands observable
// this.ShowDetails = model.ShowDetails;
// break;
case nameof(Commands):
var more = model.Commands;
if (more != null)
{
var newContextMenu = more
.Where(contextItem => contextItem is ICommandContextItem)
.Select(contextItem => (contextItem as ICommandContextItem)!)
.Select(contextItem => new CommandContextItemViewModel(contextItem, PageContext))
.ToList();
lock (Commands)
{
ListHelpers.InPlaceUpdateList(Commands, newContextMenu);
}
Commands.ForEach(contextItem =>
{
contextItem.InitializeProperties();
});
}
else
{
Commands.Clear();
}
UpdateProperty(nameof(PrimaryCommand));
UpdateProperty(nameof(SecondaryCommand));
UpdateProperty(nameof(SecondaryCommandName));
UpdateProperty(nameof(HasCommands));
UpdateProperty(nameof(HasMoreCommands));
UpdateProperty(nameof(AllCommands));
Task.Factory.StartNew(
() =>
{
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(this));
},
CancellationToken.None,
TaskCreationOptions.None,
PageContext.Scheduler);
break;
case nameof(Details):
var extensionDetails = model.Details;
Details = extensionDetails != null ? new(extensionDetails, PageContext) : null;
@@ -163,4 +228,25 @@ public partial class ContentPageViewModel : PageViewModel
TaskCreationOptions.None,
PageContext.Scheduler);
}
// InvokeItemCommand is what this will be in Xaml due to source generator
// this comes in on Enter keypresses in the SearchBox
[RelayCommand]
private void InvokePrimaryCommand(ContentPageViewModel page)
{
if (PrimaryCommand != null)
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(PrimaryCommand.Command.Model, PrimaryCommand.Model));
}
}
// this comes in on Ctrl+Enter keypresses in the SearchBox
[RelayCommand]
private void InvokeSecondaryCommand(ContentPageViewModel page)
{
if (SecondaryCommand != null)
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(SecondaryCommand.Command.Model, SecondaryCommand.Model));
}
}
}

View File

@@ -14,7 +14,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ListItemViewModel(IListItem model, IPageContext context)
: CommandItemViewModel(new(model), context)
{
public ExtensionObject<IListItem> Model { get; } = new(model);
public new ExtensionObject<IListItem> Model { get; } = new(model);
[ObservableProperty]
public partial ObservableCollection<TagViewModel> Tags { get; set; } = [];

View File

@@ -30,6 +30,12 @@ public record PerformCommandMessage
Context = context.Unsafe;
}
public PerformCommandMessage(ExtensionObject<ICommand> command, ExtensionObject<ICommandItem> context)
{
Command = command;
Context = context.Unsafe;
}
public PerformCommandMessage(ExtensionObject<ICommand> command, ExtensionObject<ICommandContextItem> context)
{
Command = command;

View File

@@ -2,11 +2,33 @@
// 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.ComponentModel;
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
/// <summary>
/// Used to update the command bar at the bottom to reflect the commands for a list item
/// </summary>
public record UpdateCommandBarMessage(ListItemViewModel? ViewModel)
public record UpdateCommandBarMessage(ICommandBarContext? ViewModel)
{
}
// Represents everything the command bar needs to know about to show command
// buttons at the bottom.
//
// This is implemented by both ListItemViewModel and ContentPageViewModel,
// the two things with sub-commands.
public interface ICommandBarContext : INotifyPropertyChanged
{
public IEnumerable<CommandContextItemViewModel> MoreCommands { get; }
public bool HasMoreCommands { get; }
public string SecondaryCommandName { get; }
public CommandItemViewModel? PrimaryCommand { get; }
public CommandItemViewModel? SecondaryCommand { get; }
public List<CommandContextItemViewModel> AllCommands { get; }
}

View File

@@ -10,7 +10,6 @@ using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Input;
using Windows.System;
namespace Microsoft.CmdPal.UI.Controls;
@@ -55,12 +54,16 @@ public sealed partial class CommandBar : UserControl,
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS has a tendency to delete XAML bound methods over-aggressively")]
private void PrimaryButton_Tapped(object sender, TappedRoutedEventArgs e) =>
WeakReferenceMessenger.Default.Send<ActivateSelectedListItemMessage>();
private void PrimaryButton_Tapped(object sender, TappedRoutedEventArgs e)
{
ViewModel.InvokePrimaryCommand();
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS has a tendency to delete XAML bound methods over-aggressively")]
private void SecondaryButton_Tapped(object sender, TappedRoutedEventArgs e) =>
WeakReferenceMessenger.Default.Send<ActivateSecondaryCommandMessage>();
private void SecondaryButton_Tapped(object sender, TappedRoutedEventArgs e)
{
ViewModel.InvokeSecondaryCommand();
}
private void PageIcon_Tapped(object sender, TappedRoutedEventArgs e)
{

View File

@@ -2,7 +2,9 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -13,7 +15,9 @@ namespace Microsoft.CmdPal.UI;
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class ContentPage : Page
public sealed partial class ContentPage : Page,
IRecipient<ActivateSelectedListItemMessage>,
IRecipient<ActivateSecondaryCommandMessage>
{
private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread();
@@ -30,6 +34,8 @@ public sealed partial class ContentPage : Page
public ContentPage()
{
this.InitializeComponent();
WeakReferenceMessenger.Default.Register<ActivateSelectedListItemMessage>(this);
WeakReferenceMessenger.Default.Register<ActivateSecondaryCommandMessage>(this);
}
protected override void OnNavigatedTo(NavigationEventArgs e)
@@ -42,5 +48,25 @@ public sealed partial class ContentPage : Page
base.OnNavigatedTo(e);
}
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) => base.OnNavigatingFrom(e);
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
base.OnNavigatingFrom(e);
WeakReferenceMessenger.Default.Unregister<ActivateSelectedListItemMessage>(this);
WeakReferenceMessenger.Default.Unregister<ActivateSecondaryCommandMessage>(this);
// Clean-up event listeners
ViewModel = null;
}
// this comes in on Enter keypresses in the SearchBox
public void Receive(ActivateSelectedListItemMessage message)
{
ViewModel?.InvokePrimaryCommandCommand?.Execute(ViewModel);
}
// this comes in on Ctrl+Enter keypresses in the SearchBox
public void Receive(ActivateSecondaryCommandMessage message)
{
ViewModel?.InvokeSecondaryCommandCommand?.Execute(ViewModel);
}
}

View File

@@ -18,8 +18,8 @@ namespace Microsoft.CmdPal.UI;
public sealed partial class ListPage : Page,
IRecipient<NavigateNextCommand>,
IRecipient<NavigatePreviousCommand>,
IRecipient<ActivateSelectedListItemMessage>,
IRecipient<ActivateSecondaryCommandMessage>
IRecipient<ActivateSelectedListItemMessage>,
IRecipient<ActivateSecondaryCommandMessage>
{
public ListViewModel? ViewModel
{
@@ -56,9 +56,9 @@ public sealed partial class ListPage : Page,
// RegisterAll isn't AOT compatible
WeakReferenceMessenger.Default.Register<NavigateNextCommand>(this);
WeakReferenceMessenger.Default.Register<NavigatePreviousCommand>(this);
WeakReferenceMessenger.Default.Register<ActivateSelectedListItemMessage>(this);
WeakReferenceMessenger.Default.Register<ActivateSecondaryCommandMessage>(this);
base.OnNavigatedTo(e);
}