Является ли BackgroundWorker единственным способом обеспечить адаптацию приложения WCF / WPF? - PullRequest
6 голосов
/ 16 марта 2011

Клиент / серверное настольное приложение с использованием C #, WCF, WPF. Поскольку почти каждое действие потребует поездки на сервер (список / создание / сохранение / удаление / и т. Д.), Каждое действие может заморозить весь пользовательский интерфейс. Вот пример наивной реализации с вызовом service.GetAll(), который может занять «долгое» время (более нескольких сотен миллисекунд):

private void btnRefresh_Click(object sender, RoutedEventArgs e)
{
    vm.Users.Clear();
    foreach (var user in service.GetAllUsers())
        vm.Users.Add(user);
}

(Помимо: я хотел бы знать, почему Список имеет AddRange, а ObservableCollection нет.)

BackgroundWorker на помощь:

private void btnRefresh_Click(object sender, RoutedEventArgs e)
{
    var worker = new BackgroundWorker();

    worker.DoWork += (s, e) =>
    {
        Dispatcher.BeginInvoke((Action)delegate() { btnRefresh.IsEnabled = false; });
        e.Result = service.GetAllUsers();
    };

    worker.RunWorkerCompleted += (s, e) =>
    {
        vm.Users.Clear();
        foreach (var user in (List<UserDto>)e.Result)
            vm.Users.Add(user);
        Dispatcher.BeginInvoke((Action)delegate() { btnRefresh.IsEnabled = true; });
    };

    worker.RunWorkerAsync();
}

(Помимо: приведенный выше код был упрощен, но в этом суть.)

Код, использующий BackgroundWorker, работает именно так, как я хочу. Приложение постоянно реагирует, и кнопка отключена на время разговора. Однако это означает добавление 15 строк к каждому возможному действию, которое может выполнить пользователь.

Скажи, что это не так.

Ответы [ 8 ]

6 голосов
/ 16 марта 2011

Нет, BackgroundWorker не единственный способ, но это один из способов.Любой другой способ также будет включать некоторую форму асинхронной конструкции с необходимостью использовать Dispatch.BeginInvoke для обновления пользовательского интерфейса.Например, вы можете использовать ThreadPool:

ThreadPool.QueueUserWorkItem(state => {
    Dispatcher.BeginInvoke((Action)delegate() { btnRefresh.IsEnabled = false; });
    foreach (var user in service.GetAllUsers())
        vm.Users.Add(user);
    Dispatcher.BeginInvoke((Action)delegate() { btnRefresh.IsEnabled = true; });

});

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

private void PerformAsync(Action action, Control triggeringControl)
{
    ThreadPool.QueueUserWorkItem(state => {
        Dispatcher.BeginInvoke((Action)delegate() { triggeringControl.IsEnabled = false; });
        action();
        Dispatcher.BeginInvoke((Action)delegate() { triggeringControl.IsEnabled = true; });     
    });
}

... и вызовите его:

PerformAsync(() => 
{
    foreach (var user in service.GetAllUsers())
        vm.Users.Add(user);
}, btnRefresh);

В качестве опции для использования ThreadPool, вам, возможно, также стоит заглянуть в Task Parallel Library .

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

Примечание: это просто быстрые идеи.Код не был протестирован, поэтому он может содержать ошибки.Это больше следует рассматривать как материал для обсуждения, чем готовые решения.

5 голосов
/ 16 марта 2011

WCF предоставляет возможность выполнять все сервисные вызовы асинхронно. Когда вы создаете ссылку на службу в своем проекте, в диалоговом окне добавления ссылки на службу есть кнопка «Дополнительно ...». Нажав, что вы увидите опцию «Генерировать асинхронные операции». Если вы установите этот флажок, то каждая операция будет сгенерирована как синхронно, так и асинхронно.

Например, если у меня есть операция «DoSomething ()», то после установки этого флажка я получу код, сгенерированный для вызова DoSomething () и DoSomethingAsync ().

Вы также получите событие Service.DoSomethingCompleted, которое можно использовать для определения обработчика обратного вызова при возврате вызова службы.

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

Вот довольно сложный пример, предоставленный Microsoft о том, как это сделать: http://msdn.microsoft.com/en-us/library/ms730059.aspx

3 голосов
/ 16 марта 2011

Это не единственный способ.Я рекомендую Task (или одну из высокоуровневых абстракций для Task, например Parallel или PLINQ).

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

Текущее состояние требует некоторого стандартного кода, независимо от того, какой подход вы выберете. асинхронный CTP показывает, куда идут дела - к гораздо более понятному синтаксису для асинхронных операций.(Обратите внимание, что - на момент написания - асинхронная CTP несовместима с VS SP1).

2 голосов
/ 16 марта 2011

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

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

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

// assuming the the following code resides in a WPF control
//  hence "this" is a reference to a WPF control which has a Dispatcher
System.Threading.ThreadPool.QueueUserWorkItem((WaitCallback)delegate{
    // put long-running code here

    // get the result

    // now use the Dispatcher to invoke code back on the UI thread
    this.Dispatcher.Invoke(DispatcherPriority.Normal,
             (Action)delegate(){
                  // this code would be scheduled to run on the UI
             });
});

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

Доступны и другие варианты, включая использование BeginXXX - EndXXX методов классов, которые вы используете, если они предоставляют какие-либо (например, SqlCommand класс имеет BeginExecuteReader EndExecuteReader ). Или, используя методы XXXAsync, если классы имеют это. Например, класс System.Net.Sokets.Socket имеет ReceiveAsync и SendAsync.

1 голос
/ 16 марта 2011

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

Вы можете взглянуть на Windows Composite Applicaiton Framework (Prism) , который предоставляет такие функции, как EventAggregator, который может помочь вам публиковать события всего приложения и подписывать его в нескольких местах в вашем приложении и выполнять действия на основе на что.

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

0 голосов
/ 16 марта 2011

Нет, конечно, нет.

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

См. this для получения большой информации о потоках в c #.

0 голосов
/ 16 марта 2011

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

0 голосов
/ 16 марта 2011

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

...