I want to display a large collection of points as a chart (at least 300 000 points), using wpf toolkit chart.
I have the following XAML
<chartingToolkit:Chart Name="chartHistory">
<chartingToolkit:Chart.Axes>
<chartingToolkit:LinearAxis x:Name="horizontalAxis" Orientation="X" Title="Time [s]" ShowGridLines="True"/>
<chartingToolkit:LinearAxis x:Name="verticalAxis" Orientation="Y" Title="Value [mm]" ShowGridLines="True"/>
</chartingToolkit:Chart.Axes>
<chartingToolkit:AreaSeries x:Name="chartSeries" DataPointStyle="{StaticResource chartDataPoint}"
IndependentValuePath="TimeInSeconds"
DependentValuePath="Value">
</chartingToolkit:AreaSeries>
</chartingToolkit:Chart>
And in code behind:
public class PointData
{
public double TimeInSeconds { get; set; }
public double Value { get; set; }
}
private List<PointData> points;
private void Screen_Loaded(object sender, RoutedEventArgs e)
{
// this is a large collection of points (minimum 300 000)
points = LoadPointsFromFile();
// and it takes a lot of time to read from the file and load in the UI
chartSeries.ItemsSource = points;
// additional chart display properties (setting min/max on the axes etc.)
}
So, I have 2 time consuming operations that block my UI. What I want is to display a "please load dialog" while the time consuming operations take place, so that the user knows the application is still doing something.
The time consuming operations are:
reading the points from the file (this operation could be done on a separate thread, but since the next operation (loading the points in the chart) depends on it and is a UI operation, I didn't put it in a separate thread)
loading the points as ItemsSource in the chart - this is a UI operation and should be done on the UI thread. But how can I still make the application responsive since I do not have any control on how the points are displayed - this is the chart's own logic?
So, any ideas? Did you have any similar problems?
Thank you,
Nadia
Actually, what I did was to create a separate thread that displays a different "loading dialog" that shows up while the data is loading. There still is about one second from the moment the dialog is closed until the UI gets fully responsive, but it's still better than looking at a unresponsive UI for 5-10 seconds.
public class PointData
{
public double TimeInSeconds { get; set; }
public double Value { get; set; }
}
#region Wait Dialog Thread
private class MainWindowSize
{
public double Left;
public double Top;
public double Width;
public double Height;
}
private Thread newWindowThread = null;
private void ThreadStartingPoint(object obj)
{
// WaitDialog is a new window from your project - you can place a animation or message inside it
WaitDialog tempWindow = new WaitDialog();
// since we don't have an owner for the window, we need
// to compute the location of the popup dialog, so that
// its centered inside the main window
MainWindowSize wndSize = obj as MainWindowSize;
if (wndSize != null)
{
tempWindow.Left = wndSize.Left + wndSize.Width / 2 - tempWindow.Width / 2;
tempWindow.Top = wndSize.Top + wndSize.Height / 2 - tempWindow.Height / 2;
}
// it's very important not to set the owner of this dialog
// otherwise it won't work in a separate thread
tempWindow.Owner = null;
// it shouldn't be a modal dialog
tempWindow.Show();
tempWindow.Closed += (sender1, e1) => tempWindow.Dispatcher.InvokeShutdown();
System.Windows.Threading.Dispatcher.Run();
}
private void CreateAndStartWaitWindowThread()
{
// create a thread only if you don't have an active one
if (newWindowThread == null)
{
// use ParameterizedThreadStart instead of ThreadStart
// in order to send a parameter to the thread start method
newWindowThread = new Thread(new ParameterizedThreadStart(ThreadStartingPoint));
newWindowThread.SetApartmentState(ApartmentState.STA);
newWindowThread.IsBackground = true;
// get the properties of the window, in order to compute the location of the new dialog
Window mainWnd = App.CurrentApp.MainWindow;
MainWindowSize threadParams = new MainWindowSize { Left = mainWnd.Left, Top = mainWnd.Top, Width = mainWnd.ActualWidth, Height = mainWnd.ActualHeight };
// start thread with parameters
newWindowThread.Start(threadParams);
}
}
private void AbortAndDeleteWaitWindowThread()
{
// abort a thread only if you have an active one
if (newWindowThread != null)
{
newWindowThread.Abort();
newWindowThread = null;
}
}
#endregion
private List<PointData> points;
private void Screen_Loaded(object sender, RoutedEventArgs e)
{
try
{
// call this before long operation
this.Cursor = Cursors.Wait;
CreateAndStartWaitWindowThread();
// this is a large collection of points (minimum 300 000)
points = LoadPointsFromFile();
// and it takes a lot of time to read from the file and load in the UI
chartSeries.ItemsSource = points;
// additional chart display properties (setting min/max on the axes etc.)
}
catch(Exception ex)
{
// do something with the exception
}
finally
{
// call this after long operation - and make sure it's getting called
// so put it in the finally block - to call it even if an exception is raised
this.Cursor = Cursors.Arrow;
AbortAndDeleteWaitWindowThread();
}
}
Source: http://www.c-sharpcorner.com/uploadfile/suchit_84/creating-wpf-windows-on-dedicated-threads/
Related
I would like a “Pause” the chart's series updates to do some job (like i have a button when i click it will suspend the chart update and then when I click resume button, it will update all suspended point in series.
I know about
chart1.Series.SuspendUpdates();
but it does not seem to work with me. I use mschart sample -- realtime data (thread safe).
Here is the full code
public partial class RealTimeSample : Form
{
public RealTimeSample()
{
InitializeComponent();
}
private Thread addDataRunner;
private Random rand = new Random();
public delegate void AddDataDelegate();
public AddDataDelegate addDataDel;
private void RealTimeSample_Load(object sender, System.EventArgs e)
{
// create the Adding Data Thread but do not start until start button clicked
ThreadStart addDataThreadStart = new ThreadStart(AddDataThreadLoop);
addDataRunner = new Thread(addDataThreadStart);
// create a delegate for adding data
addDataDel += new AddDataDelegate(AddData);
}
/// Main loop for the thread that adds data to the chart.
/// The main purpose of this function is to Invoke AddData
/// function every 1000ms (1 second).
private void AddDataThreadLoop()
{
while (true)
{
chart1.Invoke(addDataDel);
Thread.Sleep(1000);
}
}
public void AddData()
{
DateTime timeStamp = DateTime.Now;
foreach (Series ptSeries in chart1.Series)
{
AddNewPoint(timeStamp, ptSeries);
}
}
/// The AddNewPoint function is called for each series in the chart when
/// new points need to be added. The new point will be placed at specified
/// X axis (Date/Time) position with a Y value in a range +/- 1 from the previous
/// data point's Y value, and not smaller than zero.
public void AddNewPoint(DateTime timeStamp, System.Windows.Forms.DataVisualization.Charting.Series ptSeries)
{
double newVal = 0;
if (ptSeries.Points.Count > 0)
{
newVal = ptSeries.Points[ptSeries.Points.Count - 1].YValues[0] + ((rand.NextDouble() * 2) - 1);
}
if (newVal < 0)
newVal = 0;
// Add new data point to its series.
chart1.Series.SuspendUpdates();
ptSeries.Points.AddXY(timeStamp.ToOADate(), rand.Next(10, 20));
chart1.Series.SuspendUpdates();
// remove all points from the source series older than 1.5 minutes.
double removeBefore = timeStamp.AddSeconds((double)(90) * (-1)).ToOADate();
//remove oldest values to maintain a constant number of data points
while (ptSeries.Points[0].XValue < removeBefore)
{
ptSeries.Points.RemoveAt(0);
}
chart1.ChartAreas[0].AxisX.Minimum = ptSeries.Points[0].XValue;
chart1.ChartAreas[0].AxisX.Maximum = DateTime.FromOADate(ptSeries.Points[0].XValue).AddMinutes(2).ToOADate();
}
/// Clean up any resources being used.
protected override void Dispose(bool disposing)
{
if ((addDataRunner.ThreadState & ThreadState.Suspended) == ThreadState.Suspended)
{
addDataRunner.Resume();
}
addDataRunner.Abort();
if (disposing)
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose(disposing);
}
private void startTrending_Click_1(object sender, EventArgs e)
{
// Disable all controls on the form
startTrending.Enabled = false;
// and only Enable the Stop button
stopTrending.Enabled = true;
// Predefine the viewing area of the chart
var minValue = DateTime.Now;
var maxValue = minValue.AddSeconds(120);
chart1.ChartAreas[0].AxisX.Minimum = minValue.ToOADate();
chart1.ChartAreas[0].AxisX.Maximum = maxValue.ToOADate();
// Reset number of series in the chart.
chart1.Series.Clear();
// create a line chart series
Series newSeries = new Series("Series1");
newSeries.ChartType = SeriesChartType.Line;
newSeries.BorderWidth = 2;
newSeries.Color = Color.OrangeRed;
newSeries.XValueType = ChartValueType.DateTime;
chart1.Series.Add(newSeries);
// start worker threads.
if (addDataRunner.IsAlive == true)
{
addDataRunner.Resume();
}
else
{
addDataRunner.Start();
}
}
private void stopTrending_Click_1(object sender, EventArgs e)
{
if (addDataRunner.IsAlive == true)
{
addDataRunner.Suspend();
}
// Enable all controls on the form
startTrending.Enabled = true;
// and only Disable the Stop button
stopTrending.Enabled = false;
}
}
EDIT:
I figured out that as long as you set the minmum or the maximum property for the Axis the chart will keep display even if you have used
chart1.Series.SuspendUpdates();
I had to to remove those lines after i call SuspendUpdates() and now i can see the chart series suspended
chart1.ChartAreas[0].AxisX.Minimum = ptSeries.Points[0].XValue;
chart1.ChartAreas[0].AxisX.Maximum = DateTime.FromOADate(ptSeries.Points[0].XValue).AddMinutes(2).ToOADate();
MsChart does support this directly and indeed using Series.SuspendUpdates() is a good way but you need to do it right. (See however the update below for a drawback)
MSDN says this:
A call to the Invalidate method will have no effect after the
SuspendUpdates method is called.
If you call the SuspendUpdates method several times, you will need to
call the ResumeUpdates method an equal number of times.
This would explain why it doesn't work for you: Keeping the calls balanced is crucial. You need to keep track of them yourself as there is no counter you could query. But if you overshoot the ResumeUpdates calls, nothing bad happens, extra calls are simply ignored and the next SuspendUpdates will pause again.
Here is an example screenshot, watch the suspension counter..!
Note that normally adding points will automatically triggger an Invalidate. If you are doing other things, like drawing in a Paint event etc.. you may need to call Chart.Invalidate(), which SuspendUpdates will prevent, until cancelled by the same number of ResumeUpdates..
Alternatively you can also use one of these simple workarounds:
The most straightforward will create the DataPoints via a constructor and then either
use series.Add(theNewPoint) for normal, or..
use someList<DataPoint>.Add(theNewPoint) for paused mode.
When setting to pause mode simply add all points to the series.Points before clearing it. Unfortunately there is no points.AddRange so you will have to use a foreach loop. Maybe chart.SuspendLayout could help with performance.
The other workaround that comes to mind may or may not be suitable: You could play with the xAxis.Maximum and maybe xAxis.Minimum values. By setting them to fixed values you would allow addding points to the right without displaying them. To show the whole set of points you would reset them to double.NaN. This may work for you but it may also interfer with what you have.
Update: As noted by OP, the data are updated when he changes the Minimum and/or Maximum of an Axis. The same effect will show on many other occasions:
Calling chart.AreasRecalculateAxesScale();
Changing the chart's Size
Changing any Axis property like Color or Width..
Changing the LegendText of a Series
and many more..
So I guess the updated data are needed whenever the ChartArea is manipulated and forced to update itself..
So, this may well make the 1st workaround the better because the more robust solution.
I have a C# application (winforms and wpf, but for this question I'm focusing on the winforms one) where a backgroundWorker is used to work on a data set, and the call to ProgressChanged which then calls the form Refresh method to force a repaint. This then paints a bunch of ellipses based on the current frame of the data set.
A given frame may involve drawing anywhere between zero and several hundred ellipses.
In addition, I have a slider control that allows the user to adjust the playback rate (basically the thread.sleep value within the loop.)
When the user sets the sleep value too low, sometimes the repainting methods get queued up, and the UI becomes unresponsive. (This depends on the number of ellipses in the frame, and the speed of the computer. And the delay is 100% with the repainting on the UI, not with any other processing, which is basically just incrementing a counter and setting a label text.)
I would like to be able to detect the queuing up and automatically adjust the speed slider to accommodate a larger data set and/or slower computer. How can I tell if the UI thread is backed up with multiple calls to Map_Paint?
Current code (paraphrased):
public Map()
{
InitializeComponent();
_worker = new BackgroundWorker();
_worker.DoWork += _worker_DoWork;
_worker.ProgressChanged += _worker_ProgressChanged;
_worker.WorkerReportsProgress = true;
}
private void _worker_DoWork(object sender, DoWorkEventArgs e)
{
_frameCount = _frames.FrameCount();
// For this specific example, _frameCount may be around 30000-40000
for (var i = 0; i < _frameCount; i++)
{
var f = _frames.Frame(i + 1);
_worker.ReportProgress(i, f);
Thread.Sleep(_tickCount);
_suspend.WaitOne(); // Used to Pause the playback
}
}
void _worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
// set some variables according to state and progresspercentage snipped here
// ...
// I would like to detect at this point whether the screen repainting is backed up
// and if so, adjust the value of _tickCount to slow down the program.
this.Refresh();
}
private void Map_Paint(object sender, PaintEventArgs e)
{
// Lots of ellipsis drawing stuff here
// Maybe 0-1000 ellipses drawn per cycle.
}
private void tbSpeed_Scroll(object sender, EventArgs e)
{
// This is the Scroll event for the slider.
// Value range is 10-300
// The slider becomes unresponsive when the UI thread backs up.
// I'd like to detect the back up and override the value of _tickCount
_tickCount = tbSpeed.Value;
}
private static object _lock = new object();
private static int _queuedCount = 0;
public Map()
{
InitializeComponent();
_worker = new BackgroundWorker();
_worker.DoWork += _worker_DoWork;
_worker.ProgressChanged += _worker_ProgressChanged;
_worker.WorkerReportsProgress = true;
}
private void _worker_DoWork(object sender, DoWorkEventArgs e)
{
_frameCount = _frames.FrameCount();
// For this specific example, _frameCount may be around 30000-40000
for (var i = 0; i < _frameCount; i++)
{
var f = _frames.Frame(i + 1);
lock(_lock)
{
_queuedCount++;
}
_worker.ReportProgress(i, f);
Thread.Sleep(_tickCount);
_suspend.WaitOne(); // Used to Pause the playback
}
}
void _worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
if (_queuedCount > 1)
//now queue is building up
this.Refresh();
lock(_lock)
{
_queuedCount--;
}
}
Before reading, I want everyone reading this to know that I have tried multiple delegate/Cross-Threading/Invoking Solutions from all over stack overflow.
With that said, this is what my program is supposed to do:
Worker Thread 1 is called to start Async Operation.
If it detects a line that has a typical PRIVMSG header along with the word subscribed!
Create a new MetroTaskWindow with a TaskWindowControl and Add it to the queue
Worker Thread 2 is called after worker thread 1
Worker Thread 2 checks every 5 seconds if queue contains something
If it does, show it and get rid of it
Here is the associated Code If you need more, let me know to the above requirements:
Worker Thread 1 Segment
string line = "";
while (!backgroundWorker1.CancellationPending)
{
try
{
line = reader.ReadLine();
}
catch { }
if (line != null && !line.Contains("JOIN"))
{
try
{
if (line.Contains("PING") && !line.Contains("PRIVMSG"))
{
writer.Write(line.Replace("PING", "PONG"));
Trace.WriteLine(line.Replace("PING", "PONG"));
}
else if (line.Split(new char[] { ' ' })[0].Equals(":twitchnotify!twitchnotify#twitchnotify.tmi.twitch.tv") ||
line.Split(new char[] { ' ' })[0].Equals(":stds_catchemall!stds_catchemall#stds_catchemall.tmi.twitch.tv") && line.Contains("subscribed!"))
{
total += 1;
checkNotifications();
}
}
catch
{
continue;
}
}
if (!String.IsNullOrEmpty(line))
Trace.WriteLine(line);
}
}
private void checkNotifications()
{
List<Achievement> tempQueue = new List<Achievement>();
foreach (Achievement a in achievements) {
//I know i could shorten this, but i need it left like this...
if (a.AfterSub)
tempQueue.Add(a);
if (total - a.Goal == start)
tempQueue.Add(a);
if (total == a.Goal)
tempQueue.Add(a);
}
foreach (Achievement a in Sort(tempQueue))
{
MetroTaskWindow m = new MetroTaskWindow(a, this, a.Type.ToString(), new TaskWindowControl(a.Name, a.Message, a), 4, r, ((ScreenRegion)r).getGS());
queue.Add(m);
}
}
Worker Thread 2
private void CheckAvailable_DoWork(object sender, DoWorkEventArgs e)
{
while (true)
{
Thread.Sleep(5250);
BeginInvoke((MethodInvoker)delegate
{
if (queue.Count > 0)
{
queue[0].Show(); // <---- Error Occurs Here
//Cross-thread operation not valid: Control 'TaskWindowControl' accessed from a thread other than the thread it was created on.
queue.RemoveAt(0);
}
});
}
}
Metro Task Window
public MetroTaskWindow(Achievement a, IWin32Window parent, string title, Control userControl, int secToClose, MetroForm r, Form gs)
{
controlContainer = new MetroPanel();
Controls.Add(controlContainer);
controlContainer.Controls.Add(userControl);
userControl.Dock = DockStyle.Fill;
closeTime = secToClose * 500;
this.a = a;
form = r;
chroma = gs;
p = (Form1)parent;
this.Text = title;
this.Resizable = false;
this.Movable = true;
this.StartPosition = FormStartPosition.Manual;
if (parent != null && parent is IMetroForm)
{
this.Theme = ((IMetroForm)parent).Theme;
this.Style = ((IMetroForm)parent).Style;
this.StyleManager = ((IMetroForm)parent).StyleManager.Clone(this) as MetroStyleManager;
this.ShadowType = MetroFormShadowType.None;
}
switch (a.Type)
{
case PopupType.Achievement:
Text = "Achievement!";
break;
case PopupType.Milestone:
Text = "Milestone!";
break;
case PopupType.Notification:
Text = "Notification";
break;
}
}
TaskWindowControl
public partial class TaskWindowControl : UserControl
{
public TaskWindowControl(string name, string info, Achievement a)
{
InitializeComponent();
metroLabel1.Text = name;
metroTextBox1.Text = info;
metroTextBox1.Select(0, 0);
try
{
Trace.WriteLine(Directory.GetCurrentDirectory() + "\\" + a.Picture);
pictureBox1.Image = Image.FromFile(Directory.GetCurrentDirectory() + "\\" + a.Picture);
}
catch
{
MessageBox.Show("There was an error loading the image for this achievement.");
}
}
}
And as I stated above, there are a LOT of duplicates, none of which have helped my answer. I also don't know much about the Delegate/Invoking process which is why I need some extra help.
Update #1
Anywhere I had queue.Add(m); is now replaced with queue.Enqueue(a);
And my update method (Without timer so far) is just:
public void DisplayDialog()
{
Achievement a = null;
queue.TryDequeue(out a);
MetroTaskWindow m = new MetroTaskWindow(a, this, a.Type.ToString(), new TaskWindowControl(a.Name, a.Message, a), 4, r, ((ScreenRegion)r).getGS());
m.Show();
}
Something that I found is that when I changed the code to this, the Thread that does the animations and things on my MetroTaskWindow doesn't get activated. The Windows Stays in with a windows loading circle and it never goes away. Any ideas? I'm using the OnActivated event, so it SHOULD fire when i .Show();
Edit #2
What I ended up doing to get my above error to work, was to switch all of my code to a new Windows Form Timer. This polls every 5 seconds, and keeps the MetroTaskWindow from hanging due to the while loop.
Form1.Designer.cs
private System.Windows.Forms.Timer timer2;
timer2 = new System.Windows.Forms.Timer(this.components);
timer2.Interval = 5250;
timer2.Tick += new System.EventHandler(this.timer2_Tick);
Form1.cs
private void timer2_Tick(object sender, EventArgs e)
{
if (queue.Count > 0)
{
DisplayDialog();
}
}
The error makes sense. The queue contains MetroTaskWindow instances created on a worker thread, not on the UI thread. You call checkNotifications in the background worker's entry point method, which runs on a separate thread.
What I recommend you do is:
Store in the queue only metadata about the windows. Create a new class with name, message and anything MetroTaskWindow needs to be created. Add instances of this class to the queue in checkNotifications. By the looks of it you might be able to use the Achievement class directly and push that to the queue.
Create the windows and show them in CheckAvailable_DoWork, where you now call queue[0].Show();.
After you solve this you should post a new question about how to refactor your code into async/await and get rid of that ugly background worker.
A quick refactoring idea
I would remove the second background worker altogether and use a timer instead that polls the queue every X seconds (or 5250ms, if you prefer).
To make this work you need to change the queue's type which is now actually a List<T> to a ConcurrentQueue<T>. This will allow you to push stuff from the worker, and pop from the UI thread (in the timer callback).
This way you can remove the second background worker and that while(true).
I am writing a program to monitor the status of various devices across a network. The details of these devices are stored in a file. The HandleClientComm class reads information about these devices from the file and establishes a tcp connection with them. After the file has been read, the manEvent is used for notification. The UpdateGUI function is invoked for each device. When the data.caller is equal to 1, it adds controls for that device, but the group boxes are disabled. The function hd.StartThreads, listens for connections from various devices using the Threadpool. Once the connection is accepted, the UpdateGUI function is invoked again with a data.caller value of 2. My problem is that the groupbox is not being enabled. The message box displays "begin", but does not got to end. Tried accessing other controls, other than the groupbox, but found out that I cannot access any of the controls from there. Is it a problem with the message loop, because there is no infinite loop running in my code?
namespace FA
{
public partial class EditDevice : Form
{
public struct DisplayComponents
{
public GroupBox gp;
public List<Panel> labelPanel;
public List<FlowLayoutPanel> picPanel;
public List<Label> LabelList;
public List<PictureBox> Pics;
public Label Mid, Date, Time;
public int gpHeight, gppt;
};
public DisplayComponents[] comps;
private DeviceList[] dev;
private ManualResetEvent manEvent;
private int devCount;
private HandleClientComm hd;
public EditDevice()
{
InitializeComponent();
//Create event to notify whether device file has been read
manEvent = new ManualResetEvent(false);
//Create object of the client communication class
hd = new HandleClientComm(manEvent);
//wait for the file read notification
manEvent.WaitOne();
//get the device count
devCount = hd.devCount;
//get the device details
dev = hd.dv;
initializeForm();
//Add event handler for device status change
hd.StatusChanged += new HandleClientComm.StatusChangeHandler(UpdateGUI);
//Start thread to monitor device status
Thread th = new Thread(hd.StartThreads);
th.Start();
th.Join();
}
public void initializeForm()
{
//Create components
comps = new DisplayComponents[hd.devCount];
// Groupbox initial point
int gppt = 40;
//Calculate Groupbox point and heights for each devices
for (int i = 0; i < devCount; i++)
{
comps[i].gpHeight = 60;
comps[i].gpHeight = comps[i].gpHeight + (dev[i].Zones / 21) * 77;
if (dev[i].Zones % 21 != 0)
comps[i].gpHeight += 77;
comps[i].gppt = gppt;
gppt += comps[i].gpHeight+10;
}
}
private void UpdateGUI(object sender, StatusChangeEventArgs data)
{
if (data.caller == 1)
{
//Function to add controls to the form
addDeviceControls(data.index);
}
else
{
MessageBox.Show("begin");
comps[data.index].gp.Enabled = true;
MessageBox.Show("end");
}
}
}
public class StatusChangeEventArgs : EventArgs
{
public int index { get; internal set; }
public int caller { get; internal set; }
public StatusChangeEventArgs(int index1, int callno)
{
this.index = index1;
this.caller = callno;
}
}
}
It sounds like your HandleClient.Comm.StatusChangehandler(UpdateGUI); call is consuming the errors or something higher level is catching them. Have you tried to break pointing and stepping through the code?
In order to update controls from different thread, you need to Invoke the changes on your main thread.
Please see this question as it may help you out more. How to update the GUI from another thread in C#?
If you are using WPF, then it is done slightly differently and need to call Dispatcher.Invoke in order to update.
I hope this helps you out. It really does sound like your errors are being consumed without your knowledge.
In my application I have a queue download list which consists of progress bars and the file names. When the user clicks a button the file name and progress bar is instantiated and added to the queue. Files download one at a time and asynchronously. What I want to do is keep all the progress bars of the files that are waiting to be downloaded yellow in color and then turn green when it is being downloaded and then turn blue when they are completed. It currently works if I have CheckForIllegalCrossThreadCalls = false; in the constructor of the custom progress bar. I want to see if there is a way to make thread safe changes to the progress bars.
I have each queue item set up as an object. The queue item objects are created from the main form code (Form1.cs) when a button is pressed and the progress bars are created in the queue item constructor, which is probably where my problem begins. The downloads are started through a function in the queue item object.
Queue Item Snippet
public class QueueItem
{
public bool inProgress;
public QueueBar bar;
public QueueItem(args)
{
bar = new QueueBar();
inProgress = false;
// handle arguments
}
public void Download()
{
// process info
WebClient client = new WebClient();
client.DownloadFileCompleted += new AsyncCompletedEventHandler(client_DownloadFileCompleted);
client.DownloadProgressChanged += new DownloadProgressChangedEventHandler(client_DownloadProgressChanged);
client.DownloadFileAsync(url, #savePath);
}
private long lastByte = 0;
private long newByte = 0;
private void client_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
{
percentValue = e.ProgressPercentage;
bar.Value = e.ProgressPercentage;
newByte = e.BytesReceived;
}
private void client_DownloadFileCompleted(object sender, AsyncCompletedEventArgs e)
{
// change bar color
bar.Value = 100;
}
}
Queue Bar Snippet
public class QueueBar : ProgressBar
{
// variables
public QueueBar()
{
this.SetStyle(ControlStyles.UserPaint, true);
// initialize variables
}
// function to change text properties
// function to change color
protected override void OnPaint(PaintEventArgs e)
{
// painting
}
}
Main Function Snippet
public partial class Form1 : Form
{
private List<QueueItem> qItems;
private BackgroundWorker queue;
private void button_Click(object sender, EventArgs e)
{
// basic gist of it
qItems.Add(new QueueItem(args));
Label tmpLabel = new Label();
tmpLabel.Text = filename;
tmpLabel.Dock = DockStyle.Bottm;
splitContainerQueue.Panel2.Controls.Add(tmpLabel);
splitContainerQueue.Panel2.Controls.Add(qItems[qItems.Count - 1].bar);
if (!queue.IsBusy) { queue.RunWorkerAsync(); }
}
private void queue_DoWork(object sender, DoWorkEventArgs e)
{
while (qItems.Count > 0)
{
if (!qItems[0].inProgress && qItems[0].percentValue == 0)
{
qItems[0].inProgress = true;
qItems[0].Download();
}
// else if statements
}
}
I also just tried creating a background worker to create the Queue Items and add the controls asynchronously but that doesn't work since the split container was created on a different thread.
You cannot call a UI control (created on your UI thread) from another thread safely - you need to use InvokeRequired / BeginInvoke() for such calls. When calling BeginInvoke() you'll pass a delegate; something like this (just some sample code, yours will look slightly different):
private void SomeEventHandler ( object oSender, EventArgs oE )
{
if ( InvokeRequired )
{
MethodInvoker oDelegate = (MethodInvoker) delegate
{
SomeEventHandler ( oSender, oE );
};
BeginInvoke ( oDelegate );
return;
}
else
{
// already on the correct thread; access UI controls here
}
}
You also cannot create your progress bars away from the UI thread - you need to create all your controls as part of your UI and then if you need to access these progress bars from your queue items, you'll have to pass in a reference to the progress bar. When you try to access the progress bar, you'll do
if ( bar.InvokeRequired ) { ... }
to determine if you're trying to call it from the right thread.
The reason for this mess is because controls handle many of their property updates through messages and those messages must be delivered synchronously, in the correct order. The only way to ensure this (without some very complex coding) is to create all controls on the same thread where the thread runs a message pump.