Сценарий многопоточной привязки данных WinForms, лучшая практика? - PullRequest
10 голосов
/ 02 марта 2009

В настоящее время я разрабатываю / переделываю часть привязки данных приложения, в которой интенсивно используется привязка данных winforms и обновления, поступающие из фонового потока (раз в секунду для> 100 записей).

Предположим, что это приложение для торговли акциями, где фоновый поток отслеживает изменения данных и помещает их в объекты данных. Эти объекты хранятся в BindingList<> и реализуют INotifyPropertyChanged для распространения изменений посредством привязки данных к элементам управления winforms. Кроме того, объекты данных в настоящее время направляют изменения через WinformsSynchronizationContext.Send в поток пользовательского интерфейса. Пользователь может ввести некоторые значения в пользовательском интерфейсе, что означает, что некоторые значения могут быть изменены с обеих сторон. И пользовательские значения не должны быть перезаписаны обновлениями.

Итак, у меня возникает несколько вопросов:

  • Есть ли общая линия дизайна-гильдии, как это сделать (фоновые обновления в привязке данных)?
  • Когда и как маршалировать в потоке пользовательского интерфейса?
  • Каков наилучший способ взаимодействия фонового потока с объекты привязки / данных?
  • Какие классы / интерфейсы следует использовать? (BindingSource, ...)
  • ...

Пользовательский интерфейс на самом деле не знает, что существует фоновый поток, который обновляет элемент управления, и, насколько я понимаю, в сценариях привязки данных пользовательский интерфейс не должен знать, откуда поступают данные ... Вы можете подумать о фоновый поток как нечто, передающее данные в пользовательский интерфейс, поэтому я не уверен, является ли фоновый работник нужным вариантом.

Иногда вы хотите получить какой-либо ответ пользовательского интерфейса во время операции с данным / бизнес-объектом (например, установка фона во время пересчетов). Недостаточно поднять свойство, измененное для свойства status, которое связано с фоном, так как элемент управления перекрашивается после завершения расчета? Моя идея заключалась бы в том, чтобы подключить событие propertyloaded и вызвать .update () для элемента управления ... Есть другие идеи по этому поводу?

Ответы [ 8 ]

6 голосов
/ 03 марта 2009

Это сложная проблема, так как большинство «решений» приводит к большому количеству пользовательского кода и множеству вызовов на BeginInvoke() или System.ComponentModel.BackgroundWorker (который сам по себе является просто тонкой оболочкой над BeginInvoke).

В прошлом я также обнаружил, что вскоре вы захотите отложить отправку событий INotifyPropertyChanged до тех пор, пока данные не станут стабильными. Код, который обрабатывает одно событие, измененное для правильности, часто должен читать другие свойства. У вас также часто есть элемент управления, который необходимо перерисовывать каждый раз, когда изменяется состояние одного из многих свойств, и вы не хотите, чтобы элемент управления перерисовывал себя слишком часто.

Во-первых, каждый пользовательский элемент управления WinForms должен читать все данные, которые ему нужны для рисования, в обработчике событий PropertyChanged, поэтому ему не нужно блокировать объекты данных, когда это было сообщение WM_PAINT (OnPaint). Элемент управления не должен немедленно перерисовываться при получении новых данных; вместо этого он должен вызвать Control.Invalidate(). Windows объединит сообщения WM_PAINT в как можно меньшее количество запросов и отправляет их только тогда, когда потоку пользовательского интерфейса больше нечего делать. Это минимизирует количество перерисовок и время блокировки объектов данных. (Стандартные элементы управления в большинстве случаев делают это с привязкой данных)

Объекты данных должны записывать то, что изменилось по мере внесения изменений, а затем, как только набор изменений будет завершен, «пихнуть» поток пользовательского интерфейса в вызов метода SendChangeEvents, который затем вызывает обработчик события PropertyChanged (в потоке пользовательского интерфейса) для всех свойств, которые изменились. Во время работы метода SendChangeEvents() объекты данных должны быть заблокированы, чтобы фоновые потоки не обновляли их.

Поток пользовательского интерфейса может быть «запущен» вызовом BeginInvoke всякий раз, когда набор обновлений читает бин из базы данных. Часто лучше проводить опрос потока пользовательского интерфейса с использованием таймера, поскольку Windows отправляет сообщение WM_TIMER только тогда, когда очередь сообщений пользовательского интерфейса пуста, что приводит к тому, что пользовательский интерфейс чувствует себя более отзывчивым.

Также подумайте о том, чтобы вообще не использовать привязку данных, и чтобы пользовательский интерфейс запрашивал каждый объект данных «что изменилось» при каждом срабатывании таймера. Привязка данных всегда выглядит красиво, но может быстро стать частью проблемы, а не частью решения.

Поскольку блокировка / разблокировка объектов данных является проблемой и может не позволить считываниям обновлений из базы данных достаточно быстро, вы можете передать потоку пользовательского интерфейса (виртуальную) копию объектов данных. Наличие объекта данных должно быть постоянным / неизменным, чтобы любые изменения в объекте данных возвращали новый объект данных, а не изменяли текущий объект данных, что может сделать это возможным.

Постоянные объекты звучат очень медленно, но не обязательно, см. это и , для некоторых указателей. Также посмотрите на это и , что на переполнении стека.

Также взгляните на retlang - Параллелизм на основе сообщений в .NET . Его пакетирование сообщений может быть полезным.

(Для WPF у меня была бы View-Model, устанавливаемая в потоке пользовательского интерфейса, который затем обновлялся в «пакетах» из многопоточной модели фоновым потоком. Однако WPF намного лучше сочетал привязку данных события то WinForms.)

