DataGridView How to Validate&Select cell - winforms

I have a grid view contains multiple columns and I adjusted their input type to be double.
When the user selects a cell, enters its data, I want to check if it matches the type of double.
If the user clicks on another cell and the one he is about to leave doesn't match and it failed in parsing, I want the focus return to the failed cell, and not moving to the new selected one, to force the user to enter a valid data before continuing filling the rest of the cells.
The problem is that it always leaves the failed cell. Even when I used other functions like validating or validated.
Here is my code sample:
private void dataGridView1_CellLeave(object sender, DataGridViewCellEventArgs e)
{
var selectedCell = dataGridView1.SelectedCells[0];
if (selectedCell.OwningColumn.ValueType == typeof(double))
{
try
{
double result = Convert.ToDouble(selectedCell.EditedFormattedValue.ToString());
if (result <= 0)
{
MessageBox.Show("Please Positive Numbers Only");
dataGridView1.Focus();
dataGridView1.CurrentCell = dataGridView1.Rows[selectedCell.RowIndex].Cells[selectedCell.ColumnIndex];
dataGridView1.BeginEdit(true);
}
else
{
dataGridView1.CurrentCell.Value = result;
}
}
catch
{
MessageBox.Show("Please Enter Numbers Only");
}
}
}

You can use event DataGridView.CellValidating. This should fix your problem.
Example:
private void dataGridView1_CellValidating(object sender,
DataGridViewCellValidatingEventArgs e)
{
if (e.ColumnIndex == 1) // 1 should be your column index
{
int i;
if (!int.TryParse(Convert.ToString(e.FormattedValue), out i))
{
e.Cancel = true;
label1.Text ="please enter numeric";
}
else
{
// the input is numeric
}
}
}
DataGridView.CellValidating Event
How to: Validate Data in the Windows Forms DataGridView Control
CellValueChanged vs CellValidating Events for DataGridView

Related

ArgOutOfRangeEx while calling ListViews base.WndProc when click occurs outside subitems, C# 10.0

