1
votes

I have a derived ObservableCollection class called ItemsObservableObservableCollection. This was created because I want to bind it and get notified when properties within its items got changed.

When updating an item's properties from a background thread it fails. I would like some help to solve this.

This project has two collections to compare behaviour, the first set is type ObservableCollection and the second is ItemsObservableObservableCollection.

MainWindow.xaml

<Window x:Class="MultiBindTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:MultiBindTest"
        Title="Multibind Test" Height="482" Width="976">

    <Window.DataContext>
        <vm:MainViewModel />
    </Window.DataContext>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Label Content="First Set - ObservableCollection" FontWeight="Bold" Grid.Row="0" HorizontalAlignment="Center" HorizontalContentAlignment="Left" />
        <ItemsControl Grid.Row="1" Width="Auto" ItemsSource="{Binding Path=ModuleCollection1}" >
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <WrapPanel HorizontalAlignment="Stretch" Name="WrapPanel1" VerticalAlignment="Center" />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <StackPanel Margin="10">
                        <Button Content="{Binding ModuleAbbreviation}"  
                                    Background="{Binding ModuleColor}"
                                    FontSize="32" FontFamily="Tahoma" Width="130" Height="100">
                            <Button.Resources>
                                <vm:IsEnabledMultiValueConverter x:Key="converter" />
                            </Button.Resources>
                            <Button.IsEnabled>
                                <MultiBinding Converter="{StaticResource converter}">
                                    <Binding Path="ModuleID" />
                                    <Binding Path="ModuleEnabled" />
                                    <Binding Path="ModuleLicenseDate" />
                                </MultiBinding>
                            </Button.IsEnabled>
                        </Button>
                        <Label Content="{Binding ModuleName}" FontSize="18" FontWeight="Medium" HorizontalAlignment="Center" HorizontalContentAlignment="Center" Width="210" />
                    </StackPanel>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
        <Button Content="Switch the second Module Enable/Disabled in First Set" Width="400" Grid.Row="2" Command="{Binding SwitchCommand1}" />
        <Button Content="Switch the second Module Enable/Disabled in First Set, by background thread" Width="500" Grid.Row="3" Command="{Binding SwitchCommand2}" />
        <Label Content="Second Set - ItemsObservableObservableCollection" FontWeight="Bold" Grid.Row="4" HorizontalAlignment="Center" HorizontalContentAlignment="Left" />
        <ItemsControl Grid.Row="5" Width="Auto" ItemsSource="{Binding Path=ModuleCollection2}" >
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <WrapPanel HorizontalAlignment="Stretch" Name="WrapPanel1" VerticalAlignment="Center" />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <StackPanel Margin="10">
                        <Button Content="{Binding ModuleAbbreviation}"  
                                    Background="{Binding ModuleColor}"
                                    FontSize="32" FontFamily="Tahoma" Width="130" Height="100">
                            <Button.Resources>
                                <vm:IsEnabledMultiValueConverter x:Key="converter" />
                            </Button.Resources>
                            <Button.IsEnabled>
                                <MultiBinding Converter="{StaticResource converter}">
                                    <Binding Path="ModuleID" />
                                    <Binding Path="ModuleEnabled" />
                                    <Binding Path="ModuleLicenseDate" />
                                </MultiBinding>
                            </Button.IsEnabled>
                        </Button>
                        <Label Content="{Binding ModuleName}" FontSize="18" FontWeight="Medium" HorizontalAlignment="Center" HorizontalContentAlignment="Center" Width="210" />
                    </StackPanel>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
        <Button Content="Switch the second Module Enable/Disabled in Second set" Width="400" Grid.Row="6" Command="{Binding SwitchCommand3}" />
        <Button Content="Switch the second Module Enable/Disabled in Second Set, by background thread" Width="500" Grid.Row="7" Command="{Binding SwitchCommand4}" />
    </Grid>
</Window>

