Creating Replacable Placeholder Text in WPF ComboBox - c#

Say I have a combobox, where the contents are going to be some function options that I'd like to be able to fill out with parameters. For instance, the options might be
[ComboBox option menu drops down to show the following text options]
Foo(int x)
Bar(int y, int z)
HelloWorld(string q)
When any of these options are selected by the user, the combobox will close the options menu (as normal) and show the selected option. However, I'd like the parameter portion to be a sort of 'pre-highlighted' block of text, which upon clicking gives you focus of that highlight so you can immediately over-write it with your parameter choice.

Cheap'n'cheeesy, and it doesn't dream of handling Bradley's case of multiple parameters: It just selects everything between the parens. But it handles the cases you listed.
In real life you'd want to set up this event with an attached behavior, of course.
private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var cb = (ComboBox)sender;
var item = cb.SelectedItem as SelectMethodCallItem;
// This event is raised when user alters the text, but
// SelectedItem will be null in that case.
if (item != null && item.HasSelection)
{
var edit = (TextBox)cb.Template.FindName("PART_EditableTextBox", cb);
Action setsel = () =>
{
edit.SelectionStart = item.SelStart;
edit.SelectionLength = item.SelLEngth;
};
// the BeginInvoke/application idle gimmick is so it happens
// after this event is over with, so the change we make isn't stepped on
App.Current.Dispatcher.BeginInvoke(
System.Windows.Threading.DispatcherPriority.ApplicationIdle, setsel);
}
}
In real life, you could do something much more clever and robust to identify the replaceable parameter text.
public class SelectMethodCallItem
{
public SelectMethodCallItem(String text)
{
Text = text;
SelStart = text.IndexOf('(');
SelEnd = text.IndexOf(')');
if (SelStart > -1 && SelEnd > -1)
{
++SelStart;
++SelEnd;
}
else
{
SelStart = SelEnd = -1;
}
}
public String Text { get; set; }
public int SelStart { get; private set; }
public int SelEnd { get; private set; }
public int SelLEngth => (SelEnd - SelStart) - 1;
public bool HasSelection => SelStart > -1 && SelEnd > -1;
}
XAML
<ComboBox
IsEditable="True"
ItemsSource="{Binding CBItems}"
SelectionChanged="ComboBox_SelectionChanged"
DisplayMemberPath="Text"
/>
Code behind
public Form()
{
InitializeComponent();
DataContext = new
{
CBItems = new[] {
new SelectMethodCallItem("sin(float x)"),
new SelectMethodCallItem("cos(float x)"),
new SelectMethodCallItem("foobar(string s)"),
}
};
}

Related

How to show a spelling error suggestions popup on mouse over a WPF TextBox with spell checking enabled in Winforms C#?

