Избегайте вызова BeginInvoke () из объектов ViewModel в многопоточном приложении c # MVVM

1

В моем приложении С# есть компонент поставщика данных, который обновляется асинхронно в своем потоке. Классы ViewModel наследуют от базового класса, который реализует INotifyPropertyChanged. Чтобы поставщик асинхронных данных обновил свойства в представлении с помощью события PropertyChanged, я обнаружил, что мой ViewModel очень тесно связан с представлением из-за необходимости только поднимать событие из потока GUI!

#region INotifyPropertyChanged

/// <summary>
/// Raised when a property on this object has a new value.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;

/// <summary>
/// Raises this object PropertyChanged event.
/// </summary>
/// <param name="propertyName">The property that has a new value.</param>
protected void OnPropertyChanged(String propertyName)
{
    PropertyChangedEventHandler RaisePropertyChangedEvent = PropertyChanged;
    if (RaisePropertyChangedEvent!= null)
    {
        var propertyChangedEventArgs = new PropertyChangedEventArgs(propertyName);

        // This event has to be raised on the GUI thread!
        // How should I avoid the unpleasantly tight coupling with the View???
        Application.Current.Dispatcher.BeginInvoke(
            (Action)(() => RaisePropertyChangedEvent(this, propertyChangedEventArgs)));
    }
}

#endregion

Существуют ли какие-либо стратегии устранения этой связи между реализацией ViewModel и View?

ИЗМЕНИТЬ 1

Этот ответ связан и подчеркивает проблему обновления коллекций. Однако в предлагаемом решении также используется текущий диспетчер, который я не хочу беспокоить для своей модели ViewModel.

EDIT 2 Копаем немного глубже в вопрос выше, и я нашел ответ на ссылку, который отвечает на мой вопрос: создайте Action <> DependencyProperty в представлении, которое модель View может использовать для получения представления (независимо от того, что может быть) при необходимости, обращаться с диспетчером.

РЕДАКТИРОВАТЬ 3 Кажется, вопрос, заданный "спорный". Однако, когда мой ViewModel предоставляет Observable Collection как свойство для привязки к представлению (см. РЕДАКТИРОВАНИЕ 1), он по-прежнему требует доступа к диспетчеру для Add() в коллекцию. Например:

App.xaml.cs

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace MultiThreadingGUI
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        public App()
        {
            Startup += new StartupEventHandler(App_Startup);
        }

        void App_Startup(object sender, StartupEventArgs e)
        {
            TestViewModel vm = new TestViewModel();
            MainWindow window = new MainWindow();
            window.DataContext = vm;
            vm.Start();

            window.Show();
        }
    }

    public class TestViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public ObservableCollection<String> ListFromElsewhere { get; private set; }
        public String TextFromElsewhere { get; private set; }

        private Task _testTask;

        internal void Start()
        {
            ListFromElsewhere = new ObservableCollection<string>();
            _testTask = new Task(new Action(()=>
            {
                int count = 0;
                while (true)
                {
                    TextFromElsewhere = Convert.ToString(count++);
                    PropertyChangedEventHandler RaisePropertyChanged = PropertyChanged;
                    if (null != RaisePropertyChanged)
                    {
                        RaisePropertyChanged(this, new PropertyChangedEventArgs("TextFromElsewhere"));
                    }

                    // This throws
                    //ListFromElsewhere.Add(TextFromElsewhere);

                    // This is needed
                    Application.Current.Dispatcher.BeginInvoke(
                        (Action)(() => ListFromElsewhere.Add(TextFromElsewhere)));

                    Thread.Sleep(1000);
                }
            }));
            _testTask.Start();
        }
    }
}

MainWindow.xaml

<Window x:Class="MultiThreadingGUI.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        SizeToContent="WidthAndHeight">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Label Grid.Row="0" Grid.Column="0" Content="TextFromElsewhere:" />
        <Label Grid.Row="0" Grid.Column="1" Content="{Binding Path=TextFromElsewhere}" />
        <Label Grid.Row="1" Grid.Column="0" Content="ListFromElsewhere:" />
        <ListView x:Name="itemListView" Grid.Row="1" Grid.Column="1"
            ItemsSource="{Binding Path=ListFromElsewhere}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <Label Content="{Binding}" />
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</Window>

