Some intial clean-up for Current Page and test Filtering

Has some issues still... (in part needs to update toolkit package for debounce extension update...)
This commit is contained in:
Michael Hawker
2024-12-06 18:11:55 -08:00
parent 42a9131800
commit cc13026941
16 changed files with 147 additions and 44 deletions

View File

@@ -11,7 +11,6 @@ using Microsoft.CmdPal.UI.ViewModels.Messages;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ActionBarViewModel : ObservableObject,
IRecipient<UpdateActionBarPage>,
IRecipient<UpdateActionBarMessage>
{
public ListItemViewModel? SelectedItem
@@ -33,15 +32,11 @@ public partial class ActionBarViewModel : ObservableObject,
[ObservableProperty]
public partial bool ShouldShowContextMenu { get; set; } = false;
[ObservableProperty]
public partial PageViewModel? CurrentPage { get; private set; }
[ObservableProperty]
public partial ObservableCollection<CommandContextItemViewModel> ContextActions { get; set; } = [];
public ActionBarViewModel()
{
WeakReferenceMessenger.Default.Register<UpdateActionBarPage>(this);
WeakReferenceMessenger.Default.Register<UpdateActionBarMessage>(this);
}
@@ -75,6 +70,4 @@ public partial class ActionBarViewModel : ObservableObject,
// InvokeItemCommand is what this will be in Xaml due to source generator
[RelayCommand]
private void InvokeItem(CommandContextItemViewModel item) => WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.Command));
public void Receive(UpdateActionBarPage message) => CurrentPage = message.Page;
}

View File

@@ -18,7 +18,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel
// itself, in the sense that they get raised by PropChanged events from the
// extension. However, we don't want to actually make them
// [ObservableProperty]s, because PropChanged comes in off the UI thread,
// and ObservableProperty is not smart enough to raisee the PropertyChanged
// and ObservableProperty is not smart enough to raise the PropertyChanged
// on the UI thread.
public string Name { get; private set; } = string.Empty;
@@ -80,11 +80,11 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel
Title = model.Title;
Subtitle = model.Subtitle;
IconUri = model.Icon.Icon;
MoreCommands = model.MoreCommands
/*MoreCommands = model.MoreCommands
.Where(contextItem => contextItem is ICommandContextItem)
.Select(contextItem => (contextItem as ICommandContextItem)!)
.Select(contextItem => new CommandContextItemViewModel(contextItem, Scheduler))
.ToList();
.ToList();*/
// Here, we're already theoretically in the async context, so we can
// use Initialize straight up

View File

@@ -80,4 +80,9 @@ public partial class ListItemViewModel(IListItem model, TaskScheduler scheduler)
UpdateProperty(propertyName);
}
// TODO: Do we want filters to match descriptions and other properties? Tags, etc... Yes?
public bool MatchesFilter(string filter) => Title.Contains(filter) || Name.Contains(filter);
public override string ToString() => $"{Name} ListItemViewModel";
}

View File

