Setup DependencyInjection for the initialization of the Built-In Providers and the main view model

Does some quick plumbing to show things in the existing UI via MVVM.
TODO: Need to sort out how to better separate the models and viewmodels and how things behave within our special top-level 'main' page vs. extension pages, etc...
This commit is contained in:
Michael Hawker
2024-09-18 16:46:02 -07:00
parent 9ab84cb3a5
commit 4cba257bdd
16 changed files with 336 additions and 34 deletions

View File

@@ -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 Microsoft.CmdPal.Extensions;
using Microsoft.CmdPal.Extensions.Helpers;
using Microsoft.CmdPal.UI.ViewModels;
namespace Microsoft.CmdPal.UI.Pages;
/// <summary>
/// This class encapsulates the data we load from built-in providers and extensions to use within the same extension-UI system for a <see cref="ListPage"/>.
/// TODO: Need to think about how we structure/interop for the page -> section -> item between the main setup, the extensions, and our viewmodels.
/// </summary>
public partial class MainListPage : DynamicListPage
{
private readonly ISection[] _sections;
// 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)
{
_sections = [new MainListSection()
{
Items = shellViewModel.TopLevelCommands.Select(w => w.Unsafe).Where(li => li != null).ToArray(),
}
];
}
public override ISection[] GetItems() => _sections;
}
//// TODO: Temporary until we sort out proper PageViewModel and SectionViewModel containers/setup
#pragma warning disable SA1402 // File may only contain a single type
public partial class MainListSection : ISection
#pragma warning restore SA1402 // File may only contain a single type
{
public required IListItem[] Items { get; set; }
public string Title => "Commands"; // TODO: Localization
}

View File

@@ -0,0 +1,16 @@
// 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;
// Want to know what a record is? here is a TLDR
// https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record
/// <summary>
/// Message to instruct UI to navigate to a list page.
/// </summary>
/// <param name="ViewModel">The <see cref="ListViewModel"/> for the list page to navigate to.</param>
public record NavigateToListMessage(ListViewModel ViewModel)
{
}

View File

@@ -9,4 +9,9 @@
<PackageReference Include="CommunityToolkit.Mvvm" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj" />
<ProjectReference Include="..\extensionsdk\Microsoft.CmdPal.Extensions.Helpers\Microsoft.CmdPal.Extensions.Helpers.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,25 @@
// 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.Models;
public class ExtensionObject<T> // where T : IInspectable
{
private readonly T _value;
public ExtensionObject(T value)
{
_value = value;
}
// public T? Safe {
// get {
// try {
// if (_value!.Equals(_value)) return _value;
// } catch (COMException){ /* log something */ }
// return default;
// }
// }
public T Unsafe => _value;
}

View File

@@ -0,0 +1,78 @@
// 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;
namespace Microsoft.CmdPal.UI.ViewModels;
public sealed class CommandProviderWrapper
{
public bool IsExtension => extensionWrapper != null;
private readonly bool isValid;
private readonly ICommandProvider _commandProvider;
private readonly IExtensionWrapper? extensionWrapper;
private IListItem[] _topLevelItems = [];
public IListItem[] TopLevelItems => _topLevelItems;
public CommandProviderWrapper(ICommandProvider provider)
{
_commandProvider = provider;
isValid = true;
}
public CommandProviderWrapper(IExtensionWrapper extension)
{
extensionWrapper = extension;
var extensionImpl = extension.GetExtensionObject();
if (extensionImpl?.GetProvider(ProviderType.Commands) is not ICommandProvider provider)
{
throw new ArgumentException("extension didn't actually implement ICommandProvider");
}
_commandProvider = provider;
isValid = true;
}
public async Task LoadTopLevelCommands()
{
if (!isValid)
{
return;
}
var t = new Task<IListItem[]>(() => _commandProvider.TopLevelCommands());
t.Start();
var commands = await t.ConfigureAwait(false);
// On a BG thread here
if (commands != null)
{
_topLevelItems = commands;
}
}
/* This is a View/ExtensionHost piece
* public void AllowSetForeground(bool allow)
{
if (!IsExtension)
{
return;
}
var iextn = extensionWrapper?.GetExtensionObject();
unsafe
{
PInvoke.CoAllowSetForegroundWindow(iextn);
}
}*/
public override bool Equals(object? obj) => obj is CommandProviderWrapper wrapper && isValid == wrapper.isValid;
public override int GetHashCode() => _commandProvider.GetHashCode();
}

