Appending text to a line in a listbox - c#

Is there a way to append text to the last line in a ListBox? I'd like a listing to look like this:
processing file 1... OK
processing file 2... CRC error
processing file 3... OK
When I open the file for processing, I would write "processing file x" with ListBox.Add("processing file x"). When done processing, before moving on to the next file, I would like to append the processing results.
I could wait until processing is complete, and just write the whole line at once, but it can take 10-15 seconds to process a file and it makes the UI look unresponsive.
A solution would also let me append text like (% complete) or something to make the UI more active while processing. I prefer to do this with a ListBox because of its scrolling and line selection properties, if possible.
I've not been able to find any way to do this; any ideas would be welcome.

you can directly add items to the list box
listBox1.Items.Add("new item");
and you may need to refresh it
listBox1.Refresh();
Edit :
In case you want to update the last item, you need to remove the last item and then re add it
var lastItem= listBox1.Items[listBox1.Items.Count-1];
lastItem += results;
listBox1.Items.RemoveAt(listBox1.Items.Count-1);
listBox1.Add(lastItem);

Your problem is that a GUI event is processed on the same thread as the GUI rendering, so the UI will be unresponsive until it's done.
The quick and hacky solution is to just call Application.DoEvents() after each ListBox.Add call. The form will still be jittery if you try to drag it around, but that function will cause the GUI to update/render once.
The correct way to do it is to use a BackgroundWorker, which you can start from the GUI event and will process in the background on a separate thread. If you implement its ProgressChanged function, you can call this from DoWork:
(sender as BackgroundWorker).ReportProgress(i/(float)totalFiles,msgString)
And then in ProgressChanged, do your whole:
listBox.Add(e.UserState.ToString() + ", " + e.ProgressPercentage.ToString() + "% completed.")

Simple version:
var i = listBox.Items.Count - 1; // crash bug if there is no line to append to
listBox.Items[i] = listBox.Items[i] + message;
Or, for a more complete solution (works from a non UI thread):
public static class Util
{
public static async Task RunOnUiThread(Action a)
{
await Application.Current.Dispatcher.InvokeAsync(() => { a(); });
}
}
public partial class MainWindow : Window
{
private async Task Write(string message)
{
var action = new Action(() =>
{
var i = OutputListBox.Items.Count - 1;
if (i >= 0)
{
OutputListBox.Items[i] = OutputListBox.Items[i] + message;
}
else
{
OutputListBox.Items.Add(message);
}
});
await Util.RunOnUiThread(action);
}
}

Related

An ItemsControl is inconsistent with its items source - Problem when using Dispatcher.Invoke()

