I am trying to create a text input field with autocomplete functionality. The list of available options is huge (50,000+) and will need to be queried on TextChanged (after the first 3 characters have been entered).
I have a 99%-working solution with TextBox, setting AutoCompleteCustomSource to my new AutoCompleteStringCollection in the TextChanged event, but that results in occasional memory access violations due to a well-documented bug in the underlying AutoComplete implementation...
Microsoft Support say "Do not modify the AutoComplete candidate list dynamically during key events"...
Several SO threads: 1, 2, 3
These threads have some suggestions on how to prevent the exceptions but nothing seems to completely eliminate them, so I'm looking for an alternative. have tried switching to a ComboBox-based solution but can't get it to behave as I want.
After the user types the third character, I update the ComboBox's DataSource but the first item is automatically selected. The user is not able to continue typing the rest of the name.
The ComboBox items are not visible until the user clicks the triangle to expand the list
If the user selects the text they have entered and starts typing, I set DataSource to null to remove the list of suggestions. Doing this puts the cursor at the start of the text, so their characters appear in completely the wrong order!
My View:
public event EventHandler SearchTextChanged;
public event EventHandler InstrumentSelected;
public Instrument CurrentInstrument
{
get { return comboBoxQuickSearch.SelectedItem as Instrument; }
}
public IEnumerable<Instrument> Suggestions
{
get { return comboBoxQuickSearch.DataSource as IEnumerable<Instrument>; }
set
{
comboBoxQuickSearch.DataSource = value;
comboBoxQuickSearch.DisplayMember = "Name";
}
}
public string SearchText
{
get { return comboBoxQuickSearch.Text; }
}
private void comboBoxQuickSearch_TextChanged(object sender, EventArgs e)
{
if (SearchTextChanged != null)
{
SearchTextChanged(sender, e);
}
}
private void comboBoxQuickSearch_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter && InstrumentSelected != null)
{
InstrumentSelected(sender, e);
}
}
My Presenter:
private void SearchTextChanged(object sender, EventArgs e)
{
lock (searchLock)
{
// Do not update list of suggestions if:
// 1) an instrument has already been selected
// (the user may be scrolling through suggestion list)
// 2) a search has taken place within the last MIN_SEARCH_INTERVAL
if (DateTime.Now - quickSearchTimeStamp < minimumSearchInterval
|| (view.Suggestions != null && view.Suggestions.Any(i => i.Name == view.SearchText)))
{
return;
}
string searchText = view.SearchText.Trim();
if (searchText.Length < SEARCH_PREFIX_LENGTH)
{
// Do not show suggestions
view.Suggestions = null;
searchAgain = false;
}
// If the prefix has been entered or changed,
// or another search is needed to display the full sublist
else if (searchText.Length == SEARCH_PREFIX_LENGTH
|| searchText.Substring(0, SEARCH_PREFIX_LENGTH) != searchTextPrefix
|| searchAgain)
{
// Record the current time and prefix
quickSearchTimeStamp = DateTime.Now;
searchTextPrefix = searchText.Substring(0, SEARCH_PREFIX_LENGTH);
// Query matches from DB
IList<Instrument> matches = QueryMatches(searchText);
// Update suggestions
view.Suggestions = matches;
// If a large number of results was received, search again on the next chararacter
// This ensures the full match list is presented
searchAgain = matches.Count() > MAX_RESULTS;
}
}
}
(The searchAgain bit is left over from the TextBox implementation, where the AutoCompleteCustomSource wouldn't always show the complete list if it contained too many items.)
Can I get the ComboBox to work as I want it to, providing suggestions as the user types, given my requirement to query those suggestions on TextChanged?
Is there some other combination of controls I should use for a better user experience, e.g. ListBox?
Related
I have a checkedListBox where the user can select whatever they want to update. I want them to be able to freely update 1 up to 5 characteristics of a machine. So when they only want to update 1 thing, they do not have to provide the other 4 characteristics. Also, when they want to update 5 characteristics, they
can do it in one go. For that purpose I have the following if statements:
if (clbCharacteristicsToUpdate.CheckedItems.Count != 0)
{
if (clbCharacteristicsToUpdate.GetSelected(0))
{
currentITime = Convert.ToDouble(tbCurrentITime.Text);
MessageBox.Show(currentITime.ToString());
dh.UpdateCurrentITime(machineNr, currentITime);
}
if (clbCharacteristicsToUpdate.GetSelected(1))
{
cycleTime = Convert.ToDouble(tbCycleTime.Text);
MessageBox.Show(cycleTime.ToString());
dh.UpdateCycleTime(machineNr, cycleTime);
}
if (clbCharacteristicsToUpdate.GetSelected(2))
{
nrOfLinesPerCm = Convert.ToInt32(tbNrOfLinesPerCm.Text);
MessageBox.Show(nrOfLinesPerCm.ToString());
dh.UpdateNrOfLinesPerCm(machineNr, nrOfLinesPerCm);
}
if (clbCharacteristicsToUpdate.GetSelected(3))
{
heightOfLamallae = Convert.ToDouble(tbHeightOfLamallae.Text);
MessageBox.Show(heightOfLamallae.ToString());
dh.UpdateHeightOfLamallae(machineNr, heightOfLamallae);
}
if (clbCharacteristicsToUpdate.GetSelected(4))
{
if (rbLTB.Checked)
{
machineType = 2;
MessageBox.Show(machineType.ToString());
}
else if (rbSTB.Checked)
{
machineType = 1;
MessageBox.Show(machineType.ToString());
}
if(!rbLTB.Checked && !rbSTB.Checked)
{
MessageBox.Show("Select a machine type to update!");
return;
}
dh.UpdateType(machineNr, machineType);
}
}
My problem is that, when I choose and update 1 thing it works perfectly. But when I choose multiple ones, it only executes the last if statement that returns true. I thought about using if-else but then only the first one that returns true will be executed. I also thought about having if statements for each possibility. But since I have 5 characteristics I can update, this would make 25 possibilities and I do not want to have 25 if statements. Thanks in advance!
GetSelected does not check whether the item has been checked, but that it is actually selected.
For example, in the below image "Item 2" will will return true for GetSelected. It is selected, not checked.
Instead you could do something like checking the clbCharacteristicsToUpdate.CheckedItems property to get the items that are actually checked.
The GetSelected method actually comes from the ListBox class, which CheckedListBox inherits from.
Try this:
This is a helper method I created on a button.
private void button1_Click(object sender, EventArgs e)
{
CheckList();
}
This method looks if items are checked and iterates over the collection of checked ones, calling the last metod.
private void CheckList()
{
if (clbCharacteristicsToUpdate.SelectedItems.Count != 0)
{
foreach (int indexChecked in clbCharacteristicsToUpdate.CheckedIndices)
{
CheckSelectedItem(indexChecked);
}
}
}
And finally, here the appropriate actions are taken based on indexes.
private void CheckSelectedItem(int index)
{
if (index == 0)
{
//Do stuff on Zero
MessageBox.Show("Zero");
}
if (index == 3)
{
//Do stuff on One
MessageBox.Show("Three");
}
}
I created an extension for CRCaseMaint, and added the event CRCase_RowSelecting. Here is the code I am currently using:
protected virtual void CRCase_RowSelecting(PXCache sender, PXRowSelectingEventArgs e)
{
CRCase row = e.Row as CRCase;
if (row == null) return;
PXDatabase.ResetSlot<List<CRCase>>("OriginalCase");
List<CRCase> originalCaseSlot = PXDatabase.GetSlot<List<CRCase>>("OriginalCase");
if (originalCaseSlot.Count == 0)
{
originalCaseSlot.Add(sender.CreateCopy(row) as CRCase);
}
else
{
originalCaseSlot[0] = sender.CreateCopy(row) as CRCase;
}
}
When I first open a case, this event will fire a couple times, and the last time it fires, the current case is correctly stored in e.Row, so this code works great. When I click Save, I have a RowPersisting event that compares the case stored in the originalCaseSlot with the updated case. At the end, it sets the original case slot to the updated case. This also works well.
However, when I make another change without leaving the case, and click save, e.Row on the RowSelecting event now has the next case stored on it rather than the current case. Since I am not touching the next case in any way, I am surprised that this is happening.
My question is, should I be using a different event instead of RowSelecting, or is there something else I am missing?
Thank you all for your help.
Sometimes when the primary record gets updated or the user clicks on a form toolbar button, the framework selects 2 records from database: the current primary record and the next one. This is why RowSelecting is invoked 2nd time for the next CRCase record.
Honestly, using PXDatabase Slots to store user session-specific records is not a good idea. PXDatabase Slots are shared among all user sessions and should only be used to cache frequently used data from database, which is not prone to frequent updates. This makes the main purpose of PXDatabase Slots to reduce number of database queries to widely and very often used configurable data, like Segment Key or Attribute configurations.
With that said, using the RowSelecting handler is definitely a step in the right direction. Besides, the RowSelecting handler, you should additionally define a separate PrevVersionCase data view to store the original CRCase record(s) and also override the Persist method to report about changes. The Locate method used on PXCache objects searches the cache for a data record that has the same key fields as the provided data record. This approach allows to compare changes between the originally cached and modified CRCase records having identical key field values.
public class CRCaseMaintExt : PXGraphExtension<CRCaseMaint>
{
[Serializable]
public class CRPrevVersionCase : CRCase
{ }
public PXSelect<CRPrevVersionCase> PrevVersionCase;
protected virtual void CRCase_RowSelecting(PXCache sender, PXRowSelectingEventArgs e)
{
CRCase row = e.Row as CRCase;
if (row == null || e.IsReadOnly) return;
var versionCase = new CRPrevVersionCase();
var versionCache = PrevVersionCase.Cache;
sender.RestoreCopy(versionCase, row);
if (versionCache.Locate(versionCase) == null)
{
versionCache.SetStatus(versionCase, PXEntryStatus.Held);
}
}
[PXOverride]
public void Persist(Action del)
{
var origCase = Base.Case.Current;
var origCache = Base.Case.Cache;
CRPrevVersionCase versionCase;
if (origCache.GetStatus(origCase) == PXEntryStatus.Updated)
{
versionCase = new CRPrevVersionCase();
origCache.RestoreCopy(versionCase, origCase);
versionCase = PrevVersionCase.Cache.Locate(versionCase) as CRPrevVersionCase;
if (versionCase != null)
{
foreach (var field in Base.Case.Cache.Fields)
{
if (!Base.Case.Cache.FieldValueEqual(origCase, versionCase, field))
{
PXTrace.WriteInformation(string.Format(
"Field {0} was updated", field));
}
}
}
}
del();
if (origCase != null)
{
PrevVersionCase.Cache.Clear();
versionCase = new CRPrevVersionCase();
Base.Case.Cache.RestoreCopy(versionCase, origCase);
PrevVersionCase.Cache.SetStatus(versionCase, PXEntryStatus.Held);
}
}
}
public static class PXCacheExtMethods
{
public static bool FieldValueEqual(this PXCache cache,
object a, object b, string fieldName)
{
return Equals(cache.GetValue(a, fieldName), cache.GetValue(b, fieldName));
}
}
I have 2 LookUpEdit controls from DevExpress on my form. Both use an ObservableCollection as it's datasource, one being of type string and the other of type double. The LookUpEdit control has an event called ProcessNewValue which fires when, you guessed it, a new value is entered in the control. I've added some code in this event to add the newly added value to the ObservableCollection and it automatically selects it once done. This works as expected for the string LooUpEdit but when I try it with the double LookUpEdit`, it adds it to the collection but then it clears out the control.
Here's the code to load the controls, which gets called in Form_Load():
void InitControls()
{
double[] issueNumbers = new double[5];
issueNumbers[0] = 155;
issueNumbers[1] = 156;
issueNumbers[2] = 157;
issueNumbers[3] = 158;
issueNumbers[4] = 159;
ObservableCollection<double> issues = new ObservableCollection<double>(issueNumbers);
lookupIssues.Properties.DataSource = issues;
DevExpress.XtraEditors.Controls.LookUpColumnInfoCollection colInfo = lookupIssues.Properties.Columns;
colInfo.Clear();
colInfo.Add(new DevExpress.XtraEditors.Controls.LookUpColumnInfo("Column"));
colInfo[0].Caption = "Issue ID's";
string[] stringNumbers = Array.ConvertAll<double, string>(issueNumbers, Convert.ToString);
ObservableCollection<string> issuesString = new ObservableCollection<string>(stringNumbers);
lookupStringValue.Properties.DataSource = issuesString;
colInfo.Clear();
colInfo.Add(new DevExpress.XtraEditors.Controls.LookUpColumnInfo("Column"));
colInfo[0].Caption = "String Issue ID's";
}
And here's the ProcessNewValue event for both (I've renamed them to try to make it easier to see which does what):
private void OnProcessNewValue_Double(object sender, DevExpress.XtraEditors.Controls.ProcessNewValueEventArgs e)
{
ObservableCollection<double> source = (ObservableCollection<double>)(sender as LookUpEdit).Properties.DataSource;
if (source != null)
{
if ((sender as LookUpEdit).Text.Length > 0)
{
source.Add(Convert.ToDouble((sender as LookUpEdit).Text));
(sender as LookUpEdit).Refresh();
}
}
e.Handled = true;
}
private void OnProcessNewValue_String(object sender, DevExpress.XtraEditors.Controls.ProcessNewValueEventArgs e)
{
ObservableCollection<string> source = (ObservableCollection<string>)(sender as LookUpEdit).Properties.DataSource;
if (source != null)
{
if ((sender as LookUpEdit).Text.Length > 0)
{
source.Add((sender as LookUpEdit).Text);
(sender as LookUpEdit).Refresh();
}
}
e.Handled = true;
}
As you can see, the code it identical with the exception of one converting text to a double before adding it to the collection.
Anyone know why the double value gets added to the collection but the control doesn't automatically select it like it does with a string collection? I've even tried to hard-code the newly added value right after e.Handled = true; but it still doesn't select it. What's weird is that if I run it through the debugger, I can step through and see that the lookupIssues control indeed gets the newly added value AND it's Text property is set to it, but as soon as the event terminates, the control clears it out.....really strange.
Any help is greatly appreciated!
BTW, I can add a link to a sample project that duplicates the problem but you would need to have DevExpress v12.2.6 controls installed in order to compile the project.
I posted this to the DevExpress team as well and they were gracious enough to provide the solution:
I agree that this discrepancy appears confusing as-is. The reason for the discrepancy is LookUpEdit.ProcessNewValueCore makes a call to RepositoryItemLookUpEdit.GetKeyValueByDisplayValue which returns a null value from the LookUpListDataAdapter because no implicit conversion exists from double to string. You may resolve the discrepancy with the following change to your ProcessNewValue handler:
private void OnProcessNewValue_Double(object sender, DevExpress.XtraEditors.Controls.ProcessNewValueEventArgs e)
{
ObservableCollection<double> source = (ObservableCollection<double>)(sender as LookUpEdit).Properties.DataSource;
if (source != null) {
if ((sender as LookUpEdit).Text.Length > 0) {
double val = Convert.ToDouble((sender as LookUpEdit).Text);
source.Add(val);
e.DisplayValue = val;
(sender as LookUpEdit).Refresh();
}
}
e.Handled = true;
}
The control now behaves as expected. I hope this can help someone else out :)
I have a ListBox control populated with branches of a large retail chain. The staff using the system have to log in to the relevant branch, and I would like them to be able to search the ListBox to find their branch.
I have created an event handler for when text in the search box changes, and attempted to use code sound on StackOverflow already:
private int lastMatch = 0;
private void txtSearch_TextChanged(object sender, EventArgs e)
{
int x = 0;
string match = txtSearch.Text;
if (txtSearch.Text.Length != 0)
{
bool found = true;
while (found)
{
if (lbBranches.Items.Count == x)
{
lbBranches.SetSelected(lastMatch, true);
found = false;
}
else
{
lbBranches.SetSelected(x, true);
match = lbBranches.SelectedValue.ToString();
if (match.Contains(txtSearch.Text))
{
lastMatch = x;
found = false;
}
x++;
}
}
}
}
When I compile and start typing into the search box, I get this error:
Object reference not set to an instance of an object.
The line in question is:
match = lbBranches.SelectedValue.ToString();
I have no idea what could be wrong there, anyone got an idea?
Thanks!
SelectedValue of the listbox will only return a value if you have specified the ValueMember property of the listbox to indicate a property from which you would like to read the value for the selected item. The property you want to use in this case is SelectedItem:
match = lbBranches.SelectedItem.ToString();
when the user is entering text it's possible that no value has been selected (hence the error) -- keep in mind that what is being entered by the user has no mandatory or direct association with selections in the controls listbox sub-element
it's possible what you're doing might be simpler to implement with a full combo-box control and I think some of the examples at MSDN could be very helpful for you as well
I have about 20 text fields on a form that a user can fill out. I want to prompt the user to consider saving if they have anything typed into any of the text boxes. Right now the test for that is really long and messy:
if(string.IsNullOrEmpty(txtbxAfterPic.Text) || string.IsNullOrEmpty(txtbxBeforePic.Text) ||
string.IsNullOrEmpty(splitContainer1.Panel2) ||...//many more tests
Is there a way I could use something like an Array of any, where the array is made of the text boxes and I check it that way? What other ways might be a very convenient way in which to see if any changes have been made since the program started?
One other thing I should mention is there is a date time picker. I don't know if I need to test around that as the datetimepicker will never be null or empty.
EDIT:
I incorporated the answers into my program, but I can't seem to make it work correctly.
I set up the tests as below and keep triggering the Application.Exit() call.
//it starts out saying everything is empty
bool allfieldsempty = true;
foreach(Control c in this.Controls)
{
//checks if its a textbox, and if it is, is it null or empty
if(this.Controls.OfType<TextBox>().Any(t => string.IsNullOrEmpty(t.Text)))
{
//this means soemthing was in a box
allfieldsempty = false;
break;
}
}
if (allfieldsempty == false)
{
MessageBox.Show("Consider saving.");
}
else //this means nothings new in the form so we can close it
{
Application.Exit();
}
Why is it not finding any text in my text boxes based on the code above?
Sure -- enumerate through your controls looking for text boxes:
foreach (Control c in this.Controls)
{
if (c is TextBox)
{
TextBox textBox = c as TextBox;
if (textBox.Text == string.Empty)
{
// Text box is empty.
// You COULD store information about this textbox is it's tag.
}
}
}
Building on George's answer, but making use of some handy LINQ methods:
if(this.Controls.OfType<TextBox>().Any(t => string.IsNullOrEmpty(t.Text)))
{
//Your textbox is empty
}
public void YourFunction(object sender, EventArgs e) {
string[] txtBoxArr = { textBoxOne.Text, textBoxTwo.Text, textBoxThree.Text };
string[] lblBoxArr = { "textBoxOneLabel", "textBoxTwoLabel", "textBoxThreeLabel" };
TextBox[] arr = { textBoxOne, textBoxTwo, textBoxThree };
for (int i = 0; i < txtBoxArr.Length; i++)
{
if (string.IsNullOrWhiteSpace(txtBoxArr[i]))
{
MessageBox.Show(lblBoxArr[i] + " cannot be empty.");
arr[i].Focus();
return;
}
}
}