diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ActionBarViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ActionBarViewModel.cs index 9ca1415a74..a23c60cc95 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ActionBarViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ActionBarViewModel.cs @@ -11,7 +11,6 @@ using Microsoft.CmdPal.UI.ViewModels.Messages; namespace Microsoft.CmdPal.UI.ViewModels; public partial class ActionBarViewModel : ObservableObject, - IRecipient, IRecipient { 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 ContextActions { get; set; } = []; public ActionBarViewModel() { - WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(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(new(item.Command)); - - public void Receive(UpdateActionBarPage message) => CurrentPage = message.Page; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs index c13e4123fc..53965fdc34 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs @@ -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 diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListItemViewModel.cs index a35d9839cf..c5277d4419 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListItemViewModel.cs @@ -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"; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs index 3cff9dfde3..7d90a1b305 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs @@ -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 _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 Items { get; set; } = []; + public partial ObservableCollection Items { get; set; } = []; private readonly ExtensionObject _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 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) { diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateActionBarPage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/NavigateToPageMessage.cs similarity index 81% rename from src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateActionBarPage.cs rename to src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/NavigateToPageMessage.cs index 84a1e0edc0..31db5ee230 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateActionBarPage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/NavigateToPageMessage.cs @@ -4,6 +4,6 @@ namespace Microsoft.CmdPal.UI.ViewModels.Messages; -public record UpdateActionBarPage(PageViewModel? Page) +public record NavigateToPageMessage(PageViewModel? Page) { } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs index 146de651f0..3da00a85e4 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs @@ -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; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ShellViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ShellViewModel.cs index c8377a1739..2dd2e15a3c 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ShellViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ShellViewModel.cs @@ -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 { [ObservableProperty] public partial bool IsLoaded { get; set; } = false; + [ObservableProperty] + public partial PageViewModel? CurrentPage { get; set; } + [RelayCommand] public async Task LoadAsync() { + WeakReferenceMessenger.Default.Register(this); + var tlcManager = _serviceProvider.GetService(); 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; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ActionBar.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ActionBar.xaml index dd38f8d8e3..701b56222a 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ActionBar.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ActionBar.xaml @@ -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}" /> (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(); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs index 6ab99c6e7a..58792261d1 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs @@ -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 { /// /// Gets the 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/ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml index f02158a330..91fa163ea9 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml @@ -15,11 +15,12 @@ mc:Ignorable="d"> + - + Source="{x:Bind ViewModel.Items, Mode=OneWay}" />--> - - + - - - + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs index 03275d0c40..eeb7bbc522 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs @@ -89,7 +89,7 @@ public sealed partial class ListPage : Page, var result = (bool)lvm.InitializeCommand.ExecutionTask.GetResultOrDefault()!; ViewModel = lvm; - WeakReferenceMessenger.Default.Send(new(result ? lvm : null)); + WeakReferenceMessenger.Default.Send(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(new(lvm)); + WeakReferenceMessenger.Default.Send(new(lvm)); LoadedState = ViewModelLoadedState.Loaded; } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ShellPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/ShellPage.xaml index b00053d121..3b569325d4 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/ShellPage.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ShellPage.xaml @@ -20,7 +20,8 @@ + VerticalAlignment="Top" + CurrentPageViewModel="{x:Bind ViewModel.CurrentPage, Mode=OneWay}" /> - + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ShellPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/ShellPage.xaml.cs index cf2c9a41af..521fd63ea4 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/ShellPage.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ShellPage.xaml.cs @@ -80,7 +80,7 @@ public sealed partial class ShellPage : RootFrame.BackStack.Clear(); } - WeakReferenceMessenger.Default.Send(new(pageViewModel)); + ViewModel.CurrentPage = pageViewModel; }); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Button.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Button.xaml index 1e47a0dce1..3c091b7ff3 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Button.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Button.xaml @@ -1,7 +1,5 @@ - + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Views/ICurrentPageAware.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Views/ICurrentPageAware.cs new file mode 100644 index 0000000000..2aa93bafb3 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Views/ICurrentPageAware.cs @@ -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; } +}