I'm writing a WPF application (MVVM pattern using MVVM Light Toolkit) to read and display a bunch of internal log files my company uses. The goal is to read from multiple files, extract content from each line, put them in a class object and add the said object to an ObservableCollection. I've set the ItemsSource of a DataGrid on my GUI to this list so it displays the data in neat rows and columns. I have a ProgressBar control in a second window, which during the file read and display process will update the progress.
Setup
Note that all these methods are stripped down to the essentials removing all the irrelevant code bits.
Load Button
When the user selects the directory which contains log files and clicks this button, the process begins. I open up the window that contains the ProgressBar at this point. I use a BackgroundWorker for this process.
public void LoadButtonClicked()
{
_dialogService = new DialogService();
BackgroundWorker worker = new BackgroundWorker
{
WorkerReportsProgress = true
};
worker.DoWork += ProcessFiles;
worker.ProgressChanged += Worker_ProgressChanged;
worker.RunWorkerAsync();
}
ProcessFiles() Method
This reads all files in the directory selected, and processes them one by one. Here, when launching the progress bar window, I'm using Dispatcher.Invoke().
private void ProcessFiles(object sender, DoWorkEventArgs e)
{
LogLineList = new ObservableCollection<LogLine>();
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
_dialogService.ShowProgressBarDialog();
});
var fileCount = 0;
foreach (string file in FileList)
{
fileCount++;
int currProgress = Convert.ToInt32(fileCount / (double)FileList.Length * 100);
ProcessOneFile(file);
(sender as BackgroundWorker).ReportProgress(currProgress);
}
}
ProcessOneFile() Method
This, as the name suggests, reads one file, go through line-by-line, converts the content to my class objects and adds them to the list.
public void ProcessOneFile(string fileName)
{
if (FileIO.OpenAndReadAllLinesInFile(fileName, out List<string> strLineList))
{
foreach (string line in strLineList)
{
if (CreateLogLine(line, out LogLine logLine))
{
if (logLine.IsRobotLog)
{
LogLineList.Add(logLine);
}
}
}
}
}
So this works just fine, and displays my logs as I want them.
Problem
However, after displaying them, if I scroll my DataGrid, the GUI hangs and gives me the following exception.
System.InvalidOperationException: 'An ItemsControl is inconsistent
with its items source. See the inner exception for more
information.'
After reading about this on SO and with the help of Google I have figured out that this is because my LogLineList being inconsistent with the ItemsSource which results in a conflict.
Current Solution
I found out that if I put the line of code in ProcessOneFile where I add a class object to my list inside a second Dispatcher.Invoke() it solves my problem. Like so:
if (logLine.IsRobotLog)
{
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
LogLineList.Add(logLine);
});
}
Now this again works fine, but the problem is this terribly slows down the processing time. Whereas previously a log file with 10,000 lines took about 1s, now it's taking maybe 5-10 times as longer.
Am I doing something wrong, or is this to be expected? Is there a better way to handle this?
Well observable collection is not thread safe. So it works the second way because all work is being done on the UI thread via dispatcher.
You can use asynchronous operations to make this type of flow easier. By awaiting for the results and updating the collection\progress on the result, you will keep your UI responsive and code clean.
If you cant or don't want to use asynchronous operations, batch the updates to the collection and do the update on the UI thread.
Edit
Something like this as an example
private async void Button_Click(object sender, RoutedEventArgs e)
{
//dir contents
var files = new string[4] { "file1", "file2", "file3", "file4" };
//progress bar for each file
Pg.Value = 0;
Pg.Maximum = files.Length;
foreach(var file in files)
{
await ProcessOneFile(file, entries =>
{
foreach(var entry in entries)
{
LogEntries.Add(entry);
}
});
Pg.Value++;
}
}
public async Task ProcessOneFile(string fileName, Action<List<string>> onEntryBatch)
{
//Get the lines
var lines = await Task.Run(() => GetRandom());
//the max amount of lines you want to update at once
var batchBuffer = new List<string>(100);
//Process lines
foreach (string line in lines)
{
//Create the line
if (CreateLogLine(line, out object logLine))
{
//do your check
if (logLine != null)
{
//add
batchBuffer.Add($"{fileName} -{logLine.ToString()}");
//check if we need to flush
if (batchBuffer.Count != batchBuffer.Capacity)
continue;
//update\flush
onEntryBatch(batchBuffer);
//clear
batchBuffer.Clear();
}
}
}
//One last flush
if(batchBuffer.Count > 0)
onEntryBatch(batchBuffer);
}
public object SyncLock = new object();
In your constructor:
BindingOperations.EnableCollectionSynchronization(LogLineList, SyncLock);
Then in your function:
if (logLine.IsRobotLog)
{
lock(SyncLock)
{
LogLineList.Add(logLine);
}
}
This will keep the collection synchronized in which ever thread you update it from.

Popup status window during a datagrid iteration update

