|
-
May 3rd, 2016, 10:56 AM
#4
Re: Windows Forms User Interface freezing for no apparent reason
There are several ways to go about threading in a WinForms application. Before we mention any of them, there's a very important fact to know. I'm going to bold it, because if you forget or neglect it you will find the results very confusing.
There is only one thread that can interact with controls or the UI. 90% of mastering Windows Forms threading is learning how to know if you're on that thread, and how to guarantee you're on that thread when needed.
Get that tattooed on your arm. If you forget it, you're going to hurt.
There's a lot of ways to go about writing "asynchronous code". That's the fancy way of saying "bits of code that do something on another thread and coordinate with the UI thread in a controlled manner." That's the important part, a good approach to asynchronous code will guarantee parts of your code always run on a particular thread. That means a lot less worrying about whether some activity is safe.
Now one bad thing that may be going for you? If you're using a DataAdapter and binding to controls, I'm going to be talking the wrong way. If you work with that thing from another thread, it's going to end up trying to update bindings from that thread, and everything will fall apart. There's probably a way to use it asynchronously. I haven't used a DataAdapter in more than ten years. Someone else will have to teach that.
A bandage: Application.DoEvents()
Let's get one bad idea out of the way first, because you might have to use it. There's a method, Application.DoEvents(), that people will tell you "fixes" this problem. It 'makes the problem better', but does not fix it. When you understand what it does, and you understand the message queue, you'll understand what I mean.
Application.DoEvents() runs the message queue loop until there are no messages to process. So back when I said the button click handler wouldn't update text for 15 seconds? If the order were, "Update text, DoEvents(), take 15 seconds", then the text will update right away. Problem solved, right? Well, that's not the full answer. What really will happen is, "The text updates, the form freezes for 15 seconds, then the form is fine." If you're not constantly calling Application.DoEvents() during that 15-second operation, the form's still unable to process events. And if you're waiting on a stored procedure, you don't have a way to call Application.DoEvents() while waiting.
So you can probably figure in some applications, this works fine and solves the problem. But in other applications, it only solves part of the problem. This is why I tell people never use Applicaton.DoEvents(). I'd rather teach solutions that always work. But. If you're in a pickle, you might be able to buy yourself some time with it. Your choice. Don't blame me if things get worse.
What you're not picking: Thread, or IAsyncResult patterns.
Don't bother imagining you're going to create a Thread. Thread is the nuclear option. It gives you the most control, but asks you to worry about synchronization and getting to the UI thread yourself. Definitely don't learn it first. Save it for when you're real comfortable with other methods.
There's an older .NET Asynchronous pattern that uses methods named "BeginSomething()" and "EndSomething()", and uses an instance of IAsyncResult to coordinate the calls. You can live without this pattern. MS isn't making any new types that use it going forward, and in many cases has added newer methods alongside them on popular APIs. It's an OK pattern, and worth learning before Thread, but it asks you to think a little too hard about getting onto the UI thread.
BackgroundWorker
HERE is an actual asynchronous helper class. It's an implementation of a pattern MS introduced in VS 2005 called the "Event-Based Asynchronous Pattern". You can write your own BackgroundWorker from scratch, but let's save that for when we want to have some fun.
The pattern is best explained in terms of BackgroundWorker. Here's how it goes.
First, you set up. This happens on the UI thread. If you need information from controls, it's time to get it, because soon you're going to be on another thread and they will not be safe. Got everything you need? Good. Now you create a new BackgroundWorker, and configure it. You might be adding progress reporting, you might be adding cancellation, you can read about those later. Now you add event handlers. You need to handle the DoWork event. And you probably want to handle the RunWorkerCompleted event. You might need to handle the ProgressChanged event. We'll get cozy with these later. Got your data? Got your BackgroundWorker? Got your events configured? It's time to call RunWorkerAsync(). This method starts the worker thread.
When you call RunWorkerAsync(), the BackgroundWorker starts a worker thread, and from that worker thread it raises the DoWork event, which you handled. This event handler runs on a worker thread, not the UI thread. You can't safely access a control from it. This is where you do your stored procedure call or whatever. Maybe you want to update some controls while this is happening.
To do that, you can call the BackgroundWorkers's ReportProgress() method. It takes an integer for a percentage, and an optional Object that can be any other data you want to send. When you call this method, the BackgroundWorker will raise its ProgressChanged event on the UI thread. So understand: if you're about to do something that is slow, you will make the form freeze. But this also means it's perfectly safe to update controls from the ProgressChanged event. You can get the things you passed to ReportProgress() via the EventArgs that the ProgressChanged event receives.
Eventually, your DoWork work finishes. It probably has results and you probably want to put those in the UI. DoWork's event handler receives a DoWorkEventArgs. It has a Result property. That's how you "give results back" to the UI thread. When the DoWork event handler finishes, the thread is finished.
When that happens, the BackgroundWorker raises the RunWorkerCompleted event on the UI thread. Again, this means it's safe to update UI controls but you want to be quick about it. You can get the result you set in DoWork from the EventArgs that RunWorkerCompleted receives.
So if you think about it, you'll see that BackgroundWorker makes it very clear which code runs on which thread, and how you send information back and forth between them. That's why I like it, and that's why it's very popular. Here's a cheat sheet:
- Setup: gather control data, create the BackgroundWorker, handle its events, call RunWorkerAsync(). This raises DoWork on another thread. If you pass an object to RunWorkerAsync, the DoWorkEventArgs will contain that in a property named UserState.
- DoWork: Happens exclusively on the worker thread. Receives data from the UI thread via DoWorkEventArgs. Can send data to the UI thread by either calling ReportProgress() or setting its EventArgs' Result property.
- ProgressChanged: Happens exclusively on the UI thread. Receives data from the worker thread via its ProgressChangedEventArgs. Cannot send data back to the worker thread.
- RunWorkerCompleted: Happens exclusively on the UI thread. Receives data from the worker thread via its RunWorkerCompletedEventArgs. Cannot send data back to the worker thread.
Does this sound useful? I'd have loved to talk about the Task API, but I'm running out of time. I think if you play around a bit with BackgroundWorker (and find examples), you'll find it a lot easier than trying to manage a thread yourself.
This answer is wrong. You should be using TableAdapter and Dictionaries instead.
Tags for this Thread
Posting Permissions
- You may not post new threads
- You may not post replies
- You may not post attachments
- You may not edit your posts
-
Forum Rules
|
Click Here to Expand Forum to Full Width
|