Welcome to MSDN Blogs Sign in | Join | Help
  • Introduction on Windows CE\Mobile 
  • Virtual Memory on Windows CE 5.0\Mobile (a visual approach) 
  • Troubleshooting Memory Leaks for NETCF applications

Lately I’ve been involved on starting up my team blog: if you read Italian and want to listen about stories from Support Engineers working on various technologies, you can’t avoid signing up to the feed:

itasupport

Now guess what’s been the topic of the posts I’ve been writing there… Open-mouthed yes! Memory Management on Windows CE\Mobile and NETCF!! (so far the intro and then part 0, 1, 2 – don’t ask me why I made it 0-based, even after an intro… I really don’t remember) And since I’ve been adopting an approach that is not usually the one used to explain how things work, I honestly think I can reuse the same one and translate it here… probably I’m going to repeat some concepts I’ve already blogged about, however it may be worth in order to have one single document, this post, as hopefully a possible quite-ultimate way to describe how memory is handled… is the bar too high??

 

INTRODUCTION ABOUT WINDOWS CE\MOBILE

So, let’s start: Windows CE and Windows Mobile are not the same thing. After working on this for a while it can be obviuos, but if you’re at your first experience with these so-called “Smart Devices” then it may not be so. We must be clear about the terminology, specifically about terms like “platform”, “operating system”, “Platform Builder”, “Adaptation Kit”, “OEM”, “ODM”, etc.. In reality the true name of “Windows CE” would nowadays be “Windows Embedded CE”, however I don’t want to mess with a product that was once known as “Windows XP Embedded” and which nowadays is differentiated in the following products:

  • “Windows Embedded Stardard”
  • “Windows Embedded Enterprise”
  • “Windows Embedded POSReady”
  • “Windows Embedded NavReady”
  • “Windows Embedded Server”

So, when I’ll write “Windows CE” here I’ll mean the historical name of “Windows Embedded CE”: let’s forget the other “Windows Embedded X” products (out of my scope) and let’s concentrate on Windows CE\Mobile.

Windows CE is a platform for OEMs (Original Equipment Manufacturer). This means that we provide to the manufacturer an Integrated Development Environment very similar to Visual Studio (indeed, Windows CE 6.0 is integrated with Visual Studio), but with the aim of developing Operating Systems (instead of applications), based on the platform provided by Microsoft. The tool is called "Platform Builder for Windows CE”, and up to version 5.0 was a separate tool from Visual Studio.

Windows CE is a modular platform. This means that the OEM is totally free to include only the modules, drivers and applications of his interest. Microsoft provides about 90% of the source code of the Windows CE platform, as well as code examples for drivers and various recommendations (which the OEM might or might not follow). For example, if the device is not equipped with an audio output, then the OEM won’t add a sound driver. If it doesn’t have a display, then the OEM will not develop nor insert a video driver. And so on for the network connectivity, or a barcode-scanner, a camera, and so on. On a Windows CE-based device, the OEM can include whatever he wants. That's why, from the point of view of technical support to application-developers, sometimes we can’t help a programmer who is targeting a specific device whose operating system is based on Windows CE: furthermore, the OEM can decide whether to offer application-developers the opportunity to programmatically interact with special functions of the device through a so-called "Private SDK” (which may also contain a emulator image, for example).

An important detail: differently from Windows Embedded OSs (Standard \ Enterprise \ POSReady \ NavReady \ Server), for Operating Systems based on Windows CE the OEMs actually *COMPILE* the source code of the platform (apart from about 10% provided by Microsoft and corresponding to the core kernel and other features).

Now: Windows Mobile is a particular customization of Windows CE, but in this case the OEM needs to create an Operating System that meets a set of requirements, called "Windows Mobile Logo Test Kit". The tool used by Windows Mobile-OEM is called "Adaptation Kit for Windows Mobile", a special edition of "Platform Builder" and allows to adapt the "Windows Mobile”-Platform to the exact hardware that the OEM has built or that he requested to an ODM (“Original Device Manufacturer”). In the Windows Mobile’s scenario we can’t forget Mobile Operators also, which often "brand" a device requiring the OEM to include specific applications and usually configure the connectivity of the mobile network (GPRS, UMTS, WAP, etc.).. WARNING: nothing prohibits a WinMo-OEM to include special features such as a barcode-scanner or an RFID chip or anything else... the important thing is that the minimal set is the same. Moreover, also WinMo-OEM can provide a “Private SDK” to programmatically expose specific functionality related to their operating system (see for example Samsung SDK containing private APIs for the accelerometer and other features, which are documented and supported by Samsung itself).

Finally, one last thing before starting talking about memory: Windows Mobile 5.0, 6, 6.1 and 6.5 are all platforms based on Windows CE 5.0. So, they all share the same Virtual Memory management mechanisms, except in some details for the latter (mainly with some benefits for application-developers).

 

VIRTUAL MEMORY ON WINDOWS CE\MOBILE

So now we can start talking about how memory is managed on Operating Systems based on Windows CE 5.0. And I’m being specific on Windows Embedded CE *5.0* because on 6.0 memory management is (finally!) totally changed and there are no more the limitations we’re going to discuss in the remainder. Incidentally, this is part of the same limitations described for Windows CE 4.2 by Doug Boling’s Windows CE .NET Advanced Memory Management, although the article is dated 2002! Fortunately, some improvements have been introduced from Windows Mobile 6 and especially in 6.1 (and therefore also in 6.5), whereby not only the applications have more virtual memory available, but also the entire operating system is more stable as a whole.

I don’t want to repeat here what can be found in documentation and on various blogs: in contrast, I’d like to actually show the theory, because only by looking at data like the following you can realize what a good programmer a Developer for Windows Mobile has to be! Hot The following is the output of a tool developed by Symbol (later acquired by Motorola), which allowed the manufacturer to understand how the device.exe process was responsible (or not) for various problems related to memory. Why? Because

  • device.exe is the process that loads the DLL for the driver in its own address space;
  • if it hasn’t yet been loaded by other processes, a DLL will occupy a portion of the virtual memory below the "DLL Load Point";
  • each DLL may have dependencies, also to be loaded;
  • each DLL can create its own heap to store data.

The result was something like the following (I obfuscated possibly sensitive data of the software-house I worked with during the Service Request):

memory

So, above all, what did Symbol\Motorola mean by “Code” (blue) and “Data” (green)?

• "Code": these are the *RAM* DLLs loaded or mapped into a process-slot. They start from the top of the 32MB slot and goes down. If several processes use the same DLL, the second one maps it to the same address where the first one had loaded it.
• "Data" is the executable’s compiled code + Heap(s) + Stack. It starts from the bottom and grows.

Finally, the red vertical line represents the "DLL Load Point", i.e. the address where a DLL is loaded in case it hadn’t yet by any other process.

That is the situation of only the process slots, not the whole virtual memory - in particular the contents of the Large Memory Area is not shown:

clip_image003

Why did I specify *RAM* DLLs? Because those placed by the OEM in the ROM (= firmware) are executed directly there, without the need of the process loading their "compiled" code into its Address Space (they’re XIP DLL, i.e. "Executed in Place" – in Slot 1).

That picture also shows that the green part (code + heap + stack) may exceed the DLL Load Point. Indeed, the problems related to lack of available virtual memory is usually of 2 types:

  1. a process tries to load a DLL that hadn’t been loaded by any other before, trying to move the DLL Load Point in an area already occupied by code + heap + stack
  2. a process tries to allocate memory (code + heap + stack) but the process slot has got saturated even on sections left free among loaded DLLs

That's also because in general one of the advices to avoid memory problems had always been to load all DLLs used by the application itself at application startup, through an explicit call to LoadLibrary(). Another visual example is the following:

memory2

We’ll later discuss in detail the particularities of NETCF, but it's worth at this point noting a detail: apart from the actual CLR DLLs (mscoree*.dll, netcfagl*.dll), every other assembly doesn’t waste address space in the Process slot, but is loaded into the Large Memory Area. Even more, if you are using the version of the runtime included in the ROM by the OEM, also the runtime DLLs do not affect the process’ virtual memory space. Obviously it is different when the application P/Invoke native DLLs: these will be loaded in the process slot.

Moreover, if you look at the picture showing all the alive processes, you’ll notice that in the upper bound of all the slots there’s a portion of "blue" virtual memory, which is the same for all the processes. This is the memory blocked by the Operating System whose size is equal to the sum of the binaries (the simple .EXE files) active at any given moment. So large monolithic EXEs (large for example because containing “many” resources) are not recommended at all on Windows CE 5.0! And in general supporting NETCF developers I can say I’ve seen many “big” applications... that is not a good practice for this reason!

Through those pictures it is also easy to understand why the whole system stability is a function of all active processes, and in particular it is easy to see that very often DEVICE.EXE can be a source of headaches! Think of those Windows Mobile-based devices that have the radio stack (i.e. the phone), Bluetooth, WiFi, Camera, barcode-scanner, etc. .. each of these drivers is a DLL that device.exe has to load (blue line), and each can also create its own stack and heap (green line). Some OEMs allowed developers to programmatically disable some drivers (to reduce pressure done by device.exe), but obviously we can not take for granted for example that a user manually restarts that feature (or this is done by another application...).

So, what has been done to fight device.exe’s power? In many cases, the driver-DLLs were loaded by services.exe, which is the host process for Service-DLLs on Windows CE. But very often it was not enough... What Windows Mobile 6.1 introduced is that native DLLs with size > 64KB are typically loaded into the so-called slots 60 and 61, which are part of the Large Memory Area. Another improvement in Windows Mobile 6.1 was to dedicate another slot (slot 59) to the driver stack (part of the Green Line to device.exe). Of course, this means that the memory-mapped files have now less space available (and I have recently handled a request for exactly this purpose, coming by a software company that was developing a GPS navigation software that could not load some map files in WinMo6.1), but in general the whole operating system has gained a stability that hadn’t before...

To conclude, the tool I mentioned was developed by Symbol and I don’t think it’s publicly available. But a similar tool has recently been published on CodePlex (source code included!) through the article Visualizing the Windows Mobile Virtual Memory Monster. The term "Virtual Memory Monster" was invented years ago by Reed Robison… (part 1 e part 2). I've already been using it in a couple of requests and highly recommend it!