I've spent 4 hours on this and totally failed.
I know that i need to use BackgroundWorker but all the tutorials refer to running a progress script on the actual form you are running the worker on.
I have a large datagrid, which the user can use a check box to "select all" and then press "UPDATE ALL"
This updates every grid with a bunch of options they choose.
For some users this may be 5 records which is nothing, but some might update 200 records with 5 options which takes about... 10-15 secs to iterate through them.
I have tried so many variations of running BGworker which loads a FrmLoading.Showdialog
Or trying to have BGworker "do work" running the code and then the main thread having the FrmLoading.Show()
However nothing is working.
If i have the update code in the background worker, it fails because the datagrid and everything is in a different thread.
The other way round, and it just hangs on FrmLoading.Show()
Any advice would be great.
I just can't seem to get my head around how to get this working for what seems to be an easy idea!
Current Update Code:
foreach (DataGridViewRow rowx in dataGridpatients.Rows)
{
//MessageBox.Show(Convert.ToBoolean(rowx.Cells["clnselected"].Value).ToString());
if (Convert.ToBoolean(rowx.Cells["clnselected"].Value) == true)
{
//if cycle has a value.
if (cmbcycle.SelectedIndex != -1)
{
rowx.Cells["clncycletype"].Value = cycle;
rowx.Cells["clnpackscollect"].Value = packs;
}
//if location has a value
if (cmblocation.SelectedIndex != -1)
{
location = Convert.ToInt32(cmblocation.SelectedValue);
rowx.Cells["clnlocation1"].Value = location;
}
if (cmbsize.SelectedIndex != -1)
{
size = Convert.ToInt32(cmbsize.SelectedValue);
rowx.Cells["clnpacksize"].Value = size;
}
if (chkDelivery.Checked == true)
{
rowx.Cells["clnDelivery"].Value = true;
}
if (chkSignSheet.Checked == true)
{
rowx.Cells["clnSigningSheet"].Value = true;
}
}
countupdated++;
}
foreach (DataGridViewRow row in dataGridpatients.Rows)
{
row.Cells["clnselected"].Value = false;
row.DefaultCellStyle.BackColor = Color.White;
}
cmbsize.SelectedIndex = -1;
cmblocation.SelectedIndex = -1;
cmbcycle.SelectedIndex = -1;
chkDelivery.Checked = false;
chkSignSheet.Checked = false;
#countupdated++;
I also have #CountSelected.
What i want to do is run this code above but have a popup overlay (dialog) with my logo + "Updating X%"
Where X = countupdated/countselected * 100
I now know i need to use the background worker and invoke for the above, but literally have no idea regarding how to invoke the grid and go from there.
I understand i need to invoke the variables I'm using
(eg. cmbcycle.SelectedIndex)
I know iterating through 150 records and updating individual cells is probably wrong,
My other option is creating a datatable from "selected" cells on that datatable
then Running the update via SQL instead of iterating through a bound table.
Then after the SQL i can re-create the table which will now have the new cell values updated in it?
Would that be a more appropriate way to do it?
Max rows on this table would be 200. Average ~70 so we are never talking 500 or 1000
EDIT:
So the checked answer works to run the background worker and refer to the controls on the form.
The issue is that if i do this:
backgroundWorker1.RunWorkerAsync();
splashy.ShowDialog();
Then the splash screen pops up after the background worker ends
If i do this:
splashy.ShowDialog();
backgroundWorker1.RunWorkerAsync();
Then the popup semi-forms and hangs until the end of the background worker, at which time it closes
because of the RunWorkerCompleted event.
EDIT:
I have no updated the code in DoWork and used Invokes to refer to the controls.
This works and the code runs fine.
I now need a popup ot appear showing the progress through the updates.
splashy.InvokeBy(() =>
{
splashy.Show();
});
backgroundWorker1.RunWorkerAsync();
Does not work. It causes the popup but freeze
splashy.ShowDialog();
backgroundWorker1.RunWorkerAsync();
Allows the Dialog to show (not 'frozen' and distorted) However the Lab (lblprogress) does not update.
This is because the form never get to the RunWorker method, it is stuck at ShowDialog.
It would be a good idea to make modifications on your DataSource itself and then bind it with the DataGridView.
But as from your existing code if you want to access your controls/UI to update or change values from BackgroundWorker.RunWorkerAsync method or any other Thread call for that matter, you can create an extension method to .Invoke() the controls like:
public static class MyExtensions
{
public static void InvokeBy(this Control ctl, MethodInvoker method)
{
if (ctl.InvokeRequired)
ctl.Invoke(method);
else method();
}
}
Keep this static class under the same Namespace as your main class for convenience.
Thus this code:
foreach (DataGridViewRow rowx in dataGridpatients.Rows)
{
//your codes
}
Will become:
dataGridpatients.InvokeBy(() =>
{
foreach (DataGridViewRow rowx in dataGridpatients.Rows)
{
//your codes
}
});
Similarly,
if (cmbcycle.SelectedIndex != -1)
{
//your codes
}
Will become:
cmbcycle.InvokeBy(() =>
{
if (cmbcycle.SelectedIndex != -1)
{
//your codes
}
});
This way you van safely access your controls, while keeping your UI responsive at the same time. Update your Popup Status UI the same way!
This answer is based around o_O's answer.
The main issue is that i wanted the UI to actually update and the background worker to supply the splash.
Instead of running all the 'hard code' in the BGW, i left it in the original thread, but called a BGW to display a popup Dialog form.
so at the start of the "hard code" I used:
backgroundWorker1.RunWorkerAsync();
This called:
FrmSplash splashy;
private void backgroundWorker1_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
{
splashy = new FrmSplash();
splashy.ShowDialog();
}
In order to remove the dialog box, at the end of the code in the GUI thread, i used:
splashy.InvokeBy(() =>
{
splashy.Close();
}
);
backgroundWorker1.CancelAsync();
Which uses the extension supplied by O_o
public static class MyExtensions
{
public static void InvokeBy(this Control ctl, MethodInvoker method)
{
if (ctl.InvokeRequired)
ctl.Invoke(method);
else method();
}
}
I have also built a label update into splashy
So i could call
splashy.InvokeBy(() =>
{
splashy.SetStatus(countupdated.ToString());
}
);
As i iterated through the datagridview rows. This updated the label on the splash screen :)

