【问题标题】:Loading the list of items asynchronously in a WPF listbox using Dispatcher使用 Dispatcher 在 WPF 列表框中异步加载项目列表
【发布时间】:2014-12-09 21:14:14
【问题描述】:

我正在创建一个 WPF 解决方案,它使用 MVVM 模式异步加载搜索控件中的搜索项。作为 WPF 用户控件的搜索控件是用一个文本框创建的,用于输入搜索文本和搜索按钮,以及一个隐藏的列表框,当它在其中加载搜索项目列表时,该列表框将可见。此用户控件依次嵌入到另一个 WPF 视图中,该视图具有某些项目的树视图。此视图有一个视图模型,其中加载树视图的搜索项的逻辑将加载到搜索控件中。一直以来,这一直在同步发生,无需使用任何 Dispatcher 调用。但是,在更改请求之后,我想使用 Dispatcher 在不同的线程中异步发生这种情况。

谁能告诉我如何在视图模型类中处理搜索控件的调度程序,以便使用我的视图模型不知道视图的 MVVM 模式对其调用 BeginInvoke?任何线索将不胜感激。

public ObservableCollection<Details> CatalogSearchResults { get; private set; }

private void ExecuteSearchCommand(object parameter)
    {
        CatalogSearchResults.Clear();
        if (string.IsNullOrEmpty(parameter.ToString())) return;

        searchtext = (string)parameter;
        searchtext.Trim();

        SetSearchResults();
    }

private void SetSearchResults() 
    { 
    BackgroundWorker bw = new BackgroundWorker(); 

    bw.DoWork += LoadResults; 
    bw.RunWorkerCompleted += this.LoadResultsCompleted; 

    bw.RunWorkerAsync(); 
    } 

private void LoadResults(object sender, DoWorkEventArgs args) 
{ 
        IsSearchInProgress = true;
        foreach (var category in _rootCategory.Recurse(FindChildren))
        {
            if (category.CommentDetails != null)
            {
                //limitation - there is no direct way to add range to observable collection.
                //Using linq query would result in two loops rather than one.
                foreach (var node in category.Details)
                {
                    if (node.Name.IndexOf(searchtext, StringComparison.CurrentCultureIgnoreCase) >= 0
                        || node.PrecannedText.IndexOf(searchtext,            StringComparison.CurrentCultureIgnoreCase) >= 0)
                    {
                        Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
                            (ThreadStart)delegate { CatalogSearchResults.Add(node); }); 
                          Thread.Sleep(100); 
                    }
                }
            }
        }
        IsSearchInProgress = false;
}

在 xaml 中,我将 Search 控件的 Items 属性绑定到 CatalogSearchResults:

 <ctrl:SearchControl x:Name="Ctrl" Grid.RowSpan="2" HorizontalAlignment="Stretch" VerticalAlignment="Top"   ToolTip="Search" Command="{Binding SearchCommand}"   Grid.ColumnSpan="3"                                                                             
            CommandParameter="{Binding Text, RelativeSource={RelativeSource Self}}"                                                                                
            Items ="{Binding CatalogSearchResults}" > </ctrl:SearchControl>

谢谢, 苏米亚

