Класс DispatcherObject

160

Большую часть времени вы не будете взаимодействовать с диспетчером напрямую. Однако немало времени придется тратить на использование экземпляров DispatcherObject, потому что каждый визуальный объект WPF наследуется от этого класса. DispatcherObject — это просто объект, привязанный к диспетчеру. Другими словами — объект, привязанный к потоку диспетчера.

DispatcherObject имеет всего три члена, которые перечислены в таблице:

Члены класса DispatherObject
Имя Описание
Dispather Возвращает диспетчер, управляющий данным объектом.
CheckAccess() Возвращает true, если код находится в правильном потоке для использования объекта; в противном случае возвращает false.
VerifyAccess() Ничего не делает, если код находится в правильном потоке для использования объекта; в противном случае генерирует исключение InvalidOperationException.

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

Например, следующий код реагирует на щелчок на кнопке, создавая новый объект System.Threading.Thread. Затем он использует этот поток для вызова небольшого фрагмента кода, который изменяет текстовое поле в текущем окне:

private void Button_Click(object sender, RoutedEventArgs e)
{
            Thread thread = new Thread(UpdateTextWrong);
            thread.Start();
}

private void UpdateTextWrong()
{
            // Эмулирует некоторую работу посредством пятисекундной задержки
            Thread.Sleep(TimeSpan.FromSeconds(5));
            txb.Text = "Вставить новый текст";
}

Этот код специально задуман так, чтобы выдать сбой. Метод UpdateTextWrong() будет выполнен в новом потоке, которому не разрешен доступ к объектам WPF. В этом случае объект TextBox перехватывает нарушение, вызывая VerifyAccess(), при этом генерируется исключение InvalidOperationException.

Чтобы исправить код, понадобится получить ссылку на диспетчер, владеющий объектом TextBox (тот же самый диспетчер, который владеет окном и всеми прочими объектами WPF в приложении). Получив доступ к этому диспетчеру, можно вызывать Dispatcher.BeginInvoke(), чтобы маршализировать некоторый код потоку диспетчера. По сути, BeginInvoke() планирует указанный код в качестве задачи для диспетчера. Затем диспетчер выполняет этот код. Ниже показан корректный код:

private void Button_Click(object sender, RoutedEventArgs e)
{
            Thread thread = new Thread(UpdateTextWrong);
            thread.Start();
}

private void UpdateTextWrong()
{
            // Эмулирует некоторую работу посредством пятисекундной задержки
            Thread.Sleep(TimeSpan.FromSeconds(5));

            // Получить диспетчер от текущего окна и использовать его для вызова кода обновления
            this.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
                (ThreadStart)delegate()
            {
                txb.Text = "Вставить новый текст";
            }
            );
}

Метод Dispatcher.BeginInvoke() принимает два параметра. Первый указывает свойство задачи. В большинстве случаев будет применяться DispatcherPriority.Normal, но можно также использовать более низкий приоритет, если есть задача, которая не обязательно должна быть завершена немедленно, и которую можно отложить до того момента, когда диспетчеру нечего будет делать.

Например, это может иметь смысл, если нужно отобразить сообщение о состоянии длительно выполняющейся операции где-то в рамках пользовательского интерфейса. Можно использовать DispatcherPriority.ApplicationIdle, чтобы подождать, пока приложение завершит всю прочую работу, либо еще более "сдержанный" метод DispatcherPriority.SystemIdle, чтобы подождать, пока вся система не придет в состояние ожидания, и центральный процессор не станет простаивать.

Допускается также применять пониженный приоритет, чтобы отвлечь внимание диспетчера на что-то другое. Однако рекомендуется оставлять высокие приоритеты для событий ввода (таких как нажатия клавиш). Они должны обрабатываться почти постоянно, или же возникнет впечатление, что приложение несколько медлительно. С другой стороны, добавление нескольких миллисекунд ко времени выполнения фоновой операции не будет заметно, так что приоритет DispatcherPriority.Normal более оправдан в такой ситуации.

Второй параметр BeginInvoke() — это делегат, указывающий на метод с кодом, который необходимо выполнить. Этот метод может находиться где-то в другом месте кода, или же его можно определить встроенным (как в приведенном примере). Подход на основе встроенного кода хорош для простых операций, таких как обновление в одной строке. Однако если нужно использовать более сложный процесс для обновления пользовательского интерфейса, лучше будет вынести такой код в отдельный метод.

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

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

private void UpdateTextWrong()
{
            // Получить диспетчер от текущего окна и использовать его для вызова кода обновления
            this.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
                (ThreadStart)delegate()
            {
                // Эмулирует некоторую работу посредством пятисекундной задержки
                Thread.Sleep(TimeSpan.FromSeconds(5));
                txb.Text = "Вставить новый текст";
            }
            );
}

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

Диспетчер также предоставляет метод Invoke(). Подобно BeginInvoke(), он маршализирует указанный код потоку диспетчера. Но в отличие от BeginInvoke(), метод Invoke() останавливает поток до тех пор, пока диспетчер выполняет код. Метод Invoke() можно использовать, если нужно приостановить асинхронную операцию до тех пор, пока от пользователя не поступит какой-нибудь отклик.

Например, метод Invoke() можно вызвать для запуска фрагмента кода, отображающего диалоговое окно с кнопками ОК и Cancel. После того как пользователь щелкнет на кнопке и маршализируемый код завершится, Invoke() вернет управление, и можно будет продолжить работу в соответствии с ответом пользователя.

Лучший чат для C# программистов