andyxl987

I have a program which performs a few simple download tasks. I'm using a class which I've called FileDownload.cs. I'm using a Form (simply called Form2 for now) which contains two buttons. When you press a button, it creates a new FileDownload(Label label, String location). Where label is from Form2 and needs to be updated depending on the download status. Location is the url for download.

I am creating a new FileDownload by the following code in Form2:

Code Snippet


public partial class Form2 : Form
{
private String location;
private Label label;
private Thread downloadA;

public Form2()
{
InitializeComponent();
}

private void button1_Click(object sender, EventArgs e)
{
location = "http://url/file.ext";
label = label2;
label.Text = "Connecting";
button1.Text = "Cancel";
downloadA = new Thread(new ThreadStart(downloadFile));
downloadA.Start();
}

private void downloadFile()
{
new FileDownload(label, location);
}
}


FileDownload.cs looks like this:

Code Snippet



public partial class FileDownload : Form
{
private delegate void updateProgressDelegate(long downloaded, Label label);
private long fileSize;
private String path;

public FileDownload(Label label, String location)
{
try
{
WebClient client = new WebClient();
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(location);
request.Credentials = CredentialCache.DefaultCredentials;
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
fileSize = response.ContentLength;
Stream remote = client.OpenRead(location);
int slash = location.LastIndexOf("/");
String filename = location.Substring(slash + 1);
Stream local = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None);
path = Path.GetFileName(filename);
int read = 0;
byte[] buffer = new byte[1024];
while ((read = remote.Read(buffer, 0, buffer.Length)) > 0)
{
local.Write(buffer, 0, read );
Invoke(new updateProgressDelegate(updateProgress), new object[] { local.Length, label });
}
remote.Close();
local.Close();
}
finally
{

}
}

private void updateProgress(long downloaded, Label label)
{
if (downloaded == fileSize)
{
label.Text = "Completed";
return;
}
int percentage = Convert.ToInt32(downloaded * 100 / fileSize);
label.Text = "Downloading: " + percentage + "%";
}
}



What I am attempting to do (in the above example), is to update Form2.label2 with the download percentage completed.

I keep getting the runtime error "Invoke or BeginInvoke cannot be called on a control until the window handle has been created."

I have searched various sites about this problem but couldn't find anything specific about making such changes when using another class. If I did find any information I didn't really understand it. I've only been playing about in C# for a couple of hours and prior to this, have some limited Java knowledge; I haven't covered threading yet so it's a new concept to me.

I would really appreciate any help that you would be able to give me. Thanks for your time, looking forward to hearing your answers.

Andrew


Re: Windows Forms General Updating UI from a different Class using a new Thread

sirjis

Don't do anything with the controls in the form's constructor. The constructor is called before the window or its controls have been created (the window handles, etc. have not been assigned). Instead, put the code that is in the FileDownload form's constructor into the Form.Load event. The Load event is called just after all of the controls have been created, but before the form is displayed.



Re: Windows Forms General Updating UI from a different Class using a new Thread

andyxl987

Thank you for your reply. I'm not sure my original code for FileDownload is correct so I'm not sure how to implement your suggestion. FileDownload should be a standard class, although I added the (partial class FileDownload : Form) decleration because this is the only way I know how to use Invoke. I'm not sure Invoke is the correct function that I want to be using.

I am trying to use the information from FileDownload (which is in a new thread) to update the label which exists in Form2 (in a different thread).

I'm really sorry if I'm being unclear or if I have not understood your answer which may have been correct. I just need a few pointers and then hopefully I'll be able to figure it out.




Re: Windows Forms General Updating UI from a different Class using a new Thread

Peter Ritchie

I would suggest using a BackgroundWorker object instead of manually creating a thread. The BackgroundWorker object will always raise the Progress and Completed event handlers on the UI's thread and the DoWork event handler is always raised on a thread pool thread. That way you don't have to worry about marshaling anything back to the UI thread and sufficiently decoupled the existence of a Form from your background logic.




Re: Windows Forms General Updating UI from a different Class using a new Thread

andyxl987

Which class do I create the background worker in I've read through a few documents about this method and I think that I need to put it in the FileDownload class, although I am still quite confused about how to use it relating to the above two-class example. I was able to have everything working fine using Invoke when it was all in one class but I think my project will be easier doing it in two classes once I have this update problem out of the way.




Re: Windows Forms General Updating UI from a different Class using a new Thread

andyxl987

I managed to get it working in the end. I added Form2 to the constructor of FileDownload:

public FileDownload(Form2 f, Label label, String location)
{
this.f = f;
etc....
}

