Load extensions into TRA (#176)

Targets both #175 (which targets #171), and #172, so the delta is very sloppy. 

There's also some sloppy bits of delta here where I tried to see if the single-instance thing fixed the RoOriginate thing. 

I'm gonna try to tidy all this up a bit. 

Refs #73
This commit is contained in:
Mike Griese
2024-12-02 16:35:56 -08:00
committed by GitHub
parent 7f9608f21d
commit 0ccbfd955d
14 changed files with 778 additions and 64 deletions

View File

@@ -700,6 +700,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirtualDesktopExtension", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkspacesCsharpLibrary", "src\modules\Workspaces\WorkspacesCsharpLibrary\WorkspacesCsharpLibrary.csproj", "{89D0E199-B17A-418C-B2F8-7375B6708357}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Project Templates", "Project Templates", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -3543,7 +3545,7 @@ Global
{C831231F-891C-4572-9694-45062534B42A} = {071E18A4-A530-46B8-AB7D-B862EE55E24E}
{2C2AF4F4-3DB2-416F-9F38-2B710D961E6F} = {071E18A4-A530-46B8-AB7D-B862EE55E24E}
{BE349BD2-7FF8-47B6-8F74-42B52DDF730A} = {071E18A4-A530-46B8-AB7D-B862EE55E24E}
{EEBDB1CA-BBDD-421D-9D86-67145D0C6EEB} = {071E18A4-A530-46B8-AB7D-B862EE55E24E}
{EEBDB1CA-BBDD-421D-9D86-67145D0C6EEB} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{8E47BF33-A402-4582-930C-95E35418F653} = {071E18A4-A530-46B8-AB7D-B862EE55E24E}
{7520A2FE-00A2-49B8-83ED-DB216E874C04} = {3846508C-77EB-4034-A702-F8BB263C4F79}
{8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90} = {7520A2FE-00A2-49B8-83ED-DB216E874C04}
@@ -3552,6 +3554,7 @@ Global
{8ABE2195-7514-425E-9A89-685FA42CEFC3} = {071E18A4-A530-46B8-AB7D-B862EE55E24E}
{B79B52FB-8B2E-4CF5-B0FE-37E3E981AC7A} = {071E18A4-A530-46B8-AB7D-B862EE55E24E}
{89D0E199-B17A-418C-B2F8-7375B6708357} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF}
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {3846508C-77EB-4034-A702-F8BB263C4F79}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

View File

@@ -16,9 +16,8 @@ public sealed class CommandProviderWrapper
private readonly ICommandProvider _commandProvider;
private readonly IExtensionWrapper? extensionWrapper;
private ICommandItem[] _topLevelItems = [];
public ICommandItem[] TopLevelItems => _topLevelItems;
public ICommandItem[] TopLevelItems { get; private set; } = [];
public CommandProviderWrapper(ICommandProvider provider)
{
@@ -29,8 +28,14 @@ public sealed class CommandProviderWrapper
public CommandProviderWrapper(IExtensionWrapper extension)
{
extensionWrapper = extension;
if (!extensionWrapper.IsRunning())
{
throw new ArgumentException("You forgot to start the extension. This is a coding error - make sure to call StartExtensionAsync");
}
var extensionImpl = extension.GetExtensionObject();
if (extensionImpl?.GetProvider(ProviderType.Commands) is not ICommandProvider provider)
var providerObject = extensionImpl?.GetProvider(ProviderType.Commands);
if (providerObject is not ICommandProvider provider)
{
throw new ArgumentException("extension didn't actually implement ICommandProvider");
}
@@ -46,14 +51,14 @@ public sealed class CommandProviderWrapper
return;
}
var t = new Task<ICommandItem[]>(() => _commandProvider.TopLevelCommands());
var t = new Task<ICommandItem[]>(_commandProvider.TopLevelCommands);
t.Start();
var commands = await t.ConfigureAwait(false);
// On a BG thread here
if (commands != null)
{
_topLevelItems = commands;
TopLevelItems = commands;
}
}

