diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/BeginInvokeMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/BeginInvokeMessage.cs new file mode 100644 index 0000000000..2c7fb406b8 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/BeginInvokeMessage.cs @@ -0,0 +1,7 @@ +// 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.UI.ViewModels.Messages; + +public record BeginInvokeMessage; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/CmdPalInvokeResultMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/CmdPalInvokeResultMessage.cs new file mode 100644 index 0000000000..1ecc1be460 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/CmdPalInvokeResultMessage.cs @@ -0,0 +1,7 @@ +// 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.UI.ViewModels.Messages; + +public record CmdPalInvokeResultMessage(Microsoft.CommandPalette.Extensions.CommandResultKind Kind); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/GoBackMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/GoBackMessage.cs new file mode 100644 index 0000000000..d200a8454f --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/GoBackMessage.cs @@ -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.UI.ViewModels.Messages; + +public record GoBackMessage(bool WithAnimation = true, bool FocusSearch = true) +{ + // TODO! sticking these properties here feels like leaking the UI into the models +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/GoHomeMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/GoHomeMessage.cs index 2200fe2c56..5a7f56d021 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/GoHomeMessage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/GoHomeMessage.cs @@ -4,6 +4,7 @@ namespace Microsoft.CmdPal.UI.ViewModels.Messages; -public record GoHomeMessage() +// TODO! sticking these properties here feels like leaking the UI into the models +public record GoHomeMessage(bool WithAnimation = true, bool FocusSearch = true) { } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/NavigateToPageMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/NavigateToPageMessage.cs new file mode 100644 index 0000000000..784ce8f2bc --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/NavigateToPageMessage.cs @@ -0,0 +1,9 @@ +// 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.UI.ViewModels.Messages; + +public record NavigateToPageMessage(PageViewModel Page, bool WithAnimation) +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ShowConfirmationMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ShowConfirmationMessage.cs new file mode 100644 index 0000000000..c4059fbecd --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ShowConfirmationMessage.cs @@ -0,0 +1,9 @@ +// 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.UI.ViewModels.Messages; + +public record ShowConfirmationMessage(Microsoft.CommandPalette.Extensions.IConfirmationArgs? Args) +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ShowToastMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ShowToastMessage.cs new file mode 100644 index 0000000000..0ec61dffd8 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ShowToastMessage.cs @@ -0,0 +1,9 @@ +// 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.UI.ViewModels.Messages; + +public record ShowToastMessage(string Message) +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ShellViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ShellViewModel.cs index b8d140e548..750bbcc022 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ShellViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ShellViewModel.cs @@ -18,8 +18,14 @@ using WinRT; namespace Microsoft.CmdPal.UI.ViewModels; -public partial class ShellViewModel(IServiceProvider _serviceProvider, TaskScheduler _scheduler) : ObservableObject +public partial class ShellViewModel : ObservableObject, + IRecipient { + private readonly IServiceProvider _serviceProvider; + private readonly TaskScheduler _scheduler; + private readonly Lock _invokeLock = new(); + private Task? _handleInvokeTask; + [ObservableProperty] public partial bool IsLoaded { get; set; } = false; @@ -29,7 +35,7 @@ public partial class ShellViewModel(IServiceProvider _serviceProvider, TaskSched [ObservableProperty] public partial bool IsDetailsVisible { get; set; } - private PageViewModel _currentPage = new LoadingPageViewModel(null, _scheduler); + private PageViewModel _currentPage; public PageViewModel CurrentPage { @@ -57,6 +63,19 @@ public partial class ShellViewModel(IServiceProvider _serviceProvider, TaskSched private MainListPage? _mainListPage; private IExtensionWrapper? _activeExtension; + private bool _isNested; + + public bool IsNested { get => _isNested; } + + public ShellViewModel(IServiceProvider serviceProvider, TaskScheduler scheduler) + { + _serviceProvider = serviceProvider; + _scheduler = scheduler; + _currentPage = new LoadingPageViewModel(null, _scheduler); + + // Register to receive messages + WeakReferenceMessenger.Default.Register(this); + } [RelayCommand] public async Task LoadAsync() @@ -164,6 +183,241 @@ public partial class ShellViewModel(IServiceProvider _serviceProvider, TaskSched } } + public void Receive(PerformCommandMessage message) + { + PerformCommand(message); + } + + private void PerformCommand(PerformCommandMessage message) + { + var command = message.Command.Unsafe; + if (command == null) + { + return; + } + + if (!CurrentPage.IsNested) + { + // on the main page here + PerformTopLevelCommand(message); + } + + IExtensionWrapper? extension = null; + + try + { + // In the case that we're coming from a top-level command, the + // current page's host is the global instance. We only really want + // to use that as the host of last resort. + var pageHost = CurrentPage?.ExtensionHost; + if (pageHost == CommandPaletteHost.Instance) + { + pageHost = null; + } + + var messageHost = message.ExtensionHost; + + // Use the host from the current page if it has one, else use the + // one specified in the PerformMessage for a top-level command, + // else just use the global one. + CommandPaletteHost host; + + // Top level items can come through without a Extension set on the + // message. In that case, the `Context` is actually the + // TopLevelViewModel itself, and we can use that to get at the + // extension object. + extension = pageHost?.Extension ?? messageHost?.Extension ?? null; + if (extension == null && message.Context is TopLevelViewModel topLevelViewModel) + { + extension = topLevelViewModel.ExtensionHost?.Extension; + host = pageHost ?? messageHost ?? topLevelViewModel?.ExtensionHost ?? CommandPaletteHost.Instance; + } + else + { + host = pageHost ?? messageHost ?? CommandPaletteHost.Instance; + } + + if (extension != null) + { + if (messageHost != null) + { + Logger.LogDebug($"Activated top-level command from {extension.ExtensionDisplayName}"); + } + else + { + Logger.LogDebug($"Activated command from {extension.ExtensionDisplayName}"); + } + } + + SetActiveExtension(extension); + + if (command is IPage page) + { + Logger.LogDebug($"Navigating to page"); + + var isMainPage = command is MainListPage; + + // Construct our ViewModel of the appropriate type and pass it the UI Thread context. + var pageViewModel = GetViewModelForPage(page, !isMainPage, host); + if (pageViewModel == null) + { + Logger.LogError($"Failed to create ViewModel for page {page.GetType().Name}"); + throw new NotSupportedException(); + } + + // Kick off async loading of our ViewModel + LoadPageViewModel(pageViewModel); + _isNested = !isMainPage; + + OnUIThread(() => { WeakReferenceMessenger.Default.Send(new(null)); }); + WeakReferenceMessenger.Default.Send(new(pageViewModel, message.WithAnimation)); + + // Note: Originally we set our page back in the ViewModel here, but that now happens in response to the Frame navigating triggered from the above + // See RootFrame_Navigated event handler. + } + else if (command is IInvokableCommand invokable) + { + Logger.LogDebug($"Invoking command"); + + WeakReferenceMessenger.Default.Send(); + StartInvoke(message, invokable); + } + } + catch (Exception ex) + { + // TODO: It would be better to do this as a page exception, rather + // than a silent log message. + CommandPaletteHost.Instance.Log(ex.Message); + } + } + + private void StartInvoke(PerformCommandMessage message, IInvokableCommand invokable) + { + // TODO GH #525 This needs more better locking. + lock (_invokeLock) + { + if (_handleInvokeTask != null) + { + // do nothing - a command is already doing a thing + } + else + { + _handleInvokeTask = Task.Run(() => + { + SafeHandleInvokeCommandSynchronous(message, invokable); + }); + } + } + } + + private void SafeHandleInvokeCommandSynchronous(PerformCommandMessage message, IInvokableCommand invokable) + { + try + { + // Call out to extension process. + // * May fail! + // * May never return! + var result = invokable.Invoke(message.Context); + + // But if it did succeed, we need to handle the result. + UnsafeHandleCommandResult(result); + + _handleInvokeTask = null; + } + catch (Exception ex) + { + _handleInvokeTask = null; + + // TODO: It would be better to do this as a page exception, rather + // than a silent log message. + CommandPaletteHost.Instance.Log(ex.Message); + } + } + + private void UnsafeHandleCommandResult(ICommandResult? result) + { + if (result == null) + { + // No result, nothing to do. + return; + } + + var kind = result.Kind; + Logger.LogDebug($"handling {kind.ToString()}"); + + WeakReferenceMessenger.Default.Send(new(kind)); + switch (kind) + { + case CommandResultKind.Dismiss: + { + // Reset the palette to the main page and dismiss + GoHome(withAnimation: false, focusSearch: false); + WeakReferenceMessenger.Default.Send(); + break; + } + + case CommandResultKind.GoHome: + { + // Go back to the main page, but keep it open + GoHome(); + break; + } + + case CommandResultKind.GoBack: + { + GoBack(); + break; + } + + case CommandResultKind.Hide: + { + // Keep this page open, but hide the palette. + WeakReferenceMessenger.Default.Send(); + break; + } + + case CommandResultKind.KeepOpen: + { + // Do nothing. + break; + } + + case CommandResultKind.Confirm: + { + if (result.Args is IConfirmationArgs a) + { + WeakReferenceMessenger.Default.Send(new(a)); + } + + break; + } + + case CommandResultKind.ShowToast: + { + if (result.Args is IToastArgs a) + { + WeakReferenceMessenger.Default.Send(new(a.Message)); + UnsafeHandleCommandResult(a.Result); + } + + break; + } + } + } + + private PageViewModel? GetViewModelForPage(IPage page, bool nested, CommandPaletteHost host) + { + return page switch + { + IListPage listPage => new ListViewModel(listPage, _scheduler, host) + { + IsNested = nested, + }, + IContentPage contentPage => new ContentPageViewModel(contentPage, _scheduler, host), + _ => null, + }; + } + public void SetActiveExtension(IExtensionWrapper? extension) { if (extension != _activeExtension) @@ -196,9 +450,15 @@ public partial class ShellViewModel(IServiceProvider _serviceProvider, TaskSched } } - public void GoHome() + public void GoHome(bool withAnimation = true, bool focusSearch = true) { SetActiveExtension(null); + WeakReferenceMessenger.Default.Send(new(withAnimation, focusSearch)); + } + + public void GoBack(bool withAnimation = true, bool focusSearch = true) + { + WeakReferenceMessenger.Default.Send(new(withAnimation, focusSearch)); } // You may ask yourself, why aren't we using CsWin32 for this? @@ -214,4 +474,13 @@ public partial class ShellViewModel(IServiceProvider _serviceProvider, TaskSched [SupportedOSPlatform("windows5.0")] internal static extern unsafe global::Windows.Win32.Foundation.HRESULT CoAllowSetForegroundWindow(nint pUnk, [Optional] void* lpvReserved); } + + private void OnUIThread(Action action) + { + _ = Task.Factory.StartNew( + action, + CancellationToken.None, + TaskCreationOptions.None, + _scheduler); + } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs index 6de2751d4d..92bb6d394d 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs @@ -144,6 +144,8 @@ public partial class App : Application services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(new TelemetryForwarder()); + // ViewModels services.AddSingleton(); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TelemetryForwarder.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TelemetryForwarder.cs new file mode 100644 index 0000000000..6c5fc255ba --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TelemetryForwarder.cs @@ -0,0 +1,40 @@ +// 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 CommunityToolkit.Mvvm.Messaging; +using Microsoft.CmdPal.UI.Events; +using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.PowerToys.Telemetry; + +namespace Microsoft.CmdPal.UI; + +/// +/// TelemetryForwarder is responsible for forwarding telemetry events from the +/// command palette core to PowerToys Telemetry. +/// This allows us to emit telemetry events as messages from the core, +/// and then handle them by logging to our PT telemetry provider. +/// +/// We may in the future want to replace this with a more generic "ITelemetryService" +/// or something similar, but this works for now. +/// +internal sealed class TelemetryForwarder : + IRecipient, + IRecipient +{ + public TelemetryForwarder() + { + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + } + + public void Receive(CmdPalInvokeResultMessage message) + { + PowerToysTelemetry.Log.WriteEvent(new CmdPalInvokeResult(message.Kind)); + } + + public void Receive(BeginInvokeMessage message) + { + PowerToysTelemetry.Log.WriteEvent(new BeginInvoke()); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs index 2b61acf467..4ae2a7f43d 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs @@ -6,11 +6,9 @@ using System.ComponentModel; using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.WinUI; using ManagedCommon; -using Microsoft.CmdPal.Common.Services; using Microsoft.CmdPal.UI.Events; using Microsoft.CmdPal.UI.Settings; using Microsoft.CmdPal.UI.ViewModels; -using Microsoft.CmdPal.UI.ViewModels.MainPage; using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CommandPalette.Extensions; using Microsoft.Extensions.DependencyInjection; @@ -28,15 +26,18 @@ namespace Microsoft.CmdPal.UI.Pages; /// public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, IRecipient, - IRecipient, IRecipient, IRecipient, IRecipient, IRecipient, IRecipient, - IRecipient, IRecipient, IRecipient, + IRecipient, + IRecipient, + IRecipient, + IRecipient, + IRecipient, INotifyPropertyChanged { private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread(); @@ -50,8 +51,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, private readonly ToastWindow _toast = new(); - private readonly Lock _invokeLock = new(); - private Task? _handleInvokeTask; private SettingsWindow? _settingsWindow; public ShellViewModel ViewModel { get; private set; } = App.Current.Services.GetService()!; @@ -64,8 +63,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, // how we are doing navigation around WeakReferenceMessenger.Default.Register(this); - WeakReferenceMessenger.Default.Register(this); - WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); @@ -76,6 +73,12 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + RootFrame.Navigate(typeof(LoadingPage), ViewModel); } @@ -103,195 +106,72 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, } } - public void Receive(PerformCommandMessage message) + public void Receive(NavigateToPageMessage message) { - PerformCommand(message); + // TODO GH #526 This needs more better locking too + _ = _queue.TryEnqueue(() => + { + // Also hide our details pane about here, if we had one + HideDetails(); + + // Navigate to the appropriate host page for that VM + RootFrame.Navigate( + message.Page switch + { + ListViewModel => typeof(ListPage), + ContentPageViewModel => typeof(ContentPage), + _ => throw new NotSupportedException(), + }, + message.Page, + message.WithAnimation ? _slideRightTransition : _noAnimation); + + PowerToysTelemetry.Log.WriteEvent(new OpenPage(RootFrame.BackStackDepth)); + + // Refocus on the Search for continual typing on the next search request + SearchBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic); + + if (!ViewModel.IsNested) + { + // todo BODGY + RootFrame.BackStack.Clear(); + } + }); } - private void PerformCommand(PerformCommandMessage message) + public void Receive(ShowConfirmationMessage message) { - var command = message.Command.Unsafe; - if (command == null) + DispatcherQueue.TryEnqueue(async () => + { + try + { + await HandleConfirmArgsOnUiThread(message.Args); + } + catch (Exception ex) + { + Logger.LogError(ex.ToString()); + } + }); + } + + public void Receive(ShowToastMessage message) + { + DispatcherQueue.TryEnqueue(() => + { + _toast.ShowToast(message.Message); + }); + } + + // This gets called from the UI thread + private async Task HandleConfirmArgsOnUiThread(IConfirmationArgs? args) + { + if (args == null) { return; } - if (!ViewModel.CurrentPage.IsNested) - { - // on the main page here - ViewModel.PerformTopLevelCommand(message); - } - - IExtensionWrapper? extension = null; - - // TODO: Actually loading up the page, or invoking the command - - // that might belong in the model, not the view? - // Especially considering the try/catch concerns around the fact that the - // COM call might just fail. - // Or the command may be a stub. Future us problem. - try - { - // In the case that we're coming from a top-level command, the - // current page's host is the global instance. We only really want - // to use that as the host of last resort. - var pageHost = ViewModel.CurrentPage?.ExtensionHost; - if (pageHost == CommandPaletteHost.Instance) - { - pageHost = null; - } - - var messageHost = message.ExtensionHost; - - // Use the host from the current page if it has one, else use the - // one specified in the PerformMessage for a top-level command, - // else just use the global one. - CommandPaletteHost host; - - // Top level items can come through without a Extension set on the - // message. In that case, the `Context` is actually the - // TopLevelViewModel itself, and we can use that to get at the - // extension object. - extension = pageHost?.Extension ?? messageHost?.Extension ?? null; - if (extension == null && message.Context is TopLevelViewModel topLevelViewModel) - { - extension = topLevelViewModel.ExtensionHost?.Extension; - host = pageHost ?? messageHost ?? topLevelViewModel?.ExtensionHost ?? CommandPaletteHost.Instance; - } - else - { - host = pageHost ?? messageHost ?? CommandPaletteHost.Instance; - } - - if (extension != null) - { - if (messageHost != null) - { - Logger.LogDebug($"Activated top-level command from {extension.ExtensionDisplayName}"); - } - else - { - Logger.LogDebug($"Activated command from {extension.ExtensionDisplayName}"); - } - } - - ViewModel.SetActiveExtension(extension); - - if (command is IPage page) - { - Logger.LogDebug($"Navigating to page"); - - // TODO GH #526 This needs more better locking too - _ = _queue.TryEnqueue(() => - { - // Also hide our details pane about here, if we had one - HideDetails(); - - WeakReferenceMessenger.Default.Send(new(null)); - - var isMainPage = command is MainListPage; - - // Construct our ViewModel of the appropriate type and pass it the UI Thread context. - PageViewModel pageViewModel = page switch - { - IListPage listPage => new ListViewModel(listPage, _mainTaskScheduler, host) - { - IsNested = !isMainPage, - }, - IContentPage contentPage => new ContentPageViewModel(contentPage, _mainTaskScheduler, host), - _ => throw new NotSupportedException(), - }; - - // Kick off async loading of our ViewModel - ViewModel.LoadPageViewModel(pageViewModel); - - // Navigate to the appropriate host page for that VM - RootFrame.Navigate( - page switch - { - IListPage => typeof(ListPage), - IContentPage => typeof(ContentPage), - _ => throw new NotSupportedException(), - }, - pageViewModel, - message.WithAnimation ? _slideRightTransition : _noAnimation); - - PowerToysTelemetry.Log.WriteEvent(new OpenPage(RootFrame.BackStackDepth)); - - // Refocus on the Search for continual typing on the next search request - SearchBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic); - - if (isMainPage) - { - // todo BODGY - RootFrame.BackStack.Clear(); - } - - // Note: Originally we set our page back in the ViewModel here, but that now happens in response to the Frame navigating triggered from the above - // See RootFrame_Navigated event handler. - }); - } - else if (command is IInvokableCommand invokable) - { - Logger.LogDebug($"Invoking command"); - PowerToysTelemetry.Log.WriteEvent(new BeginInvoke()); - HandleInvokeCommand(message, invokable); - } - } - catch (Exception ex) - { - // TODO: It would be better to do this as a page exception, rather - // than a silent log message. - CommandPaletteHost.Instance.Log(ex.Message); - } - } - - private void HandleInvokeCommand(PerformCommandMessage message, IInvokableCommand invokable) - { - // TODO GH #525 This needs more better locking. - lock (_invokeLock) - { - if (_handleInvokeTask != null) - { - // do nothing - a command is already doing a thing - } - else - { - _handleInvokeTask = Task.Run(() => - { - try - { - var result = invokable.Invoke(message.Context); - DispatcherQueue.TryEnqueue(() => - { - try - { - HandleCommandResultOnUiThread(result); - } - finally - { - _handleInvokeTask = null; - } - }); - } - catch (Exception ex) - { - _handleInvokeTask = null; - - // TODO: It would be better to do this as a page exception, rather - // than a silent log message. - CommandPaletteHost.Instance.Log(ex.Message); - } - }); - } - } - } - - // This gets called from the UI thread - private void HandleConfirmArgs(IConfirmationArgs args) - { ConfirmResultViewModel vm = new(args, new(ViewModel.CurrentPage)); var initializeDialogTask = Task.Run(() => { InitializeConfirmationDialog(vm); }); - initializeDialogTask.Wait(); + await initializeDialogTask; var resourceLoader = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance.ResourceLoader; var confirmText = resourceLoader.GetString("ConfirmationDialog_ConfirmButtonText"); @@ -322,19 +202,16 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, // }; } - DispatcherQueue.TryEnqueue(async () => + var result = await dialog.ShowAsync(); + if (result == ContentDialogResult.Primary) { - var result = await dialog.ShowAsync(); - if (result == ContentDialogResult.Primary) - { - var performMessage = new PerformCommandMessage(vm); - PerformCommand(performMessage); - } - else - { - // cancel - } - }); + var performMessage = new PerformCommandMessage(vm); + WeakReferenceMessenger.Default.Send(performMessage); + } + else + { + // cancel + } } private void InitializeConfirmationDialog(ConfirmResultViewModel vm) @@ -342,79 +219,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, vm.SafeInitializePropertiesSynchronous(); } - private void HandleCommandResultOnUiThread(ICommandResult? result) - { - try - { - if (result != null) - { - var kind = result.Kind; - Logger.LogDebug($"handling {kind.ToString()}"); - PowerToysTelemetry.Log.WriteEvent(new CmdPalInvokeResult(kind)); - switch (kind) - { - case CommandResultKind.Dismiss: - { - // Reset the palette to the main page and dismiss - GoHome(withAnimation: false, focusSearch: false); - WeakReferenceMessenger.Default.Send(); - break; - } - - case CommandResultKind.GoHome: - { - // Go back to the main page, but keep it open - GoHome(); - break; - } - - case CommandResultKind.GoBack: - { - GoBack(); - break; - } - - case CommandResultKind.Hide: - { - // Keep this page open, but hide the palette. - WeakReferenceMessenger.Default.Send(); - break; - } - - case CommandResultKind.KeepOpen: - { - // Do nothing. - break; - } - - case CommandResultKind.Confirm: - { - if (result.Args is IConfirmationArgs a) - { - HandleConfirmArgs(a); - } - - break; - } - - case CommandResultKind.ShowToast: - { - if (result.Args is IToastArgs a) - { - _toast.ShowToast(a.Message); - HandleCommandResultOnUiThread(a.Result); - } - - break; - } - } - } - } - catch - { - } - } - public void Receive(OpenSettingsMessage message) { _ = DispatcherQueue.TryEnqueue(() => @@ -467,14 +271,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, public void Receive(LaunchUriMessage message) => _ = global::Windows.System.Launcher.LaunchUriAsync(message.Uri); - public void Receive(HandleCommandResultMessage message) - { - DispatcherQueue.TryEnqueue(() => - { - HandleCommandResultOnUiThread(message.Result.Unsafe); - }); - } - private void HideDetails() { ViewModel.Details = null; @@ -554,6 +350,11 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, WeakReferenceMessenger.Default.Send(); } + public void Receive(GoBackMessage message) + { + _ = DispatcherQueue.TryEnqueue(() => GoBack(message.WithAnimation, message.FocusSearch)); + } + private void GoBack(bool withAnimation = true, bool focusSearch = true) { HideDetails(); @@ -591,14 +392,17 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, } } + public void Receive(GoHomeMessage message) + { + _ = DispatcherQueue.TryEnqueue(() => GoHome(withAnimation: message.WithAnimation, focusSearch: message.FocusSearch)); + } + private void GoHome(bool withAnimation = true, bool focusSearch = true) { while (RootFrame.CanGoBack) { GoBack(withAnimation, focusSearch); } - - WeakReferenceMessenger.Default.Send(); } private void BackButton_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e) => WeakReferenceMessenger.Default.Send(new());