Update UI from another thread work only one time

I know there's a lot of information of this on stackoverflow, but don't find anything that resolved my problem.
I made a program to use ffmpeg with some video files. This process can take several minutes, so, I'm trying to make a progress bar on another form.
Basically, when I click on a button on my main form (FormSync), a new form is showed. This form have only a progressbar and a cancel button (lets call FormProgress).
To execute the ffmpeg, I use another class ( VideoProcessing) to create a new process, execute ffmpeg, and monitor the stderror (ffmpeg show progress on stderror). Every time ffmpeg show a progress, this class parse the output, calculate the progress, and raise a event (OnMergeProgress).
Basically, this is the code:
FormSync:
public partial class FormSync : Form
{
// this form show the progress of job
private FormProgress _formProgress;
// start the ffmpeg when click on button
private void mergeButton_click(object sender, EventArgs e)
{
var files = new List<string>() {"file1.mp4", "file2.mp4"};
MergeFiles(files);
}
// join all video files on a single file (using ffmpeg)
private void MergeFiles(IEnumerable<string> videoFiles)
{
// instantiate the class that execute ffmpeg
VideoProcessing videoProcessing = new VideoProcessing();
// this class has a a event to show current progress
// seconds = total seconds of video (sum length of all video files)
// currentSeconds = current progress
videoProcessing.OnMergeProgress += (seconds, currentSeconds) =>
{
Invoke((MethodInvoker) delegate()
{
// Instantiate the form of progress if not visible
if (_formProgress = null)
{
// define the minimum and maximum value of progressbar on constructor
_formProgress = new FormProgress(0, seconds);
_formProgress.ShowDialog(this);
}
// update the progress bar value
_formProgress.SetProgress(currentSeconds);
}
}
}
}
FormProgress:
public partial class FormProgress : Form
{
public FormProgress(int min, int max)
{
InitializeComponent();
progressBar.Minimum = min;
progressBar.Maximum = max;
}
public void SetProgress(int value)
{
value = (value <= progressBar.Minimum)
? progressBar.Minimum
: (value >= progressBar.Maximum) ? progressBar.Maximum : value;
progressBar.Value = value;
Refresh();
}
}
VideoProcessing:
internal class VideoProcessing
{
// Events
public delegate void MergeProgressHandler(int totalSeconds, int currentSeconds);
public event MergeProgressHandler OnMergeProgress;
private int _totalTimeVideos;
public void MergeFiles(string[] videoFiles)
{
// calculate total time of all videos
_totalTimeVideos = SomeFunctionToCalculateTotalTime();
// create the Process object to execute FFMPEG (with stdout and stderr redirection)
_process = CreateFFMPEGProcess(videoFiles);
}
// capture the stdout and stderr of ffmpeg
private void MergeOutputHandler(object sendingProcess, DataReceivedEventArgs outline)
{
// capture the current progress
// here will go a regex, and some other code to parse the info from ffmpeg
// Raise the event
OnMergeProgress?.Invoke(_totalTimeVideos, progressSeconds);
}
}
Basically, the FFMPEG execution and capture process use the following code:
C# execute external program and capture (stream) the output
The problem occur when I try to execute the code.
When I click que button, the FormProgress is showed, but after this, the progress bar "freeze". The program works good, no hangs here, but no update on progress bar.
If, in FormSync, on InvokeMethod, I replace the original code with the following content, I can see that ffmpeg is working, and my events are working too:
videoProcessing.OnMergeProgress += (seconds, currentSeconds) =>
{
Debug.WriteLine($"{currentSeconds}/{seconds}");
}
So, the problem was not ffmpeg, or my video class, but something that update the UI.
If I change the Invoke again, but this time with the Debug, like code below, the Debug print only the first update, and nothing more:
videoProcessing.OnMergeProgress += (seconds, currentSeconds) =>
{
Invoke((MethodInvoker) delegate() {
if (_formProgress == null) {
_formProgress = new FormProgress(Resources.merging_video_files, 0, seconds);
_formProgress.ShowDialog(this);
}
_formProgress.SetProgress(currentSeconds);
});
Debug.WriteLine($"{currentSeconds}/{seconds}");
}
_formProgress.ShowDialog(this);
The bug is located here. ShowDialog() does not return until the window is closed. Not clear when that happens, but not relevant to the bug. Since it does not return, the Invoke() call deadlocks and cannot complete. Which in turn causes the worker thread to hang.
Part of the problem is that the code uses Invoke() instead of Begininvoke(), you would not have noticed the same bug had you used the latter method. Not that this is pretty, but it would have hid the problem. Note that you don't need Invoke(), you don't need the return value and BeginInvoke() works just fine.
Ultimate reason you got this bug is because you needed to initialize the ProgressBar.Maximum property. Just don't do this, 100 is a good maximum value. Just a wee bit of math required, progress is now (100 * currentSeconds) / seconds.
You still need to call ShowDialog(), that's is a bit awkward, you can use the Load event to call MergeFiles().

Executing task onTextChanged freezes UI

I have a textbox and a listbox in my app. Also i have a text file with many players. What I want is whenever the user enters some text, look in the players file and add the matching players to the matching list which is the data source for the listbox. The problem is that it seems to be very slow and UI freezes a short time but it's quite annoying.
This is the code i have:
private void tb_playername_TextChanged(object sender, EventArgs e)
{
//This method is used to show user the options he can choose with the text he has entered
List<string> matching_players = new List<string>();
foreach (var item in all_players)
{
string player = item.f_name + " " + item.l_name;
if ((player.IndexOf(tb_playername.Text, StringComparison.OrdinalIgnoreCase) >= 0))
{
matching_players.Add("(" + item.rating + ") " + item.f_name + " " + item.l_name);
}
}
if (tb_playername.Text.Length >= 4)
{
matching_players.Sort();
matching_players.Reverse()
listbox_matchingplayers.DataSource = matching_players;
}
}
The problem is that you are doing a relatively time consuming task in the event handler. Event handlers operate on the same thread which takes care of rendering your application and handle any other visual aspects of it, so if this thread is busy, it will not be in a position to react to user input immediately, hence freezing.
The standard approach to this problem is to offload the time consuming tasks to a Background Worker. The background worker will operate in a new thread thus allowing the main thread to continue handling UI events. This example should hopefully put you on the right track when it comes to using a background worker.
EDIT: As per your question, what you could do would be to start searching only when a particular amount of characters is entered, for instance 3, this would reduce the amount of time the background worker runs. If the user keeps on typing, you could stop the current background worker if running and launch a new one.
The background worker will fire an event when finished. You could use the RunWorkerCompletedEventArgs.Result to then extract the returned list act upon it.
private async void tb_playername_TextChanged(object sender, EventArgs e)
{
var text = (sender as TextBox).Text;
// Check length of the text
if (string.IsNullOrEmpty(text) || text.Length <= 3)
return;
// Check timer to not process if user still typing, by measuring the key stoke time
...
// Filtering
List<string> matching_players = await PlayerFilter(text);
// Minimize listbox layout time
listbox_matchingplayers.SuspendLayout();
listbox_matchingplayers.DataSource = matching_players;
listbox_matchingplayers.ResumeLayout();
}
//Time consuming method
private async Task<List<string>> PlayerFilter(string text)
{
//This method is used to show user the options he can choose with the text he has entered
return matching_players;
}
For details of the user typing check wait for user to finish typing in a Text Box

Threads and waiting

I have a thread that gathers a list of URLs from a website and updates the UI as it's doing so. That works fine. But, I need the Main thread to wait until the links have been gathered. I tried to do a join, but this locks up the UI. Here is my example. As you can see at the moment the foreach loop gets called at the same time as the thread is running. I want the foreach to run after the thread has ran.
Any ideas?
/** This thread will add links to list<string> linkList **/
Thread linkListThread = new Thread(new ThreadStart(getLinkList));
linkListThread.Start();
foreach (String link in linkList)
{
txtOutput.Text += link;
}
You can use a background worker. Or have the thread method call a method in main context when it is done, passing the list of items you gathered.
EDIT
I thought I should elaborate my second approach a little.
You could prepare a list instance before creating the thread:
List<string> links = new List<string>();
Then you pass this list to the thread, which fills it:
Thread t = new Thread(new ParameterizedThreadStart(FillList));
t.Start(links);
The thread method takes the list, fills it and calls a method that shows the details in the UI:
private static void FillList(object state)
{
List<string> links = (List<string>)state;
// Fill data
this.Invoke((MethodInvoker)delegate() { HandleNewLinks(links); }));
}
The HandleNewLinks method works as one would expect:
private void HandleNewLinks(List<string> links)
{
foreach (string link in links)
// Do something...
}
Move the code that needs to run after the thread has completed into an event handler for BackgroundWorker.RunWorkerCompleted
Update: The handler is invoked on the right (calling ) thread - so you can safely update the UI.
See the code snippet on the above msdn page.
It is not clear what you want: either the application waits (and is unresponsive), or the application does not wait and remains responsive. In the latter case, you might want to disable some controls / possible actions until the list has finished loading.
A dirty workaround is to do some sort of spin waiting (Join with a timeout of a few ms, returns the result whether it is done or not) with some Application.DoEvents() calls.
Something simple would be to have your worker threads call back to the main application on completion, you then keep a count of completed threads and in your main UI do something like:
while(runningThreads != 0)
{
Application.DoEvents();
}
and have the threads call:
void OnThreadCompleted()
{
runningThreads--;
}
Better to use BackgroundWorker for this instead of creating your own threads as this has all the callback mechanisms ready to go.
We have used the Background worker for something similar and it worked well, with two observations:
Don't append text to a textbox with += because it will slow you down considerably after a few hundred lines. Use AppendText instead.
If you add a lot of info to the interface and have sleep times (during processing), the thread might 'fall asleep'. We fixed it by deleting the text in the textbox every 200 lines (the results were written to a file, so we didn't lose anything).
One alternative is simply use Invoke on the main thread:
void YourMethod()
{
Thread linkListThread = new Thread(new ThreadStart(getLinkList));
linkListThread.Start();
}
void getLinkList()
{
List<string> linkList = new List<string>();
// Your tasks
// Done
LinkListComplete(linkList);
}
void LinkListComplete(List<string> linkList)
{
if (InvokeRequired)
{
Invoke(new Action<List<string>>(LinkListComplete),linkList);
return;
}
foreach (String link in linkList)
{
txtOutput.Text += link;
}
}

Categories