Winform Application Ui Hangs During Long-Running Operation

WinForm Application UI Hangs during Long-Running Operation

You need to perform the long running operation on a background thread.

There are several ways of doing this.

  1. You can queue the method call for execution on a thread pool thread (See here):

    ThreadPool.QueueUserWorkItem(new WaitCallback(YourMethod));

    In .NET 4.0 you can use the TaskFactory:

    Task.Factory.StartNew(() => YourMethod());

    And in .NET 4.5 and later, you can (and should, rather than TaskFactory.StartNew()) use Task.Run():

    Task.Run(() => YourMethod());
  2. You could use a BackgroundWorker for more control over the method if you need things like progress updates or notification when it is finished. Drag the a BackgroundWorker control onto your form and attach your method to the dowork event. Then just start the worker when you want to run your method. You can of course create the BackgroundWorker manually from code, just remember that it needs disposing of when you are finished.

  3. Create a totally new thread for your work to happen on. This is the most complex and isn't necessary unless you need really fine grained control over the thread. See the MSDN page on the Thread class if you want to learn about this.

Remember that with anything threaded, you cannot update the GUI, or change any GUI controls from a background thread. If you want to do anything on the GUI you have to use Invoke (and InvokeRequired) to trigger the method back on the GUI thread. See here.

why UI freezes during long running operations

You could use the "async and await" syntax for asynchronous action. It won't freeze the UI

private async void button1_Click(object sender, EventArgs e)
{
this.progressBar1.Style = System.Windows.Forms.ProgressBarStyle.Marquee;

var result = await RemoteFileExists("http://www.google.com/");

if (result)
{
// OK
MessageBox.Show("OK");
}
else
{
//FAIL
MessageBox.Show("Fail");
}
}

private async Task<bool> RemoteFileExists(string url)
{
try
{
HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
request.Method = "HEAD";
HttpWebResponse response = await request.GetResponseAsync() as HttpWebResponse;
return (response.StatusCode == HttpStatusCode.OK);
}
catch
{
return false;
}
}

You can read more about it here: http://blog.stephencleary.com/2012/02/async-and-await.html

C# WinForm application UI freezes after long period

Seems like previous comment made by @groverboy was the simple solution: I was regularly updating a "richTextBox" (that acted as an "instant log") with some information ... too much unfortunately.

The frequence update was less of a problem than the sheer number of added string. Though if I start with many sockets, the update frequency may become a problem as well.

I solved it with one of the many possible ways: update less information. One sentence logged in a minute instead of ~1000 per minute.

Of course, the best way of keeping a log would have been to put it inside a file. In my case, the log serves no other purpose than to say immediately "all is fine". Another way would have been to cull down the text in the richTextBox.

Thank you for the help!





EDIT -

I'll explain where my problem came from: initially my software could test socket connections on a specific IP:port. The receiving service at the given IP is supposed to handle multiple connections. For one connection, you can have multiple sendings (and for one sending, multiple messages). For each sending, i would write in the log, that is the "richTextBox" in the GUI: "informer n° X send Y correctly".

No problem for this scenario ... as long as your connections number and sendings per connection are defined. The software evolved as to be able to keep the connections, but have a limitless number of sendings.