I am receiving a ArgumentOutOfRangeException while calling base.WndProc in an OwnerDrawn listview.
The exception occurs after a click is performed to the right (empty space) of the last subitem of any ListViewItem.
The listview column widths are programmatically set to fill the entire listview, however on occasion data is not received as expected leading to some extra space, this is an edge case which usually does not occur.
The message to be processed at the time of exception is WM_LBUTTONDOWN (0x0201), or WM_RBUTTONDOWN (0x0204).
All data for painting the LV subitems is from a class referenced by the tag of the LVI, at no point do I try to read the subitems, nor do I write data to any of the subitems.
Below is the smallest reproducible code, in C# (10.0), using .NET 6.0 that shows the exception.
I attempted to simply exclude processing of WM_LBUTTONDOWN. This did remove the exception, however it also stopped left click events from reaching MouseUp which is required. (also for right clicks)
Whilst I am working to fix my errors in column sizing after receiving bad or no data, I would like to check for this possible exception and simply return from the method before the exception can occur.
Data class
namespace testCase;
class myClass
{
public string s1 = "subitem1";
public string s2 = "subitem2";
}
Form code
namespace testCase;
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
ListViewExWatch lv = new();
Controls.Add(lv);
lv.MouseUp += Lv_MouseUp;
lv.Top = 0;
lv.Left = 0;
lv.Width = ClientSize.Width;
lv.Height = ClientSize.Height;
lv.OwnerDraw = true;
lv.BackColor = Color.AntiqueWhite;
lv.Columns.Add("Row", 50);
lv.Columns.Add("sub1", 50);
lv.Columns.Add("sub2", 50);
for(int i = 0; i < 10; i++)
{
ListViewItem lvi = new(){ Text = "Row " + i, Tag = new myClass() };
lvi.SubItems.Add("");
lvi.SubItems.Add("");
lv.Items.Add(lvi);
}
}
private void Lv_MouseUp(object? sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
MessageBox.Show("Left click - doing action A");
}
if (e.Button == MouseButtons.Right)
{
MessageBox.Show("Right click - Creating context menu");
}
}
}
Custom ListView override
using System.Runtime.InteropServices;
namespace testCase;
public class ListViewExWatch : ListView
{
#region Windows API
[StructLayout(LayoutKind.Sequential)]
struct DRAWITEMSTRUCT
{
public int ctlType;
public int ctlID;
public int itemID;
public int itemAction;
public int itemState;
public IntPtr hWndItem;
public IntPtr hDC;
public int rcLeft;
public int rcTop;
public int rcRight;
public int rcBottom;
public IntPtr itemData;
}
const int LVS_OWNERDRAWFIXED = 0x0400;
const int WM_SHOWWINDOW = 0x0018;
const int WM_DRAWITEM = 0x002B;
const int WM_MEASUREITEM = 0x002C;
const int WM_REFLECT = 0x2000;
const int WM_LBUTTONDOWN = 0x0201;
#endregion
public ListViewExWatch()
{
SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true);
}
protected override CreateParams CreateParams
{
get
{
CreateParams k_Params = base.CreateParams;
k_Params.Style |= LVS_OWNERDRAWFIXED;
return k_Params;
}
}
protected override void WndProc(ref Message k_Msg)
{
//if (k_Msg.Msg == WM_LBUTTONDOWN) return;
base.WndProc(ref k_Msg); // Exception: System.ArgumentOutOfRangeException: 'InvalidArgument=Value of '-1' is not valid for 'index'.
// Only occurs when clicking to the right of the last subItem
switch (k_Msg.Msg)
{
case WM_SHOWWINDOW:
View = View.Details;
OwnerDraw = false;
break;
case WM_REFLECT + WM_MEASUREITEM:
Marshal.WriteInt32(k_Msg.LParam + 4 * sizeof(int), 14);
k_Msg.Result = (IntPtr)1;
break;
case WM_REFLECT + WM_DRAWITEM:
{
object? lParam = k_Msg.GetLParam(typeof(DRAWITEMSTRUCT));
if (lParam is null) throw new Exception("lParam shouldn't be null");
DRAWITEMSTRUCT k_Draw = (DRAWITEMSTRUCT)lParam;
using Graphics gfx = Graphics.FromHdc(k_Draw.hDC);
ListViewItem lvi = Items[k_Draw.itemID];
myClass wi = (myClass)lvi.Tag;
TextRenderer.DrawText(gfx, lvi.Text, Font, lvi.SubItems[0].Bounds, Color.Black, TextFormatFlags.Left);
TextRenderer.DrawText(gfx, wi.s1, Font, lvi.SubItems[1].Bounds, Color.Black, TextFormatFlags.Left);
TextRenderer.DrawText(gfx, wi.s2, Font, lvi.SubItems[2].Bounds, Color.Black, TextFormatFlags.Left);
break;
}
}
}
}
The error message
System.ArgumentOutOfRangeException
HResult=0x80131502
Message=InvalidArgument=Value of '-1' is not valid for 'index'. Arg_ParamName_Name
ArgumentOutOfRange_ActualValue
Source=System.Windows.Forms
StackTrace:
at System.Windows.Forms.ListViewItem.ListViewSubItemCollection.get_Item(Int32 index)
at System.Windows.Forms.ListView.HitTest(Int32 x, Int32 y)
at System.Windows.Forms.ListView.ListViewAccessibleObject.HitTest(Int32 x, Int32 y)
at System.Windows.Forms.ListView.WmMouseDown(Message& m, MouseButtons button, Int32 clicks)
at System.Windows.Forms.ListView.WndProc(Message& m)
at testCase.ListViewExWatch.WndProc(Message& k_Msg) in C:\Users\XXXX\source\repos\testCase\ListViewExWatch.cs:line 50
at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, WM msg, IntPtr wparam, IntPtr lparam)
From the error it is apparent that the base listview code it is trying to get a subitem that doesn't exist because the click is not on a subitem, hence the -1 for index within the error message.
At the time of exception there is no member of k_Msg that contains the index (-1), so I cannot check for that and simply return.
I could surround the call to base.WndProc in a try catch, since it is an edge case. I've always had the mindset to check for possible exceptions and prevent them whenever possible rather than catch them. But this one has me stumped.
Am I being too pedantic in this regard?
Most likely I am missing something basic here, could you fine folks please point me in the right direction?
I had implemented the following code to catch only this specific exception and still allow any other to bubble up as desired.
try
{
base.WndProc(ref k_Msg); // This throws a ArgOutOfRangeEx when a click is performed to the right of any subitem (empty space)
}
catch (ArgumentOutOfRangeException ex) when (ex.ParamName == "index" && (int?)ex.ActualValue == -1 && ex.TargetSite?.DeclaringType?.Name == "ListViewSubItemCollection")
{
Program.Log(LogLevel.Normal, "ListViewExWatch.WndProc()", "ArgumentOutOfRangeException: A click has been perfored outside of a valid subitem. This has been handled and indicates column witdth calcs were wrong.");
return;
}
With further research however, I managed to specifically workaround the issue
From https://learn.microsoft.com/en-us/windows/win32/inputdev/wm-lbuttondown
lParam
The low-order word specifies the x-coordinate of the cursor...
The high-order word specifies the y-coordinate of the cursor...
I ended up modifying the lParam before processing the message. By moving the x-coordinate to inside the last subitem the exception is averted and clicks are processed as normal.
protected override void WndProc(ref Message k_Msg)
{
if (k_Msg.Msg == WM_LBUTTONDOWN || k_Msg.Msg == WM_RBUTTONDOWN)
{
// Get the x position of the end of the last subitem
int width = -1;
foreach (ColumnHeader col in Columns) width += col.Width;
// Where did the click occur?
int x_click = SignedLOWord(k_Msg.LParam);
int y_click = SignedHIWord(k_Msg.LParam);
// If the click is to the right of the last subitem, set the x-coordinate to inside the last subitem.
if (x_click > width) k_Msg.LParam = MakeLparam(width, y_click);
}
base.WndProc(ref k_Msg);
...
It is interesting to note the correct x and y coordinates are still reported in the Lv_MouseUp handler. This may not be the case for other handlers such as MouseDown which has obviously been modified, since I do not use them it has not presented a problem
Here are the helper functions used above (from referencesource.microsoft.com)
public static IntPtr MakeLparam(int low, int high)
{
return (IntPtr)((high << 16) | (low & 0xffff));
}
public static int SignedHIWord(IntPtr n)
{
return SignedHIWord(unchecked((int)(long)n));
}
public static int SignedLOWord(IntPtr n)
{
return SignedLOWord(unchecked((int)(long)n));
}
public static int SignedHIWord(int n)
{
return (short)((n >> 16) & 0xffff);
}
public static int SignedLOWord(int n)
{
return (short)(n & 0xFFFF);
}

An unhandled exception of type 'System.InvalidCastException' in winform

I have project that I click Button and it's will create a new PictureBox (pb) on a PictureBox1. And when I choose item on a combobox and PictureBox (pb) will appear in the position I want and the problem appears. How can I fix it or use "pb" in void comboBox3_SelectedIndexChanged. Thank you.
private void btaddagv_Click(object sender, EventArgs e)
{
AddNewPictureBox();
}
public System.Windows.Forms.PictureBox AddNewPictureBox()
{
System.Windows.Forms.PictureBox pb = new System.Windows.Forms.PictureBox();
pictureBox1.Controls.Add(pb);
pb.Name = "STT" + tbAdd.Text;
pb.Image = Image.FromFile("AGV-1.jpg");
pb.Height = 30;
pb.Width = 40;
pb.SizeMode = PictureBoxSizeMode.Zoom;
pb.Location = new System.Drawing.Point(tdx, 500);
tdx = tdx + 200;
return pb;
}
private void comboBox3_SelectedIndexChanged(object sender, EventArgs e)
{
if(comboBox3.SelectedItem == "A")
{
PictureBox pb = (PictureBox)sender;
pb.Location = lbA.Location;
}
}
And here is an error
If we make an assumption on this line:
pb.Name = "STT" + tbAdd.Text;
that tbAdd.Text contains the A, B, C, D etc that you're checking for here:
if(comboBox3.SelectedItem == "A")
Then your SelectedIndexChanged event handler should be:
private void comboBox3_SelectedIndexChanged(object sender, EventArgs e)
{
string selected = comboBox3.SelectedItem.ToString();
if (!string.IsNullOrWhitespace(selected))
{
PictureBox pb = picturebox1.Controls.OfType<PictureBox>().Where(p => p.Name == $"STT{selected}").FirstOrDefault();
if (pb != null)
{
Label lb = picturebox1.Controls.OfType<Label>().Where(l => l.Name == $"lb{selected}").FirstOrDefault();
if (lb != null)
pb.Location = lb.Location;
}
}
}
I've made some extra assumptions here. First I've assumed that the "lbA" control you're referring to is a Label. The second assumption is that lbA and the other controls are all share the same parent (picturebox1)
What the above is doing is getting the value of the SelectedItem (if we use your example with the letter A) then attempts to find any PictureBox controls that have the name STTA. If it finds one it looks for a Label on the same parent called lbA. If that exists then it moves the picturebox control to the location of the label.

WPF. Textbox_textchanged not working for the last character, refuses to subtract last integer

I´ve got a small issue with my textbox-event...
When entering a number everything works fine and the integer in the textbos is subtracted from my sum that i have as an property in another class, but when I am deleteing the same number the last singular refuses to be subtracted from my variable that is connected to a label. I have to write 0 in the textbox to set the my label to my property.
If someone could helt me out I would be really gretfull!
private void txtBoxDiscount_TextChanged(object sender, TextChangedEventArgs e)
{
try
{
if (inv.TotalAllItems > Int32.Parse(txtBoxDiscount.Text))
{
float temp = 0;
temp = inv.TotalAllItems - Convert.ToInt32(txtBoxDiscount.Text);
lblTotalAmount.Content = temp;
}
else
{
MessageBox.Show("Discount can not be greater than the total amount.\nPlease try again", "Something went wrong", MessageBoxButton.OK, MessageBoxImage.Hand);
txtBoxDiscount.Text = "";
lblTotalAmount.Content = inv.TotalAllItems;
}
}
catch
{
}
}
Put a breakpoint in the empty catch block. You'll find that Int32.Parse(txtBoxDiscount.Text) throws an exception when txtBoxDiscount.Text is an empty string. This is why you should never use an empty catch block.
Use Int32.TryParse() instead. If the text can't be parsed as a number, it will leave discount equal to zero.
private void txtBoxDiscount_TextChanged(object sender, TextChangedEventArgs e)
{
int discount = 0;
Int32.TryParse(txtBoxDiscount.Text, out discount);
if (inv.TotalAllItems > discount)
{
float temp = 0;
temp = inv.TotalAllItems - discount;
lblTotalAmount.Content = temp;
}
else
{
MessageBox.Show("Discount can not be greater than the total amount.\nPlease try again", "Something went wrong", MessageBoxButton.OK, MessageBoxImage.Hand);
txtBoxDiscount.Text = "";
lblTotalAmount.Content = inv.TotalAllItems;
}
}
If you want to treat a non-numeric string as an error, rather than as zero, Int32.TryParse() also returns false when the parse fails:
private void txtBoxDiscount_TextChanged(object sender, TextChangedEventArgs e)
{
int discount = 0;
if (!Int32.TryParse(txtBoxDiscount.Text, out discount))
{
// Do something else
}
else if (inv.TotalAllItems > discount)
{

WinForms DataGridView set DataGridViewComboBoxCell value

I have a DataGridView with custom DataGridViewComboBoxCell columns, and I run the following code to initiaize it's cells to the initial value I want:
try
{
this.dgvTemplatesConfiguration.Columns.Add(this.CreateFaultsColumn(faultsData, "OuterFaults", 2));
this.dgvTemplatesConfiguration.Columns.Add(this.CreateFaultsColumn(faultsData, "InnerFaults", 5));
}
catch (Exception ex)
{
Log.WriteLineIf(ex.Message, TraceLevel.Info);
throw;
}
for (int i = 0; i < dgvTemplatesConfiguration.Rows.Count - 1; i++)
{
dgvTemplatesConfiguration["InnerFaults", i].Value = OkFaultName;
dgvTemplatesConfiguration["OuterFaults", i].Value = OkFaultName;
}
If I do it on OnLoad event, the code works fine and inits the cells, but if I run it afterwards on OnVisibleChanged event, I get an exception
DataGridViewComboBoxCell value is not valid
What is the proper way of setting default values on DataGridViewComboBoxCell?

Decimal Textbox in Windows Forms

I am doing an Financial Winforms application and am having some trouble with the controls.
My customer needs to insert decimal values all over the place (Prices, Discounts etc) and I'd like to avoid some of the repeating validation.
So I immediately tried the MaskedTextBox that would fit my needs (with a Mask like "€ 00000.00"), if it weren't for the focus and the length of the mask.
I can't predict how big the numbers are my customer is going to enter into the app.
I also can't expect him to start everything with 00 to get to the comma. Everything should be keyboard-friendly.
Am I missing something or is there simply no way (beyond writing a custom control) to achieve this with the standard Windows Forms controls?
This two overriden methods did it for me (disclaimer: this code is not in production yet. You may need to modify)
protected override void OnKeyPress(KeyPressEventArgs e)
{
if (!char.IsNumber(e.KeyChar) & (Keys)e.KeyChar != Keys.Back
& e.KeyChar != '.')
{
e.Handled = true;
}
base.OnKeyPress(e);
}
private string currentText;
protected override void OnTextChanged(EventArgs e)
{
if (this.Text.Length > 0)
{
float result;
bool isNumeric = float.TryParse(this.Text, out result);
if (isNumeric)
{
currentText = this.Text;
}
else
{
this.Text = currentText;
this.Select(this.Text.Length, 0);
}
}
base.OnTextChanged(e);
}
You will need a custom control. Just trap the Validating event on the control and check if the string input can be parsed as a decimal.
I don't think you need a custom control, just write a decimal validating method for the validating event and use that for all the places you need to validate. Don't forget to include the NumberFormatInfo, it will deal with commas and numebr signs.
MSDN: User Input Validation in Windows Forms
You only need to let numbers and decimal symbols through, and avoid a double decimal symbol. As an extra, this automatically adds a 0 before a starting decimal number.
public class DecimalBox : TextBox
{
protected override void OnKeyPress(KeyPressEventArgs e)
{
if (e.KeyChar == ',')
{
e.KeyChar = '.';
}
if (!char.IsNumber(e.KeyChar) && (Keys)e.KeyChar != Keys.Back && e.KeyChar != '.')
{
e.Handled = true;
}
if(e.KeyChar == '.' )
{
if (this.Text.Length == 0)
{
this.Text = "0.";
this.SelectionStart = 2;
e.Handled = true;
}
else if (this.Text.Contains("."))
{
e.Handled = true;
}
}
base.OnKeyPress(e);
}
}
Another approach is to block what you don't want, and format when your done with it.
class DecimalTextBox : TextBox
{
// Handle multiple decimals
protected override void OnKeyPress(KeyPressEventArgs e)
{
if (e.KeyChar == '.')
if (this.Text.Contains('.'))
e.Handled = true;
base.OnKeyPress(e);
}
// Block non digits
// I scrub characters here instead of handling in OnKeyPress so I can support keyboard events (ctrl + c/v/a)
protected override void OnTextChanged(EventArgs e)
{
this.Text = System.Text.RegularExpressions.Regex.Replace(this.Text, "[^.0-9]", "");
base.OnTextChanged(e);
}
// Apply our format when we're done
protected override void OnLostFocus(EventArgs e)
{
if (!String.IsNullOrEmpty(this.Text))
this.Text = string.Format("{0:N}", Convert.ToDouble(this.Text));
base.OnLostFocus(e);
}
}

Resources