Search code examples
c#wpfcaliburn.microtaskfactory

WPF Updating ObservableList from async task throws XamlParseException


When i try to update a ObservableCollection that i use in my XAML from a separate thread then the ui thread, i get a XamlParseException which says that the DependencySource must be created on the same Thread as the DependencyObject. I'm using Caliurn Micro to bind the ViewModel to the View.

I tried a few ways to reach my goal, and the one below seems to be the most reasonable approach for me. I'm passing the SyncronizationContext from the UI to the task so it can update the ui after doing the heavy workload.

What am i doing wrong?

View

<Window x:Class="QuickScope.Views.NavigatorView"
    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:local="clr-namespace:QuickScope.Views"
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
    xmlns:quickScope="clr-namespace:QuickScope"
    mc:Ignorable="d">
<Grid>
    <TextBox Grid.Row="0" 
             Name="TextBox"
             HorizontalContentAlignment="Stretch"
             VerticalContentAlignment="Center"
             Margin="5,0,5,5"
             Text="{Binding SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
    </TextBox>
    <Popup PlacementTarget="{Binding ElementName=TextBox}">
        <ListView x:Name="ItemList" ItemsSource="{Binding Items}" SelectedItem ="{Binding SelectedItem}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <WrapPanel>
                        <Image Source="{Binding IconSource}" Width="20" Height="20"></Image>
                        <Label Content="{Binding SearchName}"></Label>
                    </WrapPanel>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Popup>
</Grid>

ViewModel

using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Threading;
using Caliburn.Micro;
using QuickScope.Views;

namespace QuickScope.ViewModels
{
public class NavigatorViewModel : Screen
{
    private readonly ItemService _itemService;

    public NavigatorViewModel()
    {
        _itemService = new ItemService();
        Items = new ObservableCollection<ItemViewModel>();

    }

    public ObservableCollection<ItemViewModel> Items { get; }

    public ItemViewModel SelectedItem { get; set; }

    private string _searchText;

    public string SearchText
    {
        get => _searchText;
        set
        {
            _searchText = value;
            NotifyOfPropertyChange(() => SearchText);
            UpdateItemList(_searchText);
        }
    }

    private void UpdateItemList(string searchText)
    {
        Items.Clear();

        var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();

        Task.Factory.StartNew(() =>
        {
            //the long running workload
            var items = _itemService.GetByFilter(searchText);

            //update the ui
            Task.Factory.StartNew(() =>
            {
                foreach (var item in items)
                    Items.Add(item);

                if (items.Any())
                {
                    SelectedItem = items.First();
                    NotifyOfPropertyChange(() => SelectedItem);
                }
            }, CancellationToken.None, TaskCreationOptions.None, uiScheduler);
        });
    }
}
}

EDIT

I tried Shadrix's approach, but unfortunately it didn't work either. I also tried the answers from Peter Dunihos' marked duplicate but i didn't succeeded either(i get the same XamlParseException as descriped above).

The last thing i tried is CM's build in OnUIThread method, which basically wraps the Dispatcher.CurrentDispatcher. Obviously (considering my last two failed attempts), it failed too. The current implementation looks something like the following (i removed other, non relevant properties and methods):

public class NavigatorViewModel : Screen
{
public NavigatorViewModel()
{
    Items = new ObservableCollection<ItemViewModel>();
}

public ObservableCollection<ItemViewModel> Items { get; set; }

public ItemViewModel SelectedItem { get; set; }

private string _searchText;

public string SearchText
{
    get => _searchText;
    set
    {
        _searchText = value;
        NotifyOfPropertyChange(() => SearchText);
        UpdateItemList(_searchText);
    }
}

private void UpdateItemList(string searchText)
{
    Items.Clear();

    var updater = new ItemUpdater();
    updater.ItemsUpdated += (s, e) => {
        OnUIThread(() =>
        {
            var items = ((ItemsUpdatedEventArgs) e).Items;
            foreach (var item in items)
            {
                Items.Add(item);
            }
            NotifyOfPropertyChange(() => Items);
        });
    };
    var updateThread = new Thread(updater.GetItems);
    updateThread.Start(searchText);
}
}

public class ItemUpdater
{
public event EventHandler ItemsUpdated;
private readonly ItemService _itemService;

public ItemUpdater()
{
    _itemService = new ItemService();
}

public void GetItems(object searchText)
{
    var items = _itemService.GetByFilter((string)searchText);

    ItemsUpdated?.Invoke(this, new ItemsUpdatedEventArgs(items));
}
}

public class ItemsUpdatedEventArgs : EventArgs
{
public ItemsUpdatedEventArgs(IList<ItemViewModel> items)
{
    Items = items;
}

public IList<ItemViewModel> Items { get; }
}

It's driving me nuts that i'm not able to solve this problem, so if there's anyone out there that would like to help a young junior, i would highly apprechiate it. :)

You can find the complete source code here.

Thank you all!


Solution

  • Use the current UI thread's Dispatcher:

    //update the ui
    Application.Current.Dispatcher.BeginInvoke(new Action(() =>
    {
        foreach (var item in items)
        {
            Items.Add(item);
        }
    
        if (items.Any())
        {
            SelectedItem = items.First();
            NotifyOfPropertyChange(() => SelectedItem);
        }
    }));