ListView HitTest implementation

In my last blog entry, I showed a workaround for getting mouse events for the CF ListView.  Since I can’t leave well enough alone, I decided I’d actually try implementing the HitTest method (now that we have a valid x,y click coordinate).  Here’s the result:


namespace OpenNETCF.Windows.Forms
{
  [Flags]
  public enum ListViewHitTestLocations
  {
    None = 0x01,
    Image = 0x02,
    Label = 0x04,
    StateImage = 0x08,

    AboveClientArea = 0x08,
    BelowClientArea = 0x10,
    RightOfClientArea = 0x20,
    LeftOfClientArea = 0x40,
  }

  public class ListViewHitTestInfo
  {
    public ListViewItem Item { get; set; }
    public ListViewHitTestLocations Location { get; set; }
    public ListViewItem.ListViewSubItem SubItem { get; set; }
  }
}

namespace OpenNETCF.Core
{
  using OpenNETCF.Windows.Forms;
  using System.Runtime.InteropServices;
  using System.Diagnostics;

  public static partial class Extensions
  {
    private const int LVM_FIRST = 0x1000;
    private const int LVM_GETCOUNTPERPAGE = LVM_FIRST + 40;
    private const int LVM_SUBITEMHITTEST = LVM_FIRST + 57;

    private struct LVHITTESTINFO
    {
      public int x;
      public int y;
      public ListViewHitTestLocations flags;
      public int iItem;
      public int iSubItem;
    }

    public static int GetVisibleRowCount(this ListView lv)
    {
      return Win32Window.SendMessage(lv.Handle, LVM_GETCOUNTPERPAGE, 0, 0).ToInt32();
    }

    public static ListViewHitTestInfo HitTest(this ListView lv, int x, int y)
    {
      LVHITTESTINFO info = new LVHITTESTINFO();
      info.x = x;
      info.y = y;
      GCHandle pInfo = GCHandle.Alloc(info, GCHandleType.Pinned);
      try
      {
        Win32Window.SendMessage(lv.Handle, LVM_SUBITEMHITTEST, 0, pInfo.AddrOfPinnedObject());
        LVHITTESTINFO result = (LVHITTESTINFO)pInfo.Target;
        ListViewHitTestInfo lvhti = new ListViewHitTestInfo();

        lvhti.Location = result.flags;
        switch (lvhti.Location)
        {
          case ListViewHitTestLocations.Image:
          case ListViewHitTestLocations.Label:
          case ListViewHitTestLocations.StateImage:
            lvhti.Item = lv.Items[result.iItem];
            lvhti.SubItem = lvhti.Item.SubItems[result.iSubItem];
            break;
        }
        return lvhti;
      }
      finally
      {
        pInfo.Free();
      }
    }

    public static ListViewHitTestInfo HitTest(this ListView lv, Point point)
    {
      return lv.HitTest(point.X, point.Y);
    }
  }
}


And usage looks like this:


public partial class Foo : Form
{
  public Foo()
  {
    InitializeComponent();
    listView.Items.Add(new ListViewItem(new string[] { “Item A”, “Sub A1”, “Sub A2”, “Sub A3” }));
    listView.Items.Add(new ListViewItem(new string[] { “Item B”, “Sub B1”, “Sub B2”, “Sub B3” }));
    listView.Items.Add(new ListViewItem(new string[] { “Item C”, “Sub C1”, “Sub C2”, “Sub C3” }));
    listView.Items.Add(new ListViewItem(new string[] { “Item D”, “Sub D1”, “Sub D2”, “Sub D3” }));
    listView.Items.Add(new ListViewItem(new string[] { “Item E”, “Sub E1”, “Sub E2”, “Sub E3” }));

    ClickFilter filter = new ClickFilter(listView);
    Application2.AddMessageFilter(filter);
    filter.MouseDown += new MouseEventHandler(filter_MouseDown);

  }