MainViewModel.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using IOOC;
using ModKey;
using System.Windows.Data;
using System.Globalization;
using System.Threading;
using System.Collections.ObjectModel;
using System.Windows.Input;
using System.Threading.Tasks;
using System.Diagnostics;

namespace MultiBindTest
{
    class MainViewModel
    {
        public MainViewModel()
        {
            ModuleKey.setModules();
        }

        public ObservableCollection<Module> ModuleCollection1
        {
            get { return ModuleKey.module_objects1; }
        }

        public ItemsObservableObservableCollection<Module> ModuleCollection2
        {
            get { return ModuleKey.module_objects2; }
        }


        RelayCommand switchCommand1;
        public ICommand SwitchCommand1
        {
            get
            {
                if (switchCommand1 == null)
                {
                    switchCommand1 = new RelayCommand(SwitchExecute1, CanSwitchExecute1);
                }
                return switchCommand1;
            }
        }
        private void SwitchExecute1(object parameter)
        {
            Module item1 = ModuleKey.module_objects1.FirstOrDefault(i => i.ModuleID == 1);
            if (item1.ModuleEnabled)
                item1.ModuleEnabled = false;
            else
                item1.ModuleEnabled = true;
        }
        private bool CanSwitchExecute1(object parameter)
        {
            return true;
        }


        RelayCommand switchCommand2;
        public ICommand SwitchCommand2
        {
            get
            {
                if (switchCommand2 == null)
                {
                    switchCommand2 = new RelayCommand(SwitchExecute2, CanSwitchExecute2);
                }
                return switchCommand2;
            }
        }
        private void SwitchExecute2(object parameter)
        {
            Task.Factory.StartNew(() =>
            {
                Module item = ModuleKey.module_objects1.FirstOrDefault(i => i.ModuleID == 1);
                if (item.ModuleEnabled)
                    item.ModuleEnabled = false;
                else
                    item.ModuleEnabled = true;
            });
        }
        private bool CanSwitchExecute2(object parameter)
        {
            return true;
        }


        RelayCommand switchCommand3;
        public ICommand SwitchCommand3
        {
            get
            {
                if (switchCommand3 == null)
                {
                    switchCommand3 = new RelayCommand(SwitchExecute3, CanSwitchExecute3);
                }
                return switchCommand3;
            }
        }
        private void SwitchExecute3(object parameter)
        {
            Module item = ModuleKey.module_objects2.FirstOrDefault(i => i.ModuleID == 1);
            if (item.ModuleEnabled)
                item.ModuleEnabled = false;
            else
                item.ModuleEnabled = true;
        }
        private bool CanSwitchExecute3(object parameter)
        {
            return true;
        }


        RelayCommand switchCommand4;
        public ICommand SwitchCommand4
        {
            get
            {
                if (switchCommand4 == null)
                {
                    switchCommand4 = new RelayCommand(SwitchExecute4, CanSwitchExecute4);
                }
                return switchCommand4;
            }
        }
        private void SwitchExecute4(object parameter)
        {
            Task.Factory.StartNew(() =>
            {
                Module item = ModuleKey.module_objects2.FirstOrDefault(i => i.ModuleID == 1);
                if (item.ModuleEnabled)
                    item.ModuleEnabled = false;
                else
                    item.ModuleEnabled = true;
            });
        }
        private bool CanSwitchExecute4(object parameter)
        {
            return true;
        }
    }

    public class IsEnabledMultiValueConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            try
            {
                int ModuleID = (int)values[0];
                bool ModuleEnabled = (bool)values[1];
                string ModuleLicenseDate = (string)values[2];

                DateTimeFormatInfo dtfi = new DateTimeFormatInfo();
                dtfi.ShortDatePattern = "yyyy-MM-dd";
                dtfi.DateSeparator = "-";
                DateTime MLicenseDate = System.Convert.ToDateTime(ModuleLicenseDate, dtfi);

                return (ModuleEnabled && (MLicenseDate >= DateTime.Now));
            }
            catch
            {
                return false;
            }
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

