在新线程上使用ObservableCollection

3

几天前,我创建了这个帖子,因为我无法从另一个线程更新ObservableCollection。

以下是该帖子中的解决方案:

Dispatcher.CurrentDispatcher.BeginInvoke(new Action(delegate
{
    TheTVDB theTvdb = new TheTVDB();
    foreach (TVSeries tvSeries in theTvdb.SearchSeries("Dexter"))
    {
        this.Overview.Add(tvSeries);
    }
}),
DispatcherPriority.Background);

然而,这似乎并不是解决方案,因为在执行委托时UI仍然会冻结。我猜想上述内容并没有在另一个线程上运行,而是将所有内容都分派到了UI线程上。因此,我真正想做的是自己创建一个新线程,并在其中执行加载(这发生在theTvdb.SearchSeries()中)。然后我会遍历结果,并将它们添加到我的ObservableCollection中,这必须在UI线程上完成。
这种方法听起来正确吗?
我想出了下面的代码,我认为它可以加载结果、将其添加到ObservableCollection并在列表视图中显示,而不会使UI冻结。
Thread thread = new Thread(new ThreadStart(delegate
{
    TheTVDB theTvdb = new TheTVDB();
    List<TVSeries> dexter = theTvdb.SearchSeries("Dexter");

    foreach (TVSeries tvSeries in dexter)
    {
        Dispatcher.CurrentDispatcher.BeginInvoke(new Action(delegate
        {
            this.Overview.Add(tvSeries);
        }),
        DispatcherPriority.Normal);
    }
}));
thread.SetApartmentState(ApartmentState.STA);
thread.Start();

上述代码不会产生任何错误,但也不会有任何反应。UI 不会冻结,但也不会更新。在 Overview 中的对象没有显示在 UI 中,我已经测试了绑定是正确的。如果我不在另一个线程上加载它们并将它们添加到 ObservableCollection 中,则对象将正确显示。
我尝试过的另一个解决方案是使用这个答案中的 MTObservableCollection。当使用该 ObservableCollection 的子类时,我没有自己分发任何内容。这给了我以下错误:

必须在与 DependencyObject 相同的线程上创建 DependencySource。

请问有谁能告诉我如何:
  1. 在单独的线程上加载一些东西
  2. 使用步骤 1 的结果更新绑定到列表视图的 ObservableCollection
  3. 在 UI 中显示结果而不冻结 UI
希望你能帮助我进一步。
更新:
<UserControl
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:acb="clr-namespace:AttachedCommandBehavior"
    mc:Ignorable="d"
    x:Class="TVSeriesLibrary.OverviewView"
    x:Name="UserControl"
    d:DesignWidth="512"
    d:DesignHeight="480">

    <UserControl.Resources>
        <DataTemplate x:Key="CoverTemplate">
            <StackPanel Orientation="Horizontal">
                <Image Width="82" Height="85" Stretch="Fill" Source="{Binding Cover}" Margin="10,10,0,10"/>
            </StackPanel>
        </DataTemplate>
    </UserControl.Resources>

    <Grid x:Name="LayoutRoot" Background="#515050">
        <Grid.Resources>
            <ResourceDictionary>
                <Style x:Key="ItemContStyle" TargetType="{x:Type ListViewItem}">
                    <Setter Property="Background" Value="#282828" />
                    <Setter Property="Margin" Value="0,0,0,5" />
                    <Setter Property="Padding" Value="0" />
                </Style>
            </ResourceDictionary>
        </Grid.Resources>

        <ListView Height="112"
                  Width="488"
                  Margin="12,150,12,218"
                  Foreground="#ffffff"
                  Background="#515050"
                  VerticalContentAlignment="Center"
                  BorderThickness="0"
                  ItemTemplate="{StaticResource CoverTemplate}"
                  ItemsSource="{Binding Overview}">
            <ListView.ItemsPanel>
                <ItemsPanelTemplate>
                    <WrapPanel Orientation="Horizontal" />
                </ItemsPanelTemplate>
            </ListView.ItemsPanel>
        </ListView>

        <ListView Height="170"
                  Margin="10,298,10,0"
                  VerticalAlignment="Center"
                  Foreground="#ffffff"
                  Background="#515050"
                  VerticalContentAlignment="Center"
                  BorderThickness="0"
                  Width="488" ScrollViewer.HorizontalScrollBarVisibility="Disabled"
                  ItemsSource="{Binding Path=Overview}"
                  SelectedItem="{Binding Path=SelectedTVSeries}"
                  ItemContainerStyle="{StaticResource ItemContStyle}">
            <ListView.Resources>
                <ResourceDictionary>
                    <Style x:Key="hiddenStyle" TargetType="GridViewColumnHeader">
                        <Setter Property="Visibility" Value="Collapsed"/>
                    </Style>
                </ResourceDictionary>
            </ListView.Resources>
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Cover" Width="auto" HeaderContainerStyle="{StaticResource hiddenStyle}">
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <Image Source="{Binding Path=Cover}" Height="50" Margin="-6,0,0,0" />
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>


                    <GridViewColumn Header="Title" Width="200" HeaderContainerStyle="{StaticResource hiddenStyle}">
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <TextBlock Text="{Binding Path=Name}" FontWeight="Bold"></TextBlock>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>

                    <GridViewColumn Header="Year" Width="100" HeaderContainerStyle="{StaticResource hiddenStyle}">
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <TextBlock Text="{Binding Path=DisplayYear}"></TextBlock>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>

                    <GridViewColumn Header="Button" Width="135" HeaderContainerStyle="{StaticResource hiddenStyle}">
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <Button Content="Details" Width="100" Height="20" />
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>

                </GridView>
            </ListView.View>
        </ListView>
    </Grid>

