Advice on populating combobox - Threading/Background Worker/Application.DoEvents related

  • Thread starter Thread starter Rob W
  • Start date Start date
R

Rob W

Greetings,



I had issue updating a combobox with 30K items, whilst this activity was
running nothing else could take place and the form was not repainted.



I'm quite the newbie and read about background workers and implemented one
for the combobox but soon realised that the actual update of the combobox
could not

be done in the background worker only when it had completed as nothing done
in the backgroundwork could manipulate controls on the form.



I then had the idea to launch another form on top with a progress bar (Which
reads a textfile line by line and utilises the Application.DoEvents in the
loop which reads the data into a string array), populating the combobox in
the parent form from the child form, disabling the main form.



Then once the data had been loaded, close the form and re-enable the main
form.



This worked well but as the form contains tabcontrol when the tabpage is
accessed which contains the combobox there is a 2-3 second delay for the
first time only.



What solutions are there to handle such an event of populating combobox with
30K items whilst allowing the user to still interact with the main form?



I'm currently reading more about threading, easier threading maintenance
with the backgroundworker and application.DoEvents.



Thanks

Rob
 
Rob said:
What solutions are there to handle such an event of populating combobox with
30K items whilst allowing the user to still interact with the main form?

The bottle neck is the UI thread that you want to handle and that must
handle both jobs. It can only be done alternately. For example, add a
Timer (Windows.Forms.Timer), and in it's Tick event, add a couple of
items taken from a queue filled by a background thread to the combobox.
You can set a low Timer interval but should also only add few items each
time. Try it how many, or use a timeout while adding the items.
I'm currently reading more about threading, easier threading maintenance
with the backgroundworker and application.DoEvents.

DoEvents is actually...BAD. You'd need it only in a UI thread, but a UI
thread already has a message loop that already processes these messages.
(only if you don't have message loop, like in a game's main loop,
Doevents makes sense).

BTW, here it takes 1s to add 30,000 items (Strings; each 25 chars)
(2.2 GHz Athlon) using the Combobox' AddRange method.


Armin
 
Implement this as the solution in the form constructor

System.Windows.Forms.Form.CheckForIllegalCrossThreadCalls = False

I would imagine I should use this with caution.

Reading up further for potential issues and how to set it for just a control
rather than the form.



Rob
 
Rob said:
Implement this as the solution in the form constructor

System.Windows.Forms.Form.CheckForIllegalCrossThreadCalls = False

I would imagine I should use this with caution.

You should not use this at all. Illegal cross thread calls cause
unpredictable results by definition. Setting the property = True
_immediatelly_ informs you about you are doing something wrong.

Well, after knowing that your app doesn't perform illegal cross
thread calls, you could set it to False, but as I don't see a noteworthy
benefit, I'd keep the additional check.
Reading up further for potential issues and how to set it for just a control
rather than the form.

CheckForIllegalCrossThreadCalls is a shared member of
System.Windows.Forms.Control (not Form). It applies to all controls
(including Forms).


Armin
 
Rob said:
Greetings,



I had issue updating a combobox with 30K items, whilst this activity was
running nothing else could take place and the form was not repainted.



I'm quite the newbie and read about background workers and implemented one
for the combobox but soon realised that the actual update of the combobox
could not

be done in the background worker only when it had completed as nothing done
in the backgroundwork could manipulate controls on the form.



I then had the idea to launch another form on top with a progress bar (Which
reads a textfile line by line and utilises the Application.DoEvents in the
loop which reads the data into a string array), populating the combobox in
the parent form from the child form, disabling the main form.



Then once the data had been loaded, close the form and re-enable the main
form.



This worked well but as the form contains tabcontrol when the tabpage is
accessed which contains the combobox there is a 2-3 second delay for the
first time only.



What solutions are there to handle such an event of populating combobox with
30K items whilst allowing the user to still interact with the main form?



I'm currently reading more about threading, easier threading maintenance
with the backgroundworker and application.DoEvents.



Thanks

Rob

I tried posting earlier but it never showed, so excuse me if this is a
duplicate.