View File

@@ -3,15 +3,33 @@
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.CmdPal.Extensions;
using Microsoft.CmdPal.Models;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ListItemViewModel : ObservableObject
{
// Observable from MVVM Toolkit will auto create public properties that use INotifyPropertyChange
[ObservableProperty]
private string _header = string.Empty;
private readonly ExtensionObject<IListItem> _listItemModel;
[ObservableProperty]
private string _subHeader = string.Empty;
public string Title => _listItemModel.Unsafe.Title;
public string Subtitle => _listItemModel.Unsafe.Subtitle;
/// <summary>
/// Gets the path for the icon to load in the View layer. TODO: Converter/Cache
/// </summary>
public string IconUri => _listItemModel.Unsafe.Icon.Icon;
public ITag[] Tags => _listItemModel.Unsafe.Tags;
public bool HasTags => Tags.Length > 0;
public ListItemViewModel(IListItem model)
{
_listItemModel = new(model);
_listItemModel.Unsafe.PropChanged += Model_PropChanged;
}
private void Model_PropChanged(object sender, PropChangedEventArgs args) => OnPropertyChanged(args.PropertyName);
}

View File

@@ -7,6 +7,7 @@ using System.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Extensions;
using Microsoft.CmdPal.UI.ViewModels.Messages;
namespace Microsoft.CmdPal.UI.ViewModels;
@@ -17,12 +18,22 @@ public partial class ListViewModel : ObservableObject
[ObservableProperty]
private ObservableCollection<ListItemViewModel> _items = [];
public ListViewModel(IListPage model)
{
foreach (var section in model.GetItems())
{
// TODO: Ignoring sections for now
foreach (var item in section.Items)
{
_items.Add(new(item));
}
}
}
// InvokeItemCommand is what this will be in Xaml due to source generator
[RelayCommand]
private void InvokeItem(ListItemViewModel item)
{
WeakReferenceMessenger.Default.Send<NavigateToDetailsMessage>(new(item));
Debug.WriteLine("Hello!" + item.Header);
}
}

View File

@@ -2,18 +2,58 @@
// 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.Models;
using Microsoft.CmdPal.UI.Pages;
using Microsoft.CmdPal.UI.ViewModels.Messages;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ShellViewModel : ObservableObject
{
[ObservableProperty]
private bool _isLoaded = false;
public ObservableCollection<CommandProviderWrapper> ActionsProvider { get; set; } = [];
public ObservableCollection<ExtensionObject<IListItem>> TopLevelCommands { get; set; } = [];
private readonly IEnumerable<ICommandProvider> _builtInCommands;
public ShellViewModel(IEnumerable<ICommandProvider> builtInCommands)
{
_builtInCommands = builtInCommands;
}
[RelayCommand]
public async Task<bool> LoadAsync()
{
await Task.Delay(2000);
// Load Built In Commands First
foreach (var provider in _builtInCommands)
{
CommandProviderWrapper wrapper = new(provider);
ActionsProvider.Add(wrapper);
await LoadTopLevelCommandsFromProvider(wrapper);
}
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))));
return true;
}
private async Task LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
{
await commandProvider.LoadTopLevelCommands();
foreach (var i in commandProvider.TopLevelItems)
{
TopLevelCommands.Add(new(i));
}
}
}

View File

