Add many items to ListBox while keeping the UI resposive - c#

I am trying to update a ListBox with a large amount of data in a way that keeps the user interface (UI) responsive.
To do this, I am using the following code to collect the data into batches of 100 items, and then insert these batches into the ListBox in one go, rather than inserting each item individually. This should prevent the UI from being updated each time an item is added, but unfortunately, the code does not work as expected and the UI is only updated after all of the items have been added to the ListBox.
public partial class Form1 : Form
{
private SynchronizationContext synchronizationContext;
public Form1()
{
InitializeComponent();
}
private async void button1_Click(object sender, EventArgs e)
{
synchronizationContext = SynchronizationContext.Current;
await Task.Run(() =>
{
ConcurrentDictionary<int, int> batch = new ConcurrentDictionary<int, int>();
int count = 0;
for (var i = 0; i <= 10000; i++)
{
batch[i] = i;
count++;
if (count == 100)
{
count = 0;
UpdateUI(batch);
batch = new ConcurrentDictionary<int, int>();
}
}
});
}
private void UpdateUI(ConcurrentDictionary<int, int> items)
{
synchronizationContext.Post(o =>
{
listBox1.SuspendLayout();
foreach (var item in items)
{
listBox1.Items.Add(item.Value);
}
listBox1.ResumeLayout();
}, null);
}
}

You don't need a multithreading approach in order to update the UI. All you need is to suspend the painting of the ListBox during the mass insert, by using the ListBox.BeginUpdate and ListBox.EndUpdate methods:
private void button1_Click(object sender, EventArgs e)
{
listBox1.BeginUpdate();
for (var i = 1; i <= 10000; i++)
{
listBox1.Items.Add(i);
}
listBox1.EndUpdate();
}
The Control.SuspendLayout and Control.ResumeLayout methods are used when you add controls dynamically in a flexible container, and you want to prevent the controls from jumping around when each new control is added.

Related

How to replace previous item of listbox with next item using backgroundworker c#?

I am adding items to the listbox using backgroundworker. It is showing all the items present in the list one by one after some interval of time. I want to show only current item in the list and not all the items.
This is what I have tried.
private void backgroundWorker3_DoWork(object sender, DoWorkEventArgs e)
{
List<string> result = new List<string>();
var found = obj.getFiles();//List<strings>
if (found.Count != 0)
{
for (int i = 0; i < found.Count; i++)
{
int progress = (int)(((float)(i + 1) / found.Count) * 100);
if (found[i].Contains("SFTP"))
{
result.Add(found[i]);
(sender as BackgroundWorker).ReportProgress(progress, found[i]);
}
System.Threading.Thread.Sleep(500);
}
e.Result = result;
}
}
private void backgroundWorker3_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
if (e.UserState != null)
listBox3.Items.Add(e.UserState);
}
I want to show only current item in the list and not all the items.
Although this is a bit of a hack, I think it should work for what you describe:
// just call clear first
listBox3.Items.Clear();
listBox3.Items.Add(e.UserState);
Truth be told, are you sure you want a ListBox in this circumstance? After all, it's not really a list, it's only an item.

"Collection was modified; enumeration operation may not execute" when running the same background worker on 2+ instances of a form simultaneously

