tags:

views:

160

answers:

2

Aight, did a bit of Googling and searching here, the only question I found related was this, although the only answer it had wasn't marked as accepted, is old and is confusing.

My problem is basically what I've said in the title. What happens is that the GUI freezes while the upload is in progress. My code:

// stuff above snipped

public partial class Form1 : Form
{
    WebClient wcUploader = new WebClient();

    public Form1()
    {
        InitializeComponent();

        wcUploader.UploadFileCompleted += new UploadFileCompletedEventHandler(UploadFileCompletedCallback);
        wcUploader.UploadProgressChanged += new UploadProgressChangedEventHandler(UploadProgressCallback);
    }

    private void button1_Click(object sender, EventArgs e)
    {
        if (openFileDialog1.ShowDialog() == DialogResult.OK)
        {
            string toUpload = openFileDialog1.FileName;
            wcUploader.UploadFileAsync(new Uri("http://anyhub.net/api/upload"), "POST", toUpload);
        }
    }

    void UploadFileCompletedCallback(object sender, UploadFileCompletedEventArgs e)
    {
        textBox1.Text = System.Text.Encoding.UTF8.GetString(e.Result);
    }

    void UploadProgressCallback(object sender, UploadProgressChangedEventArgs e)
    {
        textBox1.Text = (string)e.UserState + "\n\n"
            + "Uploaded " + e.BytesSent + "/" + e.TotalBytesToSend + "b (" + e.ProgressPercentage + "%)";
    }
}

EDIT: For clarification, this is what happens in order:

  1. I click button1
  2. I select a file
  3. The GUI stops responding, as in when I click on it nothing happens
  4. After a couple of seconds 50% shows up in the textbox Aaand the realisation hits. See my comment to the question I marked as the solution
  5. After a second or so with the GUI not responding in-between it's replaced with the response
+2  A: 

Sure it is.

The code works just fine.

wcUploader.UploadFileAsync(...) initiates the request and execution continues, meanwhile the progress is updated in TextBox1 and upon completion I get some JSON.

That is Async. If you simply called wcUploader.UploadFile, execution would block there until the file was uploaded and you would get no progress events.

Bottom line:

The UI is not blocked, progress events are called and UI is updated in real time.

Update:

To eliminate the initial block when the webclient is establishing the http connection, simply call the upload on another thread. In this scenario, you must use invocation to prevent cross thread exceptions:

using System;
using System.Net;
using System.Text;
using System.Threading;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        private readonly WebClient wcUploader = new WebClient();

        public Form1()
        {
            InitializeComponent();

            wcUploader.UploadFileCompleted += UploadFileCompletedCallback;
            wcUploader.UploadProgressChanged += UploadProgressCallback;
        }


        private void UploadFileCompletedCallback(object sender, UploadFileCompletedEventArgs e)
        {
            // a clever way to handle cross-thread calls and avoid the dreaded
            // "Cross-thread operation not valid: Control 'textBox1' accessed 
            // from a thread other than the thread it was created on." exception

            // this will always be called from another thread,
            // no need to check for InvokeRequired
            BeginInvoke(
                new MethodInvoker(() =>
                    {
                        textBox1.Text = Encoding.UTF8.GetString(e.Result);
                        button1.Enabled = true;
                    }));
        }

        private void UploadProgressCallback(object sender, UploadProgressChangedEventArgs e)
        {
            // a clever way to handle cross-thread calls and avoid the dreaded
            // "Cross-thread operation not valid: Control 'textBox1' accessed 
            // from a thread other than the thread it was created on." exception

            // this will always be called from another thread,
            // no need to check for InvokeRequired

            BeginInvoke(
                new MethodInvoker(() =>
                    {
                        textBox1.Text = (string)e.UserState + "\n\n"
                                        + "Uploaded " + e.BytesSent + "/" + e.TotalBytesToSend
                                        + "b (" + e.ProgressPercentage + "%)";
                    }));
        }

        private void button1_Click(object sender, EventArgs e)
        {
            textBox1.Text = "";

            if (openFileDialog1.ShowDialog() == DialogResult.OK)
            {
                button1.Enabled = false;
                string toUpload = openFileDialog1.FileName;
                textBox1.Text = "Initiating connection";
                new Thread(() =>
                           wcUploader.UploadFileAsync(new Uri("http://anyhub.net/api/upload"), "POST", toUpload)).Start();
            }
        }
    }
}
Sky Sanders
Oh. Oh. Ohh. The reason I was thinking it wasn't asynchronous was because the file I was selecting to upload was so minuscule that there is basically no opportunity... but there is, and my mind just quickly passed that appearance of progress off as a bug. The GUI *does* freeze, but it's only for a couple of seconds between the upload starting and ending.
a2h
You can get rid of the initial pause by making the `wcUploader.UploadFileAsync` call in another thread. I will add that to my answer, BUT - then you MUST use an invoke pattern as mentioned by eamon.
Sky Sanders
Thanks for that, would upvote again if I could, ah well. :)
a2h
+1  A: 

There is a bug in your code that's worth fixing anyhow, regardless of the UI locking:

You specify two callbacks which the asynchronous uploader should trigger. In those callbacks, you'll be running on the uploader's thread; however, you may only touch the GUI from the main GUI thread - so your callbacks might corrupt the GUI's state.

You shouldn't touch textBox1.Text in either callback. It's unlikely that's the problem, but nevertheless, you should fix it to avoid crash and corruption bugs. The question you've linked illustrates one way of avoiding this: check the form's Control.InvokeRequired property (behind the scenes this checks whether you're on the right thread), or simply assume an invoke is required and then - use Control.BeginInvoke to trigger a method on the GUI thread.

Any of your controls will do since they all run in the same thread; so if (textBox1.InvokeRequired) textBox1.BeginInvoke... is just as good as if (this.InvokeRequired) this.BeginInvoke...

Eamon Nerbonne
The original code, as posted, may look as if it needs invocation, but actually does not. The updated code that I posted, however does. Also, you should only ever check for InvokeRequired when you are unsure of the source of the call. In the example given, the calls are always going to come from the same place so checking is unnecessary.
Sky Sanders
Why would the original code be safe? Are the webclient callbacks executed on the original thread? Where is that documented?
Eamon Nerbonne
Instead of answering your question as asked, let me ask you a few: Did you try to run the code? Have you written code using async calls with WebClient? I am going to guess the answer is no. Did you make assumptions about the behavior of the WebClient class? I have to assume the answer is yes. I will leave the rest as an exercise. cheers..
Sky Sanders
To the contrary: I'm not assuming the async callback necessarily executes on the original thread. Your condescending tone suggests it's an odd thing to be curious about the specified behaviour. I've learned that "it works on my machine" is a poor guarantee that it will work in general over various versions and in different contexts. I'm quite willing to believe what you say, but that doesn't make me any less interested in knowing whether that's specified behaviour I can rely on or merely an accident that may not work in future versions or in a context other than windows forms.
Eamon Nerbonne