I am using element host to use WPF spell checker textbox in my winforms.
I want to override the context menu that appears on misspelled red squiggles to mouse hover instead of right click.
How to do that?
Tried overriding the behavior but it is still the same:
using System;
using System.ComponentModel;
using System.ComponentModel.Design.Serialization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Forms.Integration;
using System.Windows.Forms.Design;
[Designer(typeof(ControlDesigner))]
class SpellCheck: ElementHost
{
privated TextBox box;
public SpellCheck()
{
box = new TextBox();
base.Child = box;
box.TextChanged += (s, e) => OnTextChanged(EventArgs.Empty);
box.SpellCheck.IsEnabled = true;
box.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
}
[DefaultValue(false)]
public bool Multiline
{
//checks for multiline
}
public bool IsEnabled
{
//checks for spell check enabled or not
}
[DefaultValue(false)]
public bool WordWrap
{
//does wordwraps
}
[DefaultValue(false)]
public int MaxLength
{
//maxlength property
}
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public new System.Windows.UIElement Child
{
get { return base.Child; }
set { }
}
}
It shows spell suggestions on right click. I want to change it to Mouse hover or any other mouse events.
You could use TextBox.PreviewMouseMove to show a popup to achieve this.
You also need to create a new Form to serve as a popup. This Form could contain a FlowLayoutPanel to host the suggestions. In this example the Form is named CustomPopup:
box.PreviewMouseMove += OpenContextMenuOnMouseMove;
The event handler for TextBox.PreviewMouseMove:
private void OpenContextMenuOnMouseMove(object sender, System.Windows.Input.MouseEventArgs mouseEventArgs)
{
var textBox = sender as System.Windows.Controls.TextBox;
var mouseOverTextIndex = textBox.GetCharacterIndexFromPoint(mouseEventArgs.GetPosition(textBox), false);
// Pointer is not over text
if (mouseOverTextIndex.Equals(-1))
{
return;
}
int spellingErrorIndex = textBox.GetNextSpellingErrorCharacterIndex(mouseOverTextIndex, LogicalDirection.Forward);
// No spelling errors
if (spellingErrorIndex.Equals(-1))
{
return;
}
int startOfWordIndex = mouseOverTextIndex;
while (startOfWordIndex != -1 && !textBox.Text[startOfWordIndex].Equals(' '))
{
startOfWordIndex--;
}
var endOfWordIndex = textBox.Text.IndexOf(" ", mouseOverTextIndex, StringComparison.OrdinalIgnoreCase);
if (endOfWordIndex.Equals(-1))
{
endOfWordIndex = textBox.Text.Length - 1;
}
// Spelling error doesn't belong to current mouse over word
if (spellingErrorIndex < startOfWordIndex || spellingErrorIndex > endOfWordIndex)
{
return;
}
using(CustomPopup popup = new CustomPopup())
{
// Create clickable suggestions for the CustomPopup.
foreach (var suggestion in textBox.GetSpellingError(spellingErrorIndex).Suggestions)
{
// Each Button represents a suggestion.
var suggestionButton = new System.Windows.Forms.Button() { Text = suggestion };
var fixSpellingErrorEventArgs = new FixSpellingErrorEventArgs()
{
TargetTextBox = textBox,
WordStartIndex = startOfWordIndex,
WordEndIndex = endOfWordIndex,
Suggestion = suggestion
};
// The Button.Click callback will apply the selected fix
suggestionButton.Click += (s, e) => FixSpellingError_OnButtonClicked(fixSpellingErrorEventArgs);
// TODO::Replace the line with a public member of CustomPopup Form: CustomPopup.AddPanelContent(Control):void.
// e.g. public void AddPanelContent(Control control) { this.FlowLayoutPanel1.Controls.Add(suggestionButton); }
// and use it like: popup.AddPanelContent(suggestionButton);
popup.FlowLayoutPanel1.Controls.Add(suggestionButton);
}
popup.SetDesktopLocation((int) mouseEventArgs.GetPosition(textBox).X, (int) mouseEventArgs.GetPosition(textBox).Y);
popup.ShowDialog(this);
}
}
// The event handler that applies the selected fix.
// Invoked on popup button clicked.
private void FixSpellingError_OnButtonClicked(FixSpellingErrorEventArgs e)
{
// Select misspelled word and replace it with the selected fix
e.TargetTextBox.SelectionStart = e.WordStartIndex;
e.TargetTextBox.SelectionLength = e.WordEndIndex - e.WordStartIndex;
e.TargetTextBox.SelectedText = e.Suggestion;
}
The custom event arg object for Button.Click event
class FixSpellingErrorEventArgs : EventArgs
{
public System.Windows.Controls.TextBox TargetTextBox { get; set; }
public int WordStartIndex { get; set; }
public int WordEndIndex { get; set; }
public string Suggestion { get; set; }
}
To enhance the example create the logic how or when the popup will disappear (time out and focus lost?).

Can't Get/set combobox's SelectedValue if its data source is null