TROUBLESHOOTING MEMORY LEAKS FOR NETCF APPLICATIONS

Instead of explaining how things work in theory, which is a task I leave to more authoritative sources like the documentation itself and various blogs – one for all that of Abhinaba Basu, who is precisely the GC Guru inside the NETCF Dev Team (Back to basic: Series on dynamic memory management), I’d like to follow the troubleshooting flow I run through when a new Service Request arrives, about for example the following issues:

  • OutOfMemoryException
  • SqlCeException: “Not enough memory to complete this operation”
  • The application can’t load a DLL
  • Other misbehaviors that may lead thinking about a problem with the memory

Firstly, we must determine whether the problem is specific to an OEM. The best approach, when possible, is to verify if the error occurs even on emulators contained in the various Windows Mobile SDK. If not, the help that Microsoft Technical Support can provide is limited, as it is possible that the error is due to a customization of the Windows Mobile platform by the OEM. In this case, it may be helpful to know about what I wrote about device.exe above.

Another initial step, in the case of applications NETCF v2 SP2, is to check if just running the application on NETCF v3.5 gives any improvement. There is no need to recompile the application with the Visual Studio 2008 - just like for .NET Desktop applications, add a configuration XML file in the same folder that contains the file TheApplication.exe, named TheApplication.exe.config and whose content is simply (I mentioned here):

<configuration>
  <startup>
    <supportedRuntime version="v3.5.*"/>
  </startup>
</configuration>

So, after having considered possible “trivial” causes, you can proceed to the analysis... Historically NETCF developers haven’t had an easy time in troubleshooting due to lack of appropriate tools – unlike Desktop cousins! – but over the years Microsoft has released tools that have gradually evolved over the current Power Toys for .NET Compact Framework 3.5. Apart from these you must know the(freeware!) EQATEC’s ones (Tracer and Profiler) and recently a tool on CodeProject that I mentioned earlier, that displays the status of virtual memory (VirtualMemory, with source code).

Regarding power-toys, when you are dealing with a problem around memory, you have 2 of them that are of great help: the "CLR Profiler" and "Remote Performance Monitor (RPM)”. The first one is useful in visually making problems with objects’ allocation almost immediate and allows you to notice the problem in a visual way. Info on how using it are available through The CLR Profiler for the .Net Compact Framework Series Index. The second one provides, both in real time and through an analysis a posteriori, counters about the usage of MANAGED memory; also, through the "GC Heap Viewer” it allows not only to study the exact content of the managed heap, but also allows you to compare contents of the heap in different moments, in order to bring out a possible unexpected growth of a certain type of objects. Some images are available on Finding Managed Memory leaks using the .Net CF Remote Performance Monitor, which is useful also to get an idea about which counters are available, while a list and related explanations are provided on Monitoring Application Performance on the .NET Compact Framework - Table of Contents and Index. What I'd like to do here is not to repeat the same explanations, already detailed in the links above, but share some practical experience...

For example, in the vast majority of cases I have handled about memory leaks, the problem was due to Forms (or Controls) that unexpectedly were NOT removed by the Garbage Collector. The instances of the Form class of the application are therefore the first thing to check through the Remote Performance Monitor and GC Heap Viewer. For this reason, where appropriate (e.g. if the total form are "not so many"), to avoid memory problems with NETCF applications it may be useful to adopt the so-called "Singleton Pattern": this way a single managed instance of a given form will exist throughout the application life cycle.

So, supposing to be in the following situation: I used the Remote Performance Monitor and saved different .GCLOG files during normal use of the application, and thanks to the GC Heap Viewer I noticed that an unexpected number of forms stays in memory, and also that this increases during the life of the application, although there have been a certain number of Garbage Collections. Why the memory of a Form is not cleaned up by the garbage collector? Thanks to the GC Heap Viewer you can know exactly who maintains a reference to what, in the "Root View" on the right pane. Obviously knowing application’s architecture will help in identifying unexpected roots.

A special consideration must be done for MODAL Forms in .NET (the dialogs, those that on Windows Mobile have the close button "Ok" instead of "X", and which permits a developer to prevent the user to return to the previous form). In many cases I have handled, the problem was simply due to the fact that the code was not invoking Close() (or. Dispose ()) after .ShowDialog():

Form2 f2 = new Form2();
f2.ShowDialog();
f2.Close();

Why should it matter? Because often (not always, for example, not when you expect a DialogResult) on Windows Mobile the user clicks on 'Ok' in the top right "close" the dialog. Also on Desktop, when a dialog is "closed" in this way the window is not closed, but "hidden"! And it could happen that the code creates a new instance of the form, without removing the old one in memory. It’s documented in “Form..::.ShowDialog Method” (the doc talks about “X” but of course for Windows Mobile refers to the 'Ok' referred to above):

[…] When a form is displayed as a modal dialog box, clicking the Close button (the button with an X at the upper-right corner of the form) causes the form to be hidden and the DialogResult property to be set to DialogResult.Cancel. Unlike modeless forms, the Close method is not called by the .NET Framework when the user clicks the close form button of a dialog box or sets the value of the DialogResult property. Instead the form is hidden and can be shown again without creating a new instance of the dialog box. Because a form displayed as a dialog box is not closed, you must call the Dispose method of the form when the form is no longer needed by your application.

Anyway, we assumed so far that the memory leak is MANAGED, but in reality it may be that leak is with the NATIVE resources that are used by a .NET instance, which have not been successfully released by implementing the so-called "IDisposable Pattern. And around this there are some peculiarities in NETCF, that Desktop-developers don’t need to worry about, particularly with respect to SQL Compact objects and "graphical" objects, i.e. classes of the System.Drawing namespace. In NETCF the Font, Image, Bitmap, Pen, Brush objects are simple wrappers around their native resources, which in the Windows CE-based operating systems are handled by the GWES (Graphics, Windowing and Event Subsystem). What does this mean? It means that in their own .Dispose() they effectively release their native resources, and therefore one *must invoke .Dispose() for Drawing objects* (or invoke methods that indirectly call it, for example .Clear() in ImageList.ImageCollection – which has not the .Dispose() itself). Note that among the counters provided by the Remote Performance Monitor, the category "Windows.Forms" contains indeed:

  • Controls Created
  • Brushes Created
  • Pens Created
  • Bitmaps Created
  • Regions Created
  • Fonts Created
  • Graphics Created (FromImage)
  • Graphics Created (CreateGraphics)

Note that I’m not talking about only objects directly “born” as Brush, Pen, etc.. I’m talking also about those objects whose properties contain graphic objects, such as a PictureBox or ImageList (or indirectly, an ImageList of a ToolBar). So, when you close a form, remember to:

this.ImageList1.Images.Clear();
this.ToolBar1.ImageList.Images.Clear();
this.PictureBox1.Image.Dispose();
//etc...

Finally, still about Forms, a simple technique I often used to identify possible problems with items not properly released at the closing of a form has been to emulate user interaction by “automatic” opening and closing of the form. I’m purely talking about a test code like this:

int N = 1000;
for (int i = 1; i <= N; i++)
{
   if (i % 200 == 0)  MessageBox.Show(i.ToString()); //just to know it's running
   // and to block execution in case you want to save counters (.STAT)
   // and GC Heap View (.GCLOG)    
   frmTest frm = new frmTest(/*...*/);    
   frm.Show();    
   frm.Close();    
   frm.Dispose(); 
}
MessageBox.Show("Over. Done. Finito.");

After running the loop N times, the Remote Performance Monitor will be of considerable help to see what is going wrong... Nerd

A final note before concluding this paragraph. It may be that an application is so complex to require "a lot of" virtual memory. This would not be a problem, as long as there is room for the lines "green" in my previous post. But requiring "a lot of" memory means that the Garbage Collector will get kicked more frequently, thus impacting general application performance (because the GC must first “lock” the threads in a safe state). The point is that if the application is so complex to require too frequent use of garbage collection (and therefore performance may not be acceptable by end-users), then it might be worthwhile to split the application into 2 parts, such as one for memory dog-guard and another for the user interface. This process at the cost of an additional process slot, but often it is something that can be paid. Or, since the managed DLLs are loaded in the Large Memory Area without wasting precious process’ address space, an idea would be to place all classes, even those of the form, not in the EXE but in the DLLs! A simple yet very effective idea, which Rob Tiffany has discussed about in his post MemMaker for the .NET Compact Framework

Enjoy!
~raffaele

  • Yet another demonstration of the added value provided by the Technical Support
  • A very impressive example of collaboration between Technical Support and Dev Team
  • FIX is public now (link to KB Article)

A few weeks ago I’ve started handling a Service Request coming from a ISV whose NETCF v3.5 Application relied on the WebBrowser control to display a HTML FORM with some links on it… Imagine simply the following form’s code:

private string m_Html = 
"<html><body>" +
"<a href=\"http://www.msn.com\">MSN</a><BR>" +
"<a href=\"http://www.msn.com\">MSN</a><BR>" +
"<a href=\"http://www.msn.com\">MSN    </a><a href=\"http://www.msn.com\">MSN    </a><BR>" +
"<a href=\"http://www.msn.com\">MSN    </a><a href=\"http://www.msn.com\">MSN    </a><a href=\"http://www.msn.com\">MSN</a><BR>" +
"<a href=\"http://www.msn.com\">MSN</a><BR>" +
"</body></html>";

private void menuItem2_Click(object sender, EventArgs e)
{
       this.webBrowser1.DocumentText = m_Html;
}

Problem is that, after the HTML is loaded, when using navigation “buttons” (up\down\left\right):

  • On WM6 I can navigate through the links: this is the expected behavior… and when pressing the action button the control navigates to the href link.
  • On WM6.1.4 the whole page is moved instead; and when tapping on a link, nothing happens (the expected behavior was that the control navigates) apart from “giving focus” to the link: at this point even if you press the action button the control doesn’t navigate
  • On WM6.5 the behavior is similar to 6.1.4’s, and here even horizontal and vertical scrollbars don’t appear

The problem is easily reproduced on the Emulators as well, thus demonstrating that the issue is not OEM-ish…

