我用 ControlTemplate 制作了一个自定义组合框, 到目前为止,我对布局非常满意,但问题是绑定到大型集合时弹出窗口的性能, 总结一下布局,新的组合框有一个标签、切换按钮和弹出窗口
弹出窗口有一个列表框,用作新布局的项目呈现器,以及一个文本框“SearchBox”来过滤 CollectionView,
我将数据虚拟化应用于列表框,但我仍然遇到性能问题
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel IsVirtualizing="True" VirtualizationMode="Recycling" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
还有类似的东西
ItemsPanel = new ItemsPanelTemplate();
var stackPanelTemplate = new FrameworkElementFactory(typeof(VirtualizingStackPanel));
ItemsPanel.VisualTree = stackPanelTemplate;
到组合框 ItemsPanel
我遇到的问题是,当弹出窗口即将出现时,它会冻结 UI 一段时间 当尝试使用 SearchBox_TextChanged 事件搜索或过滤集合时,UI 将冻结一段时间
这是 Githup
上的项目存储库谢谢你!
首先,
ListBox
默认使用VirtualizingStackPanel
。
只是不要:
将附加属性
ScrollViewer.CanContenScroll
设置为 false
显式添加 ListBoxItem 容器。 例如:
<ListBox>
<ListBoxItem>
</ListBox>
请勿混合物品容器(例如添加
Separator
)
接下来你必须改进你的过滤。您必须了解过滤器必须迭代完整的集合。
在过滤时提供繁忙指示器以避免冻结。然后在不同的线程上过滤。
如果可能,请考虑对集合进行预过滤,以减少初始大小。例如,仅提供最近三天的交易并对此数据集应用任何过滤器。强制用户显式展开列表。
在预过滤的上下文中,允许用户指定加载项目的数量似乎是无用或有害的(正如屏幕截图所暗示的那样)。至少确保这个数字与实际添加到
ItemsSource
的项目无关。
考虑禁用实时过滤,即用户键入后立即进行过滤。至少提供一个允许用户禁用它的操作,或者如果您知道项目列表将很大,则禁用它并允许用户启用它。当用户输入关键字后开始搜索(如 Visual Studion 搜索)时,您将获得更好的性能。
还支持实时过滤和排序的解决方案如下所示:
ItemsSource
MainWindow.xaml
<Window>
<StackPanel>
<ProgressBar IsIndeterminate="{Binding IsBusy}" />
<TextBox Text="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=FilterExpression}" />
<ListBox ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=Numbers}" />
</StackPanel>
</Window>
MainWindow.xaml.cs
partial class MainWindow : Window
{
public string FilterExpression
{
get => (string)GetValue(FilterExpressionProperty);
set => SetValue(FilterExpressionProperty, value);
}
public static readonly DependencyProperty FilterExpressionProperty = DependencyProperty.Register(
"FilterExpression",
typeof(string),
typeof(MainWindow),
new PropertyMetadata(default(string), OnFilterExpressionChanged));
public bool IsBusy
{
get => (bool)GetValue(IsBusyProperty);
set => SetValue(IsBusyProperty, value);
}
public static readonly DependencyProperty IsBusyProperty = DependencyProperty.Register(
"IsBusy",
typeof(bool),
typeof(MainWindow),
new PropertyMetadata(default));
public ObservableCollection<int> Numbers
{
get => (ObservableCollection<int>)GetValue(NumbersProperty);
set => SetValue(NumbersProperty, value);
}
public static readonly DependencyProperty NumbersProperty = DependencyProperty.Register(
"Numbers",
typeof(ObservableCollection<int>),
typeof(MainWindow),
new PropertyMetadata(default));
private object SyncLock { get; }
private List<int> NumbersStore { get; }
private ICollectionView NumbersStoreView { get; }
private CancellationTokenSource FilterCancellationTokenSource { get; set; }
public MainWindow()
{
InitializeComponent();
this.Numbers = new ObservableCollection<int>();
this.NumbersStore = new List<int>();
this.NumbersStoreView = CollectionViewSource.GetDefaultView(this.NumbersStore);
// Allow the collection to be modified on a background thread
this.SyncLock = new object();
BindingOperations.EnableCollectionSynchronization(this.Numbers, this.SyncLock);
// Run initialization of huge collection on a background thread
// to avoid long loading times were the UI is not available.
this. Loaded += OnLoadedAsync;
}
public async void OnLoadedAsync(object sender, EventArgs e)
{
this.IsBusy = true;
await Task.Run(Initialize);
this.IsBusy = false;
}
public void Initialize()
{
// Initialize the data pool
this.NumbersStore = Enumerable.Range(1, 30000).ToList();
// Initialize the view with N items if you have implemented data pagination
// or present an empty list that the user must fill
// by providing a filter expression
// or show all items (make sure to enable UI virtualization
// if it is not enabled by default which is the case for
// e.g., for the ComboBox. ListBox has this feature enabled by default).
foreach (int number in this.NumbersStore)
{
this.Numbers.Add(number);
}
}
private static void OnFilterExpressionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var _this = (MainWindow)d;
_this.FilterCancellationTokenSource?.Cancel();
_this.FilterCancellationTokenSource?.Dispose();
_this.FilterCancellationTokenSource = new CancellationTokenSource();
var cancellationToken = _this.FilterCancellationTokenSource.Token;
_ = Task.Run(() => _this.FilterNumbers(cancellationToken), cancellationToken);
}
private void FilterNumbers(CancellationToken cancellationToken)
{
try
{
// Show the busy indicator
this.IsBusy = true;
this.NumbersStoreView.Filter = IsItemAccepted;
if (cancellationToken.IsCancellationRequested)
{
return;
}
this.Numbers.Clear();
foreach (int filteredNumber in this.NumbersStoreView)
{
if (cancellationToken.IsCancellationRequested)
{
return;
}
this.Numbers.Add(filteredNumber);
}
}
finally
{
this.IsBusy = false;
}
}
private bool IsItemAccepted(object item)
=> int.TryParse(this.FilterExpression, out int number)
&& (int)item == number;
}