Проблема многопоточности WPF MVVM

Я видел другие проблемы, похожие на мою, но я не видел ничего, что я мог бы применить, чтобы заставить мой код работать.

Итак, я новичок в MVVM, и я пытаюсь получить некоторые вещи, которые выполняются в фоновом потоке, для обновления моего пользовательского интерфейса. Что я заметил, так это то, что в первый раз вызывается пользовательский интерфейс, и фоновый поток выполняется в первый раз, если коллекция IEnumerable‹>, пользовательский интерфейс не полностью обновляется по сравнению с резервными данными. Если коллекция ObservableCollection‹>, выдается ошибка.

Из того, что я прочитал, изменения в коллекциях должны выполняться в потоке диспетчера, но вызовы OnPropertyChanged() этого не делают. Итак, кто-нибудь, пожалуйста, скажите мне, как это может происходить:

Я изменяю свою наблюдаемую коллекцию _Printers:

foreach (PrinterViewModel pv in _Printers)
            {
                DispatcherExec(() =>
                {
                var abilities = from x in _ServerData.Types
                                select new PrinterAbility(
                                    new PrintableType() { ID = x.ID, Name = x.Name, NumInProcUnit = x.NumInProcUnit, PrintersMappedTo = x.PrintersMappedTo, SysName = x.SysName },
                                    x.PrintersMappedTo.Contains(pv.Printer.ID)
                                    );


                    pv.Printer.SetAbilities(abilities);
                });

Мой DispatcherExec выглядит так:

 private void DispatcherExec(Action action)
    {
        //Dispatcher.Invoke((Action)delegate 
        //{
        //    action.BeginInvoke(null, null); 
        //}, null);
        Dispatcher.CurrentDispatcher.Invoke((Action)delegate
        {
            action.Invoke();
        }, null);
    }

И вот код SetAbilities, который не работает:

 public void SetAbilities(IEnumerable<PrinterAbility> abilities)
    {
        if (log.IsInfoEnabled)
            log.Info("SetAbilities(IEnumerable<PrinterAbility> abilities): called on printer "+Name);

        List<PrinterAbility> l = new List<PrinterAbility>();
        abilities.ForEach(i =>
            {
                i.PrinterAbilityChanged += new PrinterAbilityChangedEventHandler(OnPrinterAbilityChanged);
                l.Add(i);
            }
            );
        lock (_Abilities)
        {
            foreach (PrinterAbility pa in l)
                _Abilities.Add(pa);
        }
        if (log.IsDebugEnabled)
            log.Debug("SetAbilities(IEnumerable<PrinterAbility> abilities): leaving");
    }

В наблюдаемой коллекции _Abilities.Add(pa) добавлено: «Этот тип CollectionView не поддерживает изменения в его SourceCollection из потока, отличного от потока Dispatcher». Я думаю: «Вы шутите?»

Кроме того, я думаю, что изменение объекта в наблюдаемой коллекции автоматически вызовет OnCollectionChanged(), верно?

Заранее спасибо всем.


person Micah    schedule 07.10.2010    source источник


Ответы (3)


Использование Dispatcher.CurrentDispatcher — это не то, что вы должны делать из потока BG. Вам нужно использовать Dispatcher для объекта, производного от DependencyObject, который был создан в потоке пользовательского интерфейса.

Кроме того, вы перебираете объекты *ViewModel (PrinterViewModel) из потока BG. Это действительно идет вразрез с MVVM. Ваша модель должна выполнять асинхронные действия, а ваши ViewModel(ы) должны обрабатывать эти асинхронные операции таким образом, чтобы представление могло их использовать (путем маршалинга в нужный поток через Dispatcher).

Кроме того, вы закрываете переменную цикла (pv). Плохо, плохо. Это (в зависимости от порядка выполнения) может означать, что к моменту появления диспетчера вы получите несколько вызовов pv.Printer.SetAbilities(...) для одного и того же экземпляра PrinterViewModel. Создайте локальную переменную внутри цикла и используйте ее в своем анонимном методе, чтобы избежать этой проблемы.

person FMM    schedule 08.10.2010
comment
Спасибо за вашу критику. - person Micah; 08.10.2010
comment
А как насчет Application.Current.Dispatcher? - person Micah; 08.10.2010
comment
Что ж, вместо того, чтобы поместить свой источник данных в xaml, я поместил его в xaml.cs и передал диспетчер от одного из элементов управления на свою виртуальную машину. Это сработало. Еще раз спасибо. - person Micah; 08.10.2010

Может быть это и это будет полезно при изменении Observable Collection через потоки.

person Eugene Cheverda    schedule 07.10.2010

Вы должны использовать Dispatcher, связанный с любым из ваших элементов управления WPF, а не Dispatcher.CurrentDispatcher для фонового потока.

Также

Dispatcher.CurrentDispatcher.Invoke((Action)delegate
        {
            action.Invoke();
        }, null);

избыточно, должно быть

wpfDispatcher.Invoke(action, null);

И, наконец, для первого блока вам обычно следует избегать передачи переменных цикла в лямбда-выражения, используйте трюк временного присваивания, чтобы обойти эти скрытые проблемы с закрытием. Хотя почти уверен, что в данном случае это не проблема.

person Grozz    schedule 07.10.2010
comment
Если я использую Dispatcher, прикрепленный к одному из элементов управления, разве я не тесно связываю виртуальную машину с V? - person Micah; 08.10.2010
comment
Это единственный способ. Вы должны использовать диспетчер, связанный с потоком WPF. Вы можете консолидировать логику, используя ее в представлении, или передать ее в качестве параметра для классов/методов виртуальной машины, которые вы вызываете. - person Grozz; 08.10.2010