Thursday, February 24, 2005

The Toolbar control provided as a part of Compact Framework v1.0 does not support tooltips (they are shown when you tap and hold toolbar button). Here is how to add this missing functionality.

First of all, let's see how the tooltips are added. The proper way to do it is to create a tooltip control and pass its handle to the TB_SETTOOLTIPS message as wParam. This sounds pretty painful if we were to do this by means of CF. Fortunately there is an easier way listed in the documentation as legacy but supported all the way through CE 5.0. You can send TB_SETTOOLTIPS passing an array of tooltip strings as lParam and string count as wParam and the toolbar control will create a tooltip control for you. Finally one has to remember to modify toolbar style to include TBS_TOOLTIP and do all of the above before adding the buttons to the toolbar.

Armed with this knowledge we start with defining a few P/Invoke functions:


[DllImport("coredll")]

extern static IntPtr GetCapture();

[DllImport("coredll")]

extern static IntPtr LocalAlloc(int flags, int size);

[DllImport("coredll")]

extern static IntPtr LocalFree(IntPtr p);

[DllImport("aygshell")]

extern static IntPtr SHFindMenuBar(IntPtr hwnd);

[DllImport("coredll")]

extern static int SendMessage(IntPtr hWnd, int msg, int wParam, IntPtr lParam);

[DllImport("coredll")]

extern static int SendMessage(IntPtr hWnd, int msg, int wParam, int lParam);

const int TB_SETTOOLTIPS = (WM_USER + 81);

const int TB_SETSTYLE = (WM_USER + 56);

const int TB_GETSTYLE = (WM_USER + 57);

const int TBSTYLE_TOOLTIPS = 0x0100;

const int WM_USER = 0x0400;

Now, that we are almost ready to set tooltips, there is one final step left. The control expects to receive an array of string pointers. The Compact Framework marshaller is not capable of creating one. Instead we write a special piece of code that would marshal an array of strings into a block of unmanaged memory.

To add tootlips we create a string array, convert it into unmanaged array and then use it as the LPARAM when sending a TB_SETTOOLTIPS message. Keep in mind that the tooltip control will expect this memory to be preserved throught the application lifetime. Release it when the form is closed (or the tooltips are changed).

private IntPtr m_pLabels;

private string[] m_labels = new string[]
{ "Button1", "Button2", "Another button" };

SendMessage(hWndToolbar, TB_SETSTYLE, 0, SendMessage(hWndToolbar, TB_GETSTYLE, 0, 0) | TBSTYLE_TOOLTIPS);

m_pLabels = AllocateStringArray(m_labels);

SendMessage(hWndToolbar, TB_SETTOOLTIPS, m_labels.Length, m_pLabels);

The end result looks like this:

The sample code can be found here.

2/24/2005 7:14:38 PM (Pacific Standard Time, UTC-08:00)  #    Comments [0]  | 
 Wednesday, February 09, 2005

There is a common technique of obtaining the hWnd of various controls in Compact Framework applications to use with native Win32 APIs. It involves setting a Capture property to true and then using GetCapture() to get the handle:

txtUser.Capture = true;
IntPtr hWndUser = GetCapture();
txtUser.Capture = false;

Suprisingly this does not work on Smartphone. It gets you a handle alright, but the handle seems to be wrong. It is wrong indeed. The reason is that on the Smartphone the native Edit control (wrapped by the TextBox) is hosted inside another child window. This has to do with the Smartphone navigation. When you set TextBox.Capture to true, the outer control gets Captrure and as a result, it's the outer control, of which you get the handle. Since we know that the outer control has just one child, we can see our way from this quandary.

txtUser.Capture = true;
IntPtr hWndUser = GetCapture();
hWndUser = GetWindow(hWndUser, GW_CHILD);
txtUser.Capture = false;

//GW_CHILD = 5;

The required PInvoke definitions are parts of Win32Window class in OpenNETCF SDF

Note: This is applicable to CF v1. I have a reason to believe that in v2 things are done differently

2/9/2005 12:24:05 PM (Pacific Standard Time, UTC-08:00)  #    Comments [0]  | 

