I am fixing a c# project that uses a user control named CompleteComboBox.
It functions partly.
The backspace does not work.
The first time the user enters a string into the textbox part, the list opens as it should, and stands on the right line. The second time - the list does not open but it does show the matched line in the textbox part. Then, if i open the list and start writing the beginning of one of the items but then click one of the items and press enter - this item is not selected rather the item that was selected while entering letters in the text box part.
Hope I am understood.
Here is the code:
namespace BestInvest.UserControls
{
public partial class CompleteComboBox : System.Windows.Forms.ComboBox
{
public event System.ComponentModel.CancelEventHandler NotInList;
private bool _limitToList = true;
private bool _inEditMode = false;
bool start;
public CompleteComboBox()
: base()
{
start = true;
this.Text = ClsConsts.InitializeTextForCombo;
}
[Category("Behavior")]
public bool LimitToList
{
get { return _limitToList; }
set { _limitToList = value; }
}
protected virtual void OnNotInList(System.ComponentModel.CancelEventArgs e)
{
if (NotInList != null)
{
NotInList(this, e);
}
}
protected override void OnTextChanged(System.EventArgs e)
{
if (_inEditMode)
{
string input = Text;
int index = FindString(input);
if (index >= 0)
{
_inEditMode = false;
SelectedIndex = index;
_inEditMode = true;
Select(input.Length, Text.Length);
//base.DroppedDown = true;
}
}
base.OnTextChanged(e);
}
protected override void OnValidating(System.ComponentModel.CancelEventArgs e)
{
if (this.LimitToList)
{
int pos = this.FindStringExact(this.Text);
if (pos == -1)
{
OnNotInList(e);
}
else
{
this.SelectedIndex = pos;
}
}
base.OnValidating(e);
}
protected override void OnKeyDown(System.Windows.Forms.KeyEventArgs e)
{
if (start)
{
base.DroppedDown = true;
//else
start = false;
}
if (e.KeyCode == Keys.Enter)
{
base.DroppedDown = false;
start = true;
}
_inEditMode = true;
base.OnKeyDown(e);
}
}
}
Your code is messy. Look at the start flag. It is not set to true in all cases you would expect. That's why first time behavior is different from second time.
Your OnKeyDown event will not trigger on all keys you would expect. That's why backspace doesn't work. There are several posts about detecting backspace out there: See for example this one
Related
I made a textbox that only accepts numbers and a "-" for negative numbers. I would like there to be an option to disable negative numbers.
In the constructor method I want to reference the allowNegatives bool that is defined properties editor and do different things depending on if it allows negatives values. I'm running into the problem that the 'allowNegatives' bool is always its default value in the constructor. If I reference it elsewhere it is the correct value.
Is there an way to get the assigned property value rather than the default value in the constructor?
public partial class ControlIntEntry : TextBox
{
private bool allowNegatives = false;
[Description("Allow negative values"), Category("Behavior")]
public bool AllowNegatives
{
get { return allowNegatives; }
set { allowNegatives = value; }
}
public ControlIntEntry()
{
// user sets AllowNegatives to true using properties editor
InitializeComponent();
Console.WriteLine(allowNegatives); // returns false
if (allowNegatives)
{
//do one thing
}
else
{
// do something else.
}
Task.Run(() => AfterConstructor()); // use for testing
}
private async Task AfterConstructor()
{
await Task.Delay(1000);
Console.WriteLine(allowNegatives); //returns true
}
}
Before you can assign a value to an instance property, the class should be instantiated, so first constructor will run and then you can assign property values.
That said, to have a better understanding of what is happening here, when you drop an instance of a control on your form at design time and set some of its properties, designer will generate a code like this:
private void InitializeComponent()
{
...
this.myControl1 = new MyControl();
...
//
// myControl1
//
this.myControl1.Location = new System.Drawing.Point(0, 0);
this.myControl1.Name = "myControl1";
this.myControl1.Size = new System.Drawing.Size(100, 22);
this.myControl1.MyProperty = true;
...
}
I believe it's now clear that what is happening here. You see first the constructor of your control will run, then later property values will be set.
To use property values to configure your object can put the logic inside the setter of the property:
private bool myProperty = false;
public bool MyProperty
{
get { return myProperty;}
set
{
myProperty = value;
// some logic here.
}
}
It's the most common scenario.
Another option is delaying the initializations to some time later, for example when the control handle is created by overriding OnHandleCreated or another suitable time.
// This is just an example, the event may not be a good one for your requirement
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
// some logic here
}
Another option for complex initialization scenarios which may involve multiple properties, you can implement ISupportInitialize and put the logic inside EndInit:
public class MyControl : TextBox, ISupportInitialize
{
public void BeginInit()
{
}
public void EndInit()
{
// some logic here
}
}
Then when you drop an instance of the control on the form, this code will be generated in addition to the common code that I showed at beginning of this answer:
...
((System.ComponentModel.ISupportInitialize)(this.myControl1)).EndInit();
this.ResumeLayout(false);
this.PerformLayout();
...
(I expect it's obvious now, that) All above options will run after running the constructor.
Putting that code in the setter worked
public partial class ControlIntEntry : TextBox
{
private bool allowNegatives = false;
[Description("Allow negative values"), Category("Behavior")]
public bool AllowNegatives
{
get { return allowNegatives; }
set
{
allowNegatives = value;
if (allowNegatives)
this.KeyPress += KeyPress_AllowNegatives;
else
this.KeyPress += KeyPress_PositiveOnly;
}
}
public ControlIntEntry()
{
InitializeComponent();
}
private void KeyPress_PositiveOnly(object sender, KeyPressEventArgs e)
{
Char newChar = e.KeyChar;
if (!Char.IsDigit(newChar) && newChar != 8)
{
e.Handled = true;
}
}
private void KeyPress_AllowNegatives(object sender, KeyPressEventArgs e)
{
Char newChar = e.KeyChar;
int cursorIndex = this.SelectionStart;
if (cursorIndex == 0)
{
if (!Char.IsDigit(newChar) && newChar != 8 && newChar != 45)
{
e.Handled = true;
}
}
else
{
if (!Char.IsDigit(newChar) && newChar != 8)
{
e.Handled = true;
}
}
}
}
I am making my own InputManager to remap keys during the game.
The problem is that I have assigned class to each of shortcut's parent and when the button is pressed on the debugger I can see data from another key.
And only this key keeps being changed over and over again.
Here is my example: Up is the parent of button, UpS is button being pressed which call the method from it's parent. In each parent-children it is set the same way as there.
On button press I call
public void ToggleChangeButtonPannel(Button button)
{
this.ActionText.text = Description;
this.CurrentKeyText.text = Name;
if (Panel.enabled)
{
Panel.enabled = false;
}
else
{
Panel.enabled = true;
}
}
And at update I check if panel is visible. I think the problem might be with the specification of Update() method. Does it work on every instance in parallel?
If that is the case - is it possible to use Input.anyKeyDown outside the Update() method?
private void Update()
{
if (!Panel.enabled)
{
return;
}
if (Input.anyKeyDown)
{
var currentKey = Key;
try
{
var newKey = (KeyCode)System.Enum.Parse(typeof(KeyCode), Input.inputString.ToUpper());
if (Shortcuts.TryChangeKeyCode(currentKey, newKey))
{
RenameButtons(newKey);
}
else
{
//Error message handling
}
}
catch
{
//Error message handling
}
}
}
The problem was as I thought with Update() method. I have fixed it by using caroutine.
public void ToggleChangeButtonPannel(Button button)
{
ActionText.text = Description;
CurrentKeyText.text = Name;
if (Panel.enabled)
{
Panel.enabled = false;
}
else
{
Panel.enabled = true;
StartCoroutine(KeyRoutine());
}
EventSystem.current.SetSelectedGameObject(null);
}
IEnumerator KeyRoutine()
{
while (!Panel.enabled || !Input.anyKeyDown)
{
yield return null;
}
try
{
var newKey = (KeyCode)System.Enum.Parse(typeof(KeyCode), Input.inputString.ToUpper());
if (Shortcuts.TryChangeKeyCode(Key, newKey))
{
RenameButtons(newKey);
Key = newKey;
Panel.enabled = false;
}
else
{
StartCoroutine(RemoveAfterSeconds(3, Panel));
}
}
catch
{
StartCoroutine(RemoveAfterSeconds(3, Panel));
}
}
I'm working on a Windows app that has a ListView containing a bunch of items.
When the user clicks on an item, the app displays the item'd details. The
user then has the opportunity to edit these details. The user should click
the Save button after each change, but of course that doesn't always happen.
If the user makes changes and doesn't click Save, the app displays a message
box asking if they'd like to save their changes. This box includes a Cancel
button, and if they click Cancel, I'd like to short-circuit the selection of
the other item and keep the user at the one they were editing.
I can't find a way to do this, I show the dialog from itemselecedchanged event if the item change and not save, if the user click cancel, I remove my function from the event and change manually the selected item and after that I return the function to event, but after this the event call and the item that I manually select is not selected.
private bool EnsureSelected()
{
bool continue_ = true;
if (_objectChange)
{
var res = MessageBox.Show("Do you want to save changes?", "Warning", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Warning);
switch (res)
{
case DialogResult.Cancel:
if (!string.IsNullOrEmpty(_selectedKey))
{
listView_Keys.ItemSelectionChanged -= listView_Keys_ItemSelectionChanged;
listView_Keys.Focus();
listView_Keys.Items[_selectedKey].Selected = true;
listView_Keys.ItemSelectionChanged += listView_Keys_ItemSelectionChanged;
}
continue_ = false;
break;
case DialogResult.Yes:
button_Save.PerformClick();
_objectChange = false;
break;
case DialogResult.No:
_objectChange = false;
break;
default:
throw new ArgumentOutOfRangeException();
}
}
return continue_;
}
UPDATE::
I tried this solution :
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private ListViewItem currentSelection = null;
private bool pending_changes = false;
private void listView1_ItemSelectionChanged(object sender, ListViewItemSelectionChangedEventArgs e)
{
if (e.Item == currentSelection)
{
// if the current Item gets unselected but there are pending changes
if (!e.IsSelected && pending_changes)
{
var res = MessageBox.Show("Do you want to save changes?", "Warning", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Warning);
switch (res)
{
case DialogResult.Cancel:
// we dont want to change the selected item, so keep it selected
e.Item.Selected = true;
break;
case DialogResult.Yes:
//button_Save.PerformClick();
pending_changes = false;
break;
case DialogResult.No:
pending_changes = false;
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
else // not the selected button
{
if (!pending_changes && e.IsSelected)
{
// Item may be selected and we save it as the new current selection
currentSelection = e.Item;
}
else if (pending_changes && e.IsSelected)
{
// Item may not be enabled, because there are pending changes
e.Item.Selected = false;
}
}
}
private void Form1_Load(object sender, EventArgs e)
{
listView1.Items[0].Selected = true;
}
private void button1_Click(object sender, EventArgs e)
{
pending_changes = true;
}
}
but this did not work, the first time that pending changes is true the message box called twice and the second time nothing happened.
First of all, whenever you select another item, the event should fire twice.
first for the Item that got deselected (where e.IsSelected is false)
second for the Item that got selected (where e.IsSelected is true)
Assuming that you have a flag pending_changes that is set, whenever there are unsaved changes, then the folowing code should cancel the item selection.
Unfortunatly whenever you show the MessageBox, the listView loses its focus again. When you click the MessageBox away, the focus is back on the listView and this causes the control to fire its events again. This is why there is a dirty workaround needed, that needs to remember that we clicked "Cancel" on the message box and performs the action on the next event again.
Here is the code including the workaround:
private ListViewItem currentSelection = null;
private bool pending_changes = false;
private bool cancel_flag = false;
private void listView1_ItemSelectionChanged(object sender, ListViewItemSelectionChangedEventArgs e)
{
Console.WriteLine("Item " + e.ItemIndex + " is now " + e.IsSelected);
if (e.Item != currentSelection)
{
// if another item gets selected but there are pending changes
if (e.IsSelected && pending_changes)
{
if (cancel_flag)
{
// this handles the second mysterious event
cancel_flag = false;
currentSelection.Selected = true;
e.Item.Selected = false;
return;
}
Console.WriteLine("uh oh. pending changes.");
var res = MessageBox.Show("Do you want to save changes?", "Warning", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Warning);
switch (res)
{
case DialogResult.Cancel:
// we dont want to change the selected item, so keep it selected
currentSelection.Selected = true;
e.Item.Selected = false;
// for some reason, we will get the same event with the same argments again,
// after we click the cancel button, so remember our decision
cancel_flag = true;
break;
case DialogResult.Yes:
// saving here. if possible without clicking the button, but by calling the method that is called to save
pending_changes = false;
currentSelection = e.Item;
break;
case DialogResult.No:
pending_changes = false;
currentSelection = e.Item;
break;
default:
throw new ArgumentOutOfRangeException();
}
}
else if (e.IsSelected && !pending_changes)
{
currentSelection = e.Item;
}
}
}
You can just reselect items and keep current state in the boolean flags to avoid causing unnecessary code to run (like reloading values for a selected item if it hasn't actually changed).
Another way is to handle LVN_ITEMCHANGING event, which is, unfortunately, isn't implemented in WinForms. You can find an extended list view class which supports this event and allows preventing changing of selection in the ListView item changing event thread on MSDN forums.
Here's the code from that thread:
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;
public partial class Form1 : Form
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
public Form1()
{
ListViewEx listView;
Controls.Add(listView = new ListViewEx { Dock = DockStyle.Fill, Items = { "One", "Two", "Three" } });
listView.ItemSelectionChanging += (s, e) =>
{
if (e.Index == 1)
e.Cancel = true;
Debug.WriteLine(e);
};
}
}
public class ItemSelectionChangingEventArgs : CancelEventArgs
{
public int Index { get; private set; }
public bool NewValue { get; private set; }
public bool CurrentValue { get; private set; }
public ItemSelectionChangingEventArgs(int index, bool newValue, bool currentValue)
{
Index = index;
NewValue = newValue;
CurrentValue = currentValue;
}
public override string ToString()
{
return String.Format("Index={0}, NewValue={1}, CurrentValue={2}", Index, NewValue, CurrentValue);
}
}
public class ListViewEx : ListView
{
private static readonly Object ItemSelectionChangingEvent = new Object();
public event EventHandler<ItemSelectionChangingEventArgs> ItemSelectionChanging
{
add { Events.AddHandler(ItemSelectionChangingEvent, value); }
remove { Events.RemoveHandler(ItemSelectionChangingEvent, value); }
}
protected virtual void OnItemSelectionChanging(ItemSelectionChangingEventArgs e)
{
EventHandler<ItemSelectionChangingEventArgs> handler =
(EventHandler<ItemSelectionChangingEventArgs>)Events[ItemSelectionChangingEvent];
if (handler != null)
handler(this, e);
}
protected override void WndProc(ref Message m)
{
if (m.Msg == 0x2000 + 0x004E) // [reflected] WM_NOTIFY
{
uint nmhdrCode = (uint)Marshal.ReadInt32(m.LParam, NmHdrCodeOffset);
if (nmhdrCode == LVN_ITEMCHANGING)
{
NMLISTVIEW nmlv = (NMLISTVIEW)Marshal.PtrToStructure(m.LParam, typeof(NMLISTVIEW));
if ((nmlv.uChanged & LVIF_STATE) != 0)
{
bool currentSel = (nmlv.uOldState & LVIS_SELECTED) == LVIS_SELECTED;
bool newSel = (nmlv.uNewState & LVIS_SELECTED) == LVIS_SELECTED;
if (newSel != currentSel)
{
ItemSelectionChangingEventArgs e = new ItemSelectionChangingEventArgs(nmlv.iItem, newSel, currentSel);
OnItemSelectionChanging(e);
m.Result = e.Cancel ? (IntPtr)1 : IntPtr.Zero;
return;
}
}
}
}
base.WndProc(ref m);
}
const int LVIF_STATE = 8;
const int LVIS_FOCUSED = 1;
const int LVIS_SELECTED = 2;
const uint LVN_FIRST = unchecked(0U - 100U);
const uint LVN_ITEMCHANGING = unchecked(LVN_FIRST - 0);
const uint LVN_ITEMCHANGED = unchecked(LVN_FIRST - 1);
static readonly int NmHdrCodeOffset = IntPtr.Size * 2;
[StructLayout(LayoutKind.Sequential)]
struct NMHDR
{
public IntPtr hwndFrom;
public IntPtr idFrom;
public uint code;
}
[StructLayout(LayoutKind.Sequential)]
struct NMLISTVIEW
{
public NMHDR hdr;
public int iItem;
public int iSubItem;
public int uNewState;
public int uOldState;
public int uChanged;
public IntPtr lParam;
}
}
I have several comboboxes with the same properties.
Dropdownstyle : Dropdownlist
AutoCompleteMode: SuggestAppend
AutoCompleteSource: ListItems
For example, I have a dropdownlist cboxStates that has 50 states of United States of America in Items Collection entered manually. When I type in WI, it is highlighted among WA,WV,WI,WY but if I tab over/press enter/mouse click on another control, WA is selected instead of WI which is highlighted. This is total random and it happens to comboboxes that are binded dynamically. And also, they do not have any events.
This seems to be an issue that has been submitted to Connect. There's a workaround there which extends the default ComboBox control and fixes the issue. The extended ComboBox code is horribly formatted on the Connect site, so here's the nicer version :)
public class BetterComboBox : ComboBox
{
private int _windows7CorrectedSelectedIndex = -1;
private int? _selectedIndexWhenDroppedDown = null;
protected override void OnDropDown(EventArgs e)
{
_selectedIndexWhenDroppedDown = SelectedIndex;
base.OnDropDown(e);
}
private bool _onDropDownClosedProcessing = false;
protected override void OnDropDownClosed(EventArgs e)
{
if (_selectedIndexWhenDroppedDown != null && _selectedIndexWhenDroppedDown != SelectedIndex)
{
try
{
_onDropDownClosedProcessing = true;
OnSelectionChangeCommitted(e);
}
finally
{
_onDropDownClosedProcessing = false;
}
}
base.OnDropDownClosed(e);
if (SelectedIndex != _windows7CorrectedSelectedIndex)
{
SelectedIndex = _windows7CorrectedSelectedIndex;
OnSelectionChangeCommitted(e);
}
}
protected override void OnSelectionChangeCommitted(EventArgs e)
{
if (!_onDropDownClosedProcessing)
_windows7CorrectedSelectedIndex = SelectedIndex;
_selectedIndexWhenDroppedDown = null;
base.OnSelectionChangeCommitted(e);
}
protected override void OnSelectedIndexChanged(EventArgs e)
{
bool alreadyMatched = true;
if (_windows7CorrectedSelectedIndex != SelectedIndex)
{
_windows7CorrectedSelectedIndex = SelectedIndex;
alreadyMatched = false;
}
base.OnSelectedIndexChanged(e);
//when not dropped down, the SelectionChangeCommitted event does not fire upon non-arrow keystrokes due (I suppose) to AutoComplete behavior
//this is not acceptable for my needs, and so I have come up with the best way to determine when to raise the event, without causing duplication of the event (alreadyMatched)
//and without causing the event to fire when programmatic changes cause SelectedIndexChanged to be raised (_processingKeyEventArgs implies user-caused)
if (!DroppedDown && !alreadyMatched && _processingKeyEventArgs)
OnSelectionChangeCommitted(e);
}
private bool _processingKeyEventArgs = false;
protected override bool ProcessKeyEventArgs(ref Message m)
{
try
{
_processingKeyEventArgs = true;
return base.ProcessKeyEventArgs(ref m);
}
finally
{
_processingKeyEventArgs = false;
}
}
}
I'm currently working on a DataGrid which in some conditions should disable or enable particular columns by changing IsReadOnly to true and vice versa.
I attached to CurrentCellChanged and CellEditEnded events in which I change the column IsReadOnly property.
I expect the application to disable / enable edit on that column.
Even though the column has IsReadOnly set to true sometimes it does allow edits.
I've also tried to call CancelEdit(); on a grid but that didn't make any effect either.
If you request I can post code but I'm pretty sure the logic is fine, I checked it like thousands of times in debug ;).
The entire idea is nothing more than changing IsReadOnly of particular column in event.
Any idea what why it's not working as I expect?
Edit1.
Code added.
private void SrfDataGrid_CurrentCellChanged(object sender, EventArgs e)
{
CellCoordinates cellCoordinates = this.GetEditedCellCoordinates();
if (!this.LockDataGridCell(cellCoordinates))
{
if (!Keyboard.Modifiers.HasFlag(ModifierKeys.Control) && !Keyboard.Modifiers.HasFlag(ModifierKeys.Shift))
this.srfDataGrid.BeginEdit();
}
else
{
this.srfDataGrid.CancelEdit();
}
}
private void SrfDataGrid_CellEditEnded(object sender, DataGridCellEditEndedEventArgs e)
{
CellCoordinates cellCoordinates = this.GetEditedCellCoordinates();
this.SetCellsRowInfluence(cellCoordinates);
this.UnlockDataGridCell(cellCoordinates);
}
public bool LockDataGridCell(CellCoordinates cellCoordinates)
{
bool result = false;
if (cellCoordinates != null)
{
DataGridColumn currentColumn = this.srfDataGrid.CurrentColumn;
if (this.spreadSheetCellState[cellCoordinates.ColumnName, cellCoordinates.RowID].Equals(CurrentCellState.WRITE))
{
currentColumn.IsReadOnly = false;
}
else
{
currentColumn.IsReadOnly = true;
}
result = currentColumn.IsReadOnly;
}
return result;
}
public void UnlockDataGridCell(CellCoordinates cellCoordinates)
{
if (cellCoordinates != null)
{
DataGridColumn currentColumn = this.srfDataGrid.CurrentColumn;
if (this.spreadSheetCellState[cellCoordinates.ColumnName, cellCoordinates.RowID].Equals(CurrentCellState.ALWAYS_READ_ONLY))
{
currentColumn.IsReadOnly = true;
}
else
{
currentColumn.IsReadOnly = false;
}
}
}
Try this:
foreach (DataGridColumn col in dataGrid1.Columns)
{
if (col.GetType() == typeof(DataGridTextColumn))
{
col.IsReadOnly = true;
}
else
{
col.IsReadOnly = false;
}
}