使用Dispatcher在WPF列表框中异步加载项列表

使用Dispatcher在WPF列表框中异步加载项列表,wpf,mvvm,dispatcher,Wpf,Mvvm,Dispatcher,我正在创建一个WPF解决方案,它使用MVVM模式异步加载搜索控件中的搜索项。搜索控件是一个WPF用户控件,它创建时带有一个文本框,用于输入搜索文本和搜索按钮,以及一个隐藏的列表框,当加载其中的搜索项列表时,该列表框将可见。该用户控件反过来嵌入到另一个WPF视图中,该视图具有特定项的树状视图。此视图有一个视图模型,其中加载树视图的搜索项的逻辑将加载到搜索控件中。一直以来,这都是同步发生的,没有使用任何调度程序调用。但是,在一个变更请求之后,我希望使用Dispatcher在不同的线程中异步实现这一点

我正在创建一个WPF解决方案,它使用MVVM模式异步加载搜索控件中的搜索项。搜索控件是一个WPF用户控件,它创建时带有一个文本框,用于输入搜索文本和搜索按钮,以及一个隐藏的列表框,当加载其中的搜索项列表时,该列表框将可见。该用户控件反过来嵌入到另一个WPF视图中,该视图具有特定项的树状视图。此视图有一个视图模型,其中加载树视图的搜索项的逻辑将加载到搜索控件中。一直以来,这都是同步发生的,没有使用任何调度程序调用。但是,在一个变更请求之后,我希望使用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中,我将搜索控件的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>
谢谢,
Sowmya

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


如果您到目前为止还没有认真处理多线程问题,我会给您最好的建议是收集尽可能多的信息,因为,正如到目前为止已经说过无数次的那样,多线程是很难的!:

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

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


不起作用的是从工作线程修改绑定的ObservableCollection:您需要使用Dispatcher.Invoke从UI线程执行此操作。您还可以使用。

根据需要进行修改。”Items’只是从VM公开的字符串的一个可观察集合

    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}/>

下面是一个简单的实现,演示了如何在DoWork运行时使用BackgroundWorker更新UI线程上的对象-在本例中,UI中有一个绑定到FilteredItems的列表框,ItemsSource是IEnumerable类型的UserControl的一个属性:


请注意,每次找到一个项时调用ReportProgress效率很低,因为您使用调用来编组跨线程找到的每个项。根据过滤实际花费的时间,最好累积一组结果并将列表传递给bw_ReportProgress,而不仅仅是一个对象。

嗨,Thomas,感谢您的回复。正如您在回复中提到的,我无法从我在视图模型中创建的工作线程修改绑定的ObservableCollection。我使用了链接中提供的方法,但是集合没有反映在UI上,但是这次我没有得到任何异常。有没有关于我会错过什么的建议?如果必须通过UI线程本身来完成,那么如何在视图模型类中获得UI线程的句柄?请建议,谢谢输出窗口中是否存在任何绑定错误?如果您正在检查集合中更改的项值,即修改的值而不是更改的集合,则应通过INotifyPropertyChanged接口的PropertyChanged通知他们。不,我不是在检查修改的值,而是检查集合本身的更改,但仍然不起作用:…有任何线索吗?感谢分享示例代码段,但我想知道Application.Current.Dispatcher是否会让后台工作进程去更新UI中的SearchControl?由于我使用上述实现在SearchControl中没有找到任何结果,尽管Items集合在xaml文件中有搜索控件绑定到的数据。您能告诉我哪里出了问题吗?
我这样做是为了在我的代码中加载大量的图像,而且效果很好。唯一的区别是我在绑定中设置了IsAsync=True,但这是出于其他原因。我认为这不适用于你的情况。我认为您应该向我们展示您的代码,有人可以提供帮助。Items={Binding CatalogSearchResults}是否应该是ItemsSource={Binding CatalogSearchResults}?Items是我在usercontrol SearchControl、public ICollection Items{get{return ICollectionGetValueItemsProperty;}set{SetValueItemsProperty,value;}}public static readonly DependencyProperty ItemsProperty=DependencyProperty.RegisterItems,TypeOfCollection,typeofSearchControl,new UIPropertyMetadata;是因为Items是在用户控件中定义的ICollection依赖属性吗?感谢Robert提供的示例代码:!!它像charm now一样工作,是唯一一个我从现有的实现中得出的结论是,我将用户控件中的Items属性从ICollection修改为IEnumerable,并且工作得非常完美!!我想知道是什么原因使异步操作现在能够与项的数据类型的更改一起工作?你能解释一下吗?我很困惑。你使用的是我的示例吗发布?是的,我使用了您发布的代码。在您的帖子中,正如您提到的ItemSource是IEnumerable,我想将用户控件中的Items属性从ICollection更改为IEnumerable。除此之外,我还必须再做一次更改,我忘了在上面的回复中提到,我删除了一行将布尔值设置为t的代码rue基于items集合中的项数。由于IEnumerable不支持Count属性,我删除了该行代码,然后它就工作了。因此,我假设它在我将其更改为IEnumerable时工作。每当集合中没有项数时,代码中就会有一个布尔变量设置为false。如果此变量为false,则“未找到结果”标签显示在列表框的位置。这是代码锁this{ExecuteCommand;HasItemsInResult=false;if null!=Items{HasItemsInResult=Items.Count!=0;}IsItemsVisible=true;}在使用backgroundworker或dispatcher时,总是Items.Count为零,因此尽管UI线程在布尔变量设置为false后使用结果集进行了更新,但始终未显示任何结果,这使我认为列表框没有更新:…当我将其更改为IEnumerable时,我忽略了HasItemsInResultt=Items.Count!=0;此代码行并使其成为真,即使Items不为null,因此列表在UI中更新。。。
    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);
        }
    }