A question popped up. Let's say we have an application \Program Files\MyApp\MyApp.exe that references a class library \Program Files\MyApp\Framework\MyLib.dll. How can we avoid a TypeLoadException in this scenario?

To answer this let's take a look at how the type resolution works in CF. When an application code attempts to load type T, it first checks if the assembly that contains the type (the one referenced in the Type's full name) is already loaded in the current AppDomain. Obviously, if the appdomain already has the assembly, there is no reason to perform a costly file operation lookihng it up and loading it again. This suggests an easy way to “help” the loader to resolve a type. All you need to do is preload the assembly before the code attempts to use the type from that assembly. In our scenario the following code need to be made:

System.Reflection.Assembly.LoadFrom(@”\Program Files\MyApp\Framework\MyLib.dll”);

This will ensure that the types that belong to this class library are successfully resolved.

The next question is - when to load the assembly. The easy answer is - to play it safe, load all such assemblies in the Main, before the Application.Run. This approach has a disadvantage - a noticeable performance hit because a number of modules are being loaded in the memory before the UI started painting. Besides some of them might be never used. Because of this I would advise staggered load. The trick is to make sure the appropriate assembly is loaded before code execution has entered a block that defines/instantiates a variable of a type defined in that assembly. For example if you have a function:

void DoSomething()
{
MyType myVar = new MyType();
}

where MyType is defined in a dynamically-loaded assembly, and your code never ever call this function, there is no reason at all to load the assembly (provided the no type from that assembly is ever used outside that function).

By cleverly structuring your code you can avoid performance hit even if you have a large amount of dynamically loaded assemblies

2/9/2005 12:15:44 PM (Pacific Standard Time, UTC-08:00)  #    Comments [0]  | 
 Tuesday, January 18, 2005

Today in the Compact Framework public newsgroup I spotted a code snippet (seemed like a piece of a newsreader application) where a variable was called neueNachricht. Try doing something like that in English. I don't think so.

Code | Computers | Life
1/18/2005 11:00:24 AM (Pacific Standard Time, UTC-08:00)  #    Comments [0]  | 
 Wednesday, January 12, 2005

I was walking around CES floor and found a booth around which a long line of people was circling. At the head of the line a scruffy guy with curly hair and a short beard was signing photos. I asked someone who already got a picture, “is that supposed to be someone famous?”. The guy sputtered and said, “Of course. It’s Weird Al Yankovich

Then I went to the next booth and spotted a much shorter line with a cute chick that was signing posters, on which she modeled in some audio hardware ad. A few chinese guys were even photographing each other with her (in a “posessive” way). I decided to get a signed poster for coworker. When it was my turn, she asked who to sign it for. I asked if this was necessary. She was kind of taken aback and said that everyone wanted it signed. I agreed and gave the guy’s name. Then I asked her if that was her in the poster. She pouted and said, of course, why else would she sign them. I brought it back and it was an instant success. I became suspicious and asked “Is that supposed to be someone famous?”. The guy sputtered and said, of course, she was in those series - and then he couldn’t remember which ones. And then in a couple of hours it struck me - she was Lori Loughlin - “Becky”, “Jesse’s” wife in Full House - series that my daughter loves. Now my daughter can’t forgive me that it’s not she who got the poster.

1/12/2005 4:48:40 PM (Pacific Standard Time, UTC-08:00)  #    Comments [26]  | 
 Sunday, December 19, 2004

Q: When I append text to the end of the current content of a multiline listbox, it is always scrolled back to the first line. Even if I later use ScrollToCaret to return to the end of the text, the whole control briefly flickers. Is there a way to avoid it?

A: Indeed there seems to be an issue in Compact Framework where replacing selection in an edit control. Apparently someone wanted to be clever and even though there is a dedicated control message EM_REPLACESEL, he implemented set_SelectedText in the following way:

string SelectedText
{
   
get
   
{
       
if
( SelectionLength == 0 )
           
return
"";
       
return new string
(Text.ToCharArray(), SelectionStart, SelectionLength);
    }
   
set
   
{
       
this.Text = this.Text.Remove(SelectionStart, SelectionLength).Insert(SelectionStart, value
);
        SelectionStart =
this
.Text.Length;
        SelectionLength = 0;
    }
}

 