  void filter_MouseDown(object sender, MouseEventArgs e)
  {
    ListView lv = sender as ListView;
    ListViewHitTestInfo hti = lv.HitTest(e.X, e.Y);

    if (hti.Item != null)
    {
      Debug.WriteLine(string.Format(“Item: {0}, SubItem: {1}”, hti.Item.Text, hti.SubItem.Text));
    }
  }
}


All of this will end up in SDF v. Next, but why do we have to implement fundamental things like this that should already be there?  It was excusable to be missing in v 1.0.  Maybe even in v 2.0, but in 3.5?  Really?


 

Getting click events for a ListView in the Compact Framework.

We’re going to be adding a full set of custom-drawn and owner-drawn controls in the next version of the SDF.  These are not “new” controls, but wrappers around the existing native control, but we’re adding the custom and owner draw hooks that the OS already provides (and that, honestly, the CF team should have given us by version 3.5). 


In testing them out, I wanted to dogfood the custom-drawn ListView in an application, but I needed to know when a user clicked on an item in the ListView.  Simple.  Or so I thought.  It turns out that the style bits that get set to make a ListView really usable means that the ListView’s *parent* gets it’s click actions, and to turn those into Click events in the managed control is a *lot* of work, and would likely have some performance penalty.


So that got me to wondering how the existing CF ListView deals with it.  Well after a very quick test it seems that the CF team made the same decision – they didn’t support the Click event either


Well my application requires that I know the difference between when the selected index changes (that’s about the only useful event you do get) due a click and and when it changes dues to a keyboard action like an up or down arrow. Being me, I refuse to change the way I want my application will work just becasue of some stupid limitation of a framework.  The OS knows when I click on that damned thing – after all, it changes the selected item – so it *will* tell my application when it happens.


The key here is pretty simple.  We know that the click event does come in, and it comes in as a Windows message.  Our application message pump routes it to the parent of the ListView – so we have two point at which we can try to get it.  Getting it from the parent would entail subclassing the parent Form, and that just does seem like fun, nor is it really extensible if I ever have another app where I want to get ListView item clicks.


So that leaves looking at the application message pump.  Unfortunately the CF team has again not seen fit in any version to provide us the ability to add an IMessageFilter, but this was something we overcame long ago in the SDF.  If you use the Application2.Run method, you can then add IMessageFilters.  So what I did here is created an IMessageFilter that looks for messages whose hWnd matches the ListView I’m interested in, and then looks for the WM_LBUTTONDOWN message:


internal class ClickFilter : IMessageFilter
{
  private Control m_control;

  public event EventHandler Click;
  public event MouseEventHandler MouseDown;

  public ClickFilter(Control control)
  {
    m_control = control;
  }

  private int LoWord(IntPtr param)
  {
    return (ushort)(param.ToInt32() & ushort.MaxValue);
  }

  private int HiWord(IntPtr param)
  {
    return (ushort)(param.ToInt32() >> 16);
  }

  public bool PreFilterMessage(ref Microsoft.WindowsCE.Forms.Message m)
  {
    if (m_control.IsDisposed) return false;

    if ((m.HWnd == m_control.Handle) && (m.Msg == (int)Microsoft.Controls.WM.WM_LMOUSEDOWN))
    {
      MouseEventArgs args = new MouseEventArgs(MouseButtons.Left, 1, LoWord(m.LParam), HiWord(m.LParam), 0);

      ThreadPool.QueueUserWorkItem(ButtonInvoker, args);
    }

    return false;
  }

  void ButtonInvoker(object o)
  {
    // let the system select the clicked listview item
    Thread.Sleep(10);
    Application2.DoEvents();

    if (m_control.IsDisposed) return;

    if (m_control.InvokeRequired)
    {
      m_control.Invoke(new WaitCallback(ButtonInvoker), new object[] { o });
      return;
    }

    // not truly a click, since it’s not a down/up pair. Fix this later if we feel like it
    if(Click != null) Click(m_control, null);
    if (MouseDown != null) MouseDown(m_control, o as MouseEventArgs);
  }
}