View File

@@ -8,10 +8,11 @@ using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Extensions;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Models;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ListViewModel(IListPage _model) : ObservableObject
public partial class ListViewModel : ObservableObject
{
// Observable from MVVM Toolkit will auto create public properties that use INotifyPropertyChange change
// https://learn.microsoft.com/dotnet/communitytoolkit/mvvm/observablegroupedcollections for grouping support
@@ -21,6 +22,39 @@ public partial class ListViewModel(IListPage _model) : ObservableObject
[ObservableProperty]
public partial bool IsInitialized { get; private set; }
private readonly ExtensionObject<IListPage> _model;
public ListViewModel(IListPage model)
{
_model = new(model);
}
private void Model_ItemsChanged(object sender, ItemsChangedEventArgs args) => FetchItems();
//// Run on background thread, from InitializeAsync or Model_ItemsChanged
private void FetchItems()
{
ObservableGroup<string, ListItemViewModel> group = new(string.Empty);
// TEMPORARY: just plop all the items into a single group
// see 9806fe5d8 for the last commit that had this with sections
// TODO unsafe
var newItems = _model.Unsafe!.GetItems();
Items.Clear();
foreach (var item in newItems)
{
ListItemViewModel viewModel = new(item);
viewModel.InitializeProperties();
group.Add(viewModel);
}
// Am I really allowed to modify that observable collection on a BG
// thread and have it just work in the UI??
Items.AddGroup(group);
}
//// Run on background thread from ListPage.xaml.cs
[RelayCommand]
private Task<bool> InitializeAsync()
@@ -29,19 +63,9 @@ public partial class ListViewModel(IListPage _model) : ObservableObject
// TODO: We may want to investigate using some sort of AsyncEnumerable or populating these as they come in to the UI layer
// Though we have to think about threading here and circling back to the UI thread with a TaskScheduler.
FetchItems();
// TEMPORARY: just plop all the items into a single group
// see 9806fe5d8 for the last commit that had this with sections
ObservableGroup<string, ListItemViewModel> group = new(string.Empty);
foreach (var item in _model.GetItems())
{
ListItemViewModel viewModel = new(item);
viewModel.InitializeProperties();
group.Add(viewModel);
}
Items.AddGroup(group);
_model.Unsafe!.ItemsChanged += Model_ItemsChanged;
IsInitialized = true;

View File

@@ -2,9 +2,12 @@
// 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 System.Collections.Specialized;
using Microsoft.CmdPal.Extensions;
using Microsoft.CmdPal.Extensions.Helpers;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.CmdPal.UI.Pages;
@@ -14,16 +17,26 @@ namespace Microsoft.CmdPal.UI.Pages;
/// </summary>
public partial class MainListPage : DynamicListPage
{
private readonly IListItem[] _items;
private readonly IServiceProvider _serviceProvider;
// TODO: Thinking we may want a separate MainViewModel from the ShellViewModel and/or a CommandService/Provider
// which holds the TopLevelCommands and anything that needs to access those functions...
public MainListPage(ShellViewModel shellViewModel)
private readonly ObservableCollection<TopLevelCommandWrapper> _commands;
public MainListPage(IServiceProvider serviceProvider)
{
_items = shellViewModel.TopLevelCommands.Select(w => w.Unsafe!).Where(li => li != null).ToArray();
_serviceProvider = serviceProvider;
var tlcManager = _serviceProvider.GetService<TopLevelCommandManager>();
// reference the TLC collection directly... maybe? TODO is this a good idea ot a terrible one?
_commands = tlcManager!.TopLevelCommands;
_commands.CollectionChanged += Commands_CollectionChanged;
}
public override IListItem[] GetItems() => _items;
private void Commands_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) => RaiseItemsChanged(_commands.Count);
public override IListItem[] GetItems() => _commands
.Select(tlc => tlc)
.ToArray();
public override void UpdateSearchText(string oldSearch, string newSearch)
{

View File

@@ -14,6 +14,10 @@
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" />
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,350 @@
// 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 Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.Extensions;
using Windows.ApplicationModel;
using Windows.ApplicationModel.AppExtensions;
using Windows.Foundation.Collections;
namespace Microsoft.CmdPal.UI.ViewModels.Models;
public class ExtensionService : IExtensionService, IDisposable
{
public event EventHandler OnExtensionsChanged = (_, _) => { };
private static readonly PackageCatalog _catalog = PackageCatalog.OpenForCurrentUser();
private static readonly Lock _lock = new();
private readonly SemaphoreSlim _getInstalledExtensionsLock = new(1, 1);
private readonly SemaphoreSlim _getInstalledWidgetsLock = new(1, 1);
// private readonly ILocalSettingsService _localSettingsService;
private bool _disposedValue;
private const string CreateInstanceProperty = "CreateInstance";
private const string ClassIdProperty = "@ClassId";
private static readonly List<IExtensionWrapper> _installedExtensions = [];
private static readonly List<IExtensionWrapper> _enabledExtensions = [];
public ExtensionService()
{
_catalog.PackageInstalling += Catalog_PackageInstalling;
_catalog.PackageUninstalling += Catalog_PackageUninstalling;
_catalog.PackageUpdating += Catalog_PackageUpdating;
//// These two were an investigation into getting updates when a package
//// gets redeployed from VS. Neither get raised (nor do the above)
//// _catalog.PackageStatusChanged += Catalog_PackageStatusChanged;
//// _catalog.PackageStaging += Catalog_PackageStaging;
// _localSettingsService = settingsService;
}
private void Catalog_PackageInstalling(PackageCatalog sender, PackageInstallingEventArgs args)
{
if (args.IsComplete)
{
lock (_lock)
{
var isCmdPalExtension = Task.Run(() =>
{
return IsValidCmdPalExtension(args.Package);
}).Result;
if (isCmdPalExtension)
{
OnPackageChange(args.Package);
}
}
}
}
private void Catalog_PackageUninstalling(PackageCatalog sender, PackageUninstallingEventArgs args)
{
if (args.IsComplete)
{
lock (_lock)
{
foreach (var extension in _installedExtensions)
{
if (extension.PackageFullName == args.Package.Id.FullName)
{
OnPackageChange(args.Package);
break;
}
}
}
}
}
private void Catalog_PackageUpdating(PackageCatalog sender, PackageUpdatingEventArgs args)
{
if (args.IsComplete)
{
lock (_lock)
{
var isCmdPalExtension = Task.Run(() =>
{
return IsValidCmdPalExtension(args.TargetPackage);
}).Result;
if (isCmdPalExtension)
{
OnPackageChange(args.TargetPackage);
}
}
}
}
private void OnPackageChange(Package package)
{
_installedExtensions.Clear();
_enabledExtensions.Clear();
OnExtensionsChanged.Invoke(this, EventArgs.Empty);
}
private static async Task<bool> IsValidCmdPalExtension(Package package)
{
var extensions = await AppExtensionCatalog.Open("com.microsoft.windows.commandpalette").FindAllAsync();
foreach (var extension in extensions)
{
if (package.Id?.FullName == extension.Package?.Id?.FullName)
{
var (cmdPalProvider, classId) = await GetCmdPalExtensionPropertiesAsync(extension);
return cmdPalProvider != null && classId.Count != 0;
}
}
return false;
}
private static async Task<(IPropertySet? CmdPalProvider, List<string> ClassIds)> GetCmdPalExtensionPropertiesAsync(AppExtension extension)
{
var classIds = new List<string>();
var properties = await extension.GetExtensionPropertiesAsync();
if (properties is null)
{
return (null, classIds);
}
var cmdPalProvider = GetSubPropertySet(properties, "CmdPalProvider");
if (cmdPalProvider is null)
{
return (null, classIds);
}
var activation = GetSubPropertySet(cmdPalProvider, "Activation");
if (activation is null)
{
return (cmdPalProvider, classIds);
}
// Handle case where extension creates multiple instances.
classIds.AddRange(GetCreateInstanceList(activation));
return (cmdPalProvider, classIds);
}
private static async Task<IEnumerable<AppExtension>> GetInstalledAppExtensionsAsync() => await AppExtensionCatalog.Open("com.microsoft.windows.commandpalette").FindAllAsync();
public async Task<IEnumerable<IExtensionWrapper>> GetInstalledExtensionsAsync(bool includeDisabledExtensions = false)
{
await _getInstalledExtensionsLock.WaitAsync();
try
{
if (_installedExtensions.Count == 0)
{
var extensions = await GetInstalledAppExtensionsAsync();
foreach (var extension in extensions)
{
var (cmdPalProvider, classIds) = await GetCmdPalExtensionPropertiesAsync(extension);
if (cmdPalProvider == null || classIds.Count == 0)
{
continue;
}
foreach (var classId in classIds)
{
var extensionWrapper = new ExtensionWrapper(extension, classId);
var supportedInterfaces = GetSubPropertySet(cmdPalProvider, "SupportedInterfaces");
if (supportedInterfaces is not null)
{
foreach (var supportedInterface in supportedInterfaces)
{
ProviderType pt;
if (Enum.TryParse(supportedInterface.Key, out pt))
{
extensionWrapper.AddProviderType(pt);
}
else
{
// TODO: throw warning or fire notification that extension declared unsupported extension interface
// https://github.com/microsoft/DevHome/issues/617
}
}
}
// var localSettingsService = Application.Current.GetService<ILocalSettingsService>();
var extensionUniqueId = extension.AppInfo.AppUserModelId + "!" + extension.Id;
var isExtensionDisabled = false; // await localSettingsService.ReadSettingAsync<bool>(extensionUniqueId + "-ExtensionDisabled");
_installedExtensions.Add(extensionWrapper);
if (!isExtensionDisabled)
{
_enabledExtensions.Add(extensionWrapper);
}
// TelemetryFactory.Get<ITelemetry>().Log(
// "Extension_ReportInstalled",
// LogLevel.Critical,
// new ReportInstalledExtensionEvent(extensionUniqueId, isEnabled: !isExtensionDisabled));
}
}
}
return includeDisabledExtensions ? _installedExtensions : _enabledExtensions;
}
finally
{
_getInstalledExtensionsLock.Release();
}
}
public IExtensionWrapper? GetInstalledExtension(string extensionUniqueId)
{
var extension = _installedExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal));
return extension.FirstOrDefault();
}
public async Task SignalStopExtensionsAsync()
{
var installedExtensions = await GetInstalledExtensionsAsync();
foreach (var installedExtension in installedExtensions)
{
if (installedExtension.IsRunning())
{
installedExtension.SignalDispose();
}
}
}
public async Task<IEnumerable<IExtensionWrapper>> GetInstalledExtensionsAsync(ProviderType providerType, bool includeDisabledExtensions = false)
{
var installedExtensions = await GetInstalledExtensionsAsync(includeDisabledExtensions);
List<IExtensionWrapper> filteredExtensions = [];
foreach (var installedExtension in installedExtensions)
{
if (installedExtension.HasProviderType(providerType))
{
filteredExtensions.Add(installedExtension);
}
}
return filteredExtensions;
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_getInstalledExtensionsLock.Dispose();
_getInstalledWidgetsLock.Dispose();
}
_disposedValue = true;
}
}
private static IPropertySet? GetSubPropertySet(IPropertySet propSet, string name) => propSet.TryGetValue(name, out var value) ? value as IPropertySet : null;
private static object[]? GetSubPropertySetArray(IPropertySet propSet, string name) => propSet.TryGetValue(name, out var value) ? value as object[] : null;
/// <summary>
/// There are cases where the extension creates multiple COM instances.
/// </summary>
/// <param name="activationPropSet">Activation property set object</param>
/// <returns>List of ClassId strings associated with the activation property</returns>
private static List<string> GetCreateInstanceList(IPropertySet activationPropSet)
{
var propSetList = new List<string>();
var singlePropertySet = GetSubPropertySet(activationPropSet, CreateInstanceProperty);
if (singlePropertySet != null)
{
var classId = GetProperty(singlePropertySet, ClassIdProperty);
// If the instance has a classId as a single string, then it's only supporting a single instance.
if (classId != null)
{
propSetList.Add(classId);
}
}
else
{
var propertySetArray = GetSubPropertySetArray(activationPropSet, CreateInstanceProperty);
if (propertySetArray != null)
{
foreach (var prop in propertySetArray)
{
if (prop is not IPropertySet propertySet)
{
continue;
}
var classId = GetProperty(propertySet, ClassIdProperty);
if (classId != null)
{
propSetList.Add(classId);
}
}
}
}
return propSetList;
}
private static string? GetProperty(IPropertySet propSet, string name) => propSet[name] as string;
public void EnableExtension(string extensionUniqueId)
{
var extension = _installedExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal));
_enabledExtensions.Add(extension.First());
}
public void DisableExtension(string extensionUniqueId)
{
var extension = _enabledExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal));
_enabledExtensions.Remove(extension.First());
}
/*
///// <inheritdoc cref="IExtensionService.DisableExtensionIfWindowsFeatureNotAvailable(IExtensionWrapper)"/>
//public async Task<bool> DisableExtensionIfWindowsFeatureNotAvailable(IExtensionWrapper extension)
//{
// // Only attempt to disable feature if its available.
// if (IsWindowsOptionalFeatureAvailableForExtension(extension.ExtensionClassId))
// {
// return false;
// }
// _log.Warning($"Disabling extension: '{extension.ExtensionDisplayName}' because its feature is absent or unknown");
// // Remove extension from list of enabled extensions to prevent Dev Home from re-querying for this extension
// // for the rest of its process lifetime.
// DisableExtension(extension.ExtensionUniqueId);
// // Update the local settings so the next time the user launches Dev Home the extension will be disabled.
// await _localSettingsService.SaveSettingAsync(extension.ExtensionUniqueId + "-ExtensionDisabled", true);
// return true;
//} */
}