Why is it done this way, I don't know. There are perfectly good tools at the edit control message level to work with selection. What's important here is that set_SelectedText internally calls Control.set_Text which translates into WM_SETTEXT message. In case of an edit control it has a side effect of moving caret to the beginning and scrolling the text to the same place, thus creating an unpleasant flicker.

To remedy this problem we simply replace set_SelectedText with our own implementation:

Instead of
textBox.SelectedText = "A quick brown fox jumped over lazy dogs. ";
use
SendMessage(hWnd, EM_REPLACESEL, false, "A quick brown fox jumped over lazy dogs. ");

You will also need the following PInvoke definitions:

 

const uint EM_REPLACESEL = 0xc2;

[DllImport("coredll")]
extern static IntPtr GetCapture();
[DllImport("coredll")]
extern static int SendMessage(IntPtr hWnd, uint Msg, bool WParam, string LParam);

 

To get HWND of your textbox use something like this:

 

private IntPtr GetHWND(Control ctl)
{
    ctl.Capture =
true
;
    IntPtr hWnd = GetCapture();
    ctl.Capture =
false
;
   
return
hWnd;
}

 

12/19/2004 10:12:42 PM (Pacific Standard Time, UTC-08:00)  #    Comments [2]  | 
 Tuesday, December 07, 2004
Pioneered by Amazon (IIRC) the unobtrusive suggestions like "Other customers who bought Terminator II, have also purchased Eraser" actually make sense. That is until someone gives a whole new meaning to the word "often".
 

"Customers who bought this item often buy"... Locking Key Box (48 hooks) and ... T D INdustrial Compound Laser Miter Saw. Really???

Or this one:  ATN Night Vision spotting Scope

"Customers who bought this item often buy" Cosmopolitan Leather Tote Bag (three colors)

Well, this one actually makes sense. After all you do need something to tote your shiny new night scope around.

12/7/2004 4:43:55 PM (Pacific Standard Time, UTC-08:00)  #    Comments [0]  | 
 Friday, December 03, 2004

It just occured to me that the way we potty-train small children so that they stop making messes, we also should train older children to look the stuff up instead of asking endless questions. More and more I find myself answering another question like Daddy, what is Cassinian Oval, with “lookitup”. And what better place there is than Google (with all caveats related to the safe search issues).

I even went as far as coining a term - “google-trained”, although in my case I send her to Encarta first. The thing is that google has already become synonymous with search (same as Xerox meant “to copy”). Training by the way includes an ability to recognize a potentially unsafe or junk result.

Are your children google-trained?

12/3/2004 6:15:08 PM (Pacific Standard Time, UTC-08:00)  #    Comments [0]  | 
 Sunday, November 21, 2004

At the heart of our production application is a paradigm of a work order. As one can surmise, the real-life work order has quite a complex data structure. In our case it is a chunk of XML couple of thousands of bytes in size. When you need to edit this XML in your app, you have to keep writing long and unpleasant queries like this:

txtWONumber.Text = objWO[”WorkOrder”].GetAttribute[”Number”];
txtWOTask.Text = objWO[”WorkOrder”][”Asset”][”Task”].InnerText;

Of course in part this is caused by lack of XQuery support in CF 1.0, but still, later you have to write the update code and make sure you haven't forgotten to call it. And if you need to populate a listbox or a datagrid from a list of XML nodes, you are quite out of luck and in for some manual work.

And yet, the databinding underneath is highly automated and quite extensible, so let's see what we can do to make it understand XML data.

First of all, we will decide on what is our data unit (component). It will be an XML node. Component properties could be either attributes or subnodes, or even any valid XQuery expression - anything that returns data. A logical choice of data source is an XML node list (not an array). Such a list is easily obtained from an XML document via ChildNodes property of its document element.

I am not going to go into details of how the databinding works for there are many good sources that cover this process1. Instead I will simply say that at the root of custom databinding is an object derived from a class called PropertyDescriptor. We are going to derive our own class from it and call it XmlPropertyDescriptor.

Let's set some gorund rules. We want to be able to take an xml document like this:

xml version="1.0" encoding="utf-8" ?>
<
bookstore xmlns:bk="urn:samples">

