I have a WPF application with the autocomplete box via the toolkit (VS 2008). I have a potential population of about 2000 records and I have tried to improve performance with a combination of the populating event procedure. I am getting inconsistent results. The filter seems to be OK but I can run the app once and result X will be there but result Y wont. Running it again can make result Y be there and not X, subsequent times both X and Y will be there, etc, etc. This is my first time using the autocomplete box so I'm sure it must be something in my code that I'm forgetting. If I check my result set just prior to the Itemsource binding, the desired results are there, but they are not made visible to the user - the drop-down autocomplete back does not show. Maybe I need an event override???
The XAML
<input:AutoCompleteBox
Name="autGlobal"
FilterMode="Contains"
Style="{DynamicResource MiniSearchAutoBoxWPF}"
IsTextCompletionEnabled="false"
Margin="5, 0, 5, 0"
HorizontalAlignment="Center"
KeyUp="autGlobal_KeyUp"
Text="Search Term"
GotFocus="autGlobal_GotFocus"
ValueMemberPath="Item"
Populating="AutoCompleteBox_Populating"
>
The Methods
private void AutoCompleteBox_Populating(object sender, PopulatingEventArgs e)
{
e.Cancel = true;
var b = new BackgroundWorker();
currSearch = autGlobal.Text;
b.DoWork += b_DoWork;
b.RunWorkerCompleted += b_RunWorkerCompleted;
b.RunWorkerAsync(autGlobal.Text);
}
private void b_DoWork(object sender, DoWorkEventArgs e)
{
Results.Clear();
int counter = 0;
string search = e.Argument.ToString();
search = search.ToUpper();
foreach (GlobalSearchList person in myGlobalList)
{
if (person.Item.ToUpper().Contains(search))
{
Results.Add(person);
counter++;
if (counter >= MAX_NUM_OF_RESULTS)
{
break;
}
}
}
}
private void b_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (this.Dispatcher.Thread == System.Threading.Thread.CurrentThread)
{
//Set the source
if (currSearch == autGlobal.Text)
{
autGlobal.ItemsSource = Results;
autGlobal.PopulateComplete();
}
}
else
{
this.Dispatcher.Invoke(new Action(() =>
{
//Set the source
if (currSearch == autGlobal.Text)
{
autGlobal.ItemsSource = Results;
autGlobal.PopulateComplete();
}
}));
}
}
I'm not sure why you need the performance boost in the first place, you're trying to calculate the elements that should be in the Autocomplete box in another thread and then assign them to the ItemsSource property of the Control. Something similar is what the AutoCompleteBox should do.
I tryed bind the control to a list with 10000 strings and it works perfect, so your problem could be the size of the objects that you're putting in the collection. One solution could be use just a string representation and then when you need the selected object you could find it based on it's representation, assuming that is unique (if not you could put some sort of ID).
One of the main problems with this approach is the thread sincronization, i will explain now why you get the extrange behavior where even when the filter is fine the items in the results are not right.
The filter seems to be OK but I can run the app once and result X will
be there but result Y wont. Running it again can make result Y be
there and not X, subsequent times both X and Y will be there, etc,
etc.
Suppose that you write "ab" in the autocomplete box, this will start a new BackGroundWorker where this search is performed. Everything should be fine if you wait long enough. But if you change the search query before the first worker has finished, now all the results will be mixed. Take for example the following lines of code:
// the user searchs for "ab"
[Thread 1] Results.Clear();
[Thread 1] Results.Add(Item[1]);
[Thread 1] Results.Add(Item[2]);
...
// the user changes the search term (to "abc" for example)
[Thread 2] Results.Clear();
[Thread 2] Results.Add(Item[3]);
// but what would happen if the first BackGroundWorker hasn't finished yet,
// this means that the first thread is still running
[Thread 1] Results.Add(Item[5]); // this items doesn't match the second search
[Thread 1] Results.Add(Item[6]); // criteria, but are added to the collection
[Thread 2] Results.Add(Item[7]);
// then you'll have two treads adding items to the Result collection
[Thread 1] Results.Add(Item[2]);
...
[Dispatcher Thread] autGlobal.ItemsSource = Results;
[Dispatcher Thread] autGlobal.PopulateComplete();
Hope this helps.
Maybe you could check this one.
All the work done for you.
http://gallery.expression.microsoft.com/WPFAutoCompleteBox/
Related
I'm trying to use System.Windows.Controls.GridView (specifically, it is wrapped by Eto.Forms.GridView) to display a large dataset (1m+ rows) and finding it is unusable.
From what I can see, when the GridView.ItemsSource property is set, the grid immediately calls GetEnumerator() and therefore causes a large lag before it can display the enumerated dataset. As such, I have implemented a workaround to quickly display the grid using the code shown below.
Basically what the code attempts to do is override the usual List.GetEnumerator() functionality and initially give a small chunk of rows from the underlying list. After that, it utilizes the INotifyCollectionChanged.CollectionChanged event to add the remaining rows, chunks at a time.
While the solution works as far as displaying the grid relatively quickly on the initial load, there are a number of problems including:
As the list is populated via the thread, it becomes very unresponsive and of course the aesthetics of seeing the scroll-bar extending doesn't look great and;
The biggest of issue is that the grid becomes entirely unresponsive for a minute (+) each time you attempt to scroll down.
Does anyone know how I can make a DataGrid work with a large IList datasource? For the record, I cannot change controls as I am using ETO.Forms for cross-platform desktop UI capability.
Thanks.
// The underlying IList containing a large list of rows
IList<T> _underlyingList;
public IEnumerator<T> GetEnumerator() {
// Create an initial chunk of data to immediately return to the grid for display
long i = 0;
for (i = 0; i < _underlyingList.Count; i++) {
yield
return _underlyingList[i];
if (i > 100) break;
}
// Record the UI context so we can update the collection in that thread
var uiContext = SynchronizationContext.Current;
// Now we create a task that will populate the rest of the grid by
// raising "CollectionChanged" events to add the remaining rows.
Task.Run(() => {
// Create a temporary list to add to
var list = new List <T> ();
// Add to our list
for (long x = i; x < _underlyingList.Count; x++) {
list.Add(_underlyingList[x]);
// Every x items, fire a "CollectionChanged" event.
if (x % 1000 == 0) {
var e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, list);
// Invoke the CollectionChanged event on the UI thread
uiContext.Send(p => CollectionChanged?.Invoke(this, e), null);
list.Clear();
}
}
// Fire any last event as required.
if (list.Count > 0) {
var e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, list);
CollectionChanged?.Invoke(this, e);
uiContext.Send(p => CollectionChanged?.Invoke(this, e), null);
}
});
}```
Subject says it all. My GUI is based on a DataTable and laid out like:
(current - 1, gray) label 1/4000 label ABC
[ Navigate Previous button ]
(current, black) label 2/4000 label DEF
[ Navigate Next button ]
(current + 1, gray) label 3/4000 label GHI
DEF is taken care of with currName.DataBindings.Add ("Text", mybindingsource, "cname");, but are ABC and GHI bindable with this approach?
Currently my workaround is to manually set the ABC and GHI labels procedurally on every call of mybindingsource_CurrentChanged event, but this seems sub-par because it's so manual and I lose the automatic formatting and DBNull handling that DataBindings.Add gives you for free.
What should I be doing instead?
(Yeah, I'm aware of DataRepeater, and I do use it on other forms, but it wasn't really suitable for this form for reasons that are too long to go into here and aren't relevant to the question.)
I believe your current solution is fine, but if you are curious to know how to have a workaround using data-binding, as an option, you can use different BindingSource components to bind controls, then set their Position based on position of the main binding source in PositionChanged event of the main binding source.
Example
In below example, the main binding source is currentBS which currentTextBox is bound to it. previousTextBox shows previous item and nextTextBox shows next item:
private void Form1_Load(object sender, EventArgs e)
{
var t = new DataTable();
var tc = t.Clone();
t.Columns.Add("C1");
t.Rows.Add("A");
t.Rows.Add("B");
t.Rows.Add("C");
t.Rows.Add("D");
t.Rows.Add("E");
currentBS.PositionChanged += (x, y) =>
{
if (currentBS.Position == 0)
previousBS.DataSource = tc;
else
{
previousBS.DataSource = t;
previousBS.Position = this.currentBS.Position - 1;
}
if (currentBS.Position == currentBS.Count - 1)
nextBS.DataSource = tc;
else
{
nextBS.DataSource = t;
nextBS.Position = this.currentBS.Position + 1;
}
};
previousBS.DataSource = tc;
nextBS.DataSource = tc;
currentBS.DataSource = t;
this.previousTextBox.DataBindings.Add("Text", previousBS, "C1");
this.currentTextBox.DataBindings.Add("Text", currentBS, "C1");
this.nextTextBox.DataBindings.Add("Text", nextBS, "C1");
}
At first I will post the code, it is short and quite clear.
cb_currentProfile is a ComboBox filled with 3 items when form is loaded:
delegate void SetCurrentProfileCallback(int index);
private void SetCurrentProfile(int index) // Set index of Combobox.SelectedItem
{
if (this.cb_currentProfile.InvokeRequired)
{
SetCurrentProfileCallback d = new SetCurrentProfileCallback(SetCurrentProfile);
this.Invoke(d, new object[] { index });
}
else
{
this.cb_currentProfile.SelectedItem = 2; // Won't work
this.cb_currentProfile.Visible = false; // It works
}
}
The problem is that when I try to change SelectedItem property, then it won't do nothing (no crash, just nothing happens).
I am sure that this code is reached in my form application.
At now I am making it in .NET 4.6 (but it was not working in v4.5 either)
The place where I am calling this method is in Task body:
Task.Run(() =>
{
while(true)
{
// ...
SetCurrentProfile(2);
// ...
Thread.Sleep(100);
}
});
I think that the problem is related to DataSource that seems to be invisible by other thread than main UI's.
I am also sure that data are loaded to ComboBox before code reaches a Task creation.
Edit 1 - selected item is null, Count property returns 0
When I used a debugger to check for some data, the results are:
var x = this.cb_currentProfile.SelectedItem; // null
var y = this.cb_currentProfile.Items.Count; // 0
It looks like, with the this.cb_currentProfile.SelectedItem = 2 statement, you intend to set the selection of the ComboBox by index. The ComboBox.SelectedItem Property accepts an Object and attempts to find it in its collection of items, selecting it if successful, and doing nothing otherwise. To select a particular index of the ComboBox, set the ComboBox.SelectedIndex Property instead.
I made a program which adds checkedListBox.Items from a text written in a TextBox . Regarding this, to make everything more esthetic , I made a rule so that if the number of Items added in CheckedListBox1 is bigger than a number I set, it will go to a second CheckedListBox and so on.
I can also save my Entries in a .txt file so I have easy access to my previous references. So naturally I also made a Load References which ,obviously, load the file I saved.
Anyhow, my dillemma is the following : When I press the Load References button it loads ALL the references (Lines) in the text into the first checkedListBox. I want it to respect the previous law. If I click Load References I want that if there are more than, lets say, 10 entries, all the other ones will go into the other checkedListBox ,by consequence, if the limit number is passed from the second checkedListBox the rest will go into the third one and so on.
I have searched StackOverflow and the Web for several solutions ,some of the more relevant ones :
First found link semi-regarding the subject
Second found link
So to not get it wrong I will state that I want to have all the entries that pass the limit be MOVED to another checkedlistBox ,not copied like the links would suggest.
This is the Line of code for my Load Reference button :
private void button8_Click(object sender, EventArgs e)
{
string fileData = File.ReadAllText(#" To-Do References .txt");
checkedListBox1.Items.AddRange(fileData.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries));
}
Also I tried several methods but this one seemed to be the closest ,even though I got almost no satisfactory result :
var i = checkedListBox1.Items.Count;
if (i >= 10)
checkedListBox2.Items.Insert(0, checkedListBox1.Items);
Regarding this line of code : It does get an entry send into the second checkedList Box it is just that the entry is called (Collection) and has nothing to do with my references.
I hope I made myself clear and thank you for support!
UPDATE
The marked answer works perfectly for this kind of program. As I have not found anything similar I believe this is most likely the best way to implement the separation of text lines into different checkedListBoxes.
if you populate listboxes properly there will be no need to move items
private void button8_Click(object sender, EventArgs e)
{
int limit = 10;
string[] fileData = File.ReadAllText(#" To-Do References .txt").Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
// this loop adds items to the 1st list until limit is reached
for(int i =0; i<limit && i<fileData.Length; i++)
checkedListBox1.Items.Add(fileData[i]);
// if there extra items, 2nd loop adds them to list №2
for(int i =limit; i<fileData.Length; i++)
checkedListBox2.Items.Add(fileData[i]);
}
Set a limit, and maybe a multiplier to control the checkedList the data will be added to.
int limit = 10;
int checkList = 1;
string[] fileData = File.ReadAllText(#" To-Do References .txt").Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < fileData.Length; i++)
{
if (i == limit * checkList)
{
checkList++;
}
switch (checkList)
{
case 1: checkedListBox1.Items.Add(fileData[i]); break;
case 2: checkedListBox2.Items.Add(fileData[i]); break;
case 3: checkedListBox3.Items.Add(fileData[i]); break;
}
}
As big as your text file gets, adding data to a checkedListBox just requires you to add a new line to the switch statement.
Well, my head is stuck in wpf land, so I would just bind it to a list of lists, in an itemscontrol, or something similar. Reading back, of course, it appears you are using winforms, so this may not be applicable...but i'll post anyways, because it can still be done this way using the WinForms DataRepeater control.
List<List<string>> mainList = new List<List<string>>();
int listIndex = 0;
string[] fileData = File.ReadAllText(#" To-Do References.txt").Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
for(int i = 0; i<=fileData.Length; i++)
{
mainList[listIndex].Add(fileData[i]);
if (i%10 == 0)
{
listIndex++;
}
}
Then bind the mainList to the control and configure your ItemTemplate.
There was lots of info on binding to the DataRepeater, but here's one link:
https://msdn.microsoft.com/en-us/library/cc488279.aspx
I have problem with my GUI and Threads.
The GUI contains DataGrid. Every X time the program do some query and getting a list of items that I want to fill into the DataGrid.
So far so good:
private void loadTaskList() //Call every X time
{
List<myObject> myList = myquery();
this.Dispatcher.Invoke((Action)(() =>
{
TaskListTable.Items.Clear(); //Clear the DataGrid
foreach (myObject O in myList) //Add the items from the new query.
{
TaskListTable.Items.Add(O);
}
}));
FindSelectionObject(); // <-- see next explanation.
}
When the user click on one of the objects in the datagrid, the line color changed (it works fine), but when the program reload the table,The painted line disappears (Becuse I clear and add new objects).
To deal with it, I created the function FindSelectionObject():
private void FindSelectionObject()
{
this.Dispatcher.Invoke((Action)(() =>
{
this.SelectedIndex = TaskListTable.Items.IndexOf((myObject)lastSelectionObject); //find index of the new object that equels to the last selection object.
var row = TaskListTable.ItemContainerGenerator.ContainerFromIndex(SelectedIndex) as DataGridRow; //get the row with the index
row.Background = Brushes.LightGoldenrodYellow; //repaint
}));
}
The problem: Everything works fine, but sometimes when the program reloads, the line flashes per second and then highlighted back, and sometimes it's not painting it at all (untill the next reload).
I can't understand why this is happening. I think maybe the FindSelectionObject() begins to run before the loadTaskList() ends to invoke all and add the new objects into the datagrid.
But if so - Why? And how can I fix it?
In the bottom line, I want that after every reload the line re-paint immediately..
Thanks for any advice!
A few things to think about:
You should keep in mind that the DataGrid uses virtualization, which means that each item in your items source does not get its very own UI element. The UI elements are created to fill the visible area, and then re-used depending on which data-source item is currently bound to each one (this changes when you scroll for instance or change the items source). This may cause you problems in the future if you use your current approach, so keep this in mind.
The other thing is that the DataGrid may require more "cycles" of the layout process in order to update its UI. You may simply be calling FindSelectionObject prematurely. You have queued FindSelectionObject right after the invocation in loadTaskList. If the DataGrid needs to perform some actions which are queued on the dispatcher after the items source has changed, these will execute after the invocation in FindSelectionObject.
Try this instead:
private void loadTaskList() //Call every X time
{
List<myObject> myList = myquery();
this.Dispatcher.Invoke((Action)(() =>
{
TaskListTable.Items.Clear(); //Clear the DataGrid
foreach (myObject O in myList) //Add the items from the new query.
{
TaskListTable.Items.Add(O);
}
// The items of the grid have changed, NOW we QUEUE the FindSelectionObject
// operation on the dispatcher.
FindSelectionObject(); // <-- (( MOVE IT HERE )) !!
}));
}
EDIT: OK, so if this fails then maybe this will cover the case in which the above solution fails: subscribe to the LoadingRow event of DataGrid and set the appropriate background color if the row is the selected one. So in the cases when new rows are created this event will be called (due to virtualization it is not called per item in items source, but per actual row UI element). In the event args you will have access to the created DataGridRow instance.
I think this issue could be a visual thread synchronization. For this you can create and use a method similar like this:
public void LockAndDoInBackground(Action action, string text, Action beforeVisualAction = null, Action afterVisualAction = null)
{
var currentSyncContext = SynchronizationContext.Current;
var backgroundWorker = new BackgroundWorker();
backgroundWorker.DoWork += (_, __) =>
{
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US");
Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("en-US");
currentSyncContext.Send((t) =>
{
IsBusy = true;
BusyText = string.IsNullOrEmpty(text) ? "Espere por favor..." : text;
if (beforeVisualAction != null)
beforeVisualAction();
}, null);
action();
currentSyncContext.Send((t) =>
{
IsBusy = false;
BusyText = "";
if (afterVisualAction != null)
afterVisualAction();
}, null);
};
backgroundWorker.RunWorkerAsync();
}
IsBusy and BusyText are particular properties, that you can remove. The action variable will be the action to do in background (load your items for instance). beforeVisualAction and afterVisualAction are the visual actions you may want to do before and after the background action. Here are any visual update, for instance select your item, change color, set a view model variable that raise a binding update,... (any action that update the view).
Hope this method helps.
Are you maintaining the reference to lastSelectionObject somewhere? You say you're adding new objects, if they are truly new then the reference will be different and the reference comparison happening in IndexOf will not find it.