</UserControl>

你可能会发现的是 - 无论如何,如果可观察集合包含大量项目并且您的UI很复杂,UI仍然会冻结 - 我们在谈论WPF / SL还是Winforms? - Charleh
现在它实际上并不包含很多元素。我不知道有多少个,但肯定不超过10个。我正在使用WPF。 - simonbs
你的线程代码看起来不错 - 你创建了一个新线程并在其上启动了一个相当简单的操作,你能发布一下你的XAML代码吗?我假设你的数据库在另一台机器上,或者你是在本地机器上运行所有东西? - Charleh
我已经更新了我的问题,展示了视图所使用的XAML。我通过来自TheTVDB的API加载数据,因此它是外部的。 - simonbs
3个回答

1

在应用程序中,如果您想保持响应性,那么多线程处理任何“繁重”的工作的方法是正确的思路,因此您正在正确的轨道上。

不过,在创建和使用其他线程时,您仍然过度依赖 Dispatcher。请考虑,使用多线程时,您的处理过程应遵循以下步骤:

  1. 在单独的线程上执行耗时操作。
  2. 完成后,请求 Dispatcher 根据需要更新 UI。

这样可以减少对 Dispatcher 的负载。

您是否考虑使用任务?它们从“清晰代码”角度来看非常好,但也适用于此处,因为通过任务连续性,您可以将任务链接在一起,在其线程上完成繁重工作后,一次调用相关代码以更新 UI。

参考此处的答案,这是一个很好的开始。

如果您在那之后需要更详细的示例,我很乐意提供。

编辑:如其他答案中所述,BackgroundWorker 在此处同样有效...从线程角度来看,最终结果完全相同。我只是喜欢任务语法!

编辑:我想提供一些代码。为了简单起见,我将避免继续。考虑以下方法,它将完成您的繁重工作:

    public void HeavyLifting(Action<List<Items>> callback)
    {
        Task<List<Items>> task = Task.Factory.StartNew(
            () =>
                {
                    var myResults = new List<Items>();

                    // do the heavy stuff.

                    return myResults;
                });

        callback.Invoke(task.Result);
    }

然后对于您的UI(例如在ViewModel中),您可以同时调用和处理回调。需要时,调用“繁重的工作”并传递您的回调:

HeavyLifting(this.HandleHeavyLiftingCompleted);

然后,你传递作为回调的方法会在任务完成时执行。请注意,这里是我要求调度程序执行工作的地方:

private void HandleHeavyLiftingCompleted(List<Items> results)
{
    this._uiDispatcher.BeginInvoke(
        new Action(() => { this.MyItems = new ObservableCollection<Items>(results); }));
}

请注意,在这种情况下,涉及的UI工作是从视图更新一个ObservableCollection,我将其绑定到其中。在这里,我使用一个随机的“Item”对象作为示例,它可以是任何你喜欢的东西!
我正在使用Cinch,并因此依赖于服务来获取相关的Dispatcher(您在此处看到的是this._uiDispatcher)。在您的情况下,您可以使用其他问题中提到的方法来获取对它的引用。
另外,如果您有时间阅读,这里有关于WPF线程模型的一些很棒的信息。

我一定会尝试这个任务。然而,BackgroundWorker似乎不是解决方案,因为我无法将后台工作器设置为STA,而我使用的某些图像转换需要它。然而,图像转换似乎真的是问题所在。我在另一个线程上创建了位图。在这些位图上调用Freeze()解决了问题。 - simonbs
很好的东西,Simon。我会添加一些代码作为使用任务的参考...祝你好运。 - Nick

0
你可以简单地这样做:
Task.Factory.StartNew(() => 
{
    var theTvdb = new TheTVDB();
    var dexterSeries = theTvdb.SearchSeries("Dexter");
    Application.Current.Dispatcher.Invoke(new Action(() => 
    {    
        foreach (var tvSeries in dexterSeries)
        {
            this.Overview.Add(tvSeries);
        }
    }));
});

0

你的方法很危险,短时间内向调度程序推送大量作业可能会使应用程序停滞或冻结。虽然你的一般方法是正确的,但你可能想考虑使用批处理将元素添加到列表中。

此外,你不能使用 Dispatcher.CurrentDispatcher,因为你现在正在使用当前线程的调度程序。因此,你要求你的线程在同一线程中处理添加操作,而不是 ui 线程。你需要从 ui 线程获取调度程序。例如,你可以使用 Application 对象。

我还建议你使用 BackgroundWorker,在我的经验中,它比普通线程更好地适用于 WPF。


当使用从Application.Current.Dispatcher检索到的调度程序而不是Dispatcher.Current时,我会收到以下错误:必须在与DependencyObject相同的线程上创建DependencySource。您知道为什么会发生这种情况吗?关于BackgroundWorker,这不是一个选项,无法将其设置为STA,但对于我使用的某些图像转换是必需的。 - simonbs
错误似乎是因为我在另一个线程上创建了位图。在位图上调用Freeze()方法(例如,myBitmap.Freeze())解决了这个错误,现在似乎它可以正常工作了。 - simonbs
1
这听起来像是你在另一个线程上创建了一些应该在 UI 线程上的东西。你是否在另一个线程上创建任何 UI 元素?哪个线程是集合的所有者? - dowhilefor
1
是的,位图必须被冻结才能在创建它们的线程之外的另一个线程中使用。 - dowhilefor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接