none
WPF MVVM коллекции и многопоточность RRS feed

  • Вопрос

  • Пишу многопоточное приложение с кучей коллекций, которые обновляются в реальном времени и должны отображаться.

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

    Типа такого, Вьюмодель:

            public string Code
            {
                get { return Model.Code; }
                set
                {
                    Model.Code = value;
                    OnPropertyChanged("Code");
                }
            }

    Однако, в случае, если это свойство является коллекцией, и при этом методы Модели работают в своих потоках, у меня получается неимоверное усложнение.

    А именно:

    1. Первая проблема. Любую коллекцию, которая используется в Модели, при обертывании этой коллекции во Вью-модели, приходится приводить к виду Observable, иначе биндинг не работает. А к Observable просто так не привести. К примеру, в Модели я использую такие коллекции, как SortedList и Dictionary. И я не могу написать во вью-модели:

            public SortedList<long, Record> ViewModelList
            {
                get { return Model.SortedList; }
                set
                {
                    Model.SortedList = value;
                    OnPropertyChanged("ViewModelList");
                }
            }

    Это не будет работать. Мне потребуется переводить мой  SortedList из Модели в ObservableCollection для Вью-модели. При этом никаких легких способов это сделать я не нашел (типа SortedList.ToObservableCollection ). Фактически, мне приходится последовательно обрабатывать каждый элемент, что негативно сказывается на производительности.

    2. Вторая проблема. Если Модель работает в другом потоке, то получается вообще какое-то нагромождение. Допустим, я использую DispatcherTimer и делаю обновление UI по таймеру. Модель при этом работет в другом потоке. Если модель содержит какую-либо коллекцию, то я уже получаю проблему, так как не могу просто так обращаться к этой коллекции из потока UI, чтобы взять оттуда изменившиеся данные, потому что с этой коллекцией может работать поток Модели. Пока я решил это тем, что использую в Модели потокобезопасные коллекции (ConcurrentDictionary вместо Dictionary), но мне кажется это какое-то неправильное решение, по крайней мере потому, что потокобезопасных коллекций не много и не они не предоставляют тот функционал, который имеют обычные коллекции.

    К примеру, так выглядит Модель. Поначалу у меня был просто Dictionary, но пришлось сделать его ConcurrentDictionary, иначе я не мог одновременно обновлять его из потока Модели и использовать его данные из потока Вью (UI). А ObservableDictionary мне приходится вводить потому, что для биндинга обязательно нужен Observable:

        public class Model
         {
            public ConcurrentDictionary<long, Record> CndOrders { get; set; }
            public ObservableDictionary<long, Record> ObdOrders { get; set; }
         }

    Так выглядит связанное с Моделью свойство во Вью-модели. Обертываю ObservableDictionary из Модели:

            public ObservableDictionary<long, Record> ObdOrders
            {
                get { return Model.ObdOrders; }
                set
                {
                    Model.ObdOrders = value;
                    OnPropertyChanged("ObdOrders");
                }
            }

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

    foreach (var record in Model.CndOrders.OrderBy(kvp => kvp.Key)) //тут сортировку делать тупо,

    //но конкурентсловарь добавляет не по порядку, а другой подходящей потокобезопасной

    //коллекции я не нашел

    if (!Model.ObdOrders.ContainsKey(record.Key)) { Model.ObdOrders.Add(record.Key, record.Value); }

    Переводить  SortedList в ObservableCollection приходится вообще тупо через :

    for (var i = SortedList.Count - 1; i >= 0; i--)
    {
      //ну и тут пошло банальное присваивание значений элементам ObservableCollection
    }

    Что я делаю неправильно, что у меня получается столько кода? То есть, задача такая, что есть поток логики, который формирует и обновляет коллекции, а есть поток UI, который должен эти коллекции показывать. Это можно решить как-то иначе? Потому что я совершенно уверен, что ну не должно быть такого нагромождения ради такой тривиальной задачи...


    • Изменено Qwester33 2 апреля 2013 г. 11:14
    2 апреля 2013 г. 10:37

