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

159 Views Asked by At

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?

1

There are 1 best solutions below

0
dno On

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);
}