    public class RelayCommand : ICommand
    {
        #region Fields

        readonly Action<object> _execute;
        readonly Predicate<object> _canExecute;

        #endregion // Fields

        #region Constructors

        /// <summary>
        /// Creates a new command that can always execute.
        /// </summary>
        /// <param name="execute">The execution logic.</param>
        public RelayCommand(Action<object> execute)
            : this(execute, null)
        {
        }

        /// <summary>
        /// Creates a new command.
        /// </summary>
        /// <param name="execute">The execution logic.</param>
        /// <param name="canExecute">The execution status logic.</param>
        public RelayCommand(Action<object> execute, Predicate<object> canExecute)
        {
            if (execute == null)
                throw new ArgumentNullException("execute");

            _execute = execute;
            _canExecute = canExecute;
        }

        #endregion // Constructors

        #region ICommand Members

        [DebuggerStepThrough]
        public bool CanExecute(object parameter)
        {
            return _canExecute == null ? true : _canExecute(parameter);
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public void Execute(object parameter)
        {
            _execute(parameter);
        }

        #endregion // ICommand Members
    }
}

IOOC.cs

using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Collections;

namespace IOOC
{
    /// <summary>
    ///     This class adds the ability to refresh the list when any property of
    ///     the objects changes in the list which implements the INotifyPropertyChanged. 
    ///
    /// </summary>
    /// <typeparam name="T">
    ///     The type of elements in the collection.
    /// </typeparam>
    public class ItemsObservableObservableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
    {
        protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == NotifyCollectionChangedAction.Add)
            {
                RegisterPropertyChanged(e.NewItems);
            }
            else if (e.Action == NotifyCollectionChangedAction.Remove)
            {
                UnRegisterPropertyChanged(e.OldItems);
            }
            else if (e.Action == NotifyCollectionChangedAction.Replace)
            {
                UnRegisterPropertyChanged(e.OldItems);
                RegisterPropertyChanged(e.NewItems);
            }

            base.OnCollectionChanged(e);
        }

        protected override void ClearItems()
        {
            UnRegisterPropertyChanged(this);
            base.ClearItems();
        }

        private void RegisterPropertyChanged(IList items)
        {
            foreach (INotifyPropertyChanged item in items)
            {
                item.PropertyChanged += new PropertyChangedEventHandler(item_PropertyChanged);
            }
        }

        private void UnRegisterPropertyChanged(IList items)
        {
            foreach (INotifyPropertyChanged item in items)
            {
                item.PropertyChanged -= new PropertyChangedEventHandler(item_PropertyChanged);
            }
        }

        private void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }
    }
}

ModKey.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using IOOC;
using System.ComponentModel;
using System.Collections.ObjectModel;

namespace ModKey
{
    public static class ModuleKey
    {
        public static void setModules()
        {
            module_objects1 = new ObservableCollection<Module>();
            module_objects1.Add(new Module(0, 0, "Customer Services", "CSM", "#FFA32A3D", true, "2014-06-30"));
            module_objects1.Add(new Module(1, 1, "Asset Management", "AMS", "#FF0C51DB", true, "2014-06-30"));
            module_objects1.Add(new Module(2, 2, "Works Management", "WKS", "#FF8BDB46", false, "2014-06-30"));
            module_objects1.Add(new Module(3, 3, "Project Management", "PRJ", "#FFC7BA00", false, "2014-06-30"));

            module_objects2 = new ItemsObservableObservableCollection<Module>();
            module_objects2.Add(new Module(0, 0, "Customer Services", "CSM", "#FFA32A3D", true, "2014-06-30"));
            module_objects2.Add(new Module(1, 1, "Asset Management", "AMS", "#FF0C51DB", true, "2014-06-30"));
            module_objects2.Add(new Module(2, 2, "Works Management", "WKS", "#FF8BDB46", false, "2014-06-30"));
            module_objects2.Add(new Module(3, 3, "Project Management", "PRJ", "#FFC7BA00", false, "2014-06-30"));
        }