Итак, как мне избежать этого небольшого звонка BeginInvoke? Должен ли я повторно изобретать колесо и создать контейнер ViewModel для списка? Или я могу каким-то образом делегировать Add() в View?

  • 2
    Вам не нужно отправлять событие PropertyChanged в поток пользовательского интерфейса.
  • 2
    Я часто делаю то же самое, но я вижу это скорее как проблему многопоточности, чем проблему пользовательского интерфейса. Если вам не нравится зависимость от WPF в ваших ViewModels, вы можете использовать статическое свойство SynchronizationContext , но тогда оно должно быть установлено из представления ( DispatcherSynchronizationContext ). И вы переключили одну зависимость на другую.
Показать ещё 5 комментариев
Теги:
multithreading
wpf
mvvm

4 ответа

1
Лучший ответ

Этот ответ основан на ответе Уилла и комментариях Марселя Б и отмечен как ответ сообщества wiki.

В простом приложении в вопросе к классу ViewModel добавляется общедоступное свойство SynchronizationContext. Это задается View, где это необходимо, и используется ViewModel для выполнения защищенных операций. В единичном тестовом контексте, который не имеет ни одного потока GUI, поток графического интерфейса пользователя может быть изделен, а SynchronizationContext используется вместо реального. Для моего фактического приложения, когда в одном из представлений нет специального SynchronizationContext, он просто не меняет ViewContext по умолчанию ViewModel.

App.xaml.cs

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace MultiThreadingGUI
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        public App()
        {
            Startup += new StartupEventHandler(App_Startup);
        }

        void App_Startup(object sender, StartupEventArgs e)
        {
            TestViewModel vm = new TestViewModel();
            MainWindow window = new MainWindow();
            window.DataContext = vm;
            vm.Start();

            window.Show();
        }
    }

    public class TestViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public ObservableCollection<String> ListFromElsewhere { get; private set; }
        public String TextFromElsewhere { get; private set; }

        // Provides a mechanism for the ViewModel to marshal operations from
        // worker threads on the View thread.  The GUI context will be set
        // during the MainWindow Loaded event handler, when both the GUI
        // thread context and an instance of this class are both available.
        public SynchronizationContext ViewContext { get; set; }

        public TestViewModel()
        {
            // Provide a default context based on the current thread that
            // can be changed by the View, should it required a different one.
            // It just happens that in this simple example the Current context
            // is the GUI context, but in a complete application that may
            // not necessarily be the case.
            ViewContext = SynchronizationContext.Current;
        }

        internal void Start()
        {
            ListFromElsewhere = new ObservableCollection<string>();
            Task testTask = new Task(new Action(()=>
            {
                int count = 0;
                while (true)
                {
                    TextFromElsewhere = Convert.ToString(count++);

                    // This is Marshalled on the correct thread by the framework.
                    PropertyChangedEventHandler RaisePropertyChanged = PropertyChanged;
                    if (null != RaisePropertyChanged)
                    {
                        RaisePropertyChanged(this, 
                            new PropertyChangedEventArgs("TextFromElsewhere"));
                    }

                    // ObservableCollections (amongst other things) are thread-centric,
                    // so use the SynchronizationContext supplied by the View to
                    // perform the Add operation.
                    ViewContext.Post(
                        (param) => ListFromElsewhere.Add((String)param), TextFromElsewhere);

                    Thread.Sleep(1000);
                }
            }));
            _testTask.Start();
        }
    }
}

В этом примере событие Window Loaded обрабатывается в коде, чтобы предоставить GUI SynchronizationContext для объекта ViewModel. (В моем приложении у меня нет кода-behand и я использовал свойство зависимой зависимости).

MainWindow.xaml.cs

using System;
using System.Threading;
using System.Windows;

namespace MultiThreadingGUI
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            // The ViewModel object that needs to marshal some actions is
            // attached as the DataContext by the time of the loaded event.
            TestViewModel vmTest = (this.DataContext as TestViewModel);
            if (null != vmTest)
            {
                // Set the ViewModel reference SynchronizationContext to
                // the View current context.
                vmTest.ViewContext = (SynchronizationContext)Dispatcher.Invoke
                    (new Func<SynchronizationContext>(() => SynchronizationContext.Current));
            }
        }
    }
}