Are you calling BeginUpdate before adding your items, and EndUpdate
after you are done? If not, then the combobox fill process will be much
slower.

Also, expecting users to select from 30,000 items in a combobox seems a
bit over the top. Just my opinion, but it seems there is a better way
to get to their choice of "Jelly Fish" without showing them every
species of animal in existence (as an example).
 
OK thanks for all the replies (I've just scan read them).
I will look at each one in more detail and then experiment with the
alternative solutions.

Thank you very much :-)
 
I've read all the replies.
Since I load the combobox with addRange and use BeginUpdate/Endupdate I
would find it difficult to use a timer as the data is loaded in one
statement.

I was reading about how delegates are assigned a sub routines/functions and
could be called from within a backgroundworker allowing the UI to be
accessed and update the comboxbo.

Is this possible?
I was reading how simple delegates are easy to understand, but for the life
of me I just don't understand them.

Could I use a delegate for my scenario to populate the combobox in a
background thread whilst not disrupting the UI thread?

Other solutions I fear would be to manually program threads and use
invoke/beginInvoke to resolve my issue.
I need to get better understanding, well An understanding of these as at the
moment I'm relatively clueless.

A very confused...
Rob
 
Armin reported :

"BTW, here it takes 1s to add 30,000 items (Strings; each 25 chars)
(2.2 GHz Athlon) using the Combobox' AddRange method."

So start first by using AddRange (fed with all 30000 items in a single call
of course), then can you tell us how much time it takes ? Take care to make
the difference between loading the combo (using AddRange) and getting the
whole data.There is no point to use a background thread if this is quicker
enough.

Also as Mike said, from a usability point of view letting the user select on
row out of 30000 is not necessarily the best option...
 
Rob said:
I've read all the replies.
Since I load the combobox with addRange and use BeginUpdate/Endupdate I
would find it difficult to use a timer as the data is loaded in one
statement.

You can split the data and call Addrange several time with smaller
packets instead of adding them all at once.
I was reading about how delegates are assigned a sub routines/functions and
could be called from within a backgroundworker allowing the UI to be
accessed and update the comboxbo.

Is this possible?

It's possible but doesn't help you. The code is executed in the main
(UI) thread and the UI doesn't react as long as it's executed. Not what
you want.
I was reading how simple delegates are easy to understand, but for the life
of me I just don't understand them.

Simply put, it's a pointer to a method. In the background thread,
whenever you call Invoke/Begininvoke on a control created in the UI
thread, you pass a delegate. The delegate points to the method that the
UI thread is to call.
Could I use a delegate for my scenario to populate the combobox in a
background thread whilst not disrupting the UI thread?

You can not do it in a background thread because only the UI thread is
allowed to do that. That's the point of this discsussion.
Other solutions I fear would be to manually program threads and use
invoke/beginInvoke to resolve my issue.

see above
I need to get better understanding, well An understanding of these as at the
moment I'm relatively clueless.

Be certain that a second thread does not help. The items must be added
in the UI thread. Either do it at once or in packets. The bigger the
packets are, the longer the UI is not responsive. Therefore my
suggestion to use a Timer. I'll make an example and post it...


Armin
 
Its a list of UK towns and takes around 7- 8 seconds to complete the
Addrange statement, the IO time i.e. populating the array from file takes
just 1 second.
The text file is also 40,165 lines so 10k off there (double checked in MS
Word -> Line count).

I did think about just create a database table as the application backed is
an MS access database (small application used by 2-3 people).