Take note of the slight kludge in there though with the ThreadPool.  The reason for this is that if you just raise the event immediately on getting the mouse down event, the ListViewItem selection won’t have happened yet, so if your handler looks at the SelectedItem, it would get the item that was selected before the click, not the one that the was clicked. 


Sure, maybe that’s what your app wants, but in my case I wanted to know which item is actually clicked.  The ListView doesn’t support a HitTest method (again, why don’t we have this yet?), so knowing the x,y coordinates of the click is still a long way from giving us the ListViewItem.  This was a kludge to easily get me the info I wanted.


So using this filter in my application was as easy as adding this to the Form that contains the ListView:


InitializeComponent();

ClickFilter filter = new ClickFilter(listView);
filter.Click += new EventHandler(filter_Click);
Application2.AddMessageFilter(filter);


No, it shouldn’t be this hard.  I wish the CF team would spend more time fixing fundamental stuff like this instead of tilting at the Silverlight windmill, but it is what it is, and as developers we still have to ship solutions.

Version 1.0 Release of IoC Framework along will full-blown sample

It took a long time to get here, but we’ve finally release what I’m calling the version 1.0 (pervious version were 0.9.x) release of the OpenNETCF.IoC Framework.  In case you’ve not been tracking this project, it is a public-domain-licensed (you can’t get any more free and unencumbered than that) framework that provides both inversion of control and dependency injection for .NET Compact Framework applications (it can be used on the desktop as well).  It’s roughly modelled after Microsoft’s SCSF and CAB frameworks, but it’s scaled down and optimized for running on mobile and embedded devices, plus I “fixed” stuff that I think the SCSF got wrong (like having a static, globally available RootWorkItem and the ability to insert IMessageFilters into the application’s message pump).


This framework is in use in a couple of commercial applications already, so it’s been pretty heavily tested and vetted.  I still want to add a few more features as well and go back through it looking for performance optimizations, but it certainly has enough features to be used in applications today.


This release also ships with a full-blown, real-world sample application, not just the typical “Northwind” type of application.  The sample is called WiFiSurvey and it can be used to survey WiFi AP coverage of a site and to monitor associated AP changes as well as network addressability of a device.


WiFiSurvey has a Configuration service, a SQL CE 3.5-backed Data Access Layer, an Infrastructure module and a an application shell all of which are fully decoupled from one another and that are all loaded dynamically using an XML definition file.  The shell makes use of both a DeckWorkspace and a TabWorkspace, showing you not just how to use them, but also how to create your own workspaces if need be.


The WiFiSurvey application has a single source base for all target platforms and has been tested on the following platforms:



  • ARM-based CE 6.0 with a 320×240 (landscape) display.
  • Pocket PC 2003 240×320 (portrait)
  • WinMo 5.0 240×320 (portrait and landscape)

The IoC framework has additionally been tested on x86-based CE 5.0 and CE 6.0 devices.


As a side note, the WiFiSurvey sample application is also a good example of using the OpenNETCF Smart Device Framework for getting wireless information.

Preventing the backlight from turning off in CE 6.0

So it seems that something has changed in CE 6.0 in the way that user inactivity is detected by the OS.  In CE 5.0 and before, if we wanted to keep the backlight on we could periodically call SystemIdleTimerReset and all would be well.  In CE 6.0, this no longer works.  Now we have to set a named event that GWE is waiting on.  Here’s what it looks like (this code uses the SDF for the named EventWaitHandle – the CF doesn’t provide one).


private EventWaitHandle m_activityEvent;

[DllImport(“coredll”, SetLastError=true)]
private static extern void SystemIdleTimerReset();

private void ResetBacklightTimer()
{
  if (Environment.OSVersion.Version.Major <= 5)
  {
    SystemIdleTimerReset();
  }
  else
  {
    if (m_activityEvent == null)
    {
      using (var key = Registry.LocalMachine.OpenSubKey(“System\GWE”))
      {
        object value = key.GetValue(“ActivityEvent”);
        key.Close();
        if (value == null) return;

        string activityEventName = (string)value;
        m_activityEvent = new EventWaitHandle(false, EventResetMode.AutoReset, activityEventName);
      }
    }

    m_activityEvent.Set();
  }
}