I'm trying to add a contains-like autocomplete to winforms combobox. I've started with Hovhannes Hakobyan's idea from this thread. I had to adjust it a bit because autocomplete didn't know where to search.
Let me start by describing my setup:
I have a 'Part' class and the combobox is to display its 'Name' property (DisplayMember). 'Name' is also where autocomplete should search for items containing given string:
public class Part
{
public int PartId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
In form's code-behind, I'm creating new AutoCompleteBehavior object, that will handle all events for me, and I'm passing the combobox and the list of objects. Although I'm refering to 'Part' class here, I'm trying to build general soltution so I'm using generics where possible:
new AutoCompleteBehavior<Part>(this.cmbPart, parts.Items);
cmbPart.DisplayMember = "Name";
cmbPart.ValueMember = "PartId";
Below is complete AutoCompleteBehavior class:
public class AutoCompleteBehavior<T>
{
private readonly ComboBox comboBox;
private string previousSearchterm;
private T[] originalList;
public AutoCompleteBehavior(ComboBox comboBox, List<T>Items)
{
this.comboBox = comboBox;
this.comboBox.AutoCompleteMode = AutoCompleteMode.Suggest; // crucial otherwise exceptions occur when the user types in text which is not found in the autocompletion list
this.comboBox.TextChanged += this.OnTextChanged;
this.comboBox.KeyPress += this.OnKeyPress;
this.comboBox.SelectionChangeCommitted += this.OnSelectionChangeCommitted;
object[] items = Items.Cast<object>().ToArray();
this.comboBox.DataSource = null;
this.comboBox.Items.AddRange(items);
}
private void OnSelectionChangeCommitted(object sender, EventArgs e)
{
if (this.comboBox.SelectedItem == null)
{
return;
}
var sel = this.comboBox.SelectedItem;
this.ResetCompletionList();
comboBox.SelectedItem = sel;
}
private void OnTextChanged(object sender, EventArgs e)
{
if (!string.IsNullOrEmpty(this.comboBox.Text) || !this.comboBox.Visible || !this.comboBox.Enabled)
{
return;
}
this.ResetCompletionList();
}
private void OnKeyPress(object sender, KeyPressEventArgs e)
{
if (e.KeyChar == '\r' || e.KeyChar == '\n')
{
e.Handled = true;
if (this.comboBox.SelectedIndex == -1 && this.comboBox.Items.Count > 0
&& this.comboBox.Items[0].ToString().ToLowerInvariant().StartsWith(this.comboBox.Text.ToLowerInvariant()))
{
this.comboBox.Text = this.comboBox.Items[0].ToString();
}
this.comboBox.DroppedDown = false;
// Guardclause when detecting any enter keypresses to avoid a glitch which was selecting an item by means of down arrow key followed by enter to wipe out the text within
return;
}
// Its crucial that we use begininvoke because we need the changes to sink into the textfield Omitting begininvoke would cause the searchterm to lag behind by one character That is the last character that got typed in
this.comboBox.BeginInvoke(new Action(this.ReevaluateCompletionList));
}
private void ResetCompletionList()
{
this.previousSearchterm = null;
try
{
this.comboBox.SuspendLayout();
if (this.originalList == null)
{
this.originalList = this.comboBox.Items.Cast<T>().ToArray();
}
if (this.comboBox.Items.Count == this.originalList.Length)
{
return;
}
while (this.comboBox.Items.Count > 0)
{
this.comboBox.Items.RemoveAt(0);
}
this.comboBox.Items.AddRange(this.originalList.Cast<object>().ToArray());
}
finally
{
this.comboBox.ResumeLayout(true);
}
}
private void ReevaluateCompletionList()
{
var currentSearchterm = this.comboBox.Text.ToLowerInvariant();
if (currentSearchterm == this.previousSearchterm)
{
return;
}
this.previousSearchterm = currentSearchterm;
try
{
this.comboBox.SuspendLayout();
if (this.originalList == null)
{
this.originalList = this.comboBox.Items.Cast<T>().ToArray(); // backup original list
}
T[] newList;
if (string.IsNullOrEmpty(currentSearchterm))
{
if (this.comboBox.Items.Count == this.originalList.Length)
{
return;
}
newList = this.originalList;
}
else
{
newList = this.originalList.Where($"{comboBox.DisplayMember}.Contains(#0)", currentSearchterm).ToArray();
//newList = this.originalList.Where(x => x.ToString().ToLowerInvariant().Contains(currentSearchterm)).ToArray();
}
try
{
// clear list by loop through it otherwise the cursor would move to the beginning of the textbox
while (this.comboBox.Items.Count > 0)
{
this.comboBox.Items.RemoveAt(0);
}
}
catch
{
try
{
this.comboBox.Items.Clear();
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
this.comboBox.Items.AddRange(newList.Cast<object>().ToArray()); // reset list
}
finally
{
if (currentSearchterm.Length >= 1 && !this.comboBox.DroppedDown)
{
this.comboBox.DroppedDown = true; // if the current searchterm is empty we leave the dropdown list to whatever state it already had
Cursor.Current = Cursors.Default; // workaround for the fact the cursor disappears due to droppeddown=true This is a known bu.g plaguing combobox which microsoft denies to fix for years now
this.comboBox.Text = currentSearchterm; // Another workaround for a glitch which causes all text to be selected when there is a matching entry which starts with the exact text being typed in
this.comboBox.Select(currentSearchterm.Length, 0);
}
this.comboBox.ResumeLayout(true);
}
}
}
Now, the autocomplete is KIND OF working - it seeks for items containing given string and does it well. The problem is, though, that for some reason combobox's SelectedValue==null and SelectedText="" after an item has been selected in combobox. At the same time SelectedItem contains proper 'Part' object and SelectedIndex also has proper value...
Unfortunately when I'm setting combobox.SelectedValue to some value when I'm filling in the form, none item is selected in the combobox. Also, when I'm trying to get combobox.SelectedValue, it also says null (even though an item is selected). I even tried to manually set SelectedValue based on SelectedItem, but I can't set it (it's still null):
private void OnSelectionChangeCommitted(object sender, EventArgs e)
{
if (this.comboBox.SelectedItem == null)
{
return;
}
var sel = this.comboBox.SelectedItem;
this.ResetCompletionList();
comboBox.SelectedItem = sel;
string valueName = comboBox.ValueMember;
comboBox.ValueMember = "";
comboBox.SelectedValue = typeof(T).GetProperty(valueName).GetValue(sel);
}
I think that maybe it's because I'm not using combobox.DataSource property that I can't set/get SelectedValue/SelectedText, but I might be wrong here. Any ideas are welcome! :)
Setting the combobox style to ComboBoxStyle.DropDownList always returns "" (empty string) as SelectedText (reference source)
public string SelectedText
{
get
{
if (DropDownStyle == ComboBoxStyle.DropDownList)
return "";
return Text.Substring(SelectionStart, SelectionLength);
}
{
// see link
}
}
SelectedValue is a member inherited from ListControl and requires the data to be managed (reference source).
public object SelectedValue {
get
{
if (SelectedIndex != -1 && dataManager != null )
{
object currentItem = dataManager[SelectedIndex];
object filteredItem = FilterItemOnProperty(currentItem, valueMember.BindingField);
return filteredItem;
}
return null;
}
set
{
// see link
}
I managed to get it working with using extension methods and reflection. It's working well, although I'm still hoping to find better solution. I've created extension class:
using System.Linq.Dynamic;
namespace JDE_Scanner_Desktop.Static
{
static class Extensions
{
public static int GetSelectedValue<T>(this ComboBox combobox)
{
return (int)typeof(T).GetProperty(combobox.ValueMember).GetValue(combobox.SelectedItem);
}
public static void SetSelectedValue<T>(this ComboBox combobox, int? selectedValue)
{
if(selectedValue != null)
{
combobox.SelectedItem = combobox.Items.Cast<T>().Where(combobox.ValueMember + $"={selectedValue}").FirstOrDefault();
}
}
}
}
Then I'm setting item to be selected with cmbPart.SetSelectedValue<Part>(this.PartId); and I'm getting selcted item's SelectedValue with cmbPart.GetSelectedValue<Part>();.
Of course I'm open for other solutions!

Edit text in Listview Custom Adapter Loses its position while scrolling? - c# - Xamarin.Android

When we enter value in row 1 the value entered in row 1 is appearing back in row 6 when we scroll to the row 6. Please see the below code and advice.
namespace Kites
{
public class Marks
{
// add any if you need more
public string StudentName { get; set; }
public string MarksScored { get; set; }
}
public class TEXTCHECK
{
public int POS { get; set; }
public string Value { get; set; }
}
public class MarksListViewAdapter : BaseAdapter<Marks>
{
private List<Marks> mstuduentmarks;
private List<TEXTCHECK> abc = new List<TEXTCHECK>();
private Context mcontext;
public MarksListViewAdapter (Context context, List<Marks> stud)
{
mstuduentmarks = stud;
mcontext = context;
}
public override int Count
{
get
{
return mstuduentmarks.Count;
// return mattendence.Count;
}
}
public override long GetItemId (int position)
{
return position;
}
public override Marks this[int position]
{
get
{
return mstuduentmarks [position];
// return mattendence [position];
}
}
class ViewHolder : Java.Lang.Object
{
public EditText comsevin;
public TextView namenmn;
}
public override View GetView (int position, View convertView, ViewGroup parent)
{
ViewHolder holder;
View view = convertView;
if (view == null) // otherwise create a new one
{
view = LayoutInflater.From(mcontext).Inflate(Resource.Layout.listview_Marks, null, false);
holder = new ViewHolder();
holder.comsevin = view.FindViewById<EditText>(Resource.Id.editTextTeacherMarks);
holder.namenmn = view.FindViewById<TextView>(Resource.Id.textStudentNameTeacherMarks);
holder.namenmn.Tag = position;
view.Tag = holder;
}
else
{
holder = (ViewHolder)view.Tag;
}
holder.namenmn.Text = mstuduentmarks[position].StudentName;
int pos = (int)holder.namenmn.Tag;
holder.comsevin.TextChanged += (sender, e) =>
{
abc[pos].Value = holder.comsevin.Text;
};
//TextView txtStudent =
//txtStudent.Text = mstuduentmarks[position].StudentName;
//txtMarks.FocusChange += (object sender, View.FocusChangeEventArgs e) =>
//{
// //txtMarks.RequestFocusFromTouch ();
// mstuduentmarks[position].MarksScored = txtMarks.Text;
//};
holder.comsevin.BeforeTextChanged += (sender, e) =>
{
abc.Add(new TEXTCHECK { POS = position, Value = mstuduentmarks[position].MarksScored });
};
holder.comsevin.AfterTextChanged += (sender, e) =>
{
int a = abc[pos].POS;
mstuduentmarks[pos].MarksScored = abc[pos].Value;
};
//txtMarks.Tag = position;
//txtMarks.TextChanged += TxtMarks_TextChanged;
return view;
}
//void TxtMarks_TextChanged (object sender, Android.Text.TextChangedEventArgs e)
//{
// EditText txtMarks = (EditText)sender;
// //var position = (int)txtMarks.Tag;
//}
}
}
When we enter value in row 1 the value entered in row 1 is appearing back in row 6 when we scroll to the row 6. Please see the below code and advice.
As a rule of thumb, when experiencing lists that don't reflect the dataset (experiencing item repetition for example) in listview / recyclerview it means that you're either using dirty views which were previously used and then uncorrectly Re-Bound, or simply using wrong positions during bind
I see where you are getting it wrong:
if (view == null) // otherwise create a new one
{
view = LayoutInflater.From(mcontext).Inflate(Resource.Layout.listview_Marks, null, false);
holder = new ViewHolder();
holder.comsevin = view.FindViewById<EditText>(Resource.Id.editTextTeacherMarks);
holder.namenmn = view.FindViewById<TextView>(Resource.Id.textStudentNameTeacherMarks);
holder.namenmn.Tag = position;//<------------here!!!
view.Tag = holder;
}
TLDR Don't save positions this way.
Whats happening: this instance of your view is being reused by listView, meaning that sometimes (many times) if (view == null) will be false and this means Tag property will not be updated for row 6 (or any other calls that will use recycled Views) and you are in fact using a dirty value.
You are then trying to use the Tag property as position, but forgetting this tag is already dirty if the view was recycled
int pos = (int)holder.namenmn.Tag;
holder.comsevin.TextChanged += (sender, e) =>
{
abc[pos].Value = holder.comsevin.Text;
};
Since you have access to the position in this method call you should use it directly
take a look at this guide from Java Code geeks even though it's in Java you will be able to see a good implementation of the old ViewHolder/ListView pattern.
Hope this helps

Change bound Property when programmatically changing SelectedIndex

I've set up a simple form. A ListBox takes values from a list in the 'business object' displaying the Name property and providing the Value property.
In additon the ListBox's SelectedItem property is bound to a property in the same business object.
Using the UI to select a value from the list correctly changes the objects property (checked when the button is clicked) and the correct value is available. So far so good.
However, if the ListBox's SelectedIndex property is changed in the code, then the UI correctly changes as expected but the business property does not change - it would appear to have missed the change event. This is true for both setting in the constructor and in the button event handler (see the code).
What have I missed or what am I doing incorrectly.
(I've only included the code I've written - not VS wizard generated stuff)
class Frequency
{
public String Name { get; set; }
public Int16 Value { get; set; }
public Frequency(String name, Int16 value)
{
Name = name;
Value = value;
}
}
class FrequencyList : System.ComponentModel.BindingList<Frequency>
{
}
class Model
{
public static FrequencyList FrequencyValues = new FrequencyList()
{
new Frequency("Slowest", 100),
new Frequency("Slow", 150),
new Frequency("Medium", 1000),
new Frequency("Fast", 5500),
new Frequency("Fastest", 10000)
};
public Frequency StartFrequency { get; set; }
public void DoStuff()
{
if (StartFrequency == null)
return;
Int16 freq = StartFrequency.Value;
}
}
public partial class Form1 : Form
{
private Model myModel = new Model();
public Form1()
{
InitializeComponent();
// Bind the list to a copy of the static model data
this.listBox1.DataSource = Model.FrequencyValues;
// Bind the control to the model value
this.listBox1.DataBindings.Add("SelectedItem", myModel, "StartFrequency");
// Select the start value
this.listBox1.SelectedIndex = 3;
}
private void button1_Click(object sender, EventArgs e)
{
Int16 f = (Int16)listBox1.SelectedValue;
this.myModel.DoStuff();
int new_index = listBox1.SelectedIndex + 1;
if (new_index >= listBox1.Items.Count)
new_index = 0;
listBox1.SelectedIndex = new_index;
}
}
You don't want the Click event, you want the SelectedIndexChanged event. This will trigger regardless of whether the user or the program instigates the change.

update a listbox item

i want from my program to select a listbox item and then update it.i have a list ecoItems.Eco is a class with 2 store variables,one string and one double variable.SetEcoValues is a set Method take two values,one string and one double.i try this code but don't change anything.any suggestions?
private void Update_Click(object sender, EventArgs e)
{
Eco y;
y = ecoItems.ElementAt<Eco>(listBox1.SelectedIndex);
y.SetEcoValues(textBox1.Text,Convert.ToDouble(textBox2.Text));
listBox5.Items.Insert(listBox1.SelectedIndex, y);
}
}
Using your code and what I would guess is your class, I'd do something like this:
class Eco {
public Eco() { }
public void SetEcoValues(string text, double value) {
Text = text;
Value = value;
}
public string Text { get; set; }
public double Value { get; set; }
public override string ToString() {
if (!String.IsNullOrEmpty(Text)) {
return Text;
}
return base.ToString();
}
}
ListView listView1; // initialized somewhere, I presume.
void Update_Click(object sender, EventArgs e) {
if ((listView1.SelectedItems != null) || (0 < listView1.SelectedItems.Count)) {
ListViewItem item = listView1.SelectedItems[0];
Eco y = item.Tag as Eco;
if (y == null) {
y = new Eco();
}
y.SetEcoValues(textBox1.Text, Convert.ToDouble(textBox2.Text));
item.Text = y.Text;
if (item.SubItems.Count < 2) {
item.SubItems.Add(y.Value.ToString());
} else {
item.SubItems[1].Text = y.Value.ToString();
}
item.Tag = y;
}
}
You're not actually getting the ListItem anywhere, and trying to add something to the ListBox which isn't a ListItem. You could try something like so:
ListItem Item = listBox1.SelectedItem;
//Update the Text and Values
Item.Text = textBox1.Text,;
Item.Value = textBox2.Text;
Or... if you have the ListBox Bound to your list of Ecos and want it updated, instead of listBox5.Items.Insert... you would need to re bind it.
listBox5.DataSource = y;
listBox5.DataBind();

Categories

Resources