Ответы

  • 1. Если некий список должен отображаться в пользовательском элементе управления и при этом обновляется из кода, то лучший вариант - это Observable коллекции.

    2. Нет абсолютно никакой разницы кто и откуда запускает поток. В вашем варианте я думаю можно сделать так:

    // Запускаем задачу по обработке данных, которая вернет массив обработанных объектов

    var task = Task<IEnumerable<SomeObject>>.Factory.StartNew(() => обработка данных )).ToAsync(ActionCallback, null)

    ...

    // Этот метод вызывается после завершения задачи

    void ActionCallback(IAsyncResult asyncResult)

    {

    var task = asyncResult as Task<IENumerable<SomeObject>>;

    // В task.Result будет хранится список обработанных объектов

    ...

    // Выполняем обновление пользовательского интерфейса

    }

    Посмотрите внимательно ту ссылку которую я вам дал, там все это есть.

    Можно конечно и через Concurrent коллекции.

    Посмотрите еще эту ветку


    • Изменено Kirill Bessonov 2 апреля 2013 г. 16:30
    • Помечено в качестве ответа Qwester33 3 апреля 2013 г. 7:52
    2 апреля 2013 г. 16:28

Все ответы

  • По первому вопросу вместо SortedList и Dictionary можете использовать ObservableDictionary. Пример можно найти здесь.

    По второму вопросу посмотрите статью Task Extensions в помощь. если будут вопросы - спрашивайте.

    2 апреля 2013 г. 14:08
  • 1. ObservableDictionary я и так использую. Проблема с том, что для приведения к ObservableDictionary или к ObservableCollection, приходится выполнять перебор элементов (явно или неявно). Или же, чтобы не приводить и не перебирать, надо сразу же, в качестве бизнес-объектов использовать только Observable-коллекции, но их меньше, чем обычных коллекций и у них хуже функционал. Но даже если так сделать (то есть полностью отказаться от всех коллекций, которые не Observable, если при этом они должны отображаться во Вью), то все равно  возникают проблемы с потокобезопасностью.

    2. Спасибо за Ваш пример. Но у Вас в примере идет необходимость сделать какую-то работу, находясь в UI потоке. То есть, пользователь уже находится в UI потоке, и из него запускает какую-то задачу в новом потоке. Это я бы мог решить.  Но у меня ситуация несколько другая, и я пока не понимаю, как в ней использовать Ваш пример.

    У меня есть ДДЕ-сервер, которые постоянно кидает события прихода новых данных. Данные парсятся и через

    var task = Task.Factory.StartNew(() => обработка данных ))

    запускается их многопоточная обработка. В результате обработки происходит либо обновление, либо дополнение различных коллекций. А сам я сижу и смотрю на GUI, который по идее должен в реалтайме мне эти коллекции показывать. То есть, я никак не влияю на момент получения данных, и поток UI просто должен каким-то образом показывать мне коллекции, формируемые случайными потоками из пула потоков.

    Какие у меня варианты? Выполнять всю работу в потоке UI я не могу, а сам GUI можно обновлять только через его поток. То есть, от потока UI никуда не уйти. Сейчас я решаю это использованием Concurrent-коллекций, к которым по таймеру обращаюсь из потока UI и произвожу их приведение к Observable-коллекциям.

    Нарыл на codeproject  Concurrent Observable Collection, попробую пока это наверное...


    • Изменено Qwester33 2 апреля 2013 г. 15:57
    2 апреля 2013 г. 15:41
  • 1. Если некий список должен отображаться в пользовательском элементе управления и при этом обновляется из кода, то лучший вариант - это Observable коллекции.

    2. Нет абсолютно никакой разницы кто и откуда запускает поток. В вашем варианте я думаю можно сделать так:

    // Запускаем задачу по обработке данных, которая вернет массив обработанных объектов

    var task = Task<IEnumerable<SomeObject>>.Factory.StartNew(() => обработка данных )).ToAsync(ActionCallback, null)

    ...

    // Этот метод вызывается после завершения задачи

    void ActionCallback(IAsyncResult asyncResult)

    {

    var task = asyncResult as Task<IENumerable<SomeObject>>;

    // В task.Result будет хранится список обработанных объектов

    ...

    // Выполняем обновление пользовательского интерфейса

    }

    Посмотрите внимательно ту ссылку которую я вам дал, там все это есть.

    Можно конечно и через Concurrent коллекции.

    Посмотрите еще эту ветку


    • Изменено Kirill Bessonov 2 апреля 2013 г. 16:30
    • Помечено в качестве ответа Qwester33 3 апреля 2013 г. 7:52
    2 апреля 2013 г. 16:28