I have a c# windows form where the user enters a set of parameters, and those parameters are then analyzed against a set of data to return a result. This analysis takes place on a background worker, by initializing a Backtest object and iterating over a string list symbolParams built from the values passed in through the form. When running the worker on one form, it works properly.
However, if I open up a second form, put in a new set of parameters, and run the worker on that form while the worker on the first form is still running, I get a "Collection was modified" error on the string list.
Seems as though the two background workers are affecting each other's symbolParams list somehow. What's happening? How can this be fixed to allow multiple instances of this form to run this background worker simultaneously?
OptimizerForm.cs
public partial class OptimizerForm : Form
{
public static List<List<String>> backtestSymbolParams = new List<List<String>> { };
private void button15_Click(object sender, EventArgs e)
{
//Get parameters from the form
//Make a list for every param
string[] startEndTimes = textBox3.Text.Split(',');
string[] incrementPrices = textBox4.Text.Split(',');
string[] incrementSizes = textBox5.Text.Split(',');
string[] autoBalances = textBox6.Text.Split(',');
string[] hardStops = textBox7.Text.Split(',');
//Add every combo to symbol test params
for (int a = 0; a < startEndTimes.Length; a++)
{
for (int b = 0; b < incrementPrices.Length; b++)
{
for (int c = 0; c < incrementSizes.Length; c++)
{
for (int d = 0; d < autoBalances.Length; d++)
{
for (int f = 0; f < hardStops.Length; f++)
{
backtestSymbolParams.Add( new List<string> { symbol, startEndTimes[a].Split('-')[0], startEndTimes[a].Split('-')[1], incrementPrices[b],
incrementSizes[c], autoBalances[d], hardStops[f] });
}
}
}
}
}
backgroundWorker1.RunWorkerAsync();
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
//Initialize Backtest instance with parameters gathered from the form
Backtest backtest = new Backtest(backtestSymbolParams, backtestSymbolDates, sender as BackgroundWorker);
TestResult testResult = new TestResult();
//Run the analysis
testResult = backtest.Run();
e.Result = testResult;
}
}
Backtest.cs
//Backtest Constructor
public Backtest(List<List<string>> _symbolParams, Dictionary<string, List<string>> _symbolDates, BackgroundWorker _bw)
{
symbolParams = _symbolParams;
symbolDates = _symbolDates;
bw = _bw;
}
//Backtest.Run()
public TestResult Run()
{
int symbolCount = 1;
//Collection modified exception occurs here
foreach (List<string> symbolParam in symbolParams) {
//do stuff
}
}
It seems like your symbolParams variable in Backtest class is static, so simply mark it as private field for your class.
Also you have some issues with naming standards - method parameters should not start with _, though fields should, so your constructor should looks like:
private List<List<String>> _symbolParams = new List<List<String>> { };
//Backtest Constructor
public Backtest(List<List<string>> symbolParams,
Dictionary<string, List<string>> symbolDates,
BackgroundWorker bw)
{
_symbolParams = symbolParams;
_symbolDates = symbolDates;
_bw = bw;
}
And, as far as I can see, you're using BackgroundWorker as Task so probably you should use TPL itself, without old legacy classes

Refreshing a UI to reflect items added to a list

While my UI is displayed, data is being passed in the back end and added to a List<string> that I would in turn like to display on my UI.
I've seen several examples using background workers however I don't have access to the actual component due to how I layout my User Controls and programmatically build them.
Question: How can I run this method repeatedly behind my UI without locking up my UI in a loop?
public void UpdatePanel()
{
foreach (var item in list)
{
AddMethod(item);
}
}
Instead of using a loop or time intervals to monitor a list, as an option when possible, you can use a BindingList<T> or ObservableCollection<T> and receive notification when list changes.
Then you can update user interface in the event handler which you attaced to ListChanged event ofBindingList<T> or CollectionChanged event of ObservableCOllection<T>.
Example
Here is an example based on ObservableCollection<string>.
ObservableCollection<string> list;
private void Form1_Load(object sender, EventArgs e)
{
list = new ObservableCollection<string>();
list.CollectionChanged += list_CollectionChanged;
list.Add("Item 1");
list.Add("Item 2");
list.RemoveAt(0);
list[0] = "New Item";
}
void list_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
var items = string.Join(",", e.NewItems.Cast<String>());
MessageBox.Show(string.Format("'{0}' Added", items));
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
var items = string.Join(",", e.OldItems.Cast<String>());
MessageBox.Show(string.Format("'{0}' Removed", items));
}
else if (e.Action == NotifyCollectionChangedAction.Replace)
{
var oldItems = string.Join(",", e.OldItems.Cast<String>());
var newItems = string.Join(",", e.NewItems.Cast<String>());
MessageBox.Show(string.Format("'{0}' replaced by '{1}'", oldItems, newItems));
}
else
{
MessageBox.Show("Reset or Move");
}
}
You can use Task, Async and await, where is some code which insert an element in a listbox each second without blocking UI.
In your case, you have to return the data from backend asynchronously.
public async void LoadItemsAsync()
{
for (int i = 0; i < 10; i++)
listBox1.Items.Add(await GetItem());
}
public Task<string> GetItem()
{
return Task<string>.Factory.StartNew(() =>
{
Thread.Sleep(1000);
return "Item";
});
}

Selecting Multiple Items From ListBox causes Collection was modified; enumeration operation may not execute