Detecting Application Idle

Detecting if your application has been idle (i.e. no user mouse or keyboard actions) for a certain period of time is straightforward using an IMessageFilter implementation.  The filter would look like this:


using System;

using System.Collections.Generic;
using System.Text;
using OpenNETCF.Win32;
using System.Windows.Forms;
using OpenNETCF.Windows.Forms;

namespace MessageFilterSample
{
  public class InactivityFilter : IMessageFilter
  {
    public event MethodInvoker InactivityElapsed;

    private Timer m_inactivityTimer;

    public InactivityFilter(int timeoutMilliseconds)
    {
      m_inactivityTimer = new Timer();
      m_inactivityTimer.Interval = timeoutMilliseconds;

      m_inactivityTimer.Tick += new EventHandler(m_inactivityTimer_Tick);
      Reset();
    }

    void m_inactivityTimer_Tick(object sender, EventArgs e)
    {
      m_inactivityTimer.Enabled = false;
      Elapsed = true;

      if (InactivityElapsed != null) InactivityElapsed();
    }

    public bool PreFilterMessage(ref Microsoft.WindowsCE.Forms.Message m)
    {
      switch ((WM)m.Msg)
      {
        case WM.KEYUP:
        case WM.LBUTTONUP:
        case WM.MOUSEMOVE:
          // reset the timer
          m_inactivityTimer.Enabled = false;
          m_inactivityTimer.Enabled = true;
          break;
      }
      return false;
    }

    public int Timeout { get; set; }
    public bool Elapsed { get; private set; }

    public void Reset()
    {
      Elapsed = false;
      m_inactivityTimer.Enabled = true;
    }
  }
}


To use it, you must use one of the Run overloads in Application2 to start your application, then you can add your filter like this:


InactivityFilter m_filter = new InactivityFilter(5000);
m_filter.InactivityElapsed += new MethodInvoker(m_filter_InactivityElapsed);
Application2.AddMessageFilter(m_filter);

void m_filter_InactivityElapsed()
{
   MessageBox.Show(“Inactivity timer fired”);
}


Download the sample here:
 

MessageFilterSample.zip (3.92 KB)

Detecting disk insertion and removal in the Compact Framework

I blogged about this feature of the SDF two years ago (almost to the day in fact), but it certainly bears repeating since it’s so useful.  If you need to know wehn a disk (SD, CF, USB, etc) has been attached or removed from a CE device (including Windows Mobile) you can use the SDF‘s DeviceStatusMonitor class (which, generally speaking, wraps the RequestDeviceNotifications API).  It’s really, really simple to use:


private void WatchForDrives()
{
  DeviceStatusMonitor monitor = new DeviceStatusMonitor(DeviceClass.FileSystem, false);
  monitor.StartStatusMonitoring();
  monitor.DeviceNotification += delegate(object sender, DeviceNotificationArgs e)
  {
    string message = string.Format(“Disk ‘{0}’ has been {1}.”, e.DeviceName, e.DeviceAttached ? “inserted” : “removed”);
    MessageBox.Show(message, “Disk Status”);
  };
}

Getting an SD Card Serial Number in the Compact Framework

Another nice addition to the latest drop of the SDF is the ability to get the serial number and manufacturer ID of storage volumes that support it (like SD cards). We did this by simply extending the existing OpenNETCF.IO.DriveInfo class to add a couple new properties.  Here’s a quick example of how it works:


foreach(var info in DriveInfo.GetDrives())
{
  Debug.WriteLine(“Info for “ + info.RootDirectory);
  Debug.WriteLine(“tSize: “ + info.TotalSize.ToString());
  Debug.WriteLine(“tFree: “ + info.AvailableFreeSpace.ToString());
  Debug.WriteLine(“tManufacturer: “ + info.ManufacturerID ?? “[Not available]”);
  Debug.WriteLine(“tSerial #: “ + info.SerialNumber ?? “[Not available]”);
  Debug.WriteLine(string.Empty);
}