Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Update an ObservableCollection with a background worker in MVVM

Ok, I recently implemented a background worker to perform saving and loading of data.

However, getting this to work on a save command has proved difficult.

Basically, my save command generates an event, that notifies a collection view model, that an Item has been added and that the item should be added to its own ObservableCollection.

At this point, I get the usual exception saying I can NOT update an ICollection on a different thread. I have tried creating a new list type that calls Dispatcher.Invoke, however this still generates the same exception.

I was wondering whether anyone else has any suggestions on how best to tackle this?

So currently I have a class that Inherits from ObservableCollection:

public class ThreadSafeObservableCollection<T> : ObservableCollection<T>
{
    public ThreadSafeObservableCollection(List<T> collection)
        : base(collection)
    {
        dispatcher = Dispatcher.CurrentDispatcher;
        rwLock = new ReaderWriterLock();
    }

    protected override void InsertItem(int index, T item)
    {
        if (dispatcher.CheckAccess())
        {
            if (index > this.Count)
                return;
            LockCookie c = rwLock.UpgradeToWriterLock(-1);
            base.InsertItem(index, item);
            rwLock.DowngradeFromWriterLock(ref c);
        }
        else
        {
            object[] obj = new object[] { index, item };
            dispatcher.Invoke(
                DispatcherPriority.Send, 
                (SendOrPostCallback)delegate { InsertItemImpl(obj); }, 
                obj);
        }
    }

I then have a view model class that has a background worker which carries out the save.

Once the save is complete, an event is fired to another view model to update its list.

    protected override void OnObjectAddedToRepository(object sender, ObjectEventArgs<cdAdministrators> e)
    {
        Dispatcher x = Dispatcher.CurrentDispatcher;
        var viewModel = new AdministratorViewModel(e.EventObject, DataAccess);
        viewModel.RecentlyAdded = true;
        viewModel.ItemSelected += this.OnItemSelected;
        this.AllViewModels.Add(viewModel);
        RecentlyAddedViewModel = viewModel;

        OnPropertyChanged(null);
    }

Both lists are created by a separate background worker thread.

like image 883
jpgooner Avatar asked Sep 02 '10 15:09

jpgooner


2 Answers

Where you've got code which adds the item to the observable collection (presumably in the view model), wrap that Add call in a Dispatcher.BeginInvoke call.

Admittedly that means the view model needs to know about the dispatcher, which then becomes awkward to test... fortunately it's not too hard to introduce your own IDispatcher interface and use dependency injection in the normal way.

like image 167
Jon Skeet Avatar answered Oct 24 '22 00:10

Jon Skeet


How about this?

public class ThreadSafeObservableCollection<T> : ObservableCollection<T>
{
    private SynchronizationContext SynchronizationContext;

    public ThreadSafeObservableCollection()
    {
        SynchronizationContext = SynchronizationContext.Current;

        // current synchronization context will be null if we're not in UI Thread
        if (SynchronizationContext == null)
            throw new InvalidOperationException("This collection must be instantiated from UI Thread, if not, you have to pass SynchronizationContext to con                                structor.");
    }

    public ThreadSafeObservableCollection(SynchronizationContext synchronizationContext)
    {
        if (synchronizationContext == null)
            throw new ArgumentNullException("synchronizationContext");

        this.SynchronizationContext = synchronizationContext;
    }

    protected override void ClearItems()
    {
        this.SynchronizationContext.Send(new SendOrPostCallback((param) => base.ClearItems()), null);
    }

    protected override void InsertItem(int index, T item)
    {
        this.SynchronizationContext.Send(new SendOrPostCallback((param) => base.InsertItem(index, item)), null);
    }

    protected override void RemoveItem(int index)
    {
        this.SynchronizationContext.Send(new SendOrPostCallback((param) => base.RemoveItem(index)), null);
    }

    protected override void SetItem(int index, T item)
    {
        this.SynchronizationContext.Send(new SendOrPostCallback((param) => base.SetItem(index, item)), null);
    }

    protected override void MoveItem(int oldIndex, int newIndex)
    {
        this.SynchronizationContext.Send(new SendOrPostCallback((param) => base.MoveItem(oldIndex, newIndex)), null);
    }
}
like image 3
Brent Beardsley Avatar answered Oct 23 '22 23:10

Brent Beardsley