Form doesn't animate properly - c#

In my application i have two Forms (that's my 1st quite big app)
After clicking start button in parent form i want loading panel to appear, and some logic to be done.
Loading panel (it is just another widowss form) contains bunifu loading circle animation (and some text).
Logic part is responsible for collecting names from directory tree, then replacing some text in Ms.Word files on the tree.
When i open loading panel without executing the logic, loading panel is animated properly and everything works fine.
private void bunifuFlatButton1_Click(object sender, EventArgs e)
{
int x = this.Location.X+this.Width/2-75;
int y = this.Location.Y +this.Height/2-175;
Loader_panel LP = new Loader_panel();
LP.Left = x;
LP.Top = y;
LP.Show();
//System.Threading.Thread.Sleep(5000); \\this doesn't help animation to start
if (FormLogic._dataList.Count > 0) \\Here Logic part starts
{
for (int i = 0; i < FormLogic._dataList.Count; i++)
GetDir.GetTarget(FormLogic._dataList[i]);
/*foreach (var directory in FormLogic._dataList)
GetDir.GetTarget(directory);*/
LogList.Items.Add(DateTime.Now + "List isn't empty");// for testing
FormLogic.ClearData();
}
LP.Close();
}
After enabling logic loading panel appears (appearance isn't smooth), but animation doesn't work (it starts to work only when logic part did the work - i tested it by disabling LP.Close(). What can be reason of this problem ?
Additinal question. In .NET environment the code is compiled to work with multiple processor threads or i have to do it manually ?
EDIT 06/08/2018 7:21 CEST
I cant access LogList from GetDir Method (due to processing it by other thread).
I tried multiple Invoke construction, but none of it seemed to work ;/
I am just too rookie to figure it out. I specified more details in code below:
namespace Docr
{
public partial class DocrForm : Form
{
.....
private async void Button1_Click(object sender, EventArgs e)
{
int x = this.Location.X + this.Width / 2 - 75;
int y = this.Location.Y + this.Height / 2 - 175;
Loader_panel LP = new Loader_panel();
LP.Left = x;
LP.Top = y;
LP.Show(); //animation
int count = FormLogic._dataList.Count;
var list = FormLogic._dataList;
await Task.Run(() =>// processing logic during showing animation
{
if (count > 0)
{
for (int i = 0; i < count; i++)
{
GetDir.GetTarget(list[i],LogList); // Passing LogList as and argument
}
Invoke((Action)(() => { LogList.Items.Add(DateTime.Now + "Hi LogList"); }));\\ works fine
}
});
FormLogic.ClearData();
LP.Close();
}
....
}
namespace DocrLogic
{
class GetDir
{
.....
static public void GetTarget(string UserDirectory, ListBox List)// passing ListBox as an Argument
{
var path = UserDirectory;
var TargetDir = new DirectoryInfo(path);
var AllDocs1 = TargetDir.GetFiles("*.doc*", SearchOption.AllDirectories);
var ProperPrefixes = new List<string> { };
Invoke((Action)(() => { List.Items.Add(DateTime.Now + "Hi Log List, GetDir here"); })); // THIS DOESN'T WORK
....
}
.....
}
}

You need to make the method async, then use await to wait for the logic to complete. This way the LP form won't be interrupted by the heavy logic.
private async void bunifuFlatButton1_Click(object sender, EventArgs e)
{
int x = this.Location.X+this.Width/2-75;
int y = this.Location.Y +this.Height/2-175;
Loader_panel LP = new Loader_panel();
LP.Left = x;
LP.Top = y;
LP.Show();
int count = FormLogic._dataList.Count;
var list = FormLogic._dataList;
await Task.Run(()=>
{
if(count > 0)
{
for (int i = 0; i < count; i++)
{
GetDir.GetTarget(list[i]);
}
this.Invoke(() => { LogList.Items.Add(DateTime.Now + "List isn't empty"); });
}
});
FormLogic.ClearData();
LP.Close();
}
You have to make your code thread-safe by using Invoke to access not-thread-safe objects such as UI objects, otherwise, it will throw a System.Threading.ThreadAbortException or a System.InvalidOperationException.
Invoke syntax may differ based on your project but you may
see this post to understand the proper ways of using Invoke()
update
You must never try to access a UI object outside invoke. invoke is provided by System.Windows.Forms.Control and depends on the thread which originally created that control. therefore having Invoke on some other random class simply does not work.
In the second part, you need to change
public static void GetTarget(string UserDirectory, ListBox List)// passing ListBox as an Argument
{
...
Invoke((Action)(() => { List.Items.Add(DateTime.Now + "Hi Log List, GetDir here"); })); // THIS DOESN'T WORK
}
to
(you need to send the whole invoke line as the action parameter)
public static void GetTarget(string UserDirectory, Action action)// passing the action as an Argument
{
...
action();
}
or
(you need to set Dispatcher to LogList before starting the Task)
public static Control Dispather;
public static void GetTarget(string UserDirectory)// passing the action as an Argument
{
...
Dispather.Invoke((Action)(() => { List.Items.Add(DateTime.Now + "Hi Log List, GetDir here"); }));
}

Related

"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

require some sort of loop or jumping statement for my code

well i am new to C#, and implementing a code, in which i have two buttons, with one acting as starting of data acquisition and storing it in a csv file and other button to stop it.
well codes for all these are as follows:
//button for start DAQ
private void stdaq_Click(object sender, EventArgs e)
{
stopped = false;
process();
}
//button for stoping DAQ
private void spdaq_Click(object sender, EventArgs e)
{
stopped = true;
}
// process function
private process()
{
int iAvail = 0;
int iRead = 0;
string filename = #textBox3.Text;// taking csv file name from user
// jit:
//a function calculating the total number of values and storing it in iAvail
int[] iRawData = new Int32[iAvail];
double[] dScaledData = new Double[iAvail];
//a function transferring the data from buffer and storing it in dscaledData array
List<double> data = new List<double>();
for (int i = 0; i < iAvail; i++)
{
data.Add(dScaledData[i]);
}
Task myFirstTask = Task.Factory.StartNew(()
=>
{
while (stopped == false)
{
Write(data.ToArray(), filename);
// goto jit;
}
});
}
// csv creater and data writer
public static void Write(double[] data, string outputPath)
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < data.GetLength(0); i++)
{
if (stopped) break;
sb.AppendLine(string.Join(",", data[i]));
}
if (File.Exists(outputPath))
{
File.AppendAllText(outputPath, sb.ToString());
}
else
{
File.WriteAllText(outputPath, sb.ToString());
}
}
this is what i am implementing, and the problem with this code is that when the data is first transferred and written to the file, then again the same data is written again and again irrespective of new data and i tried implementing that Goto statement(can be seen in comments) but it is giving error - " Control cannot leave the body of an anonymous method or lambda expression ", and if i don't use the While loop the data is not written at all.
So i want to call my process function and to transfer data to csv starting on press of a start button, take fresh data everytime and write it to csv or can say call the process method again from it's start point and to stop it on click of the stop button, but i am unable to do it irrespective of various tries with different loops and some threading functions also.
please help with this.
Assuming you only need to Write once, you should remove this or change it from while to if:
while (stopped == false)
The loop will cause Write to be called infinitely until stopped becomes true.
Also, you might want to change Write to return rather than break if stopped is true, so that you don't write anything if you are supposed to be stopping:
if (stopped) break;
to
if (stopped) return;
If you want to generate data again and really do want to loop forever, just move that code into the loop:
Task myFirstTask = Task.Factory.StartNew(()
=>
{
while (stopped == false)
{
List<double> data = new List<double>();
// TODO: Generate data here - move all relevant code here
Write(data.ToArray(), filename);
}
});
I think this is a job for the BackgroundWorker.
This code will start you up:
public partial class Form1 : Form
{
int loopCounter = 0; // variable just used for illustration
private static BackgroundWorker bw = new BackgroundWorker(); // The worker object
// This function does your task
public void doSomeStuff(object sender, DoWorkEventArgs e)
{
for (int i = 0; i < 1000; i++)
{
loopCounter = i; // Pass the loop count to this variable just to report later how far the loop was when the worker got cancelled.
Thread.Sleep(100); // Slow down the loop
// During your loop check if the user wants to cancel
if (bw.CancellationPending)
{
e.Cancel = true;
return; // quit loop
}
}
}
// This button starts your task when pressed
private void button1_Click(object sender, EventArgs e)
{
bw.WorkerSupportsCancellation = true; // Set the worker to support cancellation
bw.DoWork += doSomeStuff; // initialize the event
if (!bw.IsBusy) // Only proceed to start the worker if it is not already running.
{
bw.RunWorkerAsync(); // Start the worker
}
}
// This button stops your task when pressed
private void button2_Click(object sender, EventArgs e)
{
// Request cancellation
bw.CancelAsync();
textBox1.Text = "The bw was cancelled when 'loopCounter' was at: " + loopCounter.ToString();
}
}

using the Background Worker in C#?

One part of my application works loading of Images from the predefined folder. At this time when loading the images it takes more time. Now I figured out that the progress Bar which can let me to tell the progress of loading.
The Problem I faced is:
I can't able to Integrate the BackgroudWorker, Progress Bar with my function.
For Instance the following is the Background worker and Progress bar code:
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
// Load file list here
int totalImageCount = 10000;
// Set maximum value of the progress bar
progressBar1.Invoke(new Action(() => { progressBar1.Maximum = totalImageCount; }));
for (int i = 0; i < totalImageCount; i++)
{
// Load a single image here
Thread.Sleep(10);
// User cancelled loading (form shut down)
if (e.Cancel) return;
// Set the progress
progressBar1.Invoke(new Action(() => { progressBar1.Value = i; }));
}
// Cleanup here
}
// Starts the loading
private void button1_Click(object sender, EventArgs e)
{
// Start loading images
backgroundWorker1.WorkerSupportsCancellation = true;
backgroundWorker1.RunWorkerAsync();
}
// Stops the loading
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
// Stop the loading when the user closes the form
if (backgroundWorker1.IsBusy) backgroundWorker1.CancelAsync();
}
The following is The function which needs to be for Progress Bar
private void LoadImages()
{
string imagesPath = (Application.StartupPath + "/UnknownFaces/");
string[] extensions = new[] { ".jpg", ".jpeg", ".png" };
var allfiles = Directory.GetFiles(imagesPath);
this.imageList1.ImageSize = new Size(256, 250);
this.imageList1.ColorDepth = ColorDepth.Depth32Bit;
foreach (FileInfo fileInfo in filesSorted)
{
try
{
this.imageList1.Images.Add(fileInfo.Name,
Image.FromFile(fileInfo.FullName));
}
catch
{
Console.WriteLine(fileInfo.FullName + " is is not a valid image.");
}
}
this.lstView_un.View = View.LargeIcon;
lstView_un.LargeImageList = this.imageList1;
lstView_un.Items.Clear();
for (int j = 0; j < this.imageList1.Images.Count; j++)
{
ListViewItem item = new ListViewItem();
item.ImageIndex = j;
item.Text = imageList1.Images.Keys[j].ToString();
this.lstView_un.Items.Add(item);
}
}
catch (Exception ex)
{
MessageBox.Show("Something Wrong happen! "+ex.Message);
}
}
I think the main routine works are there:
this.lstView_un.View = View.LargeIcon;
lstView_un.LargeImageList = this.imageList1;
lstView_un.Items.Clear();
for (int j = 0; j < this.imageList1.Images.Count; j++)
{
ListViewItem item = new ListViewItem();
item.ImageIndex = j;
item.Text = imageList1.Images.Keys[j].ToString();
this.lstView_un.Items.Add(item);
}
The slow part of your code is actually the loop that reads the files, not the loop that populates your ListView.
The best way to report or otherwise present progress state in the UI from BackgroundWorker is to use the ProgressChanged event. However, the code example you are working from will work as well. That is, just update the ProgressBar object directly from your worker code. It fails to take full advantage of the features that BackgroundWorker provides (and indeed, raises the question of why bother with BackgroundWorker if you're not going to use its features), but it will work.
For example:
var allfiles = Directory.GetFiles(imagesPath);
this.imageList1.ImageSize = new Size(256, 250);
this.imageList1.ColorDepth = ColorDepth.Depth32Bit;
// Set the maximum value based on the number of files you get
progressBar1.Invoke((MethodInvoker)(() => { progressBar1.Maximum = filesSorted.Count(); }));
foreach (FileInfo fileInfo in filesSorted)
{
try
{
this.imageList1.Images.Add(fileInfo.Name,
Image.FromFile(fileInfo.FullName));
}
catch
{
Console.WriteLine(fileInfo.FullName + " is is not a valid image.");
}
// Update the ProgressBar, incrementing by 1 for each iteration of the loop
progressBar1.Invoke((MethodInvoker)(() => progressBar1.Increment(1)));
}
Note: your code example is incomplete, and doesn't make sense even as a snippet. One problem in particular is that you retrieve the file names into the array allfiles, but you are iterating on a completely different collection, filesSorted. I did my best with the code you provided to work with, but since the code you posted wouldn't work as-is, you may well have to make some minor adjustments to the example I've provided to get it to do what you want.
If you are unable to figure this out with the above, please provide a good, minimal, complete code example that reliably illustrates your scenario and what precisely you're having trouble figuring out.

How to update a label each time in for loop

I am working on a WinForm project where I have a label in a for loop. I want to show the label each time after executing the label.text statement. But it doesn't show for every time, rather it shows after for loop is finished.
I tried to achieve this by using Thread.Sleep(). But I can't. Please help me.
NOTE :- lblProgress is a Label
Here's my coding.
for (int i = 1; i <= sourceTable.Rows.Count - 1; i++)
{
string checkout;
checkout= sourceTable.Rows[i].Field<string>(0);
dest = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["local"].ConnectionString);
dest.Open();
destcmd = new SqlCommand(checkout, dest);
destcmd.ExecuteNonQuery();
dest.Close();
prcmail();
prcmessagecheck();
lblProgress.Text = "Hello World"+i;
Thread.Sleep(10000);
}
Whenever you create a WinForm application, it is spun up into a new process and a new thread is created. Any updates to the User Interface are all done on the same thread as your process. This means when your application is doing "busy work", your UI will be blocked because they are on the same thread. What this means is that, in order to achieve what it is you're trying to achieve, you have to do a little extra work.
First step we need to do is create a function for your work routine (we could use an anonymous function, but since you are new to C#, I think it'll be easier to understand if we break it out), like this:
private void DoWork()
{
for (int i = 1; i <= sourceTable.Rows.Count - 1; i++)
{
string checkout;
checkout= sourceTable.Rows[i].Field<string>(0);
dest = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["local"].ConnectionString);
dest.Open();
destcmd = new SqlCommand(checkout, dest);
destcmd.ExecuteNonQuery();
dest.Close();
prcmail();
prcmessagecheck();
lblProgress.Text = "Hello World"+i;
Thread.Sleep(1000); // I changed this from 10000 to 1000 (10 seconds down to 1 second)
}
}
Next, we need to create a new thread that executes our DoWork() function. Its unclear what the "trigger" is for doing your work, but I'm going to assume its a button click:
private void button1_click(object sender, EventArgs e)
{
var work = new Thread(DoWork);
work.Start();
}
So now, whenever someone click the button, we will start a new thread that executes our DoWork function in that thread. The new thread spawns, then execution is immediate returned and our GUI will now update in real time as our thread is executing in the background.
But wait! We still have one more problem to take care of. The problem is that Window's form controls are not thread safe and if we try to update a control from another thread, other then the GUI's thread, we will get a cross-thread operation error. The key to fixing this is to use InvokeRequired and Invoke.
First, we need to make another function that does just the label update:
private void SetProgressLabel(int progress)
{
lblProgress.Text = "Hello World" + progress;
}
In your form class, we also need to create a new delegate:
public partial class Form1 : Form
{
private delegate void ProgressCallback(int progress);
// ..
// The rest of your code
// ..
}
Finally, change your DoWork() method to something like this:
private void DoWork()
{
for (int i = 1; i <= sourceTable.Rows.Count - 1; i++)
{
string checkout;
checkout= sourceTable.Rows[i].Field<string>(0);
dest = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["local"].ConnectionString);
dest.Open();
destcmd = new SqlCommand(checkout, dest);
destcmd.ExecuteNonQuery();
dest.Close();
prcmail();
prcmessagecheck();
if (lblProgress.InvokeRequired)
{
lblProgress.Invoke(new ProgressCallback(SetProgressLabel), new object[] { i });
}
else
{
SetProgressLabel(i);
}
Thread.Sleep(1000); // I changed this from 10000 to 1000 (10 seconds down to 1 second)
}
}
This uses the label's (derived from Control) InvokeRequired property to determine if an Invoke is required. It returns true or false. If its false, we can just call our SetProgressLabel() function like we'd normally do. If its true, we must use Invoke to call our function instead.
Congratulations! You just made your first thread safe application.
Now, just as an aside note, you are not properly releasing and disposing of your objects. I recommend you change your DoWork() code to something like this:
private void DoWork()
{
for (int i = 1; i <= sourceTable.Rows.Count - 1; i++)
{
string checkout;
checkout = sourceTable.Rows[i].Field<string>(0);
using (dest = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["local"].ConnectionString))
{
dest.Open();
using (destcmd = new SqlCommand(checkout, dest))
{
destcmd.ExecuteNonQuery();
dest.Close();
prcmail();
prcmessagecheck();
if (lblProgress.InvokeRequired)
{
lblProgress.Invoke(new ProgressCallback(SetProgressLabel), new object[] { i });
}
else
{
SetProgressLabel(i);
}
Thread.Sleep(1000); // I changed this from 10000 to 1000 (10 seconds down to 1 second)
}
}
}
}
Because I wrapped your IDisposable's into using blocks, the resources will automatically be disposed of once it goes out of scope.
Although threading would be the more ideal solution another solution is:
Application.DoEvents()
this will give the UI thread time to update.
Example
for (int i = 1; i <= sourceTable.Rows.Count - 1; i++)
{
string checkout;
checkout= sourceTable.Rows[i].Field<string>(0);
dest = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["local"].ConnectionString);
dest.Open();
destcmd = new SqlCommand(checkout, dest);
destcmd.ExecuteNonQuery();
dest.Close();
prcmail();
prcmessagecheck();
lblProgress.Text = "Hello World"+i;
Application.DoEvents();
}
var ui = TaskScheduler.FromCurrentSynchronizationContext();
Task.Factory.StartNew(() =>
{
for (int i = 1; i <= sourceTable.Rows.Count - 1; i++)
{
string checkout;
checkout = sourceTable.Rows[i].Field<string>(0);
dest = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["local"].ConnectionString);
dest.Open();
destcmd = new SqlCommand(checkout, dest);
destcmd.ExecuteNonQuery();
dest.Close();
prcmail();
prcmessagecheck();
var task = Task.Factory.StartNew(() =>
{
//Thread.Sleep(1000);
lblProgress.Text = "Hello World" + i;
}, CancellationToken.None, TaskCreationOptions.None, ui);
task.Wait();
}
});
If you are executing the mentioned code on the UI thread, UI will be refreshed only after entire for loop is executed. Based on your needs, progress bar/background worker kind of set up looks suitable.

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