simonbs
simonbs

Reputation: 8042

Using ObservableCollection on a new thread

A few days ago I created this thread because I was unable to update an ObservableCollection from another thread. This was the solution from the thread:

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

However, it seems that this is not really the solution as the UI still freezes while doing executing the delegate. My guess is that the above does not really run anything on another thread but instead dispatches it all to the UI thread. So what I really want to do is to create a new thread myself and do the loading (this happens in theTvdb.SearchSeries()). Then I will iterate over the results and add those to my ObservableCollection and this must happen on the UI thread.

Does this approach sound right?

I came up with the below which I thought would load the results and add those to the ObervableCollection and show them in my list view without the UI freezing.

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();

The above does not produce any error. Instead nothing happens. The UI does not freeze but it is not updated. The objects in Overview is not presented in the UI and I have tested that the binding is correct. The objects will show correctly if I don't load them and add them to the ObservableCollection on another thread.

Another solution I have tried is to use the MTObservableCollection from this answer to a similar question. When using that subclass of the ObservableCollection, I did not dispatch anything myself. This gave me the following error:

Must create DependencySource on same Thread as the DependencyObject.

Can anybody please tell me how I can:

  1. Load something on a separate thread
  2. Use the results from step 1 to update an ObservableCollection which is bound to a listview
  3. Have the results shown in the UI without the UI freezing

I hope you can help me further.

UPDATE:

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

Upvotes: 3

Views: 3454

Answers (3)

Louis Kottmann
Louis Kottmann

Reputation: 16618

You could simply do:

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);
        }
    }));
});

Upvotes: 0

Nick
Nick

Reputation: 2325

The approach of multi-threading within any 'heavy' work within an application you want to keep responsive is the right way of thinking about it, so you're on the right track.

However, whilst you are creating and working with other threads here, you are still relying on the Dispatcher too much. Consider that, with multi-threading here, your process should be as follows:

  1. Do the heavy lifting on a separate thread.
  2. Once done, ask the Dispatcher to update the UI as necessary.

This mimimises the load on the Dispatcher.

Have you considered using Tasks? They are great from a 'clean code' point of view, but are applicable here because with Task Continuation, you can chain Tasks together to invoke the relevant code on the UI once the heavy work is complete on its thread.

Take a look at the answer here for a good start.

I'll happily provide a more detailed example if you need it after that.

EDIT: As mentioned in another answer, BackgroundWorker is just as effective here... and the end result is exactly the same from a threading perspective. I just like the Task syntax!

EDIT: Just thought I'd provide some code. I'll avoid continuation for simplicity at the moment. Consider the following method which would do your heavy lifting:

    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);
    }

Then for your UI (in your ViewModel for example), you can both call and handle the callback. When needed, call the 'heavy lifting' and pass in your callback:

HeavyLifting(this.HandleHeavyLiftingCompleted);

And then, you method you pass as the callback gets executed upon task completion. Note that here is where I'm asking the Dispatcher to do the work:

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

Note that in this case the UI work involved is updating an ObvservableCollection to which I'm bound from the View. For the example here I'm using a random 'Item' object that can be whatever you like!

I'm using Cinch, and therefore relying on a service to obtain the relevant Dispatcher (which you see as this._uiDispatcher here). In your case you can get a reference to it using the methods mentioned in other questions here.

Also, if you have the time to read, there is some great information here regarding the WPF threading model.

Upvotes: 1

dowhilefor
dowhilefor

Reputation: 11051

Your approach is dangerous, pushing alot of jobs on the dispatcher in a very short amount of time can stall or freeze your application. While your general approach is fine, you might want to consider using batches of adding elements to your list.

Also you can't use Dispatcher.CurrentDispatcher because you are now using the dispatcher of the current thread. Thus you are asking your thread, to handle the adding in the same thread, not the ui thread. You need to get the dispatcher from the ui thread. You can use the Application object for example.

I also advice you to use BackgroundWorker in my experience it works a bit nicer in WPF than just plain threads.

Upvotes: 0

Related Questions