and moved the Invoke method into Form2. I then called it using p.updateProgress() and it works fine now.




Re: Windows Forms General Updating UI from a different Class using a new Thread

Christopher Payne

Your FileDownload class does not need to inherit from Form, and you don't need to pass Form2 to it either. Since you are passing the Label control to FileDownload, you can call Invoke from the label control instead of from the Form class. This will make your FileDownload class a lot lighter in weight, and will remove all the baggage that it currently inherites from Form.

The way you have it right now, FileDownload is dependant on Form2. So when you create another form that you want to use FileDownload from, you'll still have to pass it a Form2 reference. While you could change it to use a Form reference instead of a Form2 reference, it's better to just get rid of the dependency that you don't need.





Re: Windows Forms General Updating UI from a different Class using a new Thread

andyxl987

I had realised I didn't require that dependency but didn't know how to call Invoke without using it. In the end, I realised where I was going wrong and that when I was using inheritence (I think that's the right terminology) with FileDownload, it was looking for a new instance of Form2 to use Invoke. When I changed the code to pass Form2 in FileDownload's instructor, I was able to call a method which I have writted in Form2 to update the required controls.

Here is the updated code, I think it should work better now:

Code Snippet


public partial class ProgramPath : Form
{
private delegate void updateDownloadCallback(
Button button, Label label, int percentage);
private FileDownload fdButil;
private FileDownload fdVirtual;
private String urlVirtual = "http://url/file.ext";
private String urlButil = "http://url/file.ext";

public ProgramPath()
{
InitializeComponent();
}

private void button1_Click(object sender, EventArgs e)
{
if (!btnVDownload.Text.Equals("Cancel"))
{
updateDownloadStatus(btnVDownload, lblVStatus, -2);
fdVirtual = new FileDownload(btnVDownload, this, lblVStatus, urlVirtual);
}
else
{
fdVirtual.Abort();
}
}

private void btnBDownload_Click(object sender, EventArgs e)
{
if (!btnBDownload.Text.Equals("Cancel"))
{
updateDownloadStatus(btnBDownload, lblBStatus, -2);
fdButil= new FileDownload(btnBDownload, this, lblBStatus, urlButil);
}
else
{
fdButil.Abort();
}
}

public void updateDownloadStatus(Button button, Label label, int percentage)
{
if (InvokeRequired)
{
BeginInvoke(new updateDownloadCallback(updateDownloadStatus),
new object[] { button, label, percentage });
}
else
{
if (percentage >= 0)
{
label.Text = "Downloaded: " + percentage + "%";
}
else if (percentage == 100)
{
button.Text = "Download";
label.Text = "Completed";
}
else if (percentage == -1)
{
button.Text = "Download";
label.Text = "Cancelled";
}
else if (percentage == -2)
{
button.Text = "Cancel";
label.Text = "Connecting";
}
}
}
}




Code Snippet

public class FileDownload
{
private bool isAborted;
private Button button;
private Label label;
private long fileSize;
private ProgramPath p;
private String location;
private String path;
private Thread thread;

public FileDownload(Button button, ProgramPath p, Label label, String location)
{
this.button = button;
this.p = p;
this.label = label;
this.location = location;

thread = new Thread(new ThreadStart(startDownload));
thread.Start();
}

private void startDownload()
{
Stream remote = null;
Stream local = null;

try
{
WebClient client = new WebClient();
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(location);
request.Credentials = CredentialCache.DefaultCredentials;
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
response.Close();
fileSize = response.ContentLength;
remote = client.OpenRead(location);
int slash = location.LastIndexOf("/");
String fileName = location.Substring(slash + 1);
path = Path.GetFileName(fileName);
local = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None);
int read = 0;
byte[] buffer = new byte[1024];
while ((read = remote.Read(buffer, 0, buffer.Length)) > 0)
{
local.Write(buffer, 0, read);
int percentage = Convert.ToInt32(local.Length * 100 / fileSize);
p.updateDownloadStatus(button, label, percentage);
}
p.updateDownloadStatus(button, label, 100);
}
finally
{
if (remote != null)
{
remote.Close();
local.Close();
if (isAborted)
{
File.Delete(path);
isAborted = false;
}
}
}
}

public void Abort()
{
isAborted = true;
thread.Abort();
p.updateDownloadStatus(button, label, -1);
}

public String getPath()
{
return path;
}
}





Thanks for your help everyone. I know the solution I've come up with isn't clean, and is probably a bad way to do things. If you have any suggestions, I am willing to learn but am very new with this sort of thing so please bare with me! I think it'd be beneficial to pick up a C# book or something at some point and work through that.