【问题标题】:How to display the wait cursor while a ListBox is updating after a change in its ItemsSource collection?如何在 ListBox 在其 ItemsSource 集合更改后更新时显示等待光标?
【发布时间】:2019-11-27 05:56:11
【问题描述】:

我有一个由视图模型属性 (Photos) 提供的 ListBox,它是包含图像文件路径的对象的 ObservableCollectionListBox 显示图像,可以有很多:

查看:

<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 时,我已经设法通过使用涉及 IDisposable 的 this method 显示等待光标:

using (this.BusyStackLifeTracker.GetNewLifeTracker())
    {
        // long job
    }

但是当视图被通知集合更改时,光标会在此方法结束时重置为指针。 ListBox 然后呈现自己,但完成时不显示等待光标。我有问题。

如何让ListBox 显示等待光标,直到更新完成?

【问题讨论】:

    标签: c# wpf


    【解决方案1】:

    这个模式做了这些事情:

    1. 将光标更改逻辑放在标志变量中以供其他用途。
    2. 为其他线程提供一种安全的方式来更改 gui 线程上的 GUI 项目。
    3. 在一个单独的线程中添加照片,这将允许 UI 响应。
    4. 为安全起见,添加到集合中的实际照片是在 Gui 线程上完成的。
    5. 等待光标不会 跳转/在添加照片时更改状态。
    6. 检查最短等待处理时间,以显示正在做某事。可以根据需要更改等待时间。如果没有达到最短等待时间,则没有额外时间完成。

    VM 上的通知这允许任何非 gui 线程执行安全的 gui 操作。

    public static void SafeOperationToGuiThread(Action operation)
    {
        System.Windows.Application.Current?.Dispatcher?.Invoke(operation);
    }
    

    状态标志 然后提供一个 Ongoing Flag,如果需要,可以重复使用该视图,这也会设置光标:

    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.
    

    【讨论】:

    • 感谢您的时间和标语建议 :) 我会更新我的问题以更好地解释,因为我认为我被误解了:我正在寻找一种方法来显示等待光标集合已更新,即在 ListBox 收到集合更改通知和新项目全部呈现之间的时间段(我不能延迟代码中的指针光标恢复)。也就是说,您当前的代码仍然有助于确认我使用的解决方案是有效的。
    • 如果在我的代码中您将总等待时间设置为 5 秒并且该过程需要 3 秒,则代码将再等待 2 秒才能覆盖。这将给出等待超过加载阶段的外观。否则,您需要连接到 ListBoxLoaded 事件上的事件,以可能关闭等待光标。
    • 我的代码是为最小等待时间操作而设计的。 5 秒的等待光标给人一种真正等待的印象。如果操作超过 5,则不等待。
    【解决方案2】:

    首先在您的视图模型中创建一个“忙碌”标志:

    private bool _Busy = false;
    public bool Busy
    {
        get { return this._Busy; }
        set
        {
            if (this._Busy != value)
            {
                this._Busy = value;
                RaisePropertyChanged(() => this.Busy);
            }
        }
    }
    

    然后将您的所有工作转移到一个任务中,该任务会设置此繁忙标志并随后将其重置(请注意,当您将照片添加到集合本身时,您必须如何调用 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>
    

    更新:将工作放在任务中意味着您不会饿死 GUI 线程,因此 ListBox 将随着您的进行而更新,而不是等到您完成加载所有照片。如果是 ListBox 更新本身花费的时间太长,那么这就是您应该尝试解决的问题,例如查看是否有任何转换器运行缓慢,或者您的照片数据结构是否需要以更优化的格式提供给视图。我想您会发现,简单地启用虚拟化可能会大大提高您的前端响应能力:

    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <VirtualizingStackPanel />
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
    

    不幸的是,虚拟化很容易被破坏,因此您需要通过在应用运行时检查 Live Visual Tree 来确保它正常工作,以确保仅为实际存在的项目创建 ListBoxItems在屏幕上可见。

    【讨论】:

    • 谢谢马克,但我已经这样做了(除了使用任务来构建集合),甚至阻止 ObservableCollection 在每次更新后通知视图。但是,这会在构建集合时显示等待光标,而不是在 ListBox 使用新的集合内容更新自身时显示。
    猜你喜欢
    • 2011-01-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-07-21
    • 1970-01-01
    • 2018-04-08
    • 2012-03-29
    相关资源
    最近更新 更多