I also found at least 2 other developers reporting the problem through Microsoft Connect:

Troubleshooting steps included verifying which native control is wrapped by the NETCF v3.5’s WebBrowser: I noticed that when using the NATIVE code Mike Francis provided in his blog post (code sample available here), then I get the desired behavior on all the 3 Emulators. Mike’s code is based on the WM6’s SDK Sample named “Browse” (\Samples\CPP\Win32\Browse\browse) and explains how to add gesture- and selection-support to a native application hosting the NATIVE web-browser control.

So, the problem was just with the NETCF v3.5’s WebBrowser here! And interestingly it’ NOT reproduced with NETCF v2 SP2 that ships in-ROM on WM6.x. So… what are the loaded DLLs (i.e. what is the native control wrapped by the NETCF v3.5’s WebBrowser)? And which Windows? [I was intereseted on Windows’ ClassNames due to a possible technique to customize the WebBrowser’s context-menu, which I discussed here]

using
NETCF v3.5 WebBrowser

WM6

WM6.1.4

WM6.5

WND ClassName PIEHTML Internet Explorer_Server Internet Explorer_server
Loaded DLL webview shdocvw shdocvw

 

using
Native Control (Mike’s code)

WM6

WM6.1.4

WM6.5

WND ClassName PIEHTML PIEHTML PIEHTML
Loaded DLL webview + htmlview webview + htmlview webview + htmlview

 

To recap: the issues are due to the NETCF v3.5’s WebBrowser control loading the new native IE6on6 (shdocvw.dll) instead of relying on the native webview.dll + htmlview.dll, and this makes impossible for the user to (a) click a href link and (b) to navigate through them by using the up\down\left\right pad. Also, (c) on 6.5 the scrollbars are not created. When loading the same HTML by the same NETCF application on a WM6, the behavior is as expected. There’s NO problem on WM6.1.4 and 6.5 when using the native control directly (htmlview.dll and webview.dll), for example through the WM6 SDK Sample \Samples\CPP\Win32\Browse\browse.

What happened at this point? As I mentioned other times (for example here), one of the added values provided by Technical Support is that we have a channel to the Dev Teams which we can use to advocate final users’ needs. You can imagine how many customers the Technical Support reaches every single day: we’re basically one of the ears of the Product Groups. And also this time I received an impressive collaboration from  the NETCF Dev Team, which firstly accepted to package the FIX, and then did it in a very few days: this is not the usual way it works, so if you’ll ever request a fix then don’t expect this in general. As you can imagine this depends on the code that needs to be modified, in terms of both the complexity and the possible regression risks that a modification introduces, compared to the risks of application usability and other factors if the fix is not provided. For example, many times developers and users can accept a “workaround” while waiting for next version of the product or the possible next Service Pack – I’ve discussed about an example of this here.

To conclude: a FIX is ready to be asked to the Technical Support. KB Article is:

FIX: You cannot scroll through a Web page or browse to a link by using a .NET Compact Framework 3.5-based application that hosts a WebBrowser control in Windows Mobile 6.1.4 and 6.5

For the time being it’s a separate hotfix, maybe the Dev team will consider packaging it within a Service Pack – this is not yet decided. This is another demonstration of a kind of historical change, as this is just the second separate hotfix provided for NETCF on Windows Mobile-based OSs. First one was release few months ago and I blogged about it here.

A question may arise: can ISVs distribute the FIX? I mean, imagine I’m an ISV and developed a NETCF v3.5 app based on the WebBrowser control: if I want to make my app usable on WM6.1.4 and 6.5 devices of my clients, do I need to ask each of them to open a Service Request to Microsoft asking for the hotfix? The answer is: ISVs can distribute also the “patched” NETCF’s CAB.

WARNING! Remember that analogously to every other hotfix you can ask to Technical Support about whatever Microsoft’s product, you should install it only if you're sure it’s for the problem you're being affected by. This is because a hotfix is meant to address a specific issue and has been tested precisely for that: it didn't go through the whole regression testing that it's usually done when packaging many hotfixes into a Service Pack. In this precise case, you don’t have to install this hotfix if your NETCF v3.5 applications doesn’t use the WebBrowser control.

 

Cheers,
~raffaele

Unfortunately I had to put GPS-Programming in stand-by mode… Sad but finally some days ago I was reading one of the posts I wrote about GPS roughly one year ago, and tried to navigate to a company’s webpage I remembered containing a lot of interesting info and controls about GPS, i.e. GeoFrameworks: well, I realized that GeoFrameworks is now closed! Luckily their Mastering GPS Programming is still available, but, much better… the founder of GeoFrameworks, Jon Person, “[…] decided to release the full source code of GPS.NET to the public domain for the benefit of the open source development community”. If you’re interested on GPS-Programming, you must visit http://gps3.codeplex.com!

Cheers,
~raffaele

[UPDATE 10/9/2009: Source-Code and CAB are now available at http://mobilerdpnotimeout.codeplex.com/

  • Error Message: “The remote session was ended because the idle timeout limit was reached. This limit is set by the server administrator or by network policy.”
  • RDP\TSC Clients and Windows Mobile OEMs
  • What’s new in Windows Mobile 6.5 about Remote Desktop Mobile

This issue should be somehow well known by the Community, but I couldn’t easily find a solution so I had to think one by myself… :-) After 10 minutes of inactivity of a Terminal Server session on Windows Mobile devices, the Remote Desktop Mobile application needs you to logon again stating that a timeout “set by the server administrator or by network policy” occurred. Unfortunately it’s not really a good error message, since many times the server is correctly set and the problem is just client-side... Anyway, probably it was not very frustrating since in the most of the cases one is logged via RDP only to actively work on the server. But this is not always true, so I’d guess either I wasn’t able to find someone with a possible solution, either simply people accepted to live with it…

Remote Desktop Mobile on WM6.x (and, before this, the TSC client on WM5) is a component that OEMs are not required to include to have their OSs pass the Windows Mobile Logo Test Kit, and this is basically why you may find devices with it in-ROM and others where you need to find alternative solutions. In any case, regarding the inactivity timeout issue, luckily Windows Mobile 6.5 _should_ now have an updated version of the Remote Desktop Mobile application that OEMs can include in their images, easier to use on the field as it allows, for example, to finally customize an idle timeout!! Unfortunately it’s not included in the emulators coming with the Windows Mobile 6.5 Developer Tool Kit.

Back to the problem… after some researches I understood that the timer is reset only when the Remote Desktop Mobile’s application window receives mouse or keyboard input. Refreshing the window, or sending exotic WM_xx messages wasn’t enough. So, I came to thinking what was a possible keypress or mouseevent that can be programmatically simulated (through keybd_event() or mouse_event()) that wouldn’t impact application at all?? After some tests I’ve realized that MOUSEEVENTF_MOVE, which is the event of user “moving the mouse”, has no effect on current Windows Mobile-based devices: that event is maintained as codebase with Windows CE, where an OEM may also include a non-touch screen where you really have a mouse to move... but its only effect on Windows Mobile device is for example to modify display’s brightness, in case the display entered a sort of power-safe state (well, obviously the mouse-pointer is moved on the remote server)

So, the idea was to develop a Win32 Console application that simply launches the Remote Desktop Mobile application and every X minutes (5? 9?) simulates a mouse-movement of some pixels and after a short time another one to place the mouse on the previous position.

This is NOT elegant code at all, absolutely, but it does what was meant to run a specific task… Nerd

Obviously this watch-dog application would “waste” one of the 30 process slots… is it acceptable? As you can see the application doesn’t do anything special or unsupported: it’s simply a monitor-console application with no particularly elegant code. As usual, I would strongly encourage on adding some error-checking!!


int _tmain(int argc, _TCHAR* argv[])
{
    int const FIVEMINUTES = 1000*60*5;
    HWND hWndRDM = NULL;
    
    //Firstly launch RDP Client
    SHELLEXECUTEINFO sei = {0};
    sei.cbSize = sizeof(sei);
    sei.nShow = SW_SHOWNORMAL; 
    sei.lpFile = TEXT("\\Windows\\wpctsc.exe");
    sei.lpParameters = TEXT(" ");
    if (!ShellExecuteEx(&sei))
    {
        MessageBox(NULL, TEXT("Couldn't launch RDP Client"), TEXT("Remote Desktop Launcher"), MB_OK);
        goto Exit;
    }


    //Now every X minutes check if it's still running and if so "refresh" its window
    //if it's no longer running, exit
    do 
    {
        //check if RDP Client is running, otherwise exit
        hWndRDM = FindWindow(_T("TSSHELLWND"), NULL);
        if (NULL != hWndRDM)
        {
            ////Get foreground window -- this is not needed if RDM is launched Full-Screen as it was in this case
            //hWndForeground = GetForegroundWindow();
            //Sleep(500);

            //Give focus to the RDP Client window (even if the logon screen, in case user logged out in the meantime)
            SetForegroundWindow(hWndRDM);

            //The timer is reset when the rdp window receives mouse or keyboard input
            //with no MOUSEEVENTF_ABSOLUTE the move is relative

            mouse_event(MOUSEEVENTF_MOVE, 100, 0, 0, 0);
            Sleep(250);
            mouse_event(MOUSEEVENTF_MOVE, -100, 0, 0, 0);

            ////Give focus back to previous foreground window
            //SetForegroundWindow(hWndForeground);

            //Sleep 
            Sleep(FIVEMINUTES);
        }
        else 
        {
            //RDP Client is not running - let's exit
            goto Exit;
        }
    }
    while (TRUE); //The check (NULL != hWndRDM) is already done inside the loop


Exit:
    if (NULL != hWndRDM)
        CloseHandle(hWndRDM);
    
    return 0;
}
 

I can imagine that this problem affects more the WinMo Users rather than the WinMo “Devs”, therefore I had in mind to distribute directly the CAB of the application, if anyone else doesn’t want to do it for the Community. I’m wondering if CodePlex would be a good place, I’ll see and update this post. In any case, remember that it’s up to the OEMs to decide if including RDM (\Windows\wpctsc.exe) in their OSs based on Windows Mobile platform!

