Binding DataGridView to DataSource - Throws CurrencyError IndexOutOfRangeException
I've been puzzling over this one for a few days now and it's got me pretty beaten, but to be honest I'm not all that experienced yet and I'm having trouble with DataGridView - which seems a common topic.
public partial class frmMain : Form
{
ServerConnection sabCom;
private BindingSource jobSource = new BindingSource();
private void timer1_Tick(object sender, EventArgs e)
{
if (bgUpdateThread.IsBusy == false)
{
bgUpdateThread.RunWorkerAsync(sabCom);
}
}
}
private void frmMain_Load(object sender, EventArgs e)
{
timer1.Interval = 3000;
timer1.Start();
}
private void bgUpdateThread_DoWork(object sender, DoWorkEventArgs e)
{
ServerConnection s = e.Argument as ServerConnection;
s.update();
e.Result = s;
}
private void bgUpdateThread_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
this.sabCom = e.Result as ServerConnection;
if (dgvQueueFormatted == false)
{
dgvQueue_Init(); //Applies formatting and loads column width. Inits data sources.
}
else
{
dgvQueue_Update();
}
}
private void dgvQueue_Update()
{
dgvQueue.Refresh();
}
private void dgvQueue_Init()
{
try
{
jobSource.DataSource = sabCom.queue.jobs;
dgvQueue.DataSource = jobSource;
try
{
//Apply saved column spacing to the dgvQueue
//Uses reflection to set dgvQueue to DoubleBuffer
}
catch
{ }
}
catch
{ }
}
private void frmMain_FormClosing(object sender, FormClosingEventArgs e)
{
//Saves information about the dgvQueue on shutdown.
}
Queue Class:
public class Queue
{
private string _status;
public string status { get { return _status; } set { _status = value; } }
private string _eta;
public string eta { get { return _eta; } set { _eta = value; } }
private List<Job> _jobs;
public List<Job> jobs
{
get
{
return _jobs;
}
set
{
_jobs = value;
}
}
private List<string> _categories;
public List<string> categories { get { return _categories; } set { _categories = value; } }
private XmlDocument xmld;
private ServerConnection m_parent;
private XmlNodeList _xmljobs;
public Queue(ServerConnection srvConn)
{
//fetch the Queue xml
m_parent = srvConn;
xmld = new XmlDocument();
_jobs = new List<Job>();
}
public void update()
{
updateXml();
updateQueue();
updateJobs();
}
private void updateXml()
{
//Loads xml file into xmld
}
private void updateQueue()
{
XmlNodeList elements = xmld.SelectNodes("queue");
foreach (XmlNode element in elements)
{
_status = element.SelectSingleNode("status").InnerText;
_eta = element.SelectSingleNode("eta").InnerText;
}
}
private void updateJobs()
{
_xmljobs = xmld.SelectNodes("queue/job");
jobs.Clear();
foreach (XmlNode xmljob in _xmljobs)
{
Job t_job;
_status = xmljob.SelectSingleNode("status").InnerText;
_eta = xmljob.SelectSingleNode("eta").InnerText;
//Create temp job to match against list.
t_job = new Job(_status, _eta);
jobs.Add(t_job);
}
}
Job class: In reality it holds around 30 values of varying types, but they're all in the same format:
public class Job
{
private int _status;
public int status { get { return _status; } set { _status = value; } }
private string _eta;
public string eta { get { return _eta; } set { _eta = value; } }
public Job(string status, string eta)
{
_status = status;
_eta = eta;
}
}
When interacting with the DataGridView I get the error:
The following exception occured in the DataGridView:
System.IndexOutOfRangeException: Index does not have a value. at System.Windows.Forms.CurrencyManager.get_Item(Int32 index) at System.Windows.Forms.DataGridViewDataConnection.GetError(Int32 boundColumnIndex, Int32 columnIndex, Int32 rowIndex)
And when entering the debugger it's triggered on the initial Application.Run(new frmMain(). What on earth am I doing wrong? The program still functions and updates normally but I can't even handle the event to suppress the default error message!
Edit - Answer! Instead of clearing the list and re-creating it, just updating the values within it works better. For the moment I have this code:
t_job = _jobs.FirstOrDefault(c => c.nzo_id == t_nzo_id);
if (t_job == null) //Job not in list, insert
{
t_job = new Job(t_status, i_index, t_eta, i_timeLeft, t_age, i_mbleft, i_mb, t_filename, i_priority, t_cat开发者_StackOverflow社区egory, i_percentage, t_nzo_id, this);
jobs.Add(t_job);
}
else //update object in current list
{
jobs[t_job.Index].status = t_status;
jobs[t_job.Index].priority = i_priority;
jobs[t_job.Index].category = t_category;
jobs[t_job.Index].percentage = i_percentage;
jobs[t_job.Index].timeleft = i_timeLeft;
jobs[t_job.Index].mbleft = i_mbleft;
}
Which prevents it!
The problem seems to be that with a refresh interval of say 1 second, while the user is scrolling or trying to access the fields of the data the data is constantly being removed and readded. This causes the binding to to call try call a value that it thinks might still be there, but isn't and that is why you are getting the index out of range exception
The first thing I would suggest doing is in your updateJobs method as well as all the other lists that are refreshed like the queue list, instead of clearing the list everytime is first checking to see if the job from the xml exists in the current job list, if it does then you can change the current values if the value has changed otherwise do nothing.
To check if the job exists it is as easy as:
t_job = _jobs.FirstOrDefault(c => c.Filename == t_filename);
This will return a null if the job does not exist, I would assume filename might not be unique, so might want to change it so that it really is unique i.e.
t_job = _jobs.FirstOrDefault(c => c.Filename == t_filename && c.nzo_id == t_nzo_id);
One thing you will now have to cater for is that old jobs won't be automatically removed, so whenever a new history job is added, first check to see if it exists in the queue and then remove it there before adding it to the history list.
so change your properties to be:
public int Index
{
get { return _index; }
set
{
if (_index != value)
_index = value;
}
}
instead of:
public int Index { get { return _index; } set { _index = value; } }
The other thing is that try catch exceptions are expensive, so instead of having:
try { i_percentage = double.Parse(t_percentage); } catch { }
because you know that if there is a value it is going to be a double, you can change it to:
if (!string.IsNullOrEmpty(t_percentage))
i_percentage = double.Parse(t_percentage);
now if you don't know if the value in t_percentage is going to be a double you can use a try-parse:
if (!string.IsNullOrEmpty(t_percentage))
double.TryParse(t_percentage,out i_percentage);
This way you avoid the overhead caused by an exception. This might be micro-optimizing and is not always neccessary if it doesn't actually cause a problem, but given that you can have hundreds of jobs, each with 10 or so properties refreshing everysecond, things can actually get noticeably slower if even 2 of the 10 properties throws an exception.
One more thing, after your backgroundworker is completed, in the method: dgvQueue_Update()
you are calling the ResetBindings(true);
this causes your whole datagrid to refresh instead of just the individual items.
try changing it to:
for (int i = 0; i < jobSource.List.Count; i++)
jobSource.ResetItem(i);
The difference is this:
this.OnListChanged(new ListChangedEventArgs(ListChangedType.ItemChanged, itemIndex));
compared to when you say ResetBindings:
this.OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
精彩评论