2

In my WPF project. I have:

public partial class MainWindow : Window
{
    ObservableCollection<Calls> items = new ObservableCollection<Calls>();
public MainWindow()
{
    InitializeComponent();
    icTodoList.ItemsSource = items;
    this.DataContext = new MainViewModel();
}

icTodoList is an ItemsControl, I want to add two columns for it.

<DockPanel>
<ItemsControl Height="300" Name="icTodoList" ItemsSource="{Binding Calls}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="60"/>
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>

                <TextBlock Text="{Binding DialingNumber, Mode=OneWay, FallbackValue=' '}" Grid.Column="0"/>
                <TextBlock Text="{Binding DialResult, Mode=OneWay, FallbackValue=' '}" Grid.Column="1"/>
            </Grid>
        </DataTemplate>

    </ItemsControl.ItemTemplate>
</ItemsControl></DockPanel>

As for Calls class, we have

public class Calls : NotifyUIBase
{
    private string dialingNumber;
    public string DialingNumber
    {
        get { return dialingNumber; }
        set
        {
            dialingNumber = value;
            RaisePropertyChanged();
        }
    }
    public string dialResult;
    public string DialResult {
        get { return dialResult; }
        set
        {
            dialResult = value;
            RaisePropertyChanged();
        }
    }
}

NotifyUIBase inherited from INotifyPropertyChanged, it contains RaisePropertyChanged property, ignore writing here for saving space.

Now I have a producer-consumer process by clicking Start button.

    private async void Start_Click(object sender, RoutedEventArgs e)
    {
        var producer = Producer();
        var consumer = Consumer();
        await Task.WhenAll(producer, consumer);
    }

In consumer method, we update the ItemsControl.

async Task Consumer()
{
    try
    {
        var executionDataflowBlockOptions = new ExecutionDataflowBlockOptions
        {
            MaxDegreeOfParallelism = 50
        };
        var consumerBlock = new ActionBlock<AppointmentReminder>(
remainder =>
{
   Calls c = new Calls();
   c.DialingNumber = "number..";
   c.DialResult = "result...;
   items.Add(c);
},
executionDataflowBlockOptions);
 bufferBlock.LinkTo(
 consumerBlock, new DataflowLinkOptions { PropagateCompletion = true });
                await Task.Delay(500);
          }

However I got an exception:

This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread.

It happened at the line:

items.Add(c);

I guess it is the thread issue, so how to fix it?

  • possible duplicate of [This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread](http://stackoverflow.com/questions/18331723/this-type-of-collectionview-does-not-support-changes-to-its-sourcecollection-fro) – i3arnon Oct 22 '14 at 16:09
  • https://searchcode.com/codesearch/view/29597052/ – eran otzap Oct 22 '14 at 16:24
  • You don't have to use ObservableCollection, WPF can bind to the concurrent collections. You just need to raise the NotifyPropertyChanged event when you finish processing items, or in specific inverals. This will also reduce the number of UI updates if there are a lot of calls that take a short time to process. – Panagiotis Kanavos Oct 23 '14 at 07:16
  • @PanagiotisKanavos, if you could provide code snippet, it would be great. Because I didn't follow you. –  Oct 23 '14 at 12:57

3 Answers3

4

You are already passing some options to your ActionBlock; you can easily specify the UI scheduler there:

var executionDataflowBlockOptions = new ExecutionDataflowBlockOptions
{
  TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext(),
};

Note that it no longer functions in parallel. If your action code is simple (creating a single object and setting a couple properties), this should be sufficient. However, if the object creation is CPU-intensive, you may want to keep the parallelism:

var transformOptions = new ExecutionDataflowBlockOptions
{
  MaxDegreeOfParallelism = 50
};
var actionOptions = new ExecutionDataflowBlockOptions
{
  TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext(),
};
var transformBlock = new TransformBlock<AppointmentReminder, Calls>(reminder =>
{
  Calls c = new Calls();
  c.DialingNumber = "number..";
  c.DialResult = "result...;
  return c;
}, transformOptions);
var consumerBlock = new ActionBlock<Calls>(c =>
{
  items.Add(c);
}, actionOptions);
var linkOptions = new DataflowLinkOptions { PropagateCompletion = true };
bufferBlock.LinkTo(transformBlock, linkOptions);
transformBlock.LinkTo(consumerBlock, linkOptions);
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • @@Stephen, why we can't place `TaskScheduler` and `MaxDegreeOfParallelism` together rather than using `TransformBlock`? –  Oct 22 '14 at 20:44
  • 1
    @Love: The `TaskScheduler` restricts it to a single item at a time (because it's the UI thread). – Stephen Cleary Oct 22 '14 at 22:52
1

Simply get your items.Add call executed on the UI thread:

remainder =>
{
   Calls c = new Calls();
   c.DialingNumber = "number..";
   c.DialResult = "result...;
   Dispatcher.Invoke(()=> // Get the dispatcher from your window and use it here
   {
       items.Add(c);
   }
},
Ronan Thibaudau
  • 3,413
  • 3
  • 29
  • 78
  • 3
    Avoid using `Dispatcher.Invoke` when possible; it's very easy to create deadlocks. Better to use `Dispatcher.BeginInvoke` when possible. – Mike Strobel Oct 22 '14 at 16:37
0

Check out the Asynchronous Multi-threaded ObservableCollection over on CP.

I use it in all my projects and never have any problems adding items or range of items from multiple TPL threads...

Dean Kuga
  • 11,878
  • 8
  • 54
  • 108