Cheers!
~raffaele

Yesterday my Italian colleagues in Developer Support and I officially inaugurated our new team blog: if you can read Italian then join us at http://blogs.msdn.com/itasupport. Hot

CIAO!
~raffaele

I thought this was something well known by the Mobile Dev Community, but maybe this is not true, since I’ve recently worked on a similar case.. so it may be worth sharing it here!

Under some special circumstances, which weren’t possible to reproduce for Technical Support and NETCF Dev Team at that time (about 2-3 years ago) – also because it was resolved in NETCF v3.5 – it happened that on NETCF v2 applications when a form gains back the focus after it was given to a dialog (MessageBox.Show() or anotherForm.ShowDialog()), then _sometimes_ that little squared SIP icon at the middle of the menubar at the bottom suddenly disappeared.

That nasty problem was due to some faulty interaction between the OS and the .NET runtime, in any case no longer reproduced with v3.5. In some cases it had sufficed to reset this.Menu (or this.Parent.Menu) to the main menu. But that wasn’t the case in our service request… and also the simple refresh of the form at form’s OnActivated was NOT enough:

protected override void OnActivated(EventArgs e)
{
     this.Menu = mainMnu;
     this.Refresh();

     base.OnActivated(e);
}

So we had to find a satisfactory temporary solution for NETCF v2 applications (migration to v3.5 was already in progress..). As usual I proposed to play with WINDOWS… :-) and we came up with the following propositions – the first one did the trick! (remember to add error-handling to whatever code I may suggest!!)

  1. FindWindow + ShowWindow
      protected override void OnActivated(EventArgs e)
      {
          IntPtr hSip = FindWindow("MS_SIPBUTTON", "MS_SIPBUTTON");
          if ((hSip != IntPtr.Zero) /*&& (!IsWindowVisible(hSip))) */
          {
              bool retShowWin = ShowWindow(hSip, EShowWindow.SW_SHOW);
          }
          base.OnActivated(e);
      }
      
      
      [DllImport("coredll.dll", SetLastError = true)]
      private static extern IntPtr FindWindow(string _ClassName, string _WindowName);
      
      [DllImport("coredll.dll", SetLastError = true)]
      private static extern bool IsWindowVisible(IntPtr hWnd);
      
      [DllImport("coredll.dll", SetLastError = true)]
      private static extern bool ShowWindow(IntPtr hwnd, EShowWindow nCmdShow);
      
      private enum EShowWindow : uint
      { 
          SW_HIDE = 0,
          SW_SHOWNORMAL = 1,
          //SW_SHOWNOACTIVATE = 4,
          SW_SHOW           = 5 ,
          //SW_MINIMIZE       = 6,
          SW_SHOWNA         = 8 //,
          //SW_SHOWMAXIMIZED  = 11,
          //SW_MAXIMIZE           = 12,
          //SW_RESTORE            = 13
      }
       
  2. FindWindow + BringWindowToTop
      protected override void OnActivated(EventArgs e)
      {
          IntPtr hSip = FindWindow("MS_SIPBUTTON", "MS_SIPBUTTON");
          if ((hSip != IntPtr.Zero) /*&& (!IsWindowVisible(hSip))) */
          {
              bool retBringWin = BringWindowToTop(hSip);
          }
          base.OnActivated(e);
      }
      
      
      [DllImport("coredll.dll", SetLastError = true)]
      private static extern IntPtr FindWindow(string _ClassName, string _WindowName);
      
      [DllImport("coredll.dll", SetLastError = true)]
      private static extern bool IsWindowVisible(IntPtr hWnd);
      
      [DllImport("coredll.dll", SetLastError = true)]
      private static extern bool BringWindowToTop(IntPtr hWnd);
       
  3. FindWindow + GetWindowLong
      protected override void OnActivated(EventArgs e)
      {
          IntPtr hSip = FindWindow("MS_SIPBUTTON", "MS_SIPBUTTON");
          if ((hSip != IntPtr.Zero) /*&& (!IsWindowVisible(hSip))) */
          { 
              int style = GetWindowLong(hSip, GWL_STYLE);
              style |= WS_POPUP | WS_VISIBLE;
              int retSetWin = SetWindowLong(hSip, GWL_STYLE, style);
      
              int exstyle = GetWindowLong(hSip, GWL_EXSTYLE);
              exstyle |= WS_EX_ABOVESTARTUP | WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW | WS_EX_TOPMOST;
              retSetWin = SetWindowLong(hSip, GWL_EXSTYLE, exstyle);
          }
          base.OnActivated(e);
      }
      
      
      [DllImport("coredll.dll", SetLastError = true)]
      private static extern IntPtr FindWindow(string _ClassName, string _WindowName);
      
      [DllImport("coredll.dll", SetLastError = true)]
      private static extern bool IsWindowVisible(IntPtr hWnd);
      
      private const int GWL_EXSTYLE = (-20);
      private const int GWL_STYLE = (-16);
      
      [DllImport("coredll", SetLastError = true)]
      private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
      
      [DllImport("coredll", SetLastError = true)]
      private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
      
      private const int
          WS_POPUP = unchecked((int)0x80000000),
          WS_VISIBLE = 0x10000000,
          WS_EX_ABOVESTARTUP = 0x20000000,
          WS_EX_NOACTIVATE = 0x08000000,
          WS_EX_TOOLWINDOW = 0x00000080,
          WS_EX_TOPMOST = 0x00000008;

  

Cheers,
~raffaele

  • GetSystemMetrics or GetDeviceCaps inside a CAB’s Setup.dll
  • wceload.exe is not resolution- and DPI-aware
  • RESDLL SDK Sample 

Sincerely I haven’t easily found entries about this over the web, so I think it’s worth mentioning it. Maybe many know, but possible many others don’t… smile_regular Imagine you want to perform different actions during install depending on the device’s resolution. A possible code to retrieve resolution can use GetSystemMetrics or GetDeviceCaps. My troubleshooting, as initial step, was as usual to verify if I could reproduce ISV’s problem, even on Device Emulator (thus avoiding possible problems related to OEMs’ customizations). So I included the following in a CAB’s Setup.dll’s Install_Init (Install_Exit is the same):

        HRESULT hr = S_OK;

        LPTSTR pszBuf = new TCHAR[256];
        ZeroMemory(pszBuf, 255);

        int X = GetSystemMetrics(SM_CXSCREEN);
        int Y = GetSystemMetrics(SM_CYSCREEN);

        hr = StringCchPrintf(pszBuf, 
                        LocalSize(pszBuf) / sizeof(TCHAR),
                        TEXT("X: %d\r\nY: %d"), 
                        X, Y);
        CHR(hr);

        MessageBox(hwndParent, pszBuf, TEXT("Install_Exit"), MB_OK);

    Exit:
        return codeINSTALL_INIT_CONTINUE; //or codeINSTALL_EXIT_DONE for Install_Exit

Specifically:

  • on a VGA WM6 Emulator (640x480) it returned 320x240
  • on a square VGA WM6 Emulator (480x480) it returned 240x240
  • on a square QVGA WM6 Emulator (320x320) it returned 240x240
  • on a WM6.1.4 EMULATOR (480x800) it returned 240x400

When using precisely the same code in a Win32 Smart Device Console application, the values returned were the expected ones…

After working with a colleague (thanks Manfred! smile_regular) I got the confirmation about an idea… the problem here is that a CAB’s Setup.dll gets loaded by wceload.exe when installing the CAB, and wceload.exe is not resolution- and DPI-aware . Therefore when setup.dll’s code runs inside the wceload’s context it can’t successfully call GetSystemMetrics or also GetDeviceCaps.

It turned out that to address this limitation, a sample is provided in the SDKs explaining how to do this: "C:\Program Files (x86)\Windows Mobile 6 SDK\Samples\PocketPC\CPP\win32\resdll" (MSDN Doc here). Notice that setup.dll’s Install_Exit\_Init containing code to run an external DPI-aware application, whose sample code is also provided.

Cheers,
~raffaele

I've discussed about the “wrong” approach in a previous post of mine, where I also talked about why using PIMPR_SMARTPROP to retrieve the info about the last way a Windows Mobile-device user communicated with a given contact. Recently 2 MSDN Forums users asked for help about this (“How to get the information of a selected phonecall number?” and “How to read LastNumber in contact”) and therefore I wanted to invest some time for the Community, hoping this may help others as well! smile_shades (and also to play with POOM, since managed APIs wrapped so many properties making POOM _quite_ obsolete…)

Before spreading the code, let me state a thing: the SmartProp property is not set for a contact until the first time the user
explicitly changes the default contact method, or it is otherwise explicitly set by an application
. I noticed this when coding this sample, and later I understood that this is the expected behavior.

Ehy, remember that this is not production code: this is for testing\didactic purposes only… indeed the code is simply meant to dump out to a text-file only the First Name, Last Name and the string representing the last way user communicated with the selected contact. The code doesn’t even use the IPOutlookItemCollection::Find to get a specific contact, as this was not the goal here. Yet, I think it was worth sharing as it is, so that who wants can customize it… smile_regular enjoy!!

 

NOTES:

  1. PIMPR_SMARTPROP is a property of the IItem, not of the IContact: that’s why for example the old POOM NETCF sample code didn’t contain it. So what we need is to retrieve the collection of Contacts and handle them as IItem.
  2. Once you have all the contacts as a collection (IPOutlookItemCollection), you can’t directly retrieve each item – even if IItem implements IDispatch, the right way is the one for example mentioned by my colleague Xiaoyun Li here, i.e. invoke IPOutlookItemCollection::Item by passing a IContact as IDispatch, then call IContact::get_Oid to retrieve the unique OID and finally use this with IPOutlookApp2::GetItemFromOidEx (see function DumpOutToText below).
  3. At this point you have an IItem object and can query its properties in the usual old way, considering that PIMPR_SMARTPROP will return the property id (e.g. PIMPR_MOBILE_TELEPHONE_NUMBER, PIMPR_SMS, etc); all the propIDs that can be returned as smart prop are listed in the documentation here.
  4. For each returned property, remember to check if it was really found (.wFlags != CEDB_PROPNOTFOUND) and if it’s of the expected data type (.propid == CEVT_LPWSTR).

 

If you know of any smarter way I’ll be more than welcome on continuing this saga about PIMPR_SMARTPROP… smile_teeth

 

#include "stdafx.h"
#include <pimstore.h>

// **************************************************************************
// Globals
IPOutlookApp2 * g_polApp = NULL;
IUnknown * g_pUnknown = NULL;
LPCTSTR g_pszFilename = TEXT("contacts.txt");

// **************************************************************************
//Functions
HRESULT InitPoom(void);
HRESULT GetPoomFolder(int nFolder, IFolder ** ppFolder);
HRESULT FillFileWithContacts(void);
HRESULT DumpOutToText(IPOutlookItemCollection * pItemCol);
HRESULT WriteItemSmartProp(IItem *pItem);
HRESULT Log(LPTSTR szLog);
HRESULT LogToFile(LPTSTR szLog, LPCTSTR pszFilename);

// **************************************************************************
//MAIN
int _tmain(int argc, _TCHAR* argv[])
{
    HRESULT hr = E_FAIL;

    //Initialize POOM
    hr = InitPoom();
    CHR(hr);

    //Fill file with Contacts
    hr = FillFileWithContacts();
    CHR(hr);
    
    //Success
    MessageBox(NULL, TEXT("Done"), TEXT("Test"), MB_OK);

Exit:
    return 0;
}


// ************************************************************************** 
//InitPoom
HRESULT InitPoom(void) 
{
    HRESULT hr = E_FAIL;

    hr = CoInitializeEx(NULL, 0);
    CHR(hr);

    hr = CoCreateInstance(CLSID_Application, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, (void **)&g_pUnknown);
    CHR(hr);

    hr = g_pUnknown->QueryInterface(IID_IPOutlookApp, (void**)&g_polApp); 
    CHR(hr);

    hr = g_polApp->Logon(NULL);
    CHR(hr);

    //success
    hr = S_OK;

Exit:
    RELEASE_OBJ(g_polApp);
    return hr;
}


// ************************************************************************** 
//FillFileWithContacts
HRESULT FillFileWithContacts(void)
{
    HRESULT hr = E_FAIL;
    IFolder * pCurrFldr = NULL;
    IPOutlookItemCollection * pItemCol = NULL;
    
    // Get the Contacts folder and its items
    hr = GetPoomFolder(olFolderContacts, &pCurrFldr);
    CHR(hr);
        
    if (SUCCEEDED(pCurrFldr->get_Items(&pItemCol)))
    {
        //Dump out First Name, Last Name, SMARTPROP for each item
        hr = DumpOutToText(pItemCol);
        CHR(hr);
    }

    //success 
    hr = S_OK;

Exit:
    RELEASE_OBJ(pItemCol);
    RELEASE_OBJ(pCurrFldr);
    return hr;
}


// **************************************************************************
//GetPoomFolder
HRESULT GetPoomFolder(int nFolder, IFolder ** ppFolder)
{
    HRESULT hr = E_FAIL;
    if (SUCCEEDED(g_polApp->GetDefaultFolder(nFolder, ppFolder)))
    {
        hr = S_OK;
    }

    return hr;
}


// ************************************************************************** 
//DumpOutToText
HRESULT DumpOutToText(IPOutlookItemCollection * pItemCol)
{
    HRESULT hr = E_FAIL;
    IContact * pContact = NULL;
    IItem * pItem = NULL;
    CEOID oid = 0;   
    int cItems = 0;

    //Count contacts
    pItemCol->get_Count(&cItems);    
    for (int i = 1; i <= cItems; i++)
    {
        //get the item as a IContact
        if (SUCCEEDED(pItemCol->Item (i, reinterpret_cast<IDispatch**>(&pContact))))
        {
            //convert the IContact into a IItem...
            if (SUCCEEDED(pContact->get_Oid((long*)&oid)))
            {
                //... by using the Oid
                if (SUCCEEDED(g_polApp->GetItemFromOidEx(oid, 0, &pItem)))
                {
                    //Write item's properties to file
                    hr = WriteItemSmartProp(pItem);
                    
                    RELEASE_OBJ(pItem);
                    CHR(hr);
                }
            }
        }
    }

    //success
    hr = S_OK;

Exit:
    return hr;
}


// **************************************************************************
//GetItemSmartProp
HRESULT WriteItemSmartProp(IItem *pItem)
{
    HRESULT hr = E_FAIL;
    int cProps = 20;
    CEPROPID rgPropId[20] = {0};
    CEPROPVAL *prgPropvalUser = NULL;
    ULONG cbBuffer = 0;

    // FROM http://msdn.microsoft.com/en-us/library/bb415504.aspx
    // The Smart Property (PIMPR_SMARTPROP) is the Contact property that contains the property ID of the default communication mode.
    rgPropId[0] = PIMPR_FIRST_NAME; //type: CEVT_LPWSTR
    rgPropId[1] = PIMPR_LAST_NAME;  //type: CEVT_LPWSTR
    rgPropId[2] = PIMPR_SMARTPROP;  //type: CEVT_UI4

    //all of the following are of type: CEVT_LPWSTR
    rgPropId[3] = PIMPR_IM2_ADDRESS;                    
    rgPropId[4] = PIMPR_ASSISTANT_TELEPHONE_NUMBER;        
    rgPropId[5] = PIMPR_BUSINESS_TELEPHONE_NUMBER;        
    rgPropId[6] = PIMPR_BUSINESS2_TELEPHONE_NUMBER;        
    rgPropId[7] = PIMPR_CAR_TELEPHONE_NUMBER;            
    rgPropId[8] = PIMPR_COMPANY_TELEPHONE_NUMBER;        
    rgPropId[9] = PIMPR_EMAIL1_ADDRESS;                    
    rgPropId[10] = PIMPR_EMAIL2_ADDRESS;                    
    rgPropId[11] = PIMPR_EMAIL3_ADDRESS;                
    rgPropId[12] = PIMPR_HOME_TELEPHONE_NUMBER;            
    rgPropId[13] = PIMPR_HOME2_TELEPHONE_NUMBER;        
    rgPropId[14] = PIMPR_IM1_ADDRESS;                                        
    rgPropId[15] = PIMPR_IM3_ADDRESS;                    
    rgPropId[16] = PIMPR_MOBILE_TELEPHONE_NUMBER;        
    rgPropId[17] = PIMPR_PAGER_NUMBER;                    
    rgPropId[18] = PIMPR_RADIO_TELEPHONE_NUMBER;        
    rgPropId[19] = PIMPR_SMS;                            


    //FROM: http://msdn.microsoft.com/en-us/library/ms859378.aspx
    // Allocate memory, then get item properties
    cbBuffer = 0;
    hr = pItem->GetProps(rgPropId, 0, cProps, &prgPropvalUser, &cbBuffer, NULL);
    if(HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER) == hr)
    {
        prgPropvalUser = (CEPROPVAL *) LocalAlloc(0, cbBuffer); 
    }

    // cbBuffer is set to the number of bytes required to hold the data.
    hr = pItem->GetProps(rgPropId, 0, cProps, (CEPROPVAL **)&prgPropvalUser, &cbBuffer, NULL);

    //better error-checking to do here...
    if(FAILED(hr) || 0 == cbBuffer)
    {
        goto Exit;
    }

    if(prgPropvalUser[0].wFlags!=CEDB_PROPNOTFOUND)   
    {   
        if(LOWORD(prgPropvalUser[0].propid) == CEVT_LPWSTR)
        {
            Log(prgPropvalUser[0].val.lpwstr);
            Log(TEXT("\t"));
        }
    }

    if(prgPropvalUser[1].wFlags!=CEDB_PROPNOTFOUND)   
    {   
        if(LOWORD(prgPropvalUser[1].propid) == CEVT_LPWSTR)  
        {
            Log(prgPropvalUser[1].val.lpwstr);
            Log(TEXT("\t"));
        }
    }

    if(prgPropvalUser[2].wFlags!=CEDB_PROPNOTFOUND)   
    {   
        if(LOWORD(prgPropvalUser[2].propid) == CEVT_UI4)   
        {
            switch (prgPropvalUser[2].val.ulVal)
            {
                case PIMPR_IM2_ADDRESS:
                    if(prgPropvalUser[3].wFlags!=CEDB_PROPNOTFOUND)   
                    {   
                        if(LOWORD(prgPropvalUser[3].propid) == CEVT_LPWSTR)   
                        {
                            Log(prgPropvalUser[3].val.lpwstr);
                        }
                    }
                    break;
                case PIMPR_ASSISTANT_TELEPHONE_NUMBER:
                    if(prgPropvalUser[4].wFlags!=CEDB_PROPNOTFOUND)   
                    {   
                        if(LOWORD(prgPropvalUser[4].propid) == CEVT_LPWSTR)   
                        {
                            Log(prgPropvalUser[4].val.lpwstr);
                        }
                    }
                    break;
                case PIMPR_BUSINESS_TELEPHONE_NUMBER:    
                    if(prgPropvalUser[5].wFlags!=CEDB_PROPNOTFOUND)   
                    {   
                        if(LOWORD(prgPropvalUser[5].propid) == CEVT_LPWSTR)   
                        {
                            Log(prgPropvalUser[5].val.lpwstr);
                        }
                    }
                    break;
                case PIMPR_BUSINESS2_TELEPHONE_NUMBER:    
                    if(prgPropvalUser[6].wFlags!=CEDB_PROPNOTFOUND)   
                    {   
                        if(LOWORD(prgPropvalUser[6].propid) == CEVT_LPWSTR)   
                        {
                            Log(prgPropvalUser[6].val.lpwstr);
                        }
                    }
                    break;
                case PIMPR_CAR_TELEPHONE_NUMBER:        
                    if(prgPropvalUser[7].wFlags!=CEDB_PROPNOTFOUND)   
                    {   
                        if(LOWORD(prgPropvalUser[7].propid) == CEVT_LPWSTR)   
                        {
                            Log(prgPropvalUser[7].val.lpwstr);
                        }
                    }
                    break;
                case PIMPR_COMPANY_TELEPHONE_NUMBER:
                    if(prgPropvalUser[8].wFlags!=CEDB_PROPNOTFOUND)   
                    {   
                        if(LOWORD(prgPropvalUser[8].propid) == CEVT_LPWSTR)   
                        {
                            Log(prgPropvalUser[8].val.lpwstr);
                        }
                    }
                    break;
                case PIMPR_EMAIL1_ADDRESS:                
                    if(prgPropvalUser[9].wFlags!=CEDB_PROPNOTFOUND)   
                    {   
                        if(LOWORD(prgPropvalUser[9].propid) == CEVT_LPWSTR)   
                        {
                            Log(prgPropvalUser[9].val.lpwstr);
                        }
                    }
                    break;
                case PIMPR_EMAIL2_ADDRESS:                
                    if(prgPropvalUser[10].wFlags!=CEDB_PROPNOTFOUND)   
                    {   
                        if(LOWORD(prgPropvalUser[10].propid) == CEVT_LPWSTR)   
                        {
                            Log(prgPropvalUser[10].val.lpwstr);
                        }
                    }
                    break;
                case PIMPR_EMAIL3_ADDRESS:                
                    if(prgPropvalUser[11].wFlags!=CEDB_PROPNOTFOUND)   
                    {   
                        if(LOWORD(prgPropvalUser[11].propid) == CEVT_LPWSTR)   
                        {
                            Log(prgPropvalUser[11].val.lpwstr);
                        }
                    }
                    break;
                case PIMPR_HOME_TELEPHONE_NUMBER:        
                    if(prgPropvalUser[12].wFlags!=CEDB_PROPNOTFOUND)   
                    {   
                        if(LOWORD(prgPropvalUser[12].propid) == CEVT_LPWSTR)   
                        {
                            Log(prgPropvalUser[12].val.lpwstr);
                        }
                    }
                    break;
                case PIMPR_HOME2_TELEPHONE_NUMBER:        
                    if(prgPropvalUser[13].wFlags!=CEDB_PROPNOTFOUND)   
                    {   
                        if(LOWORD(prgPropvalUser[13].propid) == CEVT_LPWSTR)   
                        {
                            Log(prgPropvalUser[13].val.lpwstr);
                        }
                    }
                    break;
                case PIMPR_IM1_ADDRESS:                    
                    if(prgPropvalUser[14].wFlags!=CEDB_PROPNOTFOUND)   
                    {   
                        if(LOWORD(prgPropvalUser[14].propid) == CEVT_LPWSTR)   
                        {
                            Log(prgPropvalUser[14].val.lpwstr);                            
                        }
                    }
                    break;
                case PIMPR_IM3_ADDRESS:                    
                    if(prgPropvalUser[15].wFlags!=CEDB_PROPNOTFOUND)   
                    {   
                        if(LOWORD(prgPropvalUser[15].propid) == CEVT_LPWSTR)   
                        {
                            Log(prgPropvalUser[15].val.lpwstr);
                        }
                    }
                    break;
                case PIMPR_MOBILE_TELEPHONE_NUMBER:        
                    if(prgPropvalUser[16].wFlags!=CEDB_PROPNOTFOUND)   
                    {   
                        if(LOWORD(prgPropvalUser[16].propid) == CEVT_LPWSTR)   
                        {
                            Log(prgPropvalUser[16].val.lpwstr);
                        }
                    }
                    break;
                case PIMPR_PAGER_NUMBER:                
                    if(prgPropvalUser[17].wFlags!=CEDB_PROPNOTFOUND)   
                    {   
                        if(LOWORD(prgPropvalUser[17].propid) == CEVT_LPWSTR)   
                        {
                            Log(prgPropvalUser[17].val.lpwstr);
                        }
                    }
                    break;
                case PIMPR_RADIO_TELEPHONE_NUMBER:        
                    if(prgPropvalUser[18].wFlags!=CEDB_PROPNOTFOUND)   
                    {   
                        if(LOWORD(prgPropvalUser[18].propid) == CEVT_LPWSTR)   
                        {
                            Log(prgPropvalUser[18].val.lpwstr);
                        }
                    }
                    break;
                case PIMPR_SMS:                            
                    if(prgPropvalUser[19].wFlags!=CEDB_PROPNOTFOUND)   
                    {   
                        if(LOWORD(prgPropvalUser[19].propid) == CEVT_LPWSTR)   
                        {
                            Log(prgPropvalUser[19].val.lpwstr);
                        }
                    }
                    break;
            } //switch
        }            
    }
    Log(TEXT("\r\n"));

    //success
    hr = S_OK;

Exit:
    LocalFree(prgPropvalUser); 
    return hr;
}
 
 
// ************************************************************************** 
// Log 
HRESULT Log(LPTSTR szLog)
{
    HRESULT hr = E_FAIL;
    hr = LogToFile(szLog, g_pszFilename);
    CHR(hr);

Exit:
    return hr;
}


// ************************************************************************** 
// LogToFile 
// Writes szLog into the file named pszFilename
HRESULT LogToFile(LPTSTR szLog, LPCTSTR pszFilename)
{
    HRESULT hr = E_FAIL;
    
    //Open the handle to the file (and create it if it doesn't exist
    HANDLE hFile = CreateFile(pszFilename, GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (INVALID_HANDLE_VALUE == hFile)
        goto Exit;

    //Set the pointer at the end so that we can append szLog
    DWORD dwFilePointer = SetFilePointer(hFile, 0, NULL, FILE_END);
    if (0xFFFFFFFF == dwFilePointer)
        goto Exit;

    //Write to the file
    DWORD dwBytesWritten = 0;
    BOOL bWriteFileRet = WriteFile(hFile, szLog, wcslen(szLog) * 2, &dwBytesWritten, NULL);
    if (!bWriteFileRet)
        goto Exit;

    //Flush the buffer
    BOOL bFlushFileBuffersRet = FlushFileBuffers(hFile);
    if (!bFlushFileBuffersRet)
        goto Exit;

    //Success
    hr = S_OK;

Exit:
    if (NULL != hFile)
    CloseHandle(hFile);

    return hr;
}
 

Cheers,
~raffaele

  • Backup\Restore: is there a standard way?
  • How to programmatically mark messages so that they are downloaded at next sync
  • About linked attachments

Yet another post about MAPI on Windows Mobile, it seems it became an common topic (not well covered over the web probably)… I recently worked with one of my colleagues in Redmond (thanks Alex! smile_regular) about the following topic: how to use MAPI, if possible, to backup and restore e-mails sync-ed with a backend Exchange server (so by using ActiveSync MAPI Store).

After some investigation about the possible combination of properties, we understood that if the message is partially downloaded (only the header, or partially downloaded, or without downloading attachments) the is no standard way on how to retrieve the rest of information, such as a link or so to click for downloading, after restoring the messages from a backup file – simply because the server at that point will consider the message as different from the initial one.

At the beginning we imagined the problem was only with message body… and you can programmatically state if a message hasn't fully downloaded by looking at its properties MSGSTATUS_HEADERONLY, MSGSTATUS_PARTIAL_DOWNLOAD or MSGSTATUS_PARTIAL. So… how to programmatically mark messages so that they are downloaded at next sync? To mark a message for download, you should close the messaging application (tmail.exe), add the property MSGSTATUS_REMOTE_DOWNLOAD to the message and then restart the messaging application (tmail.exe). Note that if you don't want the download to start immediately, then you should also modify the message's PR_CE_SUPPRESS_FETCH property in the same call to IItem::SetProps, accordingly to documentation. I’m talking about something like:

SPropValue rgProps[2];

rgProps[0].ulPropTag = PR_MSG_STATUS;
rgProps[0].Value.ul  = ulMsgStatus | MSGSTATUS_REMOTE_DOWNLOAD | MSGSTATUS_REMOTE_DOWNLOAD_ATTACH;
rgProps[1].ulPropTag = PR_CE_SUPPRESS_FETCH;
rgProps[1].Value.ul  = 1;

hr = pMsg->SetProps(2, &rgProps, NULL);
CHR(hr);

Note: MSGSTATUS_REMOTE_DOWNLOAD is not mentioned under the "Windows Mobile Defined MAPI Properties" documentation, but you can see it defined in the related SDK's header file ("C:\Program Files (x86)\Windows Mobile 6 SDK\PocketPC\Include\Armv4i\cemapi.h"), so you can use it – in other words, the documentation is not complete on that page.

Note that this may not be a complete solution, accordingly to Jay Ongg's answer to a comment on a Windows Mobile Dev Team's blog post (here):

"[...] Regarding MSGSTATUS_PARTIAL, downloading a whole message is actually more complicated than just setting a flag.  Depending on the transport, you have to think about downloading attachments, which is done different for HTML mail vs normal attachments, I believe.  With that said, this might work... try setting the MSGSTATUS_REMOTE_DOWNLOAD flag to start out, and MSGSTATUS_REMOTE_DOWNLOAD_ATTACH as well."

Indeed, as reported by Jay, the approach above resolved the problem for message bodies, but not for attachments! smile_sad So… to recap: imagine you wish to copy a MAPI message that has a non-downloaded attachment into an unspecified backup storage; later, you copy the message from the backup store into the messaging store. What we expected was that the attachment to still be part of the message, but we found that the message's link to download the attachment is missing. Is there any supported way so that the attachment remains in the message after performing the backup and then retrieving the message? The answer is simply “no”. Trying to copy (backup) the email message with a non-downloaded attachment will not work. The link to the attachment will be lost, the attachment won’t be retrievable. The email and the non-downloaded attachment are associated with each other in Exchange, however the attachment is not actually a part of the message, so copying the message will not make a copy of the attachment. So the solution is to force the attachment to be downloaded before backing up the message (if user wants to maintain the attachment). Obviously saving the message and attachment on the device might not be a workable solution due to the storage space required… but, as usual, that depends on scenarios.

 

Cheers!
~raffaele & Alex

  • System.Net.WebException: Unable to read data from the transport connection (System.Net.Sockets.SocketException: Unknown error (0x0))
  • Use HotFix responsibly!

Today my co-worker Carmelo (thanks for sharing this! smile_regular) found something that NETCF Developers were waiting since long time ago… a fixed version of NETCF CAB for Windows Mobile that would allow to address a bug about calling Web Services over SSL, which was greatly described by Andrew Arnott here some time ago. The problem was that NETCF couldn’t work with Web Services over SSL that respond with empty encryption packets.

The link to the hotfix is “FIX: A System.Net.WebException occurs when you run an application to send HTTPS Web requests to a server in an embedded device”.

Does this mean that also NETCF Developers can begin waiting for hotfixes for bugs? No, I personally think that this is a one-shot FIX coming from the NETCF Dev Team, because the problem was affecting so many customers around the globe… they simply deserve it. And, again, Technical Support is your friend!! I don’t know the story behind this fix, however to convince the Dev Team to produce it I’m sure that a “certain” number of customers made their voice be heard through Service Requests…

WARNING! Remember that analogously to every other hotfix you can ask to Technical Support about whatever Microsoft’s product, you should install it only if you're sure it’s for the problem you're being affected by. This is because a hotfix is meant to address a specific issue and has been tested precisely for that: it didn't go through the whole regression testing that it's usually done when packaging many hotfixes into a Service Pack. So, you don’t have to install this hotfix if you’re not interested on Web Services over SSL (sending empty packets).

Cheers,
~raffaele

I’m losing my expertise on this!! smile_confused After blogging about it to spread out the usual suggestions I was used to give to ISVs with troubles around memory, I simply quitted handling this kind of cases! Either v3.5 addressed all known bugs, either NETCF developers are getting skilled about things to do to prevent memory leaks… and I hope I’ve been of some little help! smile_shades

Cheers,
~raffaele

  • Some bloggers within Technical Support
  • Example of added value given by Technical Support: Devs’ engagement 
  • Comments about NETCF on ARMv6 and ARMv7

I often have to do some marketing for Microsoft Technical Support, because it’s something everyone with questions or problems around Microsoft products must test out to understand its added value… Apart from myself smile_regular we’re plenty of great people on all technologies: to name just a few, the following are representatives of the Technical Support – watch out their blogs, as they’re so plenty of “knowledge”! smile_nerd

There are many others I simply hadn’t yet the honor to work with, I simply couldn’t mention all of them!

So, what is the added value I want to present today? This time I’d like to demonstrate that one of benefit of using Technical Support is that we may have a preferential engagement with the Product Groups, when necessary. This may differ in forms depending on the technology, but no doubt that we can engage the people who actually wrote the software. And an example of this happened to me some weeks ago…

In that particular case who opened a Service Request was the OEM of a Windows CE 6.0-based device: you can say that I help ISVs, but this particular OEM was posing questions about developing a NETCF v3.5 application and wanted to receive support about Application Development rather than OS Customization through Platform Builder, therefore it fell in area of expertise. The device was based on a ARMv7 processor and was used as navigation system, hence it was heavily dependent on mathematical functions. On this processor, the OEM noticed that calling the same mathematical functions in a NETCF application required much more time than in a native application. The impression was that the native application leverages on the mathematical co-processor and asked if there’s any way to configure the NETCF Runtime to do the same either at application-layer or system-wide through an OS Customization.

To make it short, in this case I had the pleasure to work directly with Abhinaba Basu, member of the NETCF Dev Team – see his blog posts as a result of this co-operation:

Apart from the deep explanations given by Abhinaba in his blog, questions and answers were:

1- Is this an expected result?
NetCF 3.5 lacks Armv6/v7 specific optimizations as it is based on Armv4i.  In particular we don’t leverage the FPU (Floating Point Unit), as NETCF does only floating point emulation.  Hence NetCF math functions will be much slower than doing the same in a native C++ program, which in contrast could be compiled to use the hardware FPU.

2- Is ARMv7 a fully-supported architecture by the NETCF Runtime?
NetCF 3.5 is based on Armv4i and Armv7 is backward compatible with Armv4i.  Hence all of the functionality of NetCF3.5 should work fine though may not yield optimal performance.

3- Is there any possible way to achieve similar results with NETCF?
Math routines could be written in native and the same could be PInvoked from managed code. Obviously you need to ensure that the cost of P/Invoke marshalling is not offsetting the saving in computation perf. A good idea to do that is to reduce the number of P/Invoke calls by bulking the computation.

In particular, in that specific case the solution proposed was to develop a native DLL that internally invoked native math functions and exposes some “math expressions” to a .NET client. I mean a native DLL exposing f(a,b,c) which internally is like {[sin(a) x sin(b)] / cos(c)}. Moreover, if for example you know that you need to do 3 of these operations then create a buffer of inputs and outputs and push that to the native dll so that it can do all 3 in one go.

 

Cheers,
~raffaele

Recently I had to work on a sample code based on my previous post around MAPI that shows a possible way to extract the BODY of the mails in the Inbox folder of the ActiveSync account… this time I needed to grab a handle of the Outbox folder, and therefore I had to modify the function SaveMessagesFromStore of that post. Luckily the code was modularized enough… smile_regular so, once I had the LPMAPIFOLDER pointer to the Outbox I could continue using basically the same code. I thought there was a MAPI Function similar to IMsgStore::GetReceiveFolder, but found that quite many functions are not implemented in Windows Mobile 6 – see doc here. Luckily MAPI gives different ways to reach a goal, and in this particular case the code was based on the PR_IPM_OUTBOX_ENTRYID property of the MAPI store:

        // Get the Outbox folder
        ULONG rgTags[] = {1, PR_IPM_OUTBOX_ENTRYID};
        LPSPropValue rgprops = NULL;
        ULONG ulValues = 0;
        hr = pStore->GetProps((LPSPropTagArray) rgTags, MAPI_UNICODE, &ulValues, &rgprops);
        CHR(hr);
        
        cbEntryId = rgprops[0].Value.bin.cb;
        pEntryId = (LPENTRYID)rgprops[0].Value.bin.lpb;
        
        // We have the entryid of the Outbox folder, let's get the folder 
        hr = pStore->OpenEntry(cbEntryId, pEntryId, NULL, 0, &ulObjType, (LPUNKNOWN*)&pFolder); 
        CHR(hr); 
        
        //check 
        CBR(ulObjType == MAPI_FOLDER);
 

Cheers,
~raffaele

Have you ever played with EDB? I hadn’t… till the moment when I thought that what I needed was not implemented by the POOM and therefore I had to play with the “Contacts Database” contained in pim.vol... Unfortunately I understood only later that I was wrong… smile_sad And luckily this was precisely the same query raised by in the MSDN Forum ‘How to get the information of a selected phonecall number?’ which I answered having fresh mind on the topic.

Basically when opening a Contact summary card, you can see 2 info:

  1. Firstly, what is the last time you called this contact by phone, and by using what phone# (mobile, work, home, etc): this is the item at the top, which is removed when user clears the Call Log history (off-topic: have you ever tried to do programmatically clear the call log?), and therefore no longer retrievable by the Phone Functions like PhoneOpenCallLog, PhoneGetCallLogEntry, etc -- masterly wrapped by the SDF's OpenNetcf.Phone.CallLog if you’re using a managed application.
  2. Secondly, what is the last way user communicated with that contact: this is the selected item in the listview and can be phone\sms\mail etc, not necessarily phone. Well, this info is maintained also if user clears the call log history because it’s a property of the “Contacts Database” not of the “CLOG.EDB” database, and accordingly to what I wrote in the MSDN Forum post above, it's something you can retrieve by using POOM, related to the PIMPR_SMARTPROP property -- see doc here, ‘Remarks’ section: “The Smart Property (PIMPR_SMARTPROP) is the Contact property that contains the property ID of the default communication mode. This becomes the phone number or address displayed on the second line of the two-line display in the Contact list view, and highlighted in the Contact summary tab.”.

So why on the earth did I mess up with EDB Functions against the “Contacts Database”? Purely because I wasn’t aware of such ad-hoc property!! And I ended up with a code that I want to share in case anyone is approaching to EDB Functions on Windows Mobile as it shows some basic functionalities… as usual it’s provided as-is for didactic purposes and doesn’t contain enough error-check for example.

    DWORD dwError = ERROR_SUCCESS;
    CEGUID guid;
    DWORD dwBufSize, dwIndex;
    BOOL fOk = FALSE;
    HANDLE hSession, hDatabase; 
    WORD wNumProps;
    CEOID oid = 0, ceoid;
    TCHAR szBuffer[MAXBUFFERSIZE] = {0};
    PCEPROPVAL lpProp;

    //Used for CEVT_STREAM:
    HANDLE hStream;
    DWORD cbStream;
    LPBYTE pBuffer;
    DWORD cbActualRead;

    //1- Mount DB
    if (!CeMountDBVolEx(&guid, TEXT("\\pim.vol"), NULL, OPEN_ALWAYS)) 
    {
        dwError = GetLastError();
        goto Exit;
    }

    //2- Open Session
    hSession = CeCreateSession(&guid);
    if (hSession == INVALID_HANDLE_VALUE) 
    {
        dwError = GetLastError();
        goto Exit;
    }

    //3- Open Database
    hDatabase = CeOpenDatabaseInSession(hSession, &guid, &oid, TEXT("Contacts Database"), NULL, 0, NULL);
    if (hDatabase == INVALID_HANDLE_VALUE) 
    {
        dwError = GetLastError();
        goto Exit;
    }

    //4- Iterate through records (there are other ways apart from waiting for ERROR_SEEK...)
    dwIndex = 0;
    BOOL bFound = FALSE;
    while (!bFound) 
    {

        //4.1- Set index into db
        ceoid = CeSeekDatabaseEx(hDatabase, CEDB_SEEK_BEGINNING, dwIndex, 0, NULL);
        if (ceoid == 0) 
        {
            dwError = GetLastError();
            goto Exit;
        }

        //4.2- Read records at index
        wNumProps = 0;
        ceoid = CeReadRecordPropsEx(hDatabase, CEDB_ALLOWREALLOC, &wNumProps, NULL, (LPBYTE*)&lpProp, &dwBufSize, NULL);            
        if (ceoid == 0) 
        {
            dwError = GetLastError();
            //if (dwError == 122) //ERR_INSUFFICIENT_BUFFER
            //e.g. increase buffer and re-try
            
            goto Exit;
        }

        //4.3- Iterate through columns 
        for( int i = 0; i < wNumProps; i++ )
        {
            //4.4- switch based on datatype (http://msdn.microsoft.com/en-us/library/aa917573.aspx) 
            switch( TypeFromPropID(lpProp[i].propid) )
            {
                case CEVT_I2:
                    _stprintf(szBuffer, _T("%d\t %d\t %s: %s"), dwIndex, i, _T("Data Type"), _T("CEVT_I2") );
                    OutputDebugString(szBuffer);
                    _stprintf(szBuffer, _T("\t%d \r\n"), lpProp[i].val.iVal );
                    OutputDebugString(szBuffer);
                    break;

                case CEVT_UI2:
                    _stprintf(szBuffer, _T("%d\t %d\t %s: %s"), dwIndex, i, _T("Data Type"), _T("CEVT_UI2") );
                    OutputDebugString(szBuffer);
                    _stprintf(szBuffer, _T("\t%d \r\n"), lpProp[i].val.uiVal );
                    OutputDebugString(szBuffer);
                    break;

                case CEVT_I4:
                    _stprintf(szBuffer, _T("%d\t %d\t %s: %s"), dwIndex, i, _T("Data Type"), _T("CEVT_I4") );
                    OutputDebugString(szBuffer);
                    _stprintf(szBuffer, _T("\t%d \r\n"), lpProp[i].val.lVal );
                    OutputDebugString(szBuffer);
                    break;

                case CEVT_UI4:
                    _stprintf(szBuffer, _T("%d\t %d\t %s: %s"), dwIndex, i, _T("Data Type"), _T("CEVT_UI4") );
                    OutputDebugString(szBuffer);
                    _stprintf(szBuffer, _T("\t%d \r\n"), lpProp[i].val.ulVal );
                    OutputDebugString(szBuffer);
                    break;

                case CEVT_LPWSTR:
                    _stprintf(szBuffer, _T("%d\t %d\t %s: %s\t"), dwIndex, i, _T("Data Type"), _T("CEVT_LPWSTR") );
                    OutputDebugString(szBuffer);
                    OutputDebugString(lpProp[i].val.lpwstr);
                    OutputDebugString(_T("\r\n"));
                    break;

                case CEVT_BLOB:
                    _stprintf(szBuffer, _T("%d\t %d\t %s: %s \r\n"), dwIndex, i, _T("Data Type"),_T("CEVT_BLOB") );
                    OutputDebugString(szBuffer);
                    _stprintf(szBuffer, _T("\t%s: %li \r\n"), _T("Size in bytes"), lpProp[i].val.blob.dwCount );
                    OutputDebugString(szBuffer);
                    _stprintf(szBuffer, _T("\t%s: 0x%x \r\n"), _T("Buffer Address") ,lpProp[i].val.blob.lpb );
                    OutputDebugString(szBuffer);
                    break;

                case CEVT_BOOL:
                    _stprintf(szBuffer, _T("%d\t %d\t %s: %s \r\n"), dwIndex, i, _T("Data Type"),_T("CEVT_BOOL") );
                    OutputDebugString(szBuffer);
                    break;

                case CEVT_R8:
                    _stprintf(szBuffer, _T("%d\t %d\t %s: %s \r\n"), dwIndex, i, _T("Data Type"),_T("CEVT_R8") );
                    OutputDebugString(szBuffer);
                    break;

                case CEVT_STREAM:
                    _stprintf(szBuffer, _T("%d\t %d\t %s: %s \r\n"), dwIndex, i, _T("Data Type"),_T("CEVT_STREAM") );
                    OutputDebugString(szBuffer);

                    //OPEN STREAM
                    hStream = CeOpenStream(hDatabase, lpProp[i].propid, GENERIC_READ);
                    cbStream = sizeof(hStream);
                    
                    if (hStream == INVALID_HANDLE_VALUE )
                    {
                        dwError = GetLastError();
                        goto Exit;
                    }
                    
                    //SET SEEK POSITION AT BEGINNING
                    if (!CeStreamSeek(hStream, 0, STREAM_SEEK_SET, NULL))
                    {
                        dwError = GetLastError();
                        goto Exit;
                    }

                    //READ STREAM
                    pBuffer = new BYTE[cbStream];
                    if (!CeStreamRead(hStream, pBuffer, cbStream, &cbActualRead))
                    {
                        dwError = GetLastError();
                        delete [] pBuffer;
                        goto Exit;
                    }

                    _stprintf(szBuffer, _T("\tSTREAM: %s\r\n"), (LPTSTR)(pBuffer));
                    OutputDebugString(szBuffer);
                    break;

                case CEVT_RECID:
                    _stprintf(szBuffer, _T("%d\t %d\t %s: %s \r\n"), dwIndex, i, _T("Data Type"),_T("CEVT_RECID") );
                    OutputDebugString(szBuffer);
                    break;

                case CEVT_AUTO_I4:
                    _stprintf(szBuffer, _T("%d\t %d\t %s: %s"), dwIndex, i, _T("Data Type"),_T("CEVT_AUTO_I4") );
                    OutputDebugString(szBuffer);
                    _stprintf(szBuffer, _T("\t%d \r\n"), lpProp[i].val.lVal );
                    OutputDebugString(szBuffer);
                    break;

                case CEVT_AUTO_I8:
                    _stprintf(szBuffer, _T("%d\t %d\t %s: %s"), dwIndex, i, _T("Data Type"),_T("CEVT_AUTO_I8") );
                    OutputDebugString(szBuffer);
                    _stprintf(szBuffer, _T("\t%d \r\n"), lpProp[i].val.lVal );
                    OutputDebugString(szBuffer);
                    break;

                default:
                    _stprintf(szBuffer, _T("%d\t %d\t %s: %s \r\n"), dwIndex, i, _T("Data Type"),_T("Unknown") );
                    OutputDebugString(szBuffer);
                    //lpProp[i].val ??
                    break;
            } //switch
        } //for

        //move to next record
        dwIndex++;
    } //while


    //5- Unmount db
    if (!CeUnmountDBVol(&guid))
    {
        dwError = GetLastError();
        goto Exit;
    }


Exit:
    if (NULL != hDatabase) CloseHandle(hDatabase);
    if (NULL != hSession) CloseHandle(hSession);
    if (dwError == ERROR_SEEK) dwError = ERROR_SUCCESS; //ERROR_SEEK is expected to exit the while loop

    return dwError;

Cheers,

~raffaele

… so how can I grab the handle of a particular child window, considering that FindWindow retrieves all top-level windows? I worked on this when writing a previous post of mine, and got a wonderful comment from Lionel (thanks again smile_regular). Today I had to re-use that code, and found that something was missing and also the code required some enhancements… so here it is!

 

        private static IntPtr FindChildWindow(string strChildClassName, string strChildWindowCaption, IntPtr hWndTopLevel)
        {
            IntPtr hwndCur = IntPtr.Zero;
            hwndCur = GetWindow(hWndTopLevel, (uint)GetWindowFlags.GW_CHILD);
            return RecurseFindWindow(strChildClassName, strChildWindowCaption, hwndCur);
        }

        private static bool m_bFound = false;

        private static IntPtr RecurseFindWindow(string strChildClassName, string strChildWindowCaption, IntPtr hWndParent)
        {
            //bool bFound = false;
            IntPtr hwndCur = IntPtr.Zero;
            char[] chArWindowClass = new char[32];
            if (hWndParent == IntPtr.Zero)
                return IntPtr.Zero;
            else
            {
                //check if we got the searched class name
                GetClassName(hWndParent, chArWindowClass, 256);
                string strWndClass = new string(chArWindowClass);
                strWndClass = strWndClass.Substring(0, strWndClass.IndexOf('\0'));
                if (strWndClass.ToLower() == strChildClassName.ToLower())
                {
                    //check if we got the searched window name
                    int length = GetWindowTextLength(hWndParent);
                    StringBuilder sb = new StringBuilder(length + 1);
                    GetWindowText(hWndParent, sb, sb.Capacity);
                    m_bFound = (sb.ToString().ToLower() == strChildWindowCaption.ToLower());
                    if (m_bFound)
                        return hWndParent;
                }
                else
                {
                    //recurse into first child
                    IntPtr hwndChild = GetWindow(hWndParent, (uint)GetWindowFlags.GW_CHILD);
                    if (hwndChild != IntPtr.Zero)
                        hwndCur = RecurseFindWindow(strChildClassName, strChildWindowCaption, hwndChild);
                    if (!m_bFound)
                    {
                        IntPtr hwndBrother = IntPtr.Zero;
                        //enumerate each brother windows and recurse into
                        do
                        {
                            hwndBrother = GetWindow(hWndParent, (uint)GetWindowFlags.GW_HWNDNEXT);
                            hWndParent = hwndBrother;
                            if (hwndBrother != IntPtr.Zero)
                            {
                                hwndCur = RecurseFindWindow(strChildClassName, strChildWindowCaption, hwndBrother);
                                if (m_bFound)
                                    break;
                            }
                        }
                        while (hwndBrother != IntPtr.Zero);
                    }
                }
                return hwndCur;
            }
        }


        [DllImport("coredll.dll", SetLastError = true)]
        private static extern IntPtr FindWindow(string _ClassName, string _WindowName);

        [DllImport("coredll.dll", SetLastError = true)]
        private static extern IntPtr GetWindow(IntPtr hwnd, uint relationship);

        [DllImport("coredll.dll", SetLastError = true)]
        private static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, int lParam);

        [DllImport("coredll.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        private static extern int GetClassName(IntPtr hwnd, char[] windowClass, int maxText);

        [DllImport("coredll.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        private static extern int GetWindowTextLength(IntPtr hWnd);

        [DllImport("coredll.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);

        [Flags]
        private enum GetWindowFlags
        {
            GW_HWNDFIRST = 0,
            GW_HWNDLAST = 1,
            GW_HWNDNEXT = 2,
            GW_HWNDPREV = 3,
            GW_OWNER = 4,
            GW_CHILD = 5,
        }

 

Cheers,

~raffaele

More Posts Next page »
 
Page view tracker