Plumb through to CommandItemVM info about the extension...

so we can determine if we want to pin things or not. We need to know if
the extension supports the version of the API with that method on it.

It feels super weird to just have CommandItemViewModel do this though -
I think the more correct solution would be to have a ContextMenuFactory
that generates the context menu for a command item, and then have the UI
layer pass that in.

But also Jolley's refactor will probably deal away with all this so
This commit is contained in:
Mike Griese
2026-02-06 05:51:43 -06:00
parent 01164b33ec
commit c4fc884d7f
9 changed files with 146 additions and 55 deletions

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// 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.
@@ -25,6 +25,8 @@ public abstract partial class AppExtensionHost : IExtensionHost
public ObservableCollection<StatusMessageViewModel> StatusMessages { get; } = [];
public virtual bool SupportsDockBands { get; set; }
public static void SetHostHwnd(ulong hostHwnd) => _hostingHwnd = hostHwnd;
public void DebugLog(string message)

View File

@@ -212,27 +212,28 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
return;
}
var more = model.MoreCommands;
if (more is not null)
{
MoreCommands = more
.Select<IContextItem, IContextItemViewModel>(item =>
{
return item is ICommandContextItem contextItem ? new CommandContextItemViewModel(contextItem, PageContext) : new SeparatorViewModel();
})
.ToList();
}
BuildAndInitMoreCommands();
// Here, we're already theoretically in the async context, so we can
// use Initialize straight up
MoreCommands
.OfType<CommandContextItemViewModel>()
.ToList()
.ForEach(contextItem =>
{
contextItem.SlowInitializeProperties();
});
// var more = model.MoreCommands;
// if (more is not null)
// {
// MoreCommands = more
// .Select<IContextItem, IContextItemViewModel>(item =>
// {
// return item is ICommandContextItem contextItem ? new CommandContextItemViewModel(contextItem, PageContext) : new SeparatorViewModel();
// })
// .ToList();
// }
// // Here, we're already theoretically in the async context, so we can
// // use Initialize straight up
// MoreCommands
// .OfType<CommandContextItemViewModel>()
// .ToList()
// .ForEach(contextItem =>
// {
// contextItem.SlowInitializeProperties();
// });
if (!string.IsNullOrEmpty(model.Command?.Name))
{
_defaultCommandContextItemViewModel = new CommandContextItemViewModel(new CommandContextItem(model.Command!), PageContext)
@@ -382,36 +383,36 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
break;
case nameof(model.MoreCommands):
var more = model.MoreCommands;
if (more is not null)
{
var newContextMenu = more
.Select<IContextItem, IContextItemViewModel>(item =>
{
return item is ICommandContextItem contextItem ? new CommandContextItemViewModel(contextItem, PageContext) : new SeparatorViewModel();
})
.ToList();
lock (MoreCommands)
{
ListHelpers.InPlaceUpdateList(MoreCommands, newContextMenu);
}
newContextMenu
.OfType<CommandContextItemViewModel>()
.ToList()
.ForEach(contextItem =>
{
contextItem.InitializeProperties();
});
}
else
{
lock (MoreCommands)
{
MoreCommands.Clear();
}
}
// var more = model.MoreCommands;
// if (more is not null)
// {
// var newContextMenu = more
// .Select<IContextItem, IContextItemViewModel>(item =>
// {
// return item is ICommandContextItem contextItem ? new CommandContextItemViewModel(contextItem, PageContext) : new SeparatorViewModel();
// })
// .ToList();
// lock (MoreCommands)
// {
// ListHelpers.InPlaceUpdateList(MoreCommands, newContextMenu);
// }
// newContextMenu
// .OfType<CommandContextItemViewModel>()
// .ToList()
// .ForEach(contextItem =>
// {
// contextItem.InitializeProperties();
// });
// }
// else
// {
// lock (MoreCommands)
// {
// MoreCommands.Clear();
// }
// }
BuildAndInitMoreCommands();
UpdateProperty(nameof(SecondaryCommand));
UpdateProperty(nameof(SecondaryCommandName));
UpdateProperty(nameof(HasMoreCommands));
@@ -513,6 +514,68 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
base.SafeCleanup();
Initialized |= InitializedState.CleanedUp;
}
/// <remarks>
/// * Does call SlowInitializeProperties on the created items.
/// * does NOT call UpdateProperty ; caller must do that.
/// </remarks>
private void BuildAndInitMoreCommands()
{
var model = _commandItemModel.Unsafe;
if (model is null)
{
return;
}
var more = model.MoreCommands;
List<IContextItemViewModel> results = [];
if (more is not null)
{
foreach (var item in more)
{
if (item is ICommandContextItem contextItem)
{
var contextItemViewModel = new CommandContextItemViewModel(contextItem, PageContext);
contextItemViewModel.SlowInitializeProperties();
results.Add(contextItemViewModel);
}
else
{
results.Add(new SeparatorViewModel());
}
}
// MoreCommands = more
// .Select<IContextItem, IContextItemViewModel>(item =>
// {
// return item is ICommandContextItem contextItem ? new CommandContextItemViewModel(contextItem, PageContext) : new SeparatorViewModel();
// })
// .ToList();
}
if (PageContext.TryGetTarget(out var pageContext))
{
if (pageContext.ExtensionSupportsPinning)
{
// test: just add a bunch of separators
results.Add(new SeparatorViewModel());
results.Add(new SeparatorViewModel());
results.Add(new SeparatorViewModel());
}
}
// var oldMoreCommands = MoreCommands;
// MoreCommands = results;
List<IContextItemViewModel>? freedItems;
lock (MoreCommands)
{
ListHelpers.InPlaceUpdateList(MoreCommands, results, out freedItems);
}
freedItems.OfType<CommandContextItemViewModel>()
.ToList()
.ForEach(c => c.SafeCleanup());
}
}
[Flags]

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// 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.
@@ -8,6 +8,8 @@ public class GlobalLogPageContext : IPageContext
{
public TaskScheduler Scheduler { get; private init; }
bool IPageContext.ExtensionSupportsPinning => false;
public void ShowException(Exception ex, string? extensionHint)
{ /*do nothing*/
}

View File

@@ -79,6 +79,8 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
public IconInfoViewModel Icon { get; protected set; }
bool IPageContext.ExtensionSupportsPinning => ExtensionHost.SupportsDockBands;
public PageViewModel(IPage? model, TaskScheduler scheduler, AppExtensionHost extensionHost)
: base((IPageContext?)null)
{
@@ -267,6 +269,8 @@ public interface IPageContext
void ShowException(Exception ex, string? extensionHint = null);
TaskScheduler Scheduler { get; }
bool ExtensionSupportsPinning { get; }
}
public interface IPageViewModelFactoryService

View File

@@ -165,6 +165,7 @@ public sealed class CommandProviderWrapper
if (model is ICommandProvider3 supportsDockBands)
{
ExtensionHost.SupportsDockBands = true;
var bands = supportsDockBands.GetDockBands();
if (bands is not null)
{
@@ -213,7 +214,16 @@ public sealed class CommandProviderWrapper
Func<ICommandItem?, TopLevelType, TopLevelViewModel> make = (ICommandItem? i, TopLevelType t) =>
{
CommandItemViewModel commandItemViewModel = new(new(i), pageContext);
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, t, ExtensionHost, ProviderId, settings, providerSettings, serviceProvider, i);
TopLevelViewModel topLevelViewModel = new(
item: commandItemViewModel,
topLevelType: t,
extensionHost: ExtensionHost,
commandProviderId: ProviderId,
settings: settings,
providerSettings: providerSettings,
serviceProvider: serviceProvider,
commandItem: i/*,
providerSupportsPinning: SupportsDockBands*/);
topLevelViewModel.InitializeProperties();
return topLevelViewModel;

View File

@@ -33,6 +33,8 @@ public sealed partial class DockViewModel : IDisposable,
public ObservableCollection<TopLevelViewModel> AllItems => _topLevelCommandManager.DockBands;
bool IPageContext.ExtensionSupportsPinning => false;
public DockViewModel(
TopLevelCommandManager tlcManager,
SettingsModel settings,

View File

@@ -60,6 +60,8 @@ public partial class TopLevelCommandManager : ObservableObject,
}
}
bool IPageContext.ExtensionSupportsPinning => false;
public async Task<bool> LoadBuiltinsAsync()
{
var s = new Stopwatch();

View File

@@ -28,6 +28,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
private readonly string _commandProviderId;
// private readonly bool _providerSupportsPinning;
private string IdFromModel => IsFallback && !string.IsNullOrWhiteSpace(_fallbackId) ? _fallbackId : _commandItemViewModel.Command.Id;
private string _fallbackId = string.Empty;
@@ -58,6 +59,8 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
public IconInfoViewModel IconViewModel => _commandItemViewModel.Icon;
// public bool ProviderSupportsPinning => _providerSupportsPinning;
////// ICommandItem
public string Title => _commandItemViewModel.Title;
@@ -204,12 +207,15 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
SettingsModel settings,
ProviderSettings providerSettings,
IServiceProvider serviceProvider,
ICommandItem? commandItem)
ICommandItem? commandItem)/*,
bool providerSupportsPinning*/
{
_serviceProvider = serviceProvider;
_settings = settings;
_providerSettings = providerSettings;
_commandProviderId = commandProviderId;
// _providerSupportsPinning = providerSupportsPinning;
_commandItemViewModel = item;
IsFallback = topLevelType == TopLevelType.Fallback;

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// 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.
@@ -167,7 +167,7 @@ public partial class AllAppsCommandProvider : CommandProvider
}
// ... Now, combine those two
List<ICommandItem> both = bestAppMatch is null ? nameMatches : [.. nameMatches, bestAppMatch];
var both = bestAppMatch is null ? nameMatches : [.. nameMatches, bestAppMatch];
if (both.Count == 1)
{
@@ -191,7 +191,7 @@ public partial class AllAppsCommandProvider : CommandProvider
public override ICommandItem? GetCommandItemById(string id)
{
if (id == _listItem.Command.Id)
if (id == _listItem.Command?.Id)
{
return _listItem;
}