【问题讨论】:

    标签: wpf mvvm dispatcher


    【解决方案1】:

    这是一个简单的实现,展示了如何在 DoWork 运行时使用 BackgroundWorker 更新 UI 线程上的对象 - 在此示例中,UI 中有一个 ListBox 绑定到 FilteredItems 和 @987654325 @ 是 IEnumerable 类型的 UserControl 的属性:

        FilteredItems = new ObservableCollection<object>();
        BackgroundWorker bw = new BackgroundWorker();
        bw.WorkerReportsProgress = true;
        bw.DoWork += bw_DoWork;
        bw.RunWorkerCompleted += bw_RunWorkerCompleted;
        bw.ProgressChanged += bw_ProgressChanged;
        bw.RunWorkerAsync();
    
        private void bw_DoWork(object sender, DoWorkEventArgs e)
        {
            BackgroundWorker bw = (BackgroundWorker) sender;
            var result = ItemsSource
               .OfType<object>()
               .Where(x => x.ToString().Contains(_FilterText));
            foreach (object o in result)
            {
                // Pass each object found to bw_ProgressChanged in the UserState argument.
                // This updates the UI as each item is found.
                bw.ReportProgress(0, o);
            }
        }
    
        void bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            // FilteredItems is bound to the UI, but it's OK to update it here because
            // the ProgressChanged event handler runs on the UI thread.
            FilteredItems.Add(e.UserState);
        }
    
        private void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if (e.Error != null)
            {
                MessageBox.Show(e.Error.Message);
            }
        }
    

    请注意,每次找到项目时调用ReportProgress 效率非常低,因为您正在使用Invoke 调用跨线程编组找到的每个项目。根据过滤实际花费的时间,最好累积一堆结果并将List&lt;object&gt; 传递给bw_ReportProgress,而不是仅仅传递一个object

    【讨论】:

    • 感谢罗伯特提供示例代码:)!!它现在像魅力一样工作,我对现有实现所做的唯一更改是我将用户控件中的 Items 属性从 ICollection 修改为 IEnumerable ,它工作得非常完美!我想知道是什么让异步操作现在随着项目数据类型的变化而工作?你能解释一下吗?
    • 我很困惑。您在使用我发布的示例吗?
    • 是的,我使用了您发布的代码。在您提到 ItemSource 是 IEnumerable 的帖子中,我想将用户控件中的 Items 属性从 ICollection 更改为 IEnumerable。除此之外,我还必须做一个我忘记在上面的回复中提到的更改,即我删除了一行代码,该代码基于 no. 将布尔值设置为 true。项目集合中的项目。由于 IEnumerable 不支持 Count 属性,我删除了那行代码,然后它就可以工作了。所以我认为它正在工作,因为我将其更改为 IEnumerable。
    • 只要集合中没有项目,就会有一个布尔变量在代码中设置为 false。如果此变量为假,则在列表框的位置出现未找到结果标签。这是密码锁 (this) { ExecuteCommand(); HasItemsInResult = false; if (null != Items) { HasItemsInResult = Items.Count != 0; } IsItemsVisible = true; }
    • 在使用 backgroundworker 或 dispatcher 时,Items.Count 始终为零,因此尽管由于布尔变量设置为 false,UI 线程使用结果集进行了更新,但始终显示 No results found,这让我认为列表框的更新没有发生:(...当我将其更改为 IEnumerable 时,我忽略了 HasItemsInResult = Items.Count != 0; 这行代码并且即使在 Items 不为 null 并且所以列表在 UI 中更新了...
    【解决方案2】:

    这取决于很多因素(而且您的描述有点令人困惑),但我给出了一个冗长的答案here 可能会对此事有所了解。基本上,单独使用dispatcher不会自动使代码多线程;你需要一些真正的多线程机制,比如 BackgroundWorker 或 Task Parallel Library。根据您的设置方式以及您在另一个线程中所做的确切操作,您可能确实需要在调度程序线程上调用一些操作 - 但是 BackgroundWorker 在大多数情况下会自动执行此操作,所以我会使用它来做一些简单的事情.任务并行库还对调度程序进行了特殊处理,您应该在 MSDN 或任何 TPL 教程上找到更多信息。

    如果您直到现在还没有大量处理多线程,我给出的最佳建议是尽可能多地收集有关它的信息,因为到目前为止已经无数次说过,多线程是困难! :)

    【讨论】:

      【解决方案3】:

      根据需要进行修改。 'Items' 只是从 VM 公开的字符串的 observableCollection

          private void SetSearchResults()
          {
              BackgroundWorker bw = new BackgroundWorker();
      
              bw.DoWork += LoadResults;
              bw.RunWorkerCompleted += this.LoadResultsCompleted;
      
              bw.RunWorkerAsync();
          }
      
          private void LoadResultsCompleted(object sender, RunWorkerCompletedEventArgs e)
          {
          }
      
          private void LoadResults(object sender, DoWorkEventArgs args)
          {
              List<string> results = GetResults();
      
              foreach (string result in results)
              {
                   Application.Current.Dispatcher.Invoke(
                          DispatcherPriority.Normal, (ThreadStart)delegate { Items.Add(result); } //Dont worry about access to modified closure in this case
                   Thread.Sleep(100);
              }
          }
      

      在 XAML 中

      <ListBox ItemsSource={Binding Items}/>
      

      【讨论】:

      • 感谢分享示例 sn-p,但我实际上想知道 Application.Current.Dispatcher 是否会让后台工作进程去更新 UI 中的 SearchControl?由于我使用上述实现在 SearchControl 中未找到任何结果,尽管 Items 集合具有搜索控件在 xaml 文件中绑定到的数据。你能告诉我哪里出错了吗?
      • 好吧,我正是这样做以在我的代码中加载大量图像并且它工作正常。我唯一的区别是我在绑定上设置了 IsAsync=True 但这是出于其他原因。我认为它不适用于您的情况。我认为您应该向我们展示您的代码,有人可以提供帮助。
      • 不应该 Items ="{Binding CatalogSearchResults}" 是 ItemsSource ="{Binding CatalogSearchResults}" 吗?
      • Items 是我在用户控件 SearchControl 中创建的依赖属性,public ICollection Items { get { return (ICollection)GetValue(ItemsProperty); } set { SetValue(ItemsProperty, value); } } public static readonly DependencyProperty ItemsProperty = DependencyProperty.Register("Items", typeof(ICollection), typeof(SearchControl), new UIPropertyMetadata());
      • 是不是因为Items是在用户控件中定义的ICollection依赖属性?
      【解决方案4】:

      应用程序中的所有视图都有相同的调度程序,您可以使用Application.Current.Dispatcher 访问它。

      但无论如何,您不需要调度程序在工作线程上执行操作。您只需要它在 UI 上执行操作,因为 UI 元素只能从 UI 线程访问。但即便如此,您通常也不需要显式操作调度程序。您可以从工作线程更新 ViewModel 的属性,绑定到该属性的控件将正常更新,因为 PropertyChanged 事件会自动编组到 UI 调度程序。

      不起作用的工作是从工作线程修改绑定的ObservableCollection&lt;T&gt;:您需要使用Dispatcher.Invoke 从 UI 线程执行此操作。您也可以使用specialized ObservableCollection&lt;T&gt; that raises event on the UI thread

      【讨论】:

      • 您好 Thomas,感谢您的回复。正如您在回复中提到的,我无法从我在视图模型中创建的工作线程修改绑定的 ObservableCollection。我使用了链接中提供的方法,但是该集合没有反映在 UI 上,但是这次我没有遇到任何异常。关于我会错过什么的任何指示?如果我必须通过 UI 线程本身来完成,我如何在 View 模型类中获取 UI 线程的句柄?请推荐,谢谢
      • 输出窗口是否有绑定错误?如果正在检查集合中更改的项目值,即修改值而不是更改集合,则应通过 INotifyPropertyChanged 接口的 PropertyChanged 通知它们。
      • 不,我不是检查修改后的值,而是检查集合本身的变化,但它仍然不起作用:(...任何线索?谢谢
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-11-26
      • 1970-01-01
      • 1970-01-01
      • 2011-10-18
      • 1970-01-01
      • 2011-06-09
      相关资源
      最近更新 更多