View File

@@ -0,0 +1,176 @@
// 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.Runtime.InteropServices;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.Extensions;
using Windows.ApplicationModel;
using Windows.ApplicationModel.AppExtensions;
using Windows.Win32;
using Windows.Win32.System.Com;
using WinRT;
namespace Microsoft.CmdPal.UI.ViewModels.Models;
public class ExtensionWrapper : IExtensionWrapper
{
private const int HResultRpcServerNotRunning = -2147023174;
private readonly Lock _lock = new();
private readonly List<ProviderType> _providerTypes = [];
private readonly Dictionary<Type, ProviderType> _providerTypeMap = new()
{
[typeof(ICommandProvider)] = ProviderType.Commands,
};
private IExtension? _extensionObject;
public ExtensionWrapper(AppExtension appExtension, string classId)
{
PackageDisplayName = appExtension.Package.DisplayName;
ExtensionDisplayName = appExtension.DisplayName;
PackageFullName = appExtension.Package.Id.FullName;
PackageFamilyName = appExtension.Package.Id.FamilyName;
ExtensionClassId = classId ?? throw new ArgumentNullException(nameof(classId));
Publisher = appExtension.Package.PublisherDisplayName;
InstalledDate = appExtension.Package.InstalledDate;
Version = appExtension.Package.Id.Version;
ExtensionUniqueId = appExtension.AppInfo.AppUserModelId + "!" + appExtension.Id;
}
public string PackageDisplayName { get; }
public string ExtensionDisplayName { get; }
public string PackageFullName { get; }
public string PackageFamilyName { get; }
public string ExtensionClassId { get; }
public string Publisher { get; }
public DateTimeOffset InstalledDate { get; }
public PackageVersion Version { get; }
/// <summary>
/// Gets the unique id for this Dev Home extension. The unique id is a concatenation of:
/// <list type="number">
/// <item>The AppUserModelId (AUMID) of the extension's application. The AUMID is the concatenation of the package
/// family name and the application id and uniquely identifies the application containing the extension within
/// the package.</item>
/// <item>The Extension Id. This is the unique identifier of the extension within the application.</item>
/// </list>
/// </summary>
public string ExtensionUniqueId { get; }
public bool IsRunning()
{
if (_extensionObject is null)
{
return false;
}
try
{
_extensionObject.As<IInspectable>().GetRuntimeClassName();
}
catch (COMException e)
{
if (e.ErrorCode == HResultRpcServerNotRunning)
{
return false;
}
throw;
}
return true;
}
public async Task StartExtensionAsync()
{
await Task.Run(() =>
{
lock (_lock)
{
if (!IsRunning())
{
var extensionPtr = nint.Zero;
try
{
var hr = PInvoke.CoCreateInstance(Guid.Parse(ExtensionClassId), null, CLSCTX.CLSCTX_LOCAL_SERVER, typeof(IExtension).GUID, out var extensionObj);
extensionPtr = Marshal.GetIUnknownForObject(extensionObj);
if (hr < 0)
{
Marshal.ThrowExceptionForHR(hr);
}
_extensionObject = MarshalInterface<IExtension>.FromAbi(extensionPtr);
}
finally
{
if (extensionPtr != nint.Zero)
{
Marshal.Release(extensionPtr);
}
}
}
}
});
}
public void SignalDispose()
{
lock (_lock)
{
if (IsRunning())
{
_extensionObject?.Dispose();
}
_extensionObject = null;
}
}
public IExtension? GetExtensionObject()
{
lock (_lock)
{
return IsRunning() ? _extensionObject : null;
}
}
public async Task<T?> GetProviderAsync<T>()
where T : class
{
await StartExtensionAsync();
return GetExtensionObject()?.GetProvider(_providerTypeMap[typeof(T)]) as T;
}
public async Task<IEnumerable<T>> GetListOfProvidersAsync<T>()
where T : class
{
await StartExtensionAsync();
var supportedProviders = GetExtensionObject()?.GetProvider(_providerTypeMap[typeof(T)]);
if (supportedProviders is IEnumerable<T> multipleProvidersSupported)
{
return multipleProvidersSupported;
}
else if (supportedProviders is T singleProviderSupported)
{
return [singleProviderSupported];
}
return Enumerable.Empty<T>();
}
public void AddProviderType(ProviderType providerType) => _providerTypes.Add(providerType);
public bool HasProviderType(ProviderType providerType) => _providerTypes.Contains(providerType);
}