@@ -2,6 +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 Microsoft.CmdPal.Ext.Bookmarks;
using Microsoft.CmdPal.Ext.Calc;
using Microsoft.CmdPal.Ext.Settings;
using Microsoft.CmdPal.Extensions;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
// To learn more about WinUI, the WinUI project structure,
@@ -13,6 +19,16 @@ namespace Microsoft.CmdPal.UI;
/// </summary>
public partial class App : Application
{
/// <summary>
/// Gets the current <see cref="App"/> instance in use.
/// </summary>
public static new App Current => (App)Application.Current;
/// <summary>
/// Gets the <see cref="IServiceProvider"/> instance to resolve application services.
/// </summary>
public IServiceProvider Services { get; }
/// <summary>
/// Initializes a new instance of the <see cref="App"/> class.
/// Initializes the singleton application object. This is the first line of authored code
@@ -20,6 +36,8 @@ public partial class App : Application
/// </summary>
public App()
{
Services = ConfigureServices();
this.InitializeComponent();
}
@@ -34,4 +52,22 @@ public partial class App : Application
}
private Window? _window;
/// <summary>
/// Configures the services for the application
/// </summary>
private static ServiceProvider ConfigureServices()
{
ServiceCollection services = new();
// Built-in Commands
services.AddSingleton<ICommandProvider, BookmarksCommandProvider>();
services.AddSingleton<ICommandProvider, CalculatorCommandProvider>();
services.AddSingleton<ICommandProvider, SettingsCommandProvider>();
// ViewModels
services.AddSingleton<ShellViewModel>((services) => new(services.GetServices<ICommandProvider>()));
return services.BuildServiceProvider();
}
}

View File

@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="Microsoft.CmdPal.UI.ListDetailPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Microsoft.CmdPal.UI"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Microsoft.CmdPal.UI"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Background="Transparent"
mc:Ignorable="d">
<Grid>
<!-- not using Interactivity:Interaction.Behaviors due to wanting to do AoT -->
<TextBlock Text="{x:Bind ViewModel.Header}" />
<!-- not using Interactivity:Interaction.Behaviors due to wanting to do AoT -->
<TextBlock Text="{x:Bind ViewModel.Title}" />
<Button Click="Button_Click">Go Back</Button>
</Grid>
</Page>

View File