Наконец, обработчик события Loaded привязан в XAML.

MainWindow.xaml

<Window x:Class="MultiThreadingGUI.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        SizeToContent="WidthAndHeight"
        Loaded="Window_Loaded"
        >
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Label Grid.Row="0" Grid.Column="0" Content="TextFromElsewhere:" />
        <Label Grid.Row="0" Grid.Column="1" Content="{Binding Path=TextFromElsewhere}" />
        <Label Grid.Row="1" Grid.Column="0" Content="ListFromElsewhere:" />
        <ListView x:Name="itemListView" Grid.Row="1" Grid.Column="1"
            ItemsSource="{Binding Path=ListFromElsewhere}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <Label Content="{Binding}" />
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</Window>
4
  1. (из вашего редактирования) Отправка обновлений в пользовательский интерфейс для смешивания с помощью действий не только хаки, но и совершенно ненужная. Вы не получаете от этого никакой пользы от использования диспетчера или синхронного контекста в виртуальной машине. Не делай этого. Пожалуйста. Это бесполезно.

  2. Привязки будут автоматически обрабатывать вызовы обновлений в потоке пользовательского интерфейса, когда они привязаны к объектам, которые реализуют INotifyPropertyChanged *. Скупой, ты говоришь? Потратьте минутку и создайте небольшой прототип, чтобы проверить его. Преуспевать. Я буду ждать.... А я говорил тебе.

Поэтому ваш вопрос на самом деле спорный - вам не нужно об этом беспокоиться.

* Это изменение в структуре было введено в 3.5, iirc, и поэтому не применяется, если вы создаете против 3.

  • 0
    Вы правы. Мой оригинальный вопрос спорный. Я отредактировал это, чтобы показать, где это терпит неудачу на наблюдаемых коллекциях. Может мне тоже нужно отредактировать заголовок?
  • 0
    @MikeofSST: Ага! Да, ObservableCollections являются поточно ориентированной! Это неудачно. Точка 1 все еще стоит. Если вас это действительно беспокоит, лучше всего разрешить пользователям класса ViewModel назначать диспетчер (ориентированный на пользовательский интерфейс, своего рода) или (ориентированный на фреймворк) SynchronizationContext для публикации обновлений. Вы будете использовать это для выполнения как асинхронной, так и синхронной работы, что позволит пользователям вашего типа контролировать, над какими потоками вы работаете. Избавляет от проблем виртуальной машины.
1

Вы можете реализовать общее свойство PropertyChanged в своем базовом классе (ViewModel):

private void RaisePropertyChanged(string propertyName)
        {
            if (Application.Current == null || Application.Current.Dispatcher.CheckAccess())
            {
                RaisePropertyChangedUnsafe(propertyName);
            }
            else
            {
                Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.DataBind,
                    new ThreadStart(() => RaisePropertyChangedUnsafe(propertyName)));
            }
        }

А также

 private void RaisePropertyChangingUnsafe(string propertyName)
        {
            PropertyChangingEventHandler handler = PropertyChanging;
            if (handler != null)
            {
                handler(this, new PropertyChangingEventArgs(propertyName));
            }
        }

Этот код проверяет доступ к вашему главному диспетчеру графического интерфейса пользователя и поднимет событие Property Changed в текущем или в потоке графического интерфейса.

Надеюсь, этот общий подход поможет вам.

  • 0
    Знаете ли вы, что произойдет, если я «Просмотр» не приложение GUI, например, база данных, которая регистрирует события?
  • 0
    Да, извините, я забыл добавить это в свой ответ. Мой ответ перемещает логику из представления в базовый класс, но все еще связан с представлением GUI.
0

Если используется интерфейс, то MainWindow.xaml.cs теряет зависимость TestViewModel.

interface ISynchronizationContext
{
    System.Threading.SynchronizationContext ViewContext { get; set; }
} 
(this.DataContext as ISynchronizationContext).ViewContext  = 
(SynchronizationContext)Dispatcher.Invoke
(new Func<SynchronizationContext>(() => SynchronizationContext.Current));

Ещё вопросы

Сообщество Overcoder
Наверх
Меню