Is BackgroundWorker the only way to keep a WCF/WPF application responsive?

こ雲淡風輕ζ 提交于 2019-12-10 14:15:13

问题


Client/server desktop application using C#, WCF, WPF. Since pretty much every action is going to require a trip to the server (list/create/save/delete/etc), every action has the potential to freeze the entire UI. Here's an example of a naive implementation with a call to service.GetAll() which could take a "long" time (more than a few hundred milliseconds):

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

(Aside: I'd love to know why List has AddRange and ObservableCollection doesn't.)

BackgroundWorker to the rescue:

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();
}

(Aside: code above has been simplified, but that's the gist of it.)

The code using BackgroundWorker works exactly how I want it to. The app remains responsive at all times, and the button is disabled for the duration of the call. However, this means adding 15 lines to every possible action the user might make.

Say it ain't so.


回答1:


No, BackgroundWorker is not the only way, but it's one way. Any other way will allso include some form of asynchronous construct with the need to use Dispatch.BeginInvoke to update the UI. You could for instance use the 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; });

});

If this is a recurring pattern (a button will trigger some action that should be performed asynchronously, with the button being disabled during the process) you can wrap this into a method:

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; });     
    });
}

...and call it:

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

As an option to using the ThreadPool, you should also perhaps look into the Task Parallel Library.

When doing this you should pay attention to how you handle UI state. For instance of you have more than one control which triggers the same action, make sure that all of them are disabled during the action.

Note: these are just quick ideas. The code has not been tested so it may contain errors. It's more to be regarded as discussion material than finished solutions.




回答2:


WCF provides the ability to make all service calls asynchronously. When you create the service reference in your project, the add service reference dialog box has an "Advanced..." button. Clicking that you will see the option for "Generate Asynchronous operations". If you click that check-box then every operation will be generated in both a synchronous and asynchronous manner.

For example, if i have an operation "DoSomething()" then after checking this box i will get code generated for calling DoSomething() and DoSomethingAsync().

You will also get a Service.DoSomethingCompleted event that you can use to define a callback handler when the service call returns.

This is the method we used to make service calls without locking the UI.

Here is a rather complicated example provided by Microsoft on how to do this: http://msdn.microsoft.com/en-us/library/ms730059.aspx




回答3:


It is not the only way. I recommend Task (or one of the higher-level abstractions for Task, such as Parallel or PLINQ).

I have a review of various approaches to asynchronous background operations on my blog.

The current state of things does require some boilerplate code, regardless of which approach you choose. The async CTP shows where things are going - towards a much, much cleaner syntax for asynchronous operations. (Note that - at the time of writing - the async CTP is incompatible with VS SP1).




回答4:


Well, BackgroundWorker is not the only option you have but in order to accomplish what you want you still need to use multiple threads or asynchronous operations in order to not block while you wait for the long-running operations to finish.

And, because WPF requires that all code accessing the UI run on the same thread you do have to do some context switching when you call or access data or code on the UI thread. The way to ensure a call will run on the UI thread in WPF is to use the Dispatcher class.

Another simple way of keeping the UI responsive is to queue work item on a thread in the Thread Pool which is done using the ThreadPool class.

// 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
             });
});

As always, there's more than one way to skin the cat but be aware that each technique has advantages and disadvantages. For instance the method outlines above could be useful because it doesn't have that much code overhead but it may not be the most efficient way in may cases.

Other options are available including using the BeginXXX - EndXXX methods of the classes you're using if they provide any (such as the SqlCommand class has BeginExecuteReader EndExecuteReader). Or, using the XXXAsync methods if the classes have that. For instance the System.Net.Sokets.Socket class has ReceiveAsync and SendAsync.




回答5:


No this is not the only option. This question is more about how are you designing your application.

You can take a look at Windows Composite Applicaiton Framework (Prism), which provides features like EventAggregator which can help you publish application wide events out and subscribe it at multiple locations within your app and take actions based on that.

Also as far as being worried about having too many lines of code, you may want to layer your application architecture in such a way that you can refactor and reuse as much code as possible. This way you have these background workers handling all your service responses in one layer while you can leave your UI layer detached from it.




回答6:


No it's not the only way, but it is one of the simpler ones (at least compared to setting up your own thread, or pushing a task to a thread pool thread and arranging an event on completion).




回答7:


You might be able to simplify a little bit by writing a static method somewhere that takes two parameters, the callback functions, and handles the rest for you, that way you won't have to write all the same boiler plate every time you need to make an async call.




回答8:


No, certaily not.

You can create a raw Thread and execute time taking code in it and then dispatch the code to the UI Thread to access/update any UI controls.More info on Disptacher here.

Refer to this for a great information about Threads in c#.



来源:https://stackoverflow.com/questions/5327096/is-backgroundworker-the-only-way-to-keep-a-wcf-wpf-application-responsive

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!