<book genre="novel" publicationdate="1997" bk:ISBN="1-861001-57-6">

<title>Pride And Prejudicetitle>

<price>24.95price>

book>

<book genre="novel" publicationdate="1992" bk:ISBN="1-861001-45-3">

<title>The New Dawntitle>

<price>29.95price>

book>

<book genre="novel" publicationdate="1991" bk:ISBN="1-861001-57-8">

<title>Blue Smoketitle>

<price>19.95price>

book>

bookstore>

and bind to a list of “book” items using display expression like “title” or “@bk:isbn" . If we had more complex book item structure, the binding name could be a more complex path - “author/Name/@First”. What we do not want to do (since we don't really have XQuery/XPath support in CF) is to allow relative paths, functions and queries. Moreover, we will say, that in the path, every element must be a node name, except of the last one, that can be an attribute name (starting with @ ).

When you derive from the PropertyDescriptor class there are few things to keep in mind:

  1. Override IsReadonly property. This will tell the framework whether your property supports updates
  2. Override PropertyType property to return typeof(string) - this is our property type; we don't do any data conversions here. If we want data conversion/validation, we can implement it in Format/Parse events of the binding. We could to some data validation and type conversion via reading the schema (if available), but it would be outside of the scope of this article.
  3. Override ComponentType property to return typeof(XmlNode). This is the only component type we deal with.
  4. Override GetValue method. This is the most important method. It actually goes ahead and retrieves the property value given the component. In our case it gets the XmlNode (and already has the “XPath” by way of constructor parameter). Our task is to perform the query. Very simple on the desktop. A bit more complex in CF.
  5. Override SetValue method. It receives the component and the new value and sets it using the known “Xpath”.

Now, wouldn't it be nice to make the binding use our XmlPropertyDescriptor class? Simple - we just need to create our own Data Source that would report XmlPropertyDescriptors instead of whatever is used by CurrencyManager. Whatever is usually a SimplePropertyDescriptor class, ReflectPropertyDescriptor or DataColumnPropertyDescriptor.

This brings us to building our own data source object. There is no abstract class to override. When creating it, we need to make sure we implement several interfaces:

  • ITypedList
  • IEnumerable
  • IList
  • IBindingList

ITypedList::GetItemProperties creates a bit of a problem. It is supposed to return Property descriptor collection populated with all properties the data unit exposes. Unfortunately in our case properties are valid xpath expressions, which could be quite a few, expecially on a complex XML document

The reason we need to implement ITypedList::GetItemProperties is that we need for example a data grid to be able to get populated automatically, without us specifying XPath expression for each column. The problem here is that we don't want to build a full list of all possible xpath expressions valid on our XML item, so we need to decide on some way to simplify it. Let's limit the autogenerated property list to all attributes or all 1-st level child elements. If the XML is more complex than that, well, you need to specify the grid columns explicitly.

The IEnumerable and IList are simply delegated to the underlying XmlNodeList. IBindingList implementation is somewhat simplistic as we don't really want to figure out how to track the external changes in the XmlNodeList.

We end up with a class that allows us to take the XML document above and write things like:


doc.Load("data.xml");

XmlDataSource src = new XmlDataSource(doc.DocumentElement.ChildNodes, XmlDataSourceMode.DataSourceModeAuto);

txtGenre.DataBindings.Add("Text", src, "@genre");

txtISBN.DataBindings.Add("Text", src, "@bk:ISBN");

txtPrice.DataBindings.Add("Text", src, "price");

txtPubDate.DataBindings.Add("Text", src, "@publicationdate");

txtTitle.DataBindings.Add("Text", src, "title");

 

listBox1.DisplayMember = "title";

listBox1.ValueMember = "@bk:ISBN";

listBox1.DataSource = src;

dataGrid1.DataSource = new XmlDataSource(doc.DocumentElement.ChildNodes,

new string[] { "@genre", "title/#text", "price/#text" });

This produces a screen like this one:

 

Selecting a list box item moves the current position along the node list. Data edits are also supported.

The source code for this article can be found here.

11/21/2004 8:41:11 PM (Pacific Standard Time, UTC-08:00)  #    Comments [0]  |