Refactor ResultsViewModel.cs with DynamicData

1. Use DynamicData SourceCache instead of our ResultCollection to reduce code complexity
2. Stop trigger Visibility when changes from empty to not empty or vise versa
This commit is contained in:
Hongtao Zhang 2023-01-12 15:09:43 -06:00
parent c0f46f24de
commit 453e43088c
No known key found for this signature in database
GPG key ID: 75F655B91C7AC9BB
5 changed files with 75 additions and 182 deletions

View file

@ -84,6 +84,7 @@
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.0.0" />
<PackageReference Include="DynamicData" Version="7.12.11" />
<PackageReference Include="Fody" Version="6.5.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View file

@ -13,7 +13,7 @@
d:DesignWidth="100"
Focusable="False"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Results}"
ItemsSource="{Binding Display}"
KeyboardNavigation.DirectionalNavigation="Cycle"
PreviewMouseDown="ListBox_PreviewMouseDown"
PreviewMouseLeftButtonDown="ResultList_PreviewMouseLeftButtonDown"
@ -26,8 +26,8 @@
SelectionMode="Single"
Style="{DynamicResource BaseListboxStyle}"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Standard"
Visibility="{Binding Visbility}"
VirtualizingStackPanel.VirtualizationMode="Recycling"
Visibility="{Binding Visibility}"
mc:Ignorable="d">
<!-- IsSynchronizedWithCurrentItem: http://stackoverflow.com/a/7833798/2833083 -->

View file

@ -173,7 +173,7 @@ namespace Flow.Launcher.ViewModel
var token = e.Token == default ? _updateToken : e.Token;
PluginManager.UpdatePluginMetadata(e.Results, pair.Metadata, e.Query);
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(e.Results, pair.Metadata, e.Query, token)))
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(e.Results, pair.Metadata, token)))
{
Log.Error("MainViewModel", "Unable to add item to Result Update Queue");
}
@ -535,13 +535,13 @@ namespace Flow.Launcher.ViewModel
_selectedResults = value;
if (SelectedIsFromQueryResults())
{
ContextMenu.Visbility = Visibility.Collapsed;
History.Visbility = Visibility.Collapsed;
ContextMenu.Visibility = Visibility.Collapsed;
History.Visibility = Visibility.Collapsed;
ChangeQueryText(_queryTextBeforeLeaveResults);
}
else
{
Results.Visbility = Visibility.Collapsed;
Results.Visibility = Visibility.Collapsed;
_queryTextBeforeLeaveResults = QueryText;
@ -559,7 +559,7 @@ namespace Flow.Launcher.ViewModel
}
}
_selectedResults.Visbility = Visibility.Visible;
_selectedResults.Visibility = Visibility.Visible;
}
}
@ -710,7 +710,7 @@ namespace Flow.Launcher.ViewModel
if (query == null) // shortcut expanded
{
Results.Clear();
Results.Visbility = Visibility.Collapsed;
// Results.Visbility = Visibility.Collapsed;
PluginIconPath = null;
SearchIconVisibility = Visibility.Visible;
return;
@ -814,7 +814,7 @@ namespace Flow.Launcher.ViewModel
results ??= _emptyResult;
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(results, plugin.Metadata, query, currentCancellationToken)))
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(results, plugin.Metadata, currentCancellationToken)))
{
Log.Error("MainViewModel", "Unable to add item to Result Update Queue");
}
@ -1037,7 +1037,7 @@ namespace Flow.Launcher.ViewModel
/// <summary>
/// To avoid deadlock, this method should not called from main thread
/// </summary>
public void UpdateResultView(IEnumerable<ResultsForUpdate> resultsForUpdates)
public void UpdateResultView(ICollection<ResultsForUpdate> resultsForUpdates)
{
if (!resultsForUpdates.Any())
return;

View file

@ -13,14 +13,12 @@ namespace Flow.Launcher.ViewModel
public PluginMetadata Metadata { get; }
public string ID { get; }
public Query Query { get; }
public CancellationToken Token { get; }
public ResultsForUpdate(IReadOnlyList<Result> results, PluginMetadata metadata, Query query, CancellationToken token)
public ResultsForUpdate(IReadOnlyList<Result> results, PluginMetadata metadata, CancellationToken token)
{
Results = results;
Metadata = metadata;
Query = query;
Token = token;
ID = metadata.ID;
}

View file

@ -1,14 +1,18 @@
using Flow.Launcher.Infrastructure.UserSettings;
using System;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using DynamicData;
using DynamicData.Binding;
namespace Flow.Launcher.ViewModel
{
@ -16,21 +20,34 @@ namespace Flow.Launcher.ViewModel
{
#region Private Fields
public ResultCollection Results { get; }
private SourceCache<ResultsForUpdate, string> ResultCache { get; } = new(r => r.ID);
private readonly object _collectionLock = new object();
private ReadOnlyObservableCollection<ResultViewModel> display;
public ReadOnlyObservableCollection<ResultViewModel> Display => display;
private readonly object _collectionLock = new();
private readonly Settings _settings;
private int MaxResults => _settings?.MaxResultsToShow ?? 6;
public ResultsViewModel()
private IDisposable _resultsSubscription;
private ResultsViewModel()
{
Results = new ResultCollection();
BindingOperations.EnableCollectionSynchronization(Results, _collectionLock);
_resultsSubscription = ResultCache.Connect()
.TransformMany(list => list.Results.Select(r => new ResultViewModel(r, _settings)),
r => r.GetHashCode())
.Sort(SortExpressionComparer<ResultViewModel>.Descending(r => r.Result.Score))
.Bind(out display)
.DisposeMany()
.Subscribe();
BindingOperations.EnableCollectionSynchronization(Display, _collectionLock);
}
public ResultsViewModel(Settings settings) : this()
{
_settings = settings;
_settings.PropertyChanged += (s, e) =>
_settings.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(_settings.MaxResultsToShow))
{
@ -48,9 +65,8 @@ namespace Flow.Launcher.ViewModel
public int SelectedIndex { get; set; }
public ResultViewModel SelectedItem { get; set; }
public Thickness Margin { get; set; }
public Visibility Visbility { get; set; } = Visibility.Collapsed;
public Visibility Visibility { get; set; } = Visibility.Visible;
public ICommand RightClickResultCommand { get; init; }
public ICommand LeftClickResultCommand { get; init; }
@ -58,36 +74,18 @@ namespace Flow.Launcher.ViewModel
#region Private Methods
private int InsertIndexOf(int newScore, IList<ResultViewModel> list)
{
int index = 0;
for (; index < list.Count; index++)
{
var result = list[index];
if (newScore > result.Result.Score)
{
break;
}
}
return index;
}
private int NewIndex(int i)
{
var n = Results.Count;
var n = Display.Count;
if (n > 0)
{
i = (n + i) % n;
return i;
}
else
{
// SelectedIndex returns -1 if selection is empty.
return -1;
}
// SelectedIndex returns -1 if selection is empty.
return -1;
}
#endregion
#region Public Methods
@ -120,19 +118,7 @@ namespace Flow.Launcher.ViewModel
public void Clear()
{
lock (_collectionLock)
Results.RemoveAll();
}
public void KeepResultsFor(PluginMetadata metadata)
{
lock (_collectionLock)
Results.Update(Results.Where(r => r.Result.PluginID == metadata.ID).ToList());
}
public void KeepResultsExcept(PluginMetadata metadata)
{
lock (_collectionLock)
Results.Update(Results.Where(r => r.Result.PluginID != metadata.ID).ToList());
ResultCache.Clear();
}
/// <summary>
@ -140,72 +126,55 @@ namespace Flow.Launcher.ViewModel
/// </summary>
public void AddResults(List<Result> newRawResults, string resultId)
{
var newResults = NewResults(newRawResults, resultId);
UpdateResults(newResults);
lock (_collectionLock)
{
ResultCache.Edit(list =>
{
list.AddOrUpdate(new ResultsForUpdate(newRawResults, new PluginMetadata()
{
ID = resultId
}, default));
});
}
}
/// <summary>
/// To avoid deadlock, this method should not called from main thread
/// </summary>
public void AddResults(IEnumerable<ResultsForUpdate> resultsForUpdates, CancellationToken token)
{
var newResults = NewResults(resultsForUpdates);
if (token.IsCancellationRequested)
return;
UpdateResults(newResults, token);
}
private void UpdateResults(List<ResultViewModel> newResults, CancellationToken token = default)
public void AddResults(ICollection<ResultsForUpdate> resultsForUpdates, CancellationToken token)
{
lock (_collectionLock)
{
// update UI in one run, so it can avoid UI flickering
Results.Update(newResults, token);
if (Results.Any())
SelectedItem = Results[0];
ResultCache.Edit(list =>
{
list.AddOrUpdate(resultsForUpdates);
});
if (display.Any())
SelectedItem = display[0];
}
switch (Visbility)
// UpdateVisibility();
}
private void UpdateVisibility()
{
switch (Visibility)
{
case Visibility.Collapsed when Results.Count > 0:
case Visibility.Collapsed when Display.Count > 0:
SelectedIndex = 0;
Visbility = Visibility.Visible;
Visibility = Visibility.Visible;
break;
case Visibility.Visible when Results.Count == 0:
Visbility = Visibility.Collapsed;
case Visibility.Visible when Display.Count == 0:
Visibility = Visibility.Collapsed;
break;
}
}
private List<ResultViewModel> NewResults(List<Result> newRawResults, string resultId)
{
if (newRawResults.Count == 0)
return Results;
var newResults = newRawResults.Select(r => new ResultViewModel(r, _settings));
return Results.Where(r => r.Result.PluginID != resultId)
.Concat(newResults)
.OrderByDescending(r => r.Result.Score)
.ToList();
}
private List<ResultViewModel> NewResults(IEnumerable<ResultsForUpdate> resultsForUpdates)
{
if (!resultsForUpdates.Any())
return Results;
return Results.Where(r => r != null && !resultsForUpdates.Any(u => u.ID == r.Result.PluginID))
.Concat(resultsForUpdates.SelectMany(u => u.Results, (u, r) => new ResultViewModel(r, _settings)))
.OrderByDescending(rv => rv.Result.Score)
.ToList();
}
#endregion
#region FormattedText Dependency Property
public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached(
"FormattedText",
typeof(Inline),
@ -224,8 +193,7 @@ namespace Flow.Launcher.ViewModel
private static void FormattedTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var textBlock = d as TextBlock;
if (textBlock == null) return;
if (d is not TextBlock textBlock) return;
var inline = (Inline)e.NewValue;
@ -234,82 +202,8 @@ namespace Flow.Launcher.ViewModel
textBlock.Inlines.Add(inline);
}
#endregion
public class ResultCollection : List<ResultViewModel>, INotifyCollectionChanged
{
private long editTime = 0;
private CancellationToken _token;
public event NotifyCollectionChangedEventHandler CollectionChanged;
protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
CollectionChanged?.Invoke(this, e);
}
public void BulkAddAll(List<ResultViewModel> resultViews)
{
AddRange(resultViews);
// can return because the list will be cleared next time updated, which include a reset event
if (_token.IsCancellationRequested)
return;
// manually update event
// wpf use directx / double buffered already, so just reset all won't cause ui flickering
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
private void AddAll(List<ResultViewModel> Items)
{
for (int i = 0; i < Items.Count; i++)
{
var item = Items[i];
if (_token.IsCancellationRequested)
return;
Add(item);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, i));
}
}
public void RemoveAll(int Capacity = 512)
{
Clear();
if (this.Capacity > 8000 && Capacity < this.Capacity)
this.Capacity = Capacity;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
/// <summary>
/// Update the results collection with new results, try to keep identical results
/// </summary>
/// <param name="newItems"></param>
public void Update(List<ResultViewModel> newItems, CancellationToken token = default)
{
_token = token;
if (Count == 0 && newItems.Count == 0 || _token.IsCancellationRequested)
return;
if (editTime < 10 || newItems.Count < 30)
{
if (Count != 0) RemoveAll(newItems.Count);
AddAll(newItems);
editTime++;
return;
}
else
{
Clear();
BulkAddAll(newItems);
if (Capacity > 8000 && newItems.Count < 3000)
{
Capacity = newItems.Count;
}
editTime++;
}
}
}
}
}