This is a simple code in which i am Transferring Items of one listBox to one Another on btnAdd_Click Event and btnRemove_Click Event resp.
This was working Fine when The selectionMode="Single" and at that point of time i had no need to use foreach inbtn_Add_Click. But Now i've changed selectionMode="Multiple" and used foreach in btnAdd_Click and when i am selecting multiple items from ListBox it is creating the following Error:-
Collection was modified; enumeration operation may not execute
class Movies
{
private Int32 _Id;
private string _movieName;
public Int32 Id
{
get { return _Id; }
set { _Id = value; }
}
public string movieName
{
get { return _movieName; }
set { _movieName = value; }
}
public Movies(Int32 ID, string MovieName)
{
Id = ID;
movieName = MovieName;
}
}
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
List<Movies> list = new List<Movies>();
list.Add(new Movies(1, "Movie1"));
list.Add(new Movies(2, "Movie2"));
list.Add(new Movies(3, "Movie3"));
list.Add(new Movies(4, "Movie4"));
list.Add(new Movies(5, "Movie5"));
list.Add(new Movies(6, "Movie6"));
list.Add(new Movies(7, "Movie7"));
lstMain.DataSource = list;
lstMain.DataBind();
}
}
protected void btnAdd_Click(object sender, EventArgs e)
{
foreach (ListItem list in lstMain.Items)// Here the error comes
{
if(list.Selected)
{
lstFAvourite.ClearSelection();
lstFAvourite.Items.Add(list);
lstMain.Items.Remove(list);
}
}
}
protected void btnRemove_Click(object sender, EventArgs e)
{
ListItem list = lstFAvourite.SelectedItem;
lstMain.ClearSelection();
lstMain.Items.Add(list);
lstFAvourite.Items.Remove(list);
}
protected void btnSubmit_Click(object sender, EventArgs e)
{
foreach (ListItem item in lstFAvourite.Items)
{
lbl1.Text += "<li>" + item.Text;
}
}
Please tell me what is going wrong with the foreach loop in btnAdd_Click Event....
Thanks..
You can't remove items from an enumeration while you are looping through it using a foreach, which is what the error message is trying to tell you.
Switching to a straight for loop will get your code to execute:
protected void btnAdd_Click(object sender, EventArgs e)
{
for(var i=0; i<lstMain.Items.Count; i++)
{
var list = lstMain.Items[i];
lstFAvourite.ClearSelection();
lstFAvourite.Items.Add(list);
lstMain.Items.Remove(list);
i--;
}
}
However, I believe this code will add all items from lstMain to lstFAvourite. I think perhaps that we should be looking at the Selected property of the ListItem as well in the for loop. For example:
protected void btnAdd_Click(object sender, EventArgs e)
{
for (var i = 0; i < lstMain.Items.Count; i++)
{
var list = lstMain.Items[i];
if(!list.Selected) continue;
lstFAvourite.ClearSelection();
lstFAvourite.Items.Add(list);
lstMain.Items.Remove(list);
//Decrement counter since we just removed an item
i--;
}
}
Another method.
lstMain.Items
.Cast<ListItem>()
.ToList()
.ForEach( item =>
{
if(item.Selected)
{
lstFAvourite.Items.Add(item);
lstMain.Items.Remove(item);
}
}
);
lstFAvourite.ClearSelection();
N.B: much slower than the for loop method.
If you try to remove list box items using a for loop using an increment (++) statement, eventually that loop will throw out of bounds error failing to finish removing all the items you want. Use decrement (--) instead.
The error Collection was modified; enumeration operation may not execute is because, while you are deleting the items from the list its affecting the sequence. You need to iterate the items in reverse order for this.
for (int i = lstMain.Items.Count; i > 0; i--)
{
ListItem list = lstMain.Items[i];
lstFAvourite.ClearSelection();
lstFAvourite.Items.Add(list);
lstMain.Items.Remove(list);
}

Combobox = textbox + list