2 голосов
/ 03 марта 2009

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

Пользовательский интерфейс нужно обновлять только много раз в секунду, поэтому производительность никогда не будет проблемой, и опрос будет работать нормально

Мне нравится использовать граф объектов, который постоянно обновляется пулом фоновых потоков. Они проверяют фактические изменения значений данных и, когда они замечают фактические изменения, обновляют счетчик версий в корне графа объектов (или в каждом главном элементе, что имеет больше смысла) и обновляют значения

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

Этот простой метод полностью изолирует поток пользовательского интерфейса от фоновых потоков

2 голосов
/ 02 марта 2009

В этой теме есть статья MSDN . Но будьте готовы взглянуть на VB.NET. ;)

Кроме того, возможно, вы могли бы использовать System.ComponentModel.BackgroundWorker вместо обычного второго потока, поскольку он прекрасно формализует тип взаимодействия с порожденным фоновым потоком, который вы описываете. Пример, приведенный в библиотеке MSDN, довольно приличный, поэтому посмотрите на подсказку, как его использовать.

Edit: Обратите внимание: сортировка не требуется, если вы используете событие ProgressChanged для обратной связи с потоком пользовательского интерфейса. Фоновый поток вызывает ReportProgress всякий раз, когда ему необходимо связаться с пользовательским интерфейсом. Поскольку к этому событию можно прикрепить любой объект, нет причин выполнять ручную сортировку. Ход выполнения передается через другую асинхронную операцию, поэтому не нужно беспокоиться ни о том, насколько быстро пользовательский интерфейс может обрабатывать события хода выполнения, ни о том, прерывается ли фоновый поток в ожидании завершения события.

Если вы докажете, что фоновый поток поднимает событие с измененным прогрессом слишком быстро, вам может понадобиться посмотреть Модели Pull vs. Push для обновлений пользовательского интерфейса отличная статья Ayende.

1 голос
/ 30 апреля 2013

Создайте новый UserControl, добавьте свой элемент управления и отформатируйте его (возможно, dock = fill) и добавьте свойство. Теперь сконфигурируйте свойство, чтобы вызывать usercontrol и обновлять ваш элемент, каждый раз, когда вы изменяете свойство из любого потока, который вы хотите!

вот мое решение:

    private long value;
    public long Value
    {
        get { return this.value; }
        set
        {
            this.value = value;

            UpdateTextBox();
        }
    }

    private delegate void Delegate();
    private void UpdateTextBox()
    {
        if (this.InvokeRequired)
        {
            this.Invoke(new Delegate(UpdateTextBox), new object[] {});
        }
        else
        {
            textBox1.Text = this.value.ToString();
        }
    }

в моей форме я связываю свое мнение

viewTx.DataBindings.Add(new Binding("Value", ptx.CounterTX, "ReturnValue"));
1 голос
/ 07 марта 2011

Я только что боролся с подобной ситуацией - поток badkground обновляет интерфейс через BeginInvokes. В каждом цикле фон имеет задержку в 10 мс, но в дальнейшем я столкнулся с проблемами, когда обновления пользовательского интерфейса, которые иногда запускаются каждый раз в этом цикле, не успевают за частотой обновлений, и приложение фактически перестает работать (не уверен, что произойдет - взорвал стек?).

В итоге я добавил флаг в объект, переданный через invoke, который был просто флагом готовности. Я установил бы это в false перед вызовом invoke, и тогда поток bg больше не будет обновлять пользовательский интерфейс, пока этот флаг не будет переключен обратно в true. Поток пользовательского интерфейса будет делать обновления экрана и т. Д., А затем устанавливать для этой переменной значение true.

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

0 голосов
/ 11 августа 2018

Я опаздываю на вечеринку, но считаю, что это все еще актуальный вопрос.

Я бы посоветовал вам вообще не использовать привязку данных и использовать Observable объекты.

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

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

0 голосов
/ 22 января 2018

Это сообщение старое, но я подумал, что могу дать варианты другим. Кажется, что как только вы начинаете выполнять асинхронное программирование и привязку данных Windows Forms, у вас возникают проблемы с обновлением источника данных Bindingsource или обновлением списков, связанных с управлением формами Windows. Я собираюсь попробовать использовать класс Джеффри Рихтерса AsyncEnumerator из его мощных инструментов на wintellect.

Причина: 1. Его класс AsyncEnumerator автоматически размещает фоновые потоки в потоках пользовательского интерфейса, чтобы вы могли обновлять элементы управления, как если бы вы выполняли синхронный код. 2. AsyncEnumerator упрощает асинхронное программирование. Это происходит автоматически, поэтому вы пишете свой код синхронно, но код все еще выполняется асинхронно.

У Джеффри Рихтера на канале 9 MSDN есть видео, которое объясняет AsyncEnumerator.

Пожелай мне удачи.

-R

0 голосов
/ 02 марта 2009

Это проблема, которую я решил в Update Controls . Я говорю об этом не для того, чтобы предложить вам переписать свой код, а для того, чтобы дать вам некоторый источник информации для поиска идей.

Метод, который я использовал в WPF, заключался в использовании Dispatcher.BeginInvoke для уведомления потока переднего плана об изменении. Вы можете сделать то же самое в Winforms с Control.BeginInvoke. К сожалению, вы должны передать ссылку на объект Form в ваш объект данных.

Как только вы это сделаете, вы можете передать действие в BeginInvoke, которое запускает PropertyChanged. Например:

_form.BeginInvoke(new Action(() => NotifyPropertyChanged(propertyName))) );

Вам потребуется заблокировать свойства в вашем объекте данных, чтобы сделать их поточно-ориентированными.

...