In this last case, the text would grow too quickly (one connection making roughly 10 sendings, so that's 10 messages) with even just a small connections number.

I have tested a text reset in TextChanged event when the length goes above 10.000 caracters: there was no problem at all with the same settings that made my GUI freeze.

This leads me to think the string length was the main problem, though the frequency of updates could have made things worse as well.

How do I Stop my UI from freezing?

So worked out a little work around method for it, didn't use async however I did just end up creating a different thread for this task.

The new thread let me used a while(true) loop to forever loop receive response.

private void btnConnect_Click(object sender, EventArgs e)
{
ConnectToServer();
Thread timerThread = new Thread(StartTimer);
timerThread.Start();
//StartTimer();
//timerResponse.Enabled = true;

}
private static void StartTimer()
{
while (true)
{
ReceiveResponse();
}
}

This worked good enough for what I needed to do.

Thanks all for the help!

C# win forms app hangs

  1. You should always use using statements on anything that implements IDisposable, it ensures the resource is released even in the event of an Exception. In this case it ensures your DB Connections are closed.
  2. Put your Ado.Net types in the scope of the method, not the class. Do not worry about trying to reuse connection objects, it is almost always a bad idea and most RDBMs like Sql Server handle pooling of connections so you do not have to.
  3. Your screen can appear to hang if this takes a while. The best solution is to make your event handler (button click for example) mark with async. What the signature looks like can depend on the UI (winforms, wpf, something else) but the idea is to offload the work from the main message window. As it is not immediately clear from what and where this is being called I did not update the code to reflect this.
const string oQuery = "SELECT UPRN, RefuseDay, RefuseWeek FROM RefuseDay";
const string sqlQuery = "INSERT INTO Ref_RefuseDay (UPRN, RefuseDay, RefuseWeek) VALUES (@UPRN, @RefuseDay, @RefuseWeek)";
try
{
using(var oCon = new OleDbConnection(oConStr))
using(var sqlCon = new SqlConnection(sqlConStr))
using(var oCmd = new OleDbCommand(oQuery, oCon))
{
oCon.Open();
sqlCon.Open();
count = 0;
lblProcessing.Text = count.ToString();

using(var dr = oCmd.ExecuteReader())
{
while (dr.Read())
{
lblProcessing.Text = "Processing: RefuseDay " + count.ToString();
var sUPRN = dr.GetString(0);
var sRefuseDay = dr.GetString(1);
var iRefuseWeek = dr.GetInt32(2);

using(var sqlCmd = new SqlCommand(sqlQuery, sqlCon))
{
sqlCmd.Parameters.AddWithValue("@UPRN", sUPRN);
sqlCmd.Parameters.AddWithValue("@RefuseDay", sRefuseDay);
sqlCmd.Parameters.AddWithValue("@RefuseWeek", iRefuseWeek);
sqlCmd.ExecuteNonQuery();
}
count++;
}
}
}
}
catch (Exception ex) {
MessageBox.Show(ex.Message);
}

Edit with Task and async/await

In order to aleviate the freezing window you can take advantage of several async methods provided on the ado.net types. When used with async/await when an await is encountered execution is suspended and control returned to the main message window. Here is that same code but updated so that it takes advantage of the async methods. It also illustrates how you call it from a button click method.

private async void button_Click(object sender, EventArgs e)
{
await readWriteRefuseDayAsync();
}

private async Task readWriteRefuseDayAsync() {
const string oQuery = "SELECT UPRN, RefuseDay, RefuseWeek FROM RefuseDay";
const string sqlQuery = "INSERT INTO Ref_RefuseDay (UPRN, RefuseDay, RefuseWeek) VALUES (@UPRN, @RefuseDay, @RefuseWeek)";
try {
using(var oCon = new OleDbConnection(oConStr))
using(var sqlCon = new SqlConnection(sqlConStr))
using(var oCmd = new OleDbCommand(oQuery, oCon))
{
await oCon.OpenAsync();
await sqlCon.OpenAsync();
count = 0;
lblProcessing.Text = count.ToString();

using(var dr = await oCmd.ExecuteReaderAsync())
{
while (await dr.ReadAsync())
{
lblProcessing.Text = "Processing: RefuseDay " + count.ToString();

var sUPRN = dr.GetString(0);
var sRefuseDay = dr.GetString(1);
var iRefuseWeek = dr.GetInt32(2);

using(var sqlCmd = new SqlCommand(sqlQuery, sqlCon))
{
sqlCmd.Parameters.AddWithValue("@UPRN", sUPRN);
sqlCmd.Parameters.AddWithValue("@RefuseDay", sRefuseDay);
sqlCmd.Parameters.AddWithValue("@RefuseWeek", iRefuseWeek);
await sqlCmd.ExecuteNonQueryAsync();
}
count++;
}
}
}
}
catch (Exception ex) {
MessageBox.Show(ex.Message);
}
}

Keep UI thread responsive when running long task in windows forms

Sometimes it is indeed required to do some asynchronous, background operation on the UI thread (e.g., syntax highlighting, spellcheck-as-you-type, etc). I am not going to question the design issues with your particular (IMO, contrived) example - most likely you should be using the MVVM pattern here - but you can certainly keep the UI thread responsive.

You can do that by sensing for any pending user input and yielding to the main message loop, to give it the processing priority. Here's a complete, cut-paste-and-run example of how to do that in WinForms, based on the task you're trying to solve. Note await InputYield(token) which does just that:

using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WinFormsYield
{
static class Program
{
// a long-running operation on the UI thread
private static async Task LongRunningTaskAsync(Action<string> deliverText, CancellationToken token)
{
for (int i = 0; i < 10000; i++)
{
token.ThrowIfCancellationRequested();
await InputYield(token);
deliverText(await ReadLineAsync(token));
}
}

[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);

// create some UI

var form = new Form { Text = "Test", Width = 800, Height = 600 };

var panel = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
FlowDirection = FlowDirection.TopDown,
WrapContents = true
};

form.Controls.Add(panel);
var button = new Button { Text = "Start", AutoSize = true };
panel.Controls.Add(button);

var inputBox = new TextBox
{
Text = "You still can type here while we're loading the file",
Width = 640
};
panel.Controls.Add(inputBox);

var textBox = new TextBox
{
Width = 640,
Height = 480,
Multiline = true,
ReadOnly = false,
AcceptsReturn = true,
ScrollBars = ScrollBars.Vertical
};
panel.Controls.Add(textBox);

// handle Button click to "load" some text

button.Click += async delegate
{
button.Enabled = false;
textBox.Enabled = false;
inputBox.Focus();
try
{
await LongRunningTaskAsync(text =>
textBox.AppendText(text + Environment.NewLine),
CancellationToken.None);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
finally
{
button.Enabled = true;
textBox.Enabled = true;
}
};

Application.Run(form);
}

// simulate TextReader.ReadLineAsync
private static async Task<string> ReadLineAsync(CancellationToken token)
{
return await Task.Run(() =>
{
Thread.Sleep(10); // simulate some CPU-bound work
return "Line " + Environment.TickCount;
}, token);
}

//
// helpers
//

private static async Task TimerYield(int delay, CancellationToken token)
{
// yield to the message loop via a low-priority WM_TIMER message (used by System.Windows.Forms.Timer)
// https://web.archive.org/web/20130627005845/http://support.microsoft.com/kb/96006

var tcs = new TaskCompletionSource<bool>();
using (var timer = new System.Windows.Forms.Timer())
using (token.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: false))
{
timer.Interval = delay;
timer.Tick += (s, e) => tcs.TrySetResult(true);
timer.Enabled = true;
await tcs.Task;
timer.Enabled = false;
}
}

private static async Task InputYield(CancellationToken token)
{
while (AnyInputMessage())
{
await TimerYield((int)NativeMethods.USER_TIMER_MINIMUM, token);
}
}

private static bool AnyInputMessage()
{
var status = NativeMethods.GetQueueStatus(NativeMethods.QS_INPUT | NativeMethods.QS_POSTMESSAGE);
// the high-order word of the return value indicates the types of messages currently in the queue.
return status >> 16 != 0;
}

private static class NativeMethods
{
public const uint USER_TIMER_MINIMUM = 0x0000000A;
public const uint QS_KEY = 0x0001;
public const uint QS_MOUSEMOVE = 0x0002;
public const uint QS_MOUSEBUTTON = 0x0004;
public const uint QS_POSTMESSAGE = 0x0008;
public const uint QS_TIMER = 0x0010;
public const uint QS_PAINT = 0x0020;
public const uint QS_SENDMESSAGE = 0x0040;
public const uint QS_HOTKEY = 0x0080;
public const uint QS_ALLPOSTMESSAGE = 0x0100;
public const uint QS_RAWINPUT = 0x0400;

public const uint QS_MOUSE = (QS_MOUSEMOVE | QS_MOUSEBUTTON);
public const uint QS_INPUT = (QS_MOUSE | QS_KEY | QS_RAWINPUT);

[DllImport("user32.dll")]
public static extern uint GetQueueStatus(uint flags);
}
}
}

Now you should ask yourself what you're going to do if user modifies the content of the editor while it's still being populated with text on the background. Here for simplicity I just disable the button and the editor itself (the rest of the UI is accessible and responsive), but the question remains open. Also, you should look at implementing some cancellation logic, which I leave outside the scope of this sample.

Not able to minimize or move Winform Window during installation

Here multiple approaches are available.

As the code involved some steps updating the GUI controls back, couldn't use the solutions directly.

As almost all solutions are based asynchronous principle, it used to throw an error, Cross-thread operation not valid: Control 'InstallButton' accessed from a thread other than the thread it was created on.

To avoid it segregated steps involving GUI control access and executed them sequentially while as remaining independent code was run asynchronously using Task.Run() method.
It has ignorable execution time for GI

//Logic to update control
Task.Run(()=>
{
//Remaining logic
});


Related Topics



Leave a reply



Submit