I want to free type in combobox. When I stop typing I have a delayed task that populates combobox items with some input dependent results. The problem is that my input is overridden by the first item in the list. Is there a way to keep my input?
My sample code is going like this:
public void PopulateCombo(JObject result)
{
Debug.WriteLine("Thread id: " + Thread.CurrentThread.ManagedThreadId);
cbSearch.Items.Clear();
if (result.Value<bool>("success") == true)
{
JArray arr = result.Value<JArray>("data");
for (int i = 0; i < arr.Count; i++)
{
JToken item = arr[i];
cbSearch.Items.Add(new ComboBoxItem( item.Value<string>("name"), item.Value<string>("_id")));
}
cbSearch.DroppedDown = true;
}
}
Edited on 23.06
I'm giving an example of what I'm really trying to do.
Combobox is empty (no items)
User starts typing for example "ja". Combobox sends query to my backend. Should n't be a problem as the call is asynchronous with 1 second delay after user last input.
My backend returns some results (Anton Jamison, James Aaron, James Hetfield, etc., limited to 50)
I want to populate the dropdown list with results, to open it, but as a combobox text i want to keep "ja", so the user can clarify his search further.
User extends his search "ja h". Backend responds with James Hetfield. Result now is only one item and I can set the combobox text now or keep the behavior from above. Not sure which would be better yet.
All this is implemented but at step 4 when I populate the combobox using the function above, the text of the combo is changed from "ja" to the first match of the list. (Anton Jamison in the example). I'm almost sure that there was a simple option for implementing this behavior but I'm not sure if it was in C#.
On comments :
It was a good try but unsuccessful. Once I populate the combobox items my search string is changed to the first match of the list.
I think I don't try to implement the autocomplete feature.
Good catch about the DroppedDown. I move it in the edited version.
I do not have the problem you talked about. The text in the edit box stays the same all the time.
I am using VS2008 though with a standard ComboBox renamed to cbSearch and its event captured (as well as the form's show event).
Rest works nicely.
Seemed like a nice task so I did it.
I also recover the selection, though you can see some flickering.
Most difficult was the synchronization - so I found an easy not tooo ugly solution.
Still, I don't do anything different from you.. maybe you start with a blank ComobBox again, maybe you changed some of the default parameters.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;
namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void cbSearch_TextUpdate(object sender, EventArgs e)
{
lastUpdate = DateTime.Now;
allowUpdate = true;
}
DateTime lastUpdate = DateTime.Now;
volatile bool allowUpdate = false;
private void BoxUpdate()
{
while (true)
{
Thread.Sleep(250);
if (allowUpdate)
{
var diff = DateTime.Now - lastUpdate;
if (diff.TotalMilliseconds > 1500)
{
allowUpdate = false;
this.InvokeEx(x =>
{
if (x.cbSearch.Text.Length > 0)
{
x.PopulateCombo(cbSearch.Text);
}
});
}
}
}
}
public void PopulateCombo(string text)
{
int sStart = cbSearch.SelectionStart;
int sLen = cbSearch.SelectionLength;
List<string> cbItems = new List<string>();
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j)
cbItems.Add(i + text + j);
cbSearch.Items.Clear();
{
for (int i = 0; i < cbItems.Count; i++)
{
cbSearch.Items.Add(cbItems[i]);
}
cbSearch.DroppedDown = true;
}
cbSearch.SelectionStart = sStart;
cbSearch.SelectionLength = sLen;
}
private void Form1_Shown(object sender, EventArgs e)
{
ThreadPool.QueueUserWorkItem(x =>
{
BoxUpdate();
});
}
}
public static class ISynchronizeInvokeExtensions
{
public static void InvokeEx<T>(this T #this, Action<T> action)
where T : System.ComponentModel.ISynchronizeInvoke
{
if (#this.InvokeRequired)
{
#this.Invoke(action, new object[] { #this });
}
else
{
action(#this);
}
}
}
}
Managed to do the same task with the hint of comment 1 + some tweaks. Here is my final code that does the work:
private void cbSearch_TextUpdate(object sender, EventArgs e)
{
timer1.Stop();
timer1.Dispose();
timer1 = null;
timer1 = new System.Windows.Forms.Timer();
timer1.Tick += new EventHandler(timer1_Tick);
timer1.Interval = 1000;
timer1.Start();
}
delegate void MethodDelegate(JObject result);
void timer1_Tick(object sender, EventArgs e)
{
timer1.Stop();
Debug.WriteLine(this.cbSearch.Text);
Debug.WriteLine("Thread id: " + Thread.CurrentThread.ManagedThreadId);
Dictionary<string, object> parameters = new Dictionary<string, object>();
parameters["query"] = this.cbSearch.Text ?? "";
this.session.rpc["advanced_search"].execAsync(parameters, results =>
{
this.BeginInvoke(new MethodDelegate(PopulateCombo), new object[] {results.GetResult()});
});
}
public void PopulateCombo(JObject result)
{
Debug.WriteLine("Thread id: " + Thread.CurrentThread.ManagedThreadId);
this.selectedPatientId = "";
string text = cbSearch.Text;
cbSearch.DroppedDown = false;
cbSearch.Items.Clear();
if (result.Value<bool>("success") == true)
{
JArray arr = result.Value<JArray>("data");
for (int i = 0; i < arr.Count; i++)
{
JToken item = arr[i];
cbSearch.Items.Add(new ComboBoxItem( item.Value<string>("name"), item.Value<string>("_id")));
}
try
{
this.cbSearch.TextUpdate -= new System.EventHandler(this.cbSearch_TextUpdate);
cbSearch.DroppedDown = true;
cbSearch.Text = text;
cbSearch.Select(cbSearch.Text.Length, 0);
}
finally {
this.cbSearch.TextUpdate += new System.EventHandler(this.cbSearch_TextUpdate);
}
}
}

Categories

Resources