Files
PowerToys/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListItemViewModel.cs
Mike Griese c9656754bd CmdPal: fix a perf regression loading pages (#39415)
This was especially noticable with the icons extension.

Turns out in #39051, when I was experimenting with getting AoT clean,
i accidentally called this twice. Then we actually commited that
straight up.

This PR reverts that. It also moves a similar case where we were
initializing all the tags on the UI thread. That's wrong too - we need
to fetch properties off the UI thread, then update the list on the UI
thread.

Closes nothing, I didn't file this yet.
2025-05-16 10:47:44 -05:00

147 lines
4.6 KiB
C#

// 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.Diagnostics.CodeAnalysis;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ListItemViewModel(IListItem model, WeakReference<IPageContext> context)
: CommandItemViewModel(new(model), context)
{
public new ExtensionObject<IListItem> Model { get; } = new(model);
public List<TagViewModel>? Tags { get; set; }
// Remember - "observable" properties from the model (via PropChanged)
// cannot be marked [ObservableProperty]
public bool HasTags => (Tags?.Count ?? 0) > 0;
public string TextToSuggest { get; private set; } = string.Empty;
public string Section { get; private set; } = string.Empty;
public DetailsViewModel? Details { get; private set; }
[MemberNotNullWhen(true, nameof(Details))]
public bool HasDetails => Details != null;
public override void InitializeProperties()
{
if (IsInitialized)
{
return;
}
// This sets IsInitialized = true
base.InitializeProperties();
var li = Model.Unsafe;
if (li == null)
{
return; // throw?
}
UpdateTags(li.Tags);
TextToSuggest = li.TextToSuggest;
Section = li.Section ?? string.Empty;
var extensionDetails = li.Details;
if (extensionDetails != null)
{
Details = new(extensionDetails, PageContext);
Details.InitializeProperties();
UpdateProperty(nameof(Details));
UpdateProperty(nameof(HasDetails));
}
UpdateProperty(nameof(TextToSuggest));
UpdateProperty(nameof(Section));
}
protected override void FetchProperty(string propertyName)
{
base.FetchProperty(propertyName);
var model = this.Model.Unsafe;
if (model == null)
{
return; // throw?
}
switch (propertyName)
{
case nameof(Tags):
UpdateTags(model.Tags);
break;
case nameof(TextToSuggest):
this.TextToSuggest = model.TextToSuggest ?? string.Empty;
break;
case nameof(Section):
this.Section = model.Section ?? string.Empty;
break;
case nameof(Details):
var extensionDetails = model.Details;
Details = extensionDetails != null ? new(extensionDetails, PageContext) : null;
Details?.InitializeProperties();
UpdateProperty(nameof(Details));
UpdateProperty(nameof(HasDetails));
break;
}
UpdateProperty(propertyName);
}
// TODO: Do we want filters to match descriptions and other properties? Tags, etc... Yes?
// TODO: Do we want to save off the score here so we can sort by it in our ListViewModel?
public bool MatchesFilter(string filter) => StringMatcher.FuzzySearch(filter, Title).Success || StringMatcher.FuzzySearch(filter, Subtitle).Success;
public override string ToString() => $"{Name} ListItemViewModel";
public override bool Equals(object? obj) => obj is ListItemViewModel vm && vm.Model.Equals(this.Model);
public override int GetHashCode() => Model.GetHashCode();
private void UpdateTags(ITag[]? newTagsFromModel)
{
var newTags = newTagsFromModel?.Select(t =>
{
var vm = new TagViewModel(t, PageContext);
vm.InitializeProperties();
return vm;
})
.ToList() ?? [];
DoOnUiThread(
() =>
{
// Tags being an ObservableCollection instead of a List lead to
// many COM exception issues.
Tags = new(newTags);
UpdateProperty(nameof(Tags));
UpdateProperty(nameof(HasTags));
});
}
protected override void UnsafeCleanup()
{
base.UnsafeCleanup();
// Tags don't have event handlers or anything to cleanup
Tags?.ForEach(t => t.SafeCleanup());
Details?.SafeCleanup();
var model = Model.Unsafe;
if (model != null)
{
// We don't need to revoke the PropChanged event handler here,
// because we are just overriding CommandItem's FetchProperty and
// piggy-backing off their PropChanged
}
}
}