3

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>
Community
  • 1
  • 1
simonbs
  • 7,932
  • 13
  • 69
  • 115
  • What you might find is that - no matter what, if the observable collection contains a lot of items and your UI is complex, the UI freezes anyway - are we talking WPF/SL here or Winforms? – Charleh Nov 05 '12 at 10:24
  • Right now it doesn't really contain a lot of elements. I don't know how many but it's not more than like 10. I'm using WPF. – simonbs Nov 05 '12 at 10:25
  • Your threading code looks ok - you create a new thread and start a pretty simple operation on it, can you post your XAML code? I'm assuming your DB is on another box, or are you running everything on your local machine? – Charleh Nov 05 '12 at 10:28
  • I have updated my question to show the XAML used for the view. I load the data through the API from [TheTVDB](http://thetvdb.com/) so it's external. – simonbs Nov 05 '12 at 10:36

3 Answers3

1

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.

Community
  • 1
  • 1
Nick
  • 2,285
  • 2
  • 14
  • 26
  • I will definitely give the Task a try. The BackgroundWorker on the other hand does not seem to be a solution for this as I cannot set the background worker to STA which is required for some image conversion I use. However, the image conversion really seemed to be the problem. I created bitmaps on another thread. Calling `Freeze()` on these bitmaps solved the issue. – simonbs Nov 05 '12 at 11:10
  • Good stuff Simon. I'll add a bit of code for reference for use of Task anyway... good luck. – Nick Nov 05 '12 at 12:26
0

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.

dowhilefor
  • 10,971
  • 3
  • 28
  • 45
  • When using the dispatcher retrieved from `Application.Current.Dispatcher` instead of `Dispatcher.Current`, I will get the following error: `Must create DependencySource on same Thread as the DependencyObject.` Do you know why this happens? Regarding the BackgroundWorker, that's not an option it's not possible to set it to STA and it is required for some image conversion I use. – simonbs Nov 05 '12 at 10:49
  • The error seemed to occur because I was creating bitmaps on another thread. Calling `Freeze()` on the bitmap (e.g. `myBitmap.Freeze()`) solved the error and now it seems to work. – simonbs Nov 05 '12 at 11:09
  • 1
    Well this sounds like you create something on another thread which should be the ui thread. Are you creating any ui elements on a thread? Which thread is the owner of the collection? – dowhilefor Nov 05 '12 at 11:09
  • 1
    Yes Bitmaps must be frozen to be used in another thread than the thread that created them. – dowhilefor Nov 05 '12 at 11:10
0

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);
        }
    }));
});
Louis Kottmann
  • 16,268
  • 4
  • 64
  • 88