        public static ObservableCollection<Module> module_objects1
        {
            get;
            set;
        }

        public static ItemsObservableObservableCollection<Module> module_objects2
        {
            get;
            set;
        }
    }

    public class Module : INotifyPropertyChanged
    {
        public Module(int ModuleID, int ModuleIndex, string ModuleName, string ModuleAbbreviation, string ModuleColor, bool ModuleEnabled, string ModuleLicenseDate)
        {
            this.ModuleID = ModuleID;
            this.ModuleIndex = ModuleIndex;
            this.ModuleName = ModuleName;
            this.ModuleAbbreviation = ModuleAbbreviation;
            this.ModuleColor = ModuleColor;

            this.ModuleEnabled = ModuleEnabled;
            this.ModuleLicenseDate = ModuleLicenseDate;
        }

        private bool _module_enabled;

        public int ModuleID { get; private set; }
        public int ModuleIndex { get; private set; }
        public string ModuleName { get; private set; }
        public string ModuleAbbreviation { get; private set; }
        public string ModuleColor { get; private set; }
        public bool ModuleEnabled
        {
            get { return _module_enabled; }
            set
            {
                _module_enabled = value;
                RaisePropertyChanged("ModuleEnabled");
            }
        }
        public string ModuleLicenseDate { get; private set; }


        public event PropertyChangedEventHandler PropertyChanged;

        private void RaisePropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
        }
    }
}

The app looks like this: enter image description here There are two buttons for each collection.
The first runs an item property update on the main thread, this works fine on both collections.
The second runs an item property update on a background thread, this works fine on the first collection. An exception is thrown on the second. enter image description here

[EDIT1] Call Stack

    [External Code] 
>   MultiBindTest.exe!IOOC.ItemsObservableObservableCollection<ModKey.Module>.item_PropertyChanged(object sender = {ModKey.Module}, System.ComponentModel.PropertyChangedEventArgs e = {System.ComponentModel.PropertyChangedEventArgs}) Line 61 + 0x25 bytes   C#
    MultiBindTest.exe!ModKey.Module.RaisePropertyChanged(string propertyName = "ModuleEnabled") Line 79 + 0x32 bytes    C#
    MultiBindTest.exe!ModKey.Module.ModuleEnabled.set(bool value = false) Line 68 + 0xe bytes   C#
    MultiBindTest.exe!MultiBindTest.MainViewModel.SwitchExecute4.AnonymousMethod__8() Line 134 + 0xc bytes  C#
    [External Code] 

[EDIT2] Just tidying up here's the trace

System.NotSupportedException occurred HResult=-2146233067
Message=This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread.
Source=PresentationFramework StackTrace: at System.Windows.Data.CollectionView.OnCollectionChanged(Object sender, NotifyCollectionChangedEventArgs args) at System.Collections.ObjectModel.ObservableCollection1.OnCollectionChanged(NotifyCollectionChangedEventArgs e) at IOOC.ItemsObservableObservableCollection1.item_PropertyChanged(Object sender, PropertyChangedEventArgs e) in C:\Users\HFisher\Documents\Visual Studio 2010\Projects\MultiBindTest\MultiBindTest\iooc.cs:line 63
InnerException:

1
what's the exception (with stack trace)? - qujck
@qujck was this what you were after? See [EDIT1] - Hank
@Hank what we want is the full copy of the text that is copied to your clipboard when you click "Copy exception detail to the clipboard" on the error window from your last screenshot. - Scott Chamberlain
I suspect there is a way to do this with Dispatcher but the other option is a backgroundworker and then update in the RunWorkerCompleted event - paparazzo

1 Answers

3
votes

change your code to this and it will update using the UI thread

    private void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        Application.Current.Dispatcher.Invoke(() =>base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)));
    }

if .net 4.0 you can do this:

    Application.Current.Dispatcher.Invoke(new Action(() =>base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))));