@@ -16,7 +16,7 @@ namespace Microsoft.CmdPal.UI;
/// </summary>
public sealed partial class ListDetailPage : Page
{
public ListItemViewModel ViewModel { get; set; } = new ListItemViewModel();
public ListItemViewModel? ViewModel { get; set; }
public ListDetailPage()
{

View File

@@ -15,11 +15,11 @@
<Page.Resources>
<!-- https://learn.microsoft.com/en-us/windows/apps/design/controls/itemsview#specify-the-look-of-the-items -->
<DataTemplate x:Key="ListItemViewModelTemplate" x:DataType="viewmodels:ListItemViewModel">
<ItemContainer AutomationProperties.Name="{x:Bind Header}">
<ItemContainer AutomationProperties.Name="{x:Bind Title}">
<StackPanel Background="{ThemeResource SystemControlBackgroundBaseMediumBrush}" Orientation="Horizontal">
<TextBlock Text="{x:Bind Header}" />
<TextBlock Foreground="{ThemeResource SystemAccentColor}" Text="{x:Bind SubHeader}" />
<TextBlock Text="{x:Bind Title}" />
<TextBlock Foreground="{ThemeResource SystemAccentColor}" Text="{x:Bind Subtitle}" />
</StackPanel>
</ItemContainer>
</DataTemplate>

View File

@@ -4,6 +4,7 @@
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
namespace Microsoft.CmdPal.UI;
@@ -12,21 +13,28 @@ namespace Microsoft.CmdPal.UI;
/// </summary>
public sealed partial class ListPage : Page
{
public ListViewModel ViewModel { get; set; } = new();
public ListViewModel? ViewModel { get; set; }
public ListPage()
{
this.InitializeComponent();
ViewModel.Items.Add(new ListItemViewModel { Header = "Hello", SubHeader = "World" });
ViewModel.Items.Add(new ListItemViewModel { Header = "Clint", SubHeader = "Rutkas" });
ViewModel.Items.Add(new ListItemViewModel { Header = "Michael", SubHeader = "Hawker" });
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (e.Parameter is ListViewModel lvm)
{
ViewModel = lvm;
}
base.OnNavigatedTo(e);
}
private void ItemsView_ItemInvoked(ItemsView sender, ItemsViewItemInvokedEventArgs args)
{
if (args.InvokedItem is ListItemViewModel item)
{
ViewModel.InvokeItemCommand.Execute(item);
ViewModel?.InvokeItemCommand.Execute(item);
}
}
}

View File

@@ -34,15 +34,10 @@ public sealed partial class LoadingPage : Page
{
await shellVM.LoadCommand.ExecutionTask!;
if (shellVM.LoadCommand.ExecutionTask.Status == TaskStatus.RanToCompletion)
if (shellVM.LoadCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
{
await _queue.EnqueueAsync(() =>
{
Frame.Navigate(typeof(ListPage), new ListViewModel(), new DrillInNavigationTransitionInfo());
});
// TODO: Handle failure case
}
// TODO: Handle failure case
});
}

View File

@@ -35,10 +35,25 @@
<ItemGroup>
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" />
<Manifest Include="$(ApplicationManifest)" />
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<!-- The following harcoded package versions are to work around security vulnerbilities in 4.3.0 -->
<PackageReference Include="System.Net.Http" />
<PackageReference Include="System.Private.Uri" />
<PackageReference Include="System.Text.RegularExpressions" />
<PackageReference Include="System.IO.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="System.Text.Json" />
</ItemGroup>
<!--
@@ -51,7 +66,18 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj" />
<ProjectReference Include="..\extensionsdk\Microsoft.CmdPal.Extensions.Helpers\Microsoft.CmdPal.Extensions.Helpers.csproj" />
<ProjectReference Include="..\Microsoft.CmdPal.UI.ViewModels\Microsoft.CmdPal.UI.ViewModels.csproj" />
<ProjectReference Include="..\Exts\Microsoft.CmdPal.Ext.Apps\Microsoft.CmdPal.Ext.Apps.csproj" />
<ProjectReference Include="..\Exts\Microsoft.CmdPal.Ext.Bookmark\Microsoft.CmdPal.Ext.Bookmarks.csproj" />
<ProjectReference Include="..\Exts\Microsoft.CmdPal.Ext.Calc\Microsoft.CmdPal.Ext.Calc.csproj" />
<ProjectReference Include="..\Exts\Microsoft.CmdPal.Ext.CmdPalSettings\Microsoft.CmdPal.Ext.Settings.csproj" />
<ProjectReference Include="..\Microsoft.Terminal.UI\Microsoft.Terminal.UI.vcxproj">
<ReferenceOutputAssembly>True</ReferenceOutputAssembly>
<Private>True</Private>
<CopyLocalSatelliteAssemblies>True</CopyLocalSatelliteAssemblies>
</ProjectReference>
</ItemGroup>
<ItemGroup>

View File

@@ -5,7 +5,9 @@
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Animation;
namespace Microsoft.CmdPal.UI;
@@ -14,10 +16,13 @@ namespace Microsoft.CmdPal.UI;
/// </summary>
public sealed partial class ShellPage :
Page,
IRecipient<NavigateBackMessage>,
IRecipient<NavigateToDetailsMessage>,
IRecipient<NavigateBackMessage>
IRecipient<NavigateToListMessage>
{
public ShellViewModel ViewModel { get; private set; } = new ShellViewModel();
private readonly DrillInNavigationTransitionInfo _drillInNavigationTransitionInfo = new();
public ShellViewModel ViewModel { get; private set; } = App.Current.Services.GetService<ShellViewModel>()!;
public ShellPage()
{
@@ -29,10 +34,7 @@ public sealed partial class ShellPage :
RootFrame.Navigate(typeof(LoadingPage), ViewModel);
}
public void Receive(NavigateToDetailsMessage message)
{
RootFrame.Navigate(typeof(ListDetailPage), message.ListItem);
}
public void Receive(NavigateToDetailsMessage message) => RootFrame.Navigate(typeof(ListDetailPage), message.ListItem, _drillInNavigationTransitionInfo);
public void Receive(NavigateBackMessage message)
{
@@ -41,4 +43,6 @@ public sealed partial class ShellPage :
RootFrame.GoBack();
}
}
public void Receive(NavigateToListMessage message) => RootFrame.Navigate(typeof(ListPage), message.ViewModel, _drillInNavigationTransitionInfo);
}