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;
}
}
Related
I have a winforms treeview with nodes added and check state set programmatically based on the database values. I am trying to prevent users from altering the check status and am having trouble. I am not sure what event to fire to keep the check state unaltered.
Below is my code:
private void BuildRolesTree(int ParentID, TreeNode pNode, DataSet SourceDS)
{
DataView dvwData = new DataView(SourceDS.Tables[0]);
dvwData.RowFilter = "[parent_id] = " + ParentID;
if (this.InvokeRequired)
{
BuildReportTreeDelegate d = new BuildReportTreeDelegate(BuildRolesTree);
this.Invoke(d, new object[] { ParentID, pNode, SourceDS });
}
else
{
foreach (DataRowView Row in dvwData)
{
TreeNode zNode;
if (pNode == null)
zNode = tv_Permissions.Nodes.Add(Row["node_id"].ToString(), Row["display_access_description"].ToString().Trim());
else zNode = pNode.Nodes.Add(Row["node_id"].ToString(), Row["display_access_description"].ToString().Trim());
if (Convert.ToInt32(Row["is_selected"]) == 1)
zNode.Checked = true;
else if (Convert.ToInt32(Row["is_selected"]) == 0)
zNode.Checked = false;
BuildRolesTree(Convert.ToInt32(Row["node_id"].ToString()), zNode, SourceDS);
}
}
}
private void PermissionsNode_AfterCheck(object sender, TreeViewEventArgs e)
{
if(e.Action != TreeViewAction.Unknown)
{
if(e.Node.Checked) //leave it checked
e.Node.Checked = e.Node.Checked????
//I am looking for something like the below
//e.Checked.NewValue = e.Checked.CurrentValue;
}
}
}
ANy help is appreciated.
You can handle BeforeCheck and set e.Cancel = true to prevent changing the check:
private void treeView1_BeforeCheck(object sender, TreeViewCancelEventArgs e)
{
e.Cancel = true;
}
However, there is a problem in TreeView when double click on CheckBoxes the check events not work as expected. Follow the solution that I've shared in the linked post or use the following ExTreeView which has a CheckBoxEdit property (similar to LabelEdit) which allows you enable or disable checking on CheckBoxes:
public class ExTreeView : TreeView
{
private const int WM_LBUTTONDBLCLK = 0x0203;
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_LBUTTONDBLCLK)
{
var info = this.HitTest(PointToClient(Cursor.Position));
if (info.Location == TreeViewHitTestLocations.StateImage)
{
m.Result = IntPtr.Zero;
return;
}
}
base.WndProc(ref m);
}
[DefaultValue(true)]
public bool CheckBoxEdit { get; set; } = true;
protected override void OnBeforeCheck(TreeViewCancelEventArgs e)
{
base.OnBeforeCheck(e);
e.Cancel = !CheckBoxEdit;
}
}
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
I have a custom class of a button and when I trigger PerformClick for any of my custom buttons, nothing happens. This is the code:
Declaration of my custom class
public class NonFocusButton : Button
{
public NonFocusButton()
{
SetStyle(ControlStyles.Selectable, false);
}
}
List<NonFocusButton> buttons = new List<NonFocusButton>();
This is the p function:
void p()
{
for (int i = 1; i <= 5; i++)
{
NonFocusButton aux = new NonFocusButton();
aux.Font = new System.Drawing.Font("Britannic Bold", 15.75F,
System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point,
((byte)(0)));
aux.Size = new System.Drawing.Size(192, 43);
aux.UseVisualStyleBackColor = true;
aux.UseWaitCursor = false;
aux.Visible = false;
buttons.Add(aux);
this.Controls.Add(aux);
}
// button start
buttons[0].Location = new System.Drawing.Point(410, 168);
buttons[0].Text = "START GAME";
buttons[0].Click += new System.EventHandler(this.button0_Click);
}
private void button0_Click(object sender, EventArgs e)
{
this.Close();
}
buttons[0].PerformClick(); // will not work
How are buttons declared and filled? This is how I have it, and it works.
// declaration
List<Button> butons = new List<Button>();
// calling
buttons.Add(new Button());
p();
buttons[0].PerformClick();
Edit:
The button has to get focus before it can be clicked.
Why not do something like:
button0_Click(buttons[0], EventArgs.Empty);
or just call Close() from wherever you are calling PerformClick().
The Button PerformClick source code is:
public void PerformClick() {
if (CanSelect) {
bool validatedControlAllowsFocusChange;
bool validate = ValidateActiveControl(out validatedControlAllowsFocusChange);
if (!ValidationCancelled && (validate || validatedControlAllowsFocusChange))
{
//Paint in raised state...
//
ResetFlagsandPaint();
OnClick(EventArgs.Empty);
}
}
}
And CanSelect:
public bool CanSelect {
// We implement this to allow only AxHost to override canSelectCore, but still
// expose the method publicly
//
get {
return CanSelectCore();
}
}
internal virtual bool CanSelectCore() {
if ((controlStyle & ControlStyles.Selectable) != ControlStyles.Selectable) {
return false;
}
for (Control ctl = this; ctl != null; ctl = ctl.parent) {
if (!ctl.Enabled || !ctl.Visible) {
return false;
}
}
return true;
}
My guess as to the restriction is the Selectable flag was used instead of adding another control flag such as AllowsPerformClick.
Instead of using PerformClick, you could use reflection, e.g.
MethodInfo methodOnClick = typeof(Button).GetMethod("OnClick", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
// and then...
methodOnClick.Invoke(myButton, new Object[] { EventArgs.Empty });
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;
}
}
}
The functionality I'm after is a cross between a check list box and a list box in multi selection mode.
For list box items A and B
A then B results in a single selection that moves from A to B.
A then control-click B results in a multi-selection of A and B.
(What I want is):
A then A results in an A toggling on and off.
I thought this would be easy but I can't figure it out. Maybe I'm missing something obvious or maybe I'm thinking the wrong way and nobody really wants a listbox whos items toggle on/off.
If you set SelectionMode to MultiSimple this gets you the control-click multi-selection and the toggling on and off.
To get the moving selection to work you could handle the SelectedIndexChanged event and add some logic to de-select the other items if control isn't pressed. If I have more time later I could try to create some basic code for it, but this should be somewhere to start.
You already have the behavior you want if you set the ListBox.SelectionMode to MultiExtended and hold down control when making a selection.
If I understood correctly your problem, you want a ListBox with SelectionMode.One that can toggle selections with CTLR modifier similarly to SelectionMode.MultiSimple and SelectionMode.MultiExtented. Below my answer, just set ToggleSingleSelection to true. Also as a bonus it provides items click events that fires also when clicking already selected items.
using System;
using System.Drawing;
using System.Windows.Forms;
namespace Common
{
public class ListBoxEx : ListBox
{
public event ItemClickedEventHandler ItemClick;
public event ItemClickedEventHandler ItemDoubleClick;
/// <summary>
/// Toggle selections when list has SelectionMode.One
/// </summary>
public bool ToggleSingleSelection { get; set; }
int _PreSelectedIndex = -1;
int _PrevClickedItem = -1;
protected override void OnSelectedIndexChanged(EventArgs e)
{
base.OnSelectedIndexChanged(e);
}
protected override void WndProc(ref Message m)
{
const int WM_LBUTTONDOWN = 0x201;
switch (m.Msg)
{
case WM_LBUTTONDOWN:
// NOTE: Unfortunately SelectedIndex is already setted before OnMouseDown,
// so we must intercept mouse click even before to compare
_PreSelectedIndex = SelectedIndex;
break;
}
base.WndProc(ref m);
}
protected override void OnMouseClick(MouseEventArgs e)
{
base.OnMouseClick(e);
// Reset clicked event, also for double click
_PrevClickedItem = -1;
int selectedIndex = SelectedIndex;
if (selectedIndex != -1)
{
Rectangle selectedItemRectangle = GetItemRectangle(selectedIndex);
if (selectedItemRectangle.Contains(e.Location))
{
_PrevClickedItem = selectedIndex;
// Check when to toggle selection
if (SelectionMode == SelectionMode.One && ToggleSingleSelection && ModifierKeys.HasFlag(Keys.Control)
&& _PreSelectedIndex != -1 && selectedIndex == _PreSelectedIndex)
{
SelectedIndex = -1;
}
if (_PrevClickedItem != -1)
OnItemClick(new ItemClickedEventArgs() { ItemIndex = _PrevClickedItem });
}
}
}
protected override void OnMouseDoubleClick(MouseEventArgs e)
{
base.OnMouseDoubleClick(e);
if (_PrevClickedItem != -1)
OnItemDoubleClick(new ItemClickedEventArgs() { ItemIndex = _PrevClickedItem });
}
protected virtual void OnItemDoubleClick(ItemClickedEventArgs args)
{
ItemDoubleClick?.Invoke(this, args);
}
protected virtual void OnItemClick(ItemClickedEventArgs args)
{
ItemClick?.Invoke(this, args);
}
}
public class ItemClickedEventArgs : EventArgs
{
public int ItemIndex { get; set; }
}
public delegate void ItemClickedEventHandler(Control sender, ItemClickedEventArgs args);
}