My PC spec is a Pentium 4 CPU 3.40GHZ with 2GB ram, though is
underperforming imo for whatever reason :-(
 
With a Button1, a ComboBox1 and a BackgroundWorker1 :

Dim Items(40165) As String
For i As Integer = 0 To UBound(Items)
Items(i) = "I'm town #" & i.ToString
Next
Dim sw As New Stopwatch
sw.Start()
ComboBox1.Items.AddRange(Items)
sw.Stop()
Debug.WriteLine(sw.Elapsed)

gives a bit less than 2 s for filling the combo using AddRange (2.6 GHz with
3Go of RAM).

Another option could be to use the TextChanged event and load the town for
the initial (or first letters entered by the user). This way instead of
loading 40000 items and let the user choose, the user will enter one letter
and then yo'ull load about 40000/26 items rather than all 40000 and
hopefully it will be about 26 times quicker (depends on the letter but you
get the idea)...

I was about to test a backgroundworker scenario but in my experience a
background worker is better when the amount of processing done far exceed
the amount of UI update you'll do. This is because if your background thread
spents most of its time calling back the UI thread then your UI thread will
still be busy and you are basically back at your starting point... Here it
seems you are in such this case as it seems you have 1 sec of background
processing for 8 s of UI processing...
 
Armin said:
I'll make an example and post it...

Well, I did the version with the Timer..... Then the Application.Idle
event came into my mind. Try this: In a new project, put a Button and a
Combobox (default names) on a Form. Code:

Public Class Form1

Private f_ItemsToAdd As String()
Private f_watch As New Stopwatch
Private f_ApplicationIdleDelegate As New EventHandler( _
AddressOf OnApplicationIdle)

Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click

ReDim f_ItemsToAdd(30000)

For i = 0 To f_ItemsToAdd.Length - 1
f_ItemsToAdd(i) = i & "-1234567890123456789012345"
Next

ComboBox1.Items.Clear()
f_watch.Reset()
f_watch.Start()
AddHandler Application.Idle, f_ApplicationIdleDelegate

End Sub
Private Sub OnApplicationIdle( _
ByVal sender As Object, ByVal e As System.EventArgs)

AddItems()

End Sub
Private Sub AddItems()

Dim range As Object()
Dim MinIndex, MaxIndex As Integer

MinIndex = ComboBox1.Items.Count
MaxIndex = Math.Min(MinIndex + 100, f_ItemsToAdd.Length - 1)

ReDim range(MaxIndex - MinIndex)

For Index = MinIndex To MaxIndex
range(Index - MinIndex) = f_ItemsToAdd(Index)
Next

ComboBox1.Items.AddRange(range)

If ComboBox1.Items.Count = f_ItemsToAdd.Length Then
f_watch.Stop()
f_ItemsToAdd = Nothing
Debug.Print("Duration: " & f_watch.Elapsed.ToString)
RemoveHandler Application.Idle, f_ApplicationIdleDelegate
End If

End Sub

End Class


Start and click the button. Filling takes 1.4s here and the UI is
responsive.


Armin
 
Hi Rob,

What does your populating code look like? If it's something along the lines
of:

Open the File
Read a line from the file
Add it to the combo box
Keep looping while more to read

Then I'd suggest a bit of alteration to make it more efficient. First read
the entire file, split it up into the items needed to be added to the combo,
and then call .AddRange to add all the items at once to the combo box.

Find the bottleneck also - if it's during the code to read the file, then
have *that* work on a background thread while the form loads (or even while
the application loads) and then Invoke a call to AddRange all the items.
This should keep your form responsive while the file is loaded.

Steer well clear of Application.DoEvents - it's basically a legacy hack from
the VB6 era when multi-threading was clunky at best and often impossible.
Like GoTo, it has no place in modern code.

Last, and perhaps most important - are you absolutely sure you want 30,000
items in a drop-down combo? As a user I'd find that to be quite off-putting
in a user interface, and I doubt I'd ever want to scroll through 30,000
items looking for the one I want. Maybe a TextBox with auto-complete would
be more suitable, or a composite control made up of a text box and a search
button? Whatever you decide, I'd think carefully before pulling that much
data into the front end...

HTH,
Alex
 
Thanks Armin and all those who replied.

I've learnt a lot of new commands, the preferred methods and what's best
avoided and enjoyed investigating all of the possible solutions.
It also confirmed my PC is slowly dying (I knew this as my CPU temperature
is not so great) as takes over double the time with a fast processor ... :-(

I went for the application idle approach of adding 100 items when resources
allowed so, which is the scenario works very well.

Thanks again :-)
 
Back
Top