@@ -2,7 +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 CommunityToolkit.Mvvm.Collections;
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
@@ -14,10 +14,14 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ListViewModel : PageViewModel
{
private readonly HashSet<ListItemViewModel> _itemCache = [];
// TODO: Do we want a base "ItemsPageViewModel" for anything that's going to have items?
// Observable from MVVM Toolkit will auto create public properties that use INotifyPropertyChange change
// https://learn.microsoft.com/dotnet/communitytoolkit/mvvm/observablegroupedcollections for grouping support
[ObservableProperty]
public partial ObservableGroupedCollection<string, ListItemViewModel> Items { get; set; } = [];
public partial ObservableCollection<ListItemViewModel> Items { get; set; } = [];
private readonly ExtensionObject<IListPage> _model;
@@ -27,13 +31,44 @@ public partial class ListViewModel : PageViewModel
_model = new(model);
}
protected override void OnFilterUpdated(string filter)
{
//// TODO: Just temp testing, need to think about where we want to filter, as ACVS in View could be done, but then grouping need CVS, maybe we do grouping in view
//// and manage filtering below, but we should be smarter about this and understand caching and other requirements...
// Remove all items out right if we clear the filter, otherwise, recheck the items already displayed.
if (string.IsNullOrWhiteSpace(filter))
{
Items.Clear();
}
else
{
// Remove any existing items which don't match the filter
for (var i = Items.Count - 1; i >= 0; i--)
{
if (!Items[i].MatchesFilter(filter))
{
Items.RemoveAt(i);
}
}
}
// Add any new items which do match the filter
foreach (var item in _itemCache)
{
if ((filter == string.Empty || item.MatchesFilter(filter))
&& !Items.Contains(item)) //// TODO: We should be smarter here somehow
{
Items.Add(item);
}
}
}
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
@@ -41,18 +76,20 @@ public partial class ListViewModel : PageViewModel
{
var newItems = _model.Unsafe!.GetItems();
Items.Clear();
foreach (var item in newItems)
{
// TODO: When we fetch next page of items or refreshed items, we may need to check if we have an existing ViewModel in the cache?
ListItemViewModel viewModel = new(item, Scheduler);
viewModel.InitializeProperties();
group.Add(viewModel);
}
_itemCache.Add(viewModel); // TODO: Figure out when we clear/remove things from cache...
// Am I really allowed to modify that observable collection on a BG
// thread and have it just work in the UI??
Items.AddGroup(group);
if (viewModel.MatchesFilter(Filter))
{
// Am I really allowed to modify that observable collection on a BG
// thread and have it just work in the UI??
Items.Add(viewModel);
}
}
}
catch (Exception ex)
{

View File

@@ -4,6 +4,6 @@
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
public record UpdateActionBarPage(PageViewModel? Page)
public record NavigateToPageMessage(PageViewModel? Page)
{
}

View File

@@ -23,6 +23,10 @@ public partial class PageViewModel : ExtensionObjectViewModel
[ObservableProperty]
public partial string ErrorMessage { get; private set; } = string.Empty;
// This is set from the SearchBar
[ObservableProperty]
public partial string Filter { get; set; } = string.Empty;
// These are properties that are "observable" from the extension object
// itself, in the sense that they get raised by PropChanged events from the
// extension. However, we don't want to actually make them
@@ -91,6 +95,14 @@ public partial class PageViewModel : ExtensionObjectViewModel
}
}
partial void OnFilterChanged(string oldValue, string newValue) => OnFilterUpdated(newValue);
protected virtual void OnFilterUpdated(string filter)
{
// The base page has no notion of data, so we do nothing here...
// subclasses should override.
}
protected virtual void FetchProperty(string propertyName)
{
var model = this._pageModel.Unsafe;

View File

@@ -11,14 +11,20 @@ using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ShellViewModel(IServiceProvider _serviceProvider) : ObservableObject
public partial class ShellViewModel(IServiceProvider _serviceProvider) : ObservableObject,
IRecipient<NavigateToPageMessage>
{
[ObservableProperty]
public partial bool IsLoaded { get; set; } = false;
[ObservableProperty]
public partial PageViewModel? CurrentPage { get; set; }
[RelayCommand]
public async Task<bool> LoadAsync()
{
WeakReferenceMessenger.Default.Register<NavigateToPageMessage>(this);
var tlcManager = _serviceProvider.GetService<TopLevelCommandManager>();
await tlcManager!.LoadBuiltinsAsync();
IsLoaded = true;
@@ -40,4 +46,6 @@ public partial class ShellViewModel(IServiceProvider _serviceProvider) : Observa
return true;
}
public void Receive(NavigateToPageMessage message) => CurrentPage = message.Page;
}

View File

@@ -63,7 +63,7 @@
Grid.Column="1"
VerticalAlignment="Center"
FontSize="12"
Text="{x:Bind ViewModel.CurrentPage.Name, Mode=OneWay}" />
Text="{x:Bind CurrentPageViewModel.Name, Mode=OneWay}" />
<!-- TO DO: Replace with ItemsRepeater and bind "Primary commands"? -->
<StackPanel
Grid.Column="2"

View File

@@ -3,15 +3,27 @@
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.Views;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class ActionBar : UserControl
public sealed partial class ActionBar : UserControl, ICurrentPageAware
{
public ActionBarViewModel ViewModel { get; set; } = new();
public PageViewModel? CurrentPageViewModel
{
get => (PageViewModel?)GetValue(CurrentPageViewModelProperty);
set => SetValue(CurrentPageViewModelProperty, value);
}
// Using a DependencyProperty as the backing store for CurrentPage. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CurrentPageViewModelProperty =
DependencyProperty.Register(nameof(CurrentPageViewModel), typeof(PageViewModel), typeof(ActionBar), new PropertyMetadata(null));
public ActionBar()
{
this.InitializeComponent();

View File

@@ -5,9 +5,12 @@
using System.Diagnostics;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.Views;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Input;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using CoreVirtualKeyStates = Windows.UI.Core.CoreVirtualKeyStates;
@@ -15,7 +18,7 @@ using VirtualKey = Windows.System.VirtualKey;
namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class SearchBar : UserControl
public sealed partial class SearchBar : UserControl, ICurrentPageAware
{
/// <summary>
/// Gets the <see cref="DispatcherQueueTimer"/> that we create to track keyboard input and throttle/debounce before we make queries.
@@ -24,6 +27,16 @@ public sealed partial class SearchBar : UserControl
public bool Nested { get; set; }
public PageViewModel? CurrentPageViewModel
{
get => (PageViewModel?)GetValue(CurrentPageViewModelProperty);
set => SetValue(CurrentPageViewModelProperty, value);
}
// Using a DependencyProperty as the backing store for CurrentPageViewModel. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CurrentPageViewModelProperty =
DependencyProperty.Register(nameof(CurrentPageViewModel), typeof(PageViewModel), typeof(SearchBar), new PropertyMetadata(null));
public SearchBar()
{
this.InitializeComponent();
@@ -87,11 +100,16 @@ public sealed partial class SearchBar : UserControl
private void FilterBox_TextChanged(object sender, TextChangedEventArgs e)
{
// TODO: We could encapsulate this in a Behavior if we wanted to bind to the Filter property.
_debounceTimer.Debounce(
() =>
{
// TODO: Actually Plumb Filtering
Debug.WriteLine($"Filter: {FilterBox.Text}");
if (CurrentPageViewModel != null)
{
CurrentPageViewModel.Filter = FilterBox.Text;
}
},
//// Couldn't find a good recommendation/resource for value here.
//// This seems like a useful testing site for typing times: https://keyboardtester.info/keyboard-latency-test/

View File

@@ -15,11 +15,12 @@
mc:Ignorable="d">
<Page.Resources>
<!-- TODO: Figure out what we want to do here for filtering/grouping and where -->
<!-- https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.data.collectionviewsource -->
<CollectionViewSource
<!--<CollectionViewSource
x:Name="ItemsCVS"
IsSourceGrouped="True"
Source="{x:Bind ViewModel.Items, Mode=OneWay}" />
Source="{x:Bind ViewModel.Items, Mode=OneWay}" />-->
<converters:StringVisibilityConverter
x:Key="StringVisibilityConverter"
@@ -110,9 +111,9 @@
</Page.Resources>
<Grid>
<ProgressBar
IsIndeterminate="True"
<ProgressBar
VerticalAlignment="Top"
IsIndeterminate="True"
Visibility="{x:Bind ViewModel.IsLoading, Mode=OneWay}" />
<!-- not using Interactivity:Interaction.Behaviors due to wanting to do AoT -->
<!--
@@ -125,9 +126,9 @@
x:Name="ItemsList"
IsItemClickEnabled="True"
ItemClick="ListView_ItemClick"
SelectionChanged="ItemsList_SelectionChanged"
ItemTemplate="{StaticResource ListItemViewModelTemplate}"
ItemsSource="{x:Bind ItemsCVS.View, Mode=OneWay}">
ItemsSource="{x:Bind ViewModel.Items, Mode=OneWay}"
SelectionChanged="ItemsList_SelectionChanged">
<ListView.ItemContainerTransitions>
<TransitionCollection />
</ListView.ItemContainerTransitions>
@@ -152,11 +153,14 @@
VerticalAlignment="Center"
IsIndeterminate="True" />-->
</controls:Case>
<controls:Case Value="Error">
<StackPanel Orientation="Vertical" Margin="16">
<TextBlock Text="Error on page" FontSize="18" Foreground="{ThemeResource SystemErrorTextColor}" />
<TextBlock Text="{x:Bind ViewModel.ErrorMessage, Mode=OneWay}" IsTextSelectionEnabled="True"/>
<StackPanel Margin="16" Orientation="Vertical">
<TextBlock
FontSize="18"
Foreground="{ThemeResource SystemErrorTextColor}"
Text="Error on page" />
<TextBlock IsTextSelectionEnabled="True" Text="{x:Bind ViewModel.ErrorMessage, Mode=OneWay}" />
</StackPanel>
</controls:Case>
</controls:SwitchPresenter>

View File

@@ -89,7 +89,7 @@ public sealed partial class ListPage : Page,
var result = (bool)lvm.InitializeCommand.ExecutionTask.GetResultOrDefault()!;
ViewModel = lvm;
WeakReferenceMessenger.Default.Send<UpdateActionBarPage>(new(result ? lvm : null));
WeakReferenceMessenger.Default.Send<NavigateToPageMessage>(new(result ? lvm : null));
LoadedState = result ? ViewModelLoadedState.Loaded : ViewModelLoadedState.Error;
});
}
@@ -98,7 +98,7 @@ public sealed partial class ListPage : Page,
else
{
ViewModel = lvm;
WeakReferenceMessenger.Default.Send<UpdateActionBarPage>(new(lvm));
WeakReferenceMessenger.Default.Send<NavigateToPageMessage>(new(lvm));
LoadedState = ViewModelLoadedState.Loaded;
}
}

View File

@@ -20,7 +20,8 @@
<controls:SearchBar
x:Name="SearchBox"
Grid.Row="0"
VerticalAlignment="Top" />
VerticalAlignment="Top"
CurrentPageViewModel="{x:Bind ViewModel.CurrentPage, Mode=OneWay}" />
<Grid
Grid.Row="1"
@@ -31,6 +32,9 @@
</Grid>
<controls:ActionBar Grid.Row="2" VerticalAlignment="Top" />
<controls:ActionBar
Grid.Row="2"
VerticalAlignment="Top"
CurrentPageViewModel="{x:Bind ViewModel.CurrentPage, Mode=OneWay}" />
</Grid>
</Page>

View File

@@ -80,7 +80,7 @@ public sealed partial class ShellPage :
RootFrame.BackStack.Clear();
}
WeakReferenceMessenger.Default.Send<UpdateActionBarPage>(new(pageViewModel));
ViewModel.CurrentPage = pageViewModel;
});
}

View File

@@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />

View File

@@ -0,0 +1,12 @@
// 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.UI.ViewModels;
namespace Microsoft.CmdPal.UI.Views;
public interface ICurrentPageAware
{
public PageViewModel? CurrentPageViewModel { get; set; }
}