View File

@@ -0,0 +1,19 @@
GetPhysicallyInstalledSystemMemory
GlobalMemoryStatusEx
GetSystemInfo
CoCreateInstance
SetForegroundWindow
IsIconic
RegisterHotKey
SetWindowLongPtr
CallWindowProc
ShowWindow
SetForegroundWindow
SetFocus
SetActiveWindow
MonitorFromWindow
GetMonitorInfo
SHCreateStreamOnFileEx
CoAllowSetForegroundWindow
SHCreateStreamOnFileEx
SHLoadIndirectString

View File

@@ -2,15 +2,11 @@
// 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 CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Extensions;
using Microsoft.CmdPal.Extensions.Helpers;
using Microsoft.CmdPal.UI.Pages;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.CmdPal.UI.ViewModels;
@@ -20,39 +16,28 @@ public partial class ShellViewModel(IServiceProvider _serviceProvider) : Observa
[ObservableProperty]
public partial bool IsLoaded { get; set; } = false;
public ObservableCollection<CommandProviderWrapper> ActionsProvider { get; set; } = [];
public ObservableCollection<ExtensionObject<IListItem>> TopLevelCommands { get; set; } = [];
private IEnumerable<ICommandProvider>? _builtInCommands;
[RelayCommand]
public async Task<bool> LoadAsync()
{
_builtInCommands = _serviceProvider.GetServices<ICommandProvider>();
// Load Built In Commands First
foreach (var provider in _builtInCommands)
{
CommandProviderWrapper wrapper = new(provider);
ActionsProvider.Add(wrapper);
await LoadTopLevelCommandsFromProvider(wrapper);
}
var tlcManager = _serviceProvider.GetService<TopLevelCommandManager>();
await tlcManager!.LoadBuiltinsAsync();
IsLoaded = true;
// TODO: would want to hydrate this from our services provider in the View layer, need to think about construction here...
WeakReferenceMessenger.Default.Send<NavigateToListMessage>(new(new(new MainListPage(this))));
// Built-ins have loaded. We can display our page at this point.
var page = new MainListPage(_serviceProvider);
WeakReferenceMessenger.Default.Send<NavigateToListMessage>(new(new(page!)));
// After loading built-ins, and starting navigation, kick off a thread to load extensions.
tlcManager.LoadExtensionsCommand.Execute(null);
_ = Task.Run(async () =>
{
await tlcManager.LoadExtensionsCommand.ExecutionTask!;
if (tlcManager.LoadExtensionsCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
{
// TODO: Handle failure case
}
});
return true;
}
private async Task LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
{
await commandProvider.LoadTopLevelCommands();
foreach (var i in commandProvider.TopLevelItems)
{
TopLevelCommands.Add(new(new ListItem(i)));
}
}
}

View File

@@ -0,0 +1,71 @@
// 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 System.Diagnostics;
using CommunityToolkit.Mvvm.Input;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.Extensions;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class TopLevelCommandManager(IServiceProvider _serviceProvider)
{
private IEnumerable<ICommandProvider>? _builtInCommands;
public ObservableCollection<TopLevelCommandWrapper> TopLevelCommands { get; set; } = [];
public async Task<bool> LoadBuiltinsAsync()
{
// Load built-In commands first. These are all in-proc, and
// owned by our ServiceProvider.
_builtInCommands = _serviceProvider.GetServices<ICommandProvider>();
foreach (var provider in _builtInCommands)
{
CommandProviderWrapper wrapper = new(provider);
await LoadTopLevelCommandsFromProvider(wrapper);
}
return true;
}
private async Task LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
{
await commandProvider.LoadTopLevelCommands();
foreach (var i in commandProvider.TopLevelItems)
{
TopLevelCommands.Add(new(new(i)));
}
}
// Load commands from our extensions.
// Currently, this
// * queries the package catalog,
// * starts all the extensions,
// * then fetches the top-level commands from them.
// TODO In the future, we'll probably abstract some of this away, to have
// separate extension tracking vs stub loading.
[RelayCommand]
public async Task<bool> LoadExtensionsAsync()
{
var extensionService = _serviceProvider.GetService<IExtensionService>()!;
var extensions = await extensionService.GetInstalledExtensionsAsync();
foreach (var extension in extensions)
{
try
{
await extension.StartExtensionAsync();
CommandProviderWrapper wrapper = new(extension);
await LoadTopLevelCommandsFromProvider(wrapper);
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
}
return true;
}
}

View File

@@ -0,0 +1,47 @@
// 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 Microsoft.CmdPal.Extensions;
using Microsoft.CmdPal.Extensions.Helpers;
using Microsoft.CmdPal.UI.ViewModels.Models;
namespace Microsoft.CmdPal.UI.ViewModels;
/// <summary>
/// Abstraction of a top-level command. Currently owns just a live ICommandItem
/// from an extension (or in-proc command provider), but in the future will
/// also support stub top-level items.
/// </summary>
public partial class TopLevelCommandWrapper : ListItem
{
public ExtensionObject<ICommandItem> Model { get; }
public TopLevelCommandWrapper(ExtensionObject<ICommandItem> commandItem)
: base(commandItem.Unsafe?.Command ?? new NoOpCommand())
{
// TODO: In reality, we should do an async fetch when we're created
// from an extension object. Probably have an
// `static async Task<TopLevelCommandWrapper> FromExtension(ExtensionObject<ICommandItem>)`
// or a
// `async Task PromoteStub(ExtensionObject<ICommandItem>)`
Model = commandItem;
try
{
var model = Model.Unsafe;
if (model == null)
{
return;
}
Title = model.Title;
Subtitle = model.Subtitle;
Icon = new(model.Icon.Icon);
MoreCommands = model.MoreCommands;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex);
}
}
}

View File

@@ -2,6 +2,7 @@
// 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.Common.Services;
using Microsoft.CmdPal.Ext.Bookmarks;
using Microsoft.CmdPal.Ext.Calc;
using Microsoft.CmdPal.Ext.Registry;
@@ -12,6 +13,7 @@ using Microsoft.CmdPal.Ext.WindowsTerminal;
using Microsoft.CmdPal.Extensions;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
@@ -29,9 +31,7 @@ public partial class App : Application
/// </summary>
public static new App Current => (App)Application.Current;
private Window? _window;
public Window? AppWindow => _window;
public Window? AppWindow { get; private set; }
/// <summary>
/// Gets the <see cref="IServiceProvider"/> instance to resolve application services.
@@ -56,8 +56,8 @@ public partial class App : Application
/// <param name="args">Details about the launch request and process.</param>
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
_window = new MainWindow();
_window.Activate();
AppWindow = new MainWindow();
AppWindow.Activate();
}
/// <summary>
@@ -68,6 +68,9 @@ public partial class App : Application
// TODO: It's in the Labs feed, but we can use Sergio's AOT-friendly source generator for this: https://github.com/CommunityToolkit/Labs-Windows/discussions/463
ServiceCollection services = new();
// Root services
services.AddSingleton(TaskScheduler.FromCurrentSynchronizationContext());
// Built-in Commands
services.AddSingleton<ICommandProvider, BookmarksCommandProvider>();
services.AddSingleton<ICommandProvider, CalculatorCommandProvider>();
@@ -79,6 +82,10 @@ public partial class App : Application
services.AddSingleton<ICommandProvider, RegistryCommandsProvider>();
services.AddSingleton<ICommandProvider, WindowsSettingsCommandsProvider>();
// Models
services.AddSingleton<TopLevelCommandManager>();
services.AddSingleton<IExtensionService, ExtensionService>();
// ViewModels
services.AddSingleton<ShellViewModel>();

View File

@@ -2,11 +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.WinUI;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.UI.Xaml.Navigation;
namespace Microsoft.CmdPal.UI;
@@ -28,6 +26,8 @@ public sealed partial class LoadingPage : Page
if (e.Parameter is ShellViewModel shellVM
&& shellVM.LoadCommand != null)
{
// This will load the built-in commands, then navigate to the main page.
// Once the mainpage loads, we'll start loading extensions.
shellVM.LoadCommand.Execute(null);
_ = Task.Run(async () =>

View File

@@ -53,7 +53,17 @@ public sealed partial class ShellPage :
public void Receive(NavigateToListMessage message)
{
// The first time we navigate to a list (from loading -> main list),
// clear out the back stack so that we can't go back again.
var fromLoading = RootFrame.CanGoBack;
RootFrame.Navigate(typeof(ListPage), message.ViewModel, _slideRightTransition);
if (!fromLoading)
{
RootFrame.BackStack.Clear();
}
SearchBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
}