我有一个由视图模型属性(ListBox
)馈给的Photos
,该属性是ObservableCollection
对象,其中包含图像文件的路径。 ListBox
显示的图像可能很多:
查看:
<ListBox ItemsSource="{Binding Path=Photos}"
SelectionChanged="PhotoBox_SelectionChanged">
...
</ListBox>
后面的代码(可以改进以异步运行...):
void RefreshPhotoCollection (string path) {
Photos.Clear ();
var info = new DirectoryInfo (path);
try {
foreach (var fileInfo in info.EnumerateFiles ()) {
if (FileFilters.Contains (fileInfo.Extension)) {
Photos.Add (new Photo (fileInfo.FullName));
}
}
}
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) {
...
}
}
在运行RefreshPhotoCollection
时,我已经设法通过使用this method来显示等待光标,该事件涉及一个IDisposable:
using (this.BusyStackLifeTracker.GetNewLifeTracker())
{
// long job
}
但是,当向视图通知收集更改时,此方法结束时将光标重置为指针。然后ListBox
会自己渲染,但在此过程中不会显示等待光标。我有个问题。
如何使ListBox
显示等待光标,直到更新完成?
答案 0 :(得分:2)
首先在视图模型中创建一个“忙”标志:
private bool _Busy = false;
public bool Busy
{
get { return this._Busy; }
set
{
if (this._Busy != value)
{
this._Busy = value;
RaisePropertyChanged(() => this.Busy);
}
}
}
然后将所有工作移至一个Task,该任务会设置该忙碌标志并在之后将其重置(请注意,每当将照片添加到集合本身时,您将必须调用GUI线程的分派器):
private void DoSomeWork()
{
Task.Run(WorkerProc);
}
private async Task WorkerProc()
{
this.Busy = true;
for (int i = 0; i < 100; i++)
{
// simulate loading a photo in 500ms
var photo = i.ToString();
await Task.Delay(TimeSpan.FromMilliseconds(500));
// add it to the main collection
Application.Current.Dispatcher.Invoke(() => this.Photos.Add(photo));
}
this.Busy = false;
}
然后在XAML中为MainWindow提供一种样式,该样式可以在设置此标志时设置光标:
<Window.Style>
<Style TargetType="{x:Type Window}">
<Style.Triggers>
<DataTrigger Binding="{Binding Busy}" Value="True">
<Setter Property="Cursor" Value="Wait" />
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Style>
更新:将工作放在Task中意味着您不会饿死GUI线程,因此ListBox会随着您的更新而更新,而不是等到完成所有照片的加载。如果是ListBox更新本身花费的时间太长,那么这就是您应该尝试解决的问题,例如查看是否有任何转换器运行缓慢,或者是否需要以最佳格式将照片数据结构提供给视图。我想您会发现,仅启用虚拟化可能会大大改善前端响应能力:
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
不幸的是,虚拟化是一个很容易破解的东西,因此您需要通过在运行应用程序时检查Live Visual Tree来确保其正常工作,以确保仅针对实际存在的项创建ListBoxItems。在屏幕上可见。
答案 1 :(得分:2)
此模式可以执行以下操作:
VM上的通知。这允许任何非GUI线程执行安全的GUI操作。
public static void SafeOperationToGuiThread(Action operation)
{
System.Windows.Application.Current?.Dispatcher?.Invoke(operation);
}
状态标志 然后提供一个正在进行的标记,如果需要可以重新使用该视图,该标记还可以设置光标:
private bool _IsOperationOnGoing;
public bool IsOperationOnGoing
{
get { return _IsOperationOnGoing; }
set {
if (_IsOperationOnGoing != value)
{
_IsOperationOnGoing = value;
SafeOperationToGuiThread(() => Mouse.OverrideCursor = (_IsOperationOnGoing) ? Cursors.Wait : null );
OnPropertyChanged(nameof(IsOperationOnGoing));
}
}
}
在单独的线程中添加照片,然后在您的照片添加中,执行以下操作:GUI线程关闭光标,然后由单独的任务/线程加载照片。同时监视时间,以显示可行的一致等待时间光标:
private Task RunningTask { get; set; }
void RefreshPhotoCollection (string path)
{
IsOperationOnGoing = true;
RunningTask = Task.Run(() =>
{
try {
TimeIt( () =>
{
... // Enumerate photos like before, but add the safe invoke:
SafeOperationToGuiThread(() => Photos.Add (new Photo (fileInfo.FullName););
},
4 // Must run for 4 seconds to show the user.
);
}
catch() {}
finally
{
SafeOperationToGuiThread(() => IsOperationOnGoing = false);
}
});
}
}
等待时间检查
我如何设法维持等待光标,直到视图ListBox更新完成,以便用户真正知道UI何时再次响应?
这里是等待操作方法,如果未达到最小操作时间,它将填充等待时间:
public static void TimeIt(Action work, int secondsToDisplay = 2)
{
var sw = Stopwatch.StartNew();
work.Invoke();
sw.Stop();
if (sw.Elapsed.Seconds < secondsToDisplay)
Thread.Sleep((secondsToDisplay - sw.Elapsed.Seconds) * 1000);
}
如果需要,可以对其进行修改以使用异步调用,而只需进行很少的更改。
替代个人资料中的@mins标记行:
Light a man a fire and he is warm for a day.
Set a man a fire and he is warm for the rest of his life.