Welcome to MSDN Blogs Sign in | Join | Help
  • 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

Recently I've worked with a developer on an interesting issue I’ve not found any clue on the web about, and the solution is based on one of those details that you can empirically retrieve but that there are not documented anywhere, therefore on future releases may change without any warning. This was for example what happened to the ClassName of NETCF applications... see Daniel Moth's post about this: "#NETCF_AGL_". I’ve also discussed about this in a MSDN Forum post I found interesting, where the topic was something like “how to prevent the CLR to not allow a second instance of the same NETCF application to run on Windows Mobile”. As I probably wrote elsewhere, “undocumented” doesn’t mean “technically not achievable”: it means that Product Group may change it as it doesn’t have to be backward-compatible.

In this case we had an application that may have been updated at a later time: the ISV was wondering if there’s any way in the setup.dll of application’s CAB to specify, during uninstallation, if the uninstall is taking place during a version-upgrade or if it's a pure uninstallation. This is because, for example, the application's installation copies also some large files that user no longer needs after the uninstall and therefore are deleted: but it still needs them if the user is uninstalling a former version of the app in order to install a newer one. I hope I've been clear... smile_confused Things can get more complicated by the fact that the when you do an “upgrade” of the same ap

Well... we found out that there's no documented and standard way to achieve the goal, so we had to be creative - as usual... Nerd To understand how to operate, we needed to understand the actual flow when installing\uninstalling\upgrading (=installing the CAB of a newer version of the app while a older one is installed); moreover, we had to take care a particular condition, i.e. when upgrading the user is prompted with the message “The previous version of… Select Ok to continue or cancel to quit” -- and here it comes handy the “undocumented but empirically retrievable” info, that I'm going to show in a minute.

The regular flow when installing and uninstalling is:

  • Install:
    1. DLL_PROCESS_ATTACH – Setup.dll is loaded
    2. Install_Init
    3. Install_Exit
    4. DLL_PROCESS_DETACH – Setup.dll is unloaded
  • Uninstall:
    1. DLL_PROCESS_ATTACH – Setup.dll is loaded
    2. Uninstall_Init
    3. Uninstall_Exit
    4. DLL_PROCESS_DETACH – Setup.dll is unloaded

When upgrading, the flow is as follows:

  1. DLL_PROCESS_ATTACH – SetupDLL.dll is loaded
  2. Install_Init
  3. DLL_PROCESS_DETACH – SetupDLL.dll is unloaded
  4. Message prompt to the user to confirm uninstall of previous version
    • Select Ok:
      1. DLL_PROCESS_ATTACH – SetupDLL.dll is loaded, *BUT* the installer doesn’t know if we’re uninstalling because of a real uninstall or an upgrade
      2. Uninstall_Init
      3. Uninstall_Exit
      4. DLL_PROCESS_DETACH – SetupDLL.dll is unloaded
      5. DLL_PROCESS_ATTACH – SetupDLL.dll is loaded
      6. Install_Init
      7. Install_Exit
      8. running the exec
      9. DLL_PROCESS_DETACH – SetupDLL.dll is unloaded
    • Select Cancel:
      1. Nothing happens (setup.dll was already unloaded)

So the problem is how to let the installer know that it’s uninstalling or upgrading… the idea I had was to modify the flow this way, based on the fact that when “upgrading”, the flow involves firstly a Install_Init and secondly a Uninstall_Init; in contrast when “uninstalling” the flow doesn’t involve a first step through Install_Init:

a. Install

  1. DLL_PROCESS_ATTACH – SetupDLL.dll is loaded
  2. Install_Init (query if the app is already installed (through the Uninstall CSP) and set a registry key or whatever, e.g.[HKLM\UpgradeKey]Upgrade=0 if it was not installed and 1 viceversa) –> now: Upgrade=0
  3. Install_Exit
  4. DLL_PROCESS_DETACH – SetupDLL.dll is unloaded

b. Upgrade:

  1. DLL_PROCESS_ATTACH – SetupDLL.dll is loaded
  2. Install_Init (query if the app is already installed (through the Uninstall CSP) and set a registry key or whatever, e.g.[HKLM\UpgradeKey]Upgrade=0 if it was not installed and 1 viceversa) –> now: Upgrade=1
  3. DLL_PROCESS_DETACH – SetupDLL.dll is unloaded
  4. Message prompt to the user to confirm uninstall of previous version
    • Select Ok:
      1. DLL_PROCESS_ATTACH – SetupDLL.dll is loaded
      2. Uninstall_Init (QUERY [HKLM\UpgradeKey]Upgrade and act accordingly) –> now: Upgrade=1 (was just set by Install_Init at point 2. of the Upgrade flow, and then it can be set back to 0)
      3. Uninstall_Exit
      4. DLL_PROCESS_DETACH – SetupDLL.dll is unloaded
      5. DLL_PROCESS_ATTACH – SetupDLL.dll is loaded
      6. Install_Init
      7. Install_Exit
      8. running the exec
      9. DLL_PROCESS_DETACH – SetupDLL.dll is unloaded
    • Select Cancel:
      1. Nothing happens (setup.dll was already unloaded)

c. Uninstall

  1. DLL_PROCESS_ATTACH – SetupDLL.dll is loaded
  2. Uninstall_Init (query if we’re upgrading by looking at the registry key) –> now: Upgrade=0 (it wasn’t changed by anyone)
  3. Uninstall_Exit
  4. DLL_PROCESS_DETACH – SetupDLL.dll is unloaded

To conclude, the idea was to:

  • Install_Init creates the “Upgrade” registry key (or other info) and sets 0 if the application is NOT already installed and 1 viceversa. To check if an application is already installed I think I’ve already discussed once on the Uninstall Configuration Service Provider… yes, see this post.
  • Uninstall_Init checks the value of the key and act accordingly (just as an example, if that’s an “uninstall” then remove some files that are no longer used)

 

HOWEVER… smile_confused this approach had a problem… what happens if user answers “Cancel” to the prompt “The previous version of… Select Ok to continue or cancel to quit”? Nobody can restore [HKLM\UpgradeKey]Upgrade to 0 after that Install_Init set it to 1, and future possible “Uninstalls” are considered as “Upgrades”! So basically the problem is when user firstly doesn't accept to uninstall the previous version during upgrade and then secondly she uninstalls the previous version on her own: when doing this second action, the uninstall procedure would find that the Upgrade registry key is set to 1 and therefore would consider an upgrade even if in reality it's an uninstall.

So, next question was: is there any programmatic way to know if user selects “Cancel” when prompted about uninstalling previous version? The only way I could think at was to get ahold of the WCELOAD.EXE process and invoke GetExitCodeProcess() API to retrieve its return value: the assumption was that it was different when user hits “Cancel”… it turned out that this is true, but this approach involved an external application to be launched for example in setup.dll’s DLL_PROCESS_ATTACH, that can monitor WCELOAD.EXE and check its return value during Uninstall phase… Why an external process? Because the prompt comes up EVEN BEFORE the setup.dll can handle Install_Init.

The “undocumented but empirically retrievable” info I was mentioning at the beginning is precisely the return value of WCELOAD.EXE when user hits Cancel. As I said, not being documented it may change on future releases without any notice..

And now some code please!!

I’m talking about the following in setup.dll:

#define DELETE_STR(s) \
if (NULL != s) \
delete [] s;
 
 
HINSTANCE g_hinstModule;
 
BOOL APIENTRY DllMain(
    HANDLE hModule, 
    DWORD  ul_reason_for_call, 
    LPVOID lpReserved
    )
{
    //MessageBox(NULL, TEXT("Now attach the debugger"), TEXT("Test"), MB_OK);
 
    switch (ul_reason_for_call)
    {
        case DLL_THREAD_ATTACH:
        case DLL_THREAD_DETACH:
        case DLL_PROCESS_DETACH:
            g_hinstModule = (HINSTANCE)hModule;
            break;
 
        case DLL_PROCESS_ATTACH:
              g_hinstModule = (HINSTANCE)hModule;
 
              //check if UpgCheck.exe is already available on device (1st time it won't, but in any case we don't need it)
              LPCWSTR pszFileNameWithPath = new TCHAR[MAX_PATH];
              pszFileNameWithPath = TEXT("\\Windows\\UpgCheck.exe");
              WIN32_FIND_DATA wfdFindFileData;
              HANDLE hFile = FindFirstFile(pszFileNameWithPath, &wfdFindFileData);
              if(hFile == INVALID_HANDLE_VALUE)
              {
                            DELETE_STR(pszFileNameWithPath);
                            break;
              }
              FindClose(hFile);
 
              //Launch external process that will monitor wceload.exe
              BOOL bRet;
              SHELLEXECUTEINFO sei = {0};
 
              sei.cbSize = sizeof(sei);
              sei.nShow = SW_SHOWNORMAL; 
              sei.lpFile = pszFileNameWithPath;
              sei.lpParameters = TEXT(" ");
              bRet = ShellExecuteEx(&sei);
 
              //if (!bRet)
              //     MessageBox(NULL, TEXT("Could not launch UpgCheck"), TEXT("Test"), MB_OK);
 
              DELETE_STR(pszFileNameWithPath);
              break;
    }
 
return TRUE;
}
 

And I’m talking about something similar in the wceload-monitor:

int _tmain(int argc, _TCHAR* argv[])
{
       int const MAXBUF = 32;
       HRESULT hr = E_FAIL;
       HANDLE hProcess = NULL;
       BOOL bRes = FALSE;
       DWORD dwRes = 0;
 
       LPTSTR lpBuf = new TCHAR[MAXBUF];
       ZeroMemory(lpBuf, MAXBUF - 1);
 
       //retrieve process handle of wceload.exe, until it's found
       do{
              hr = GetProcessHandleByName(TEXT("wceload.exe"), &hProcess);
              CHR(hr);
              Sleep(1000);
       } while (INVALID_HANDLE_VALUE == hProcess);
 
       //hr = LogToFile(TEXT("\r\nwceload found!\r\n"), g_pszFilename);
       //CHR(hr);
 
       //retrieve wceload.exe exit code, until it exits
       do {
              Sleep(1000);
              bRes = GetExitCodeProcess(hProcess, &dwRes);
 
              if ( !bRes )
              {
                     goto Exit; //GetLastError
              }
       } while (STILL_ACTIVE == dwRes); 
       
       hr = StringCchPrintf(lpBuf, 
              LocalSize(lpBuf) / sizeof(TCHAR),
              TEXT("ExitCode %d"),
              dwRes); //2147754005 when user select Cancel (0x80042015)
       CHR(hr);
 
       hr = LogToFile(lpBuf, g_pszFilename);
       CHR(hr);      
 
       //success
       hr = S_OK;
 
Exit:
       DELETE_STR(lpBuf);
 
       return 0;
}

 

Where the helper functions are:

// **************************************************************************
// Function Name: GetProcessHandleByName
HRESULT GetProcessHandleByName (LPCTSTR pszProcessName, LPHANDLE phProcessHandle)
{
       HRESULT hr = E_FAIL;
 
       if (pszProcessName == NULL)
              goto Exit;
 
       HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
       if (hSnapshot == INVALID_HANDLE_VALUE)
              goto Exit;
 
       *phProcessHandle = NULL;
       PROCESSENTRY32 pe;
       pe.dwSize = sizeof(pe);
 
       if (Process32First(hSnapshot, &pe))
       {
              do {
                     //log Exe name
                     hr = LogToFile(pe.szExeFile, g_pszFilename);
                     CHR(hr);
                     hr = LogToFile(TEXT("\r\n"), g_pszFilename);
                     CHR(hr);
                     
                     //compare current Exe name with passed Process Name
                     if (lstrcmpi(pszProcessName, pe.szExeFile) == 0)
                     {
                           //get the handle of the Exe name in case we reached the Exe we were looking for
                           *phProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pe.th32ProcessID);
 
                           CloseHandle(hSnapshot);
                           return TRUE;
                     }
              } while (Process32Next(hSnapshot, &pe));
       }
 
       //Success
       hr = S_OK;
 
Exit:
       if (NULL != hSnapshot)
              //UPDATE: thanks Vino!
              //Contrarily to desktop Win32, don't invoke CloseHandle() to close the snapshot call.
              //Desktop (http://msdn.microsoft.com/en-us/library/ms682489(VS.85).aspx): 
              //       "[...] To destroy the snapshot, use the CloseHandle function.".
              //Windows CE\Mobile (http://msdn.microsoft.com/en-us/library/aa911386.aspx): 
              //       "[...] To close a snapshot, call the CloseToolhelp32Snapshot function."
              CloseToolhelp32Snapshot(hSnapshot);
 
       return hr;
}
 
 
// **************************************************************************
// Function Name: LogToFile 
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;
}

 

Hope this can help someone that absolutely has to distinguish if the application needs to be uninstalled or upgraded… but maybe the code above can find other meaningful usage! smile_nerd

 

Cheers,

~raffaele

  • Abhinaba’s blog
  • What’s new in Windows Mobile 6.1 and 6.5 around memory
  • Dynamic assembly unloading

Again again again… I've discussed about Memory Management in my very first post, which I suggest reading as it may help you on avoiding memory leaks and also improving memory management of your NETCF application. I’ve discussed about this other times, and to demonstrate how sensible this topic is, recently Abhinaba Basu (Developer working on the .NET Compact Framework team) started blogging about how the NETCF runtime handles memory: thanks Abhinaba for all those details! smile_regular

So, why am I still talking about memory?? Because that’s the scarcest resource whose lack mobile developers have to face sometimes… btw let me digress: remember that Windows Mobile 6.1 and 6.5 help a lot ISVs on preventing OOM by modifying some details of the memory management, in particular to alleviate memory pressure on the process slot 0 basically (considering all the "drivers" that OEMs have come up with, which need to be loaded by device.exe process -- thus lowering the DLL Load point (in previous versions of the OS)). Doug Boling discussed about this here.

Obviously a downside for this is that there’s less room for memory-mapped files in the Large Memory Area (LMA), but, apart from some special circumstances, this shouldn’t affect ISVs’ job, considering also that the LMA may be used by the OS and by other applications, therefore ISVs shouldn’t expect it to be totally available for their usage.

Also, I’m still talking about memory because from time to time I got questions like “… ok, so why don’t we dynamically load and unload assemblies?”. When this question arises, I usually answer suggesting to start by reading some quite old references:

As documented here, “[…] To unload managed code running in a process from memory so that memory can be used for other purposes, the host must unload the application domain in which the code is running. Individual assemblies or types cannot be unloaded.”. A quite complex “trick” (I wouldn’t really recommend for NETCF applications) is to create a new AppDomain (AppDomain.CreateDomain is supported by the NETCF) and load an assembly into it. The point is that as soon as you try to create objects from that assembly which is loaded into the new AppDomain, the assembly will be loaded into the current AppDomain. In order to solve this problem, you have to use a middle assembly, which is loaded into both the current AppDomain and the new AppDomain. Once the new AppDomain is unloaded, the assembly will be unloaded from the memory as well. However, the middle assembly, will stay in the memory. This approach might be known among DESKTOP .NET developers: I don’t know if anyone has ever tried to implement this within a NETCF application.

IN ANY CASE, assemblies are loaded into the system’s 1GB area, therefore the benefit would only be for PHYSICAL memory in this case (and for the JIT-ed code). And we’re usually talking about roughly 25% of the size of the assembly. If you only need to have certain assemblies loaded at certain times, but never all at once then AppDomains can help; if you plan to have all your assemblies loaded simultaneously then AppDomains don’t offer much (for memory pressure relief).

Note: what if an assembly depends on a native DLL (so loaded into the 32MB process slot) for example via P/Invoke? When the assembly is unloaded (by destroying the AppDomain), starting on NETCF v3.5, if the native DLL is used only by that assembly then its virtual memory is released from the process slot.

 

Cheers,

~raffaele

  • I can’t take WZC for granted… but, at least, can I programmatically turn WiFi on and off?
  • ossvcs.dll’s Wireless Device Power Management Functions
  • WiFi driver is a Power Manager-controllable NDIS Miniport Driver
  • Introducing: RIL Driver and IP Helper APIs on WM6

As I mentioned in one of my first posts, WiFi Programming is something that depends so much on the WLAN driver developed by the OEM, that an ISV can’t take for granted some interfaces and develop a WiFi-based application that is device-independent. But if a WLAN driver implements the WZC interfaces, then an ISV developer may technically use WZC APIs, even if this is not supported by Microsoft as it is something OEM-dependent. For example, if you want to do so with managed code, then you must read the MSDN article I had already mentioned Building a Wi-Fi Discovery Application with the .NET Compact Framework 2.0 that Chris Tacke wrote some years ago. Also, make sure not to forget that many settings can be done through the Wi-Fi Configuration Service Provider, *IF* the OEM’s WiFi stack properly integrates with the standard WiFi architecture (i.e. it’s WZC-compatible). Note: I’m saying so because I recently worked on a case where querying the Wi-Fi CSP didn’t return anything even if the driver was loaded and even if there was an open connection, and in this case the relevant OEM had to provide the developer with a specific solution.

The issue is the following… ok, ISVs are not supported by Microsoft on controlling the WiFi connection by using WZC APIs because Microsoft can’t know what the OEM did with its WLAN driver: but… can at least ISVs use whatever API to even simply do very basic stuff such as programmatically turn it off and on? Does this simple task really depend on how the OEM developed the driver?

Looking for a solution over the web, you’d find 2 possible approaches, which seem to be OEM-independent:

  1. Use some undocumented APIs exposed by a DLL that is inside every \Windows folder of a WM device (this would also allow to interact with the other wireless radios of the device)
  2. Use Power Manager APIs with the WiFi driver, which is a NDIS Miniport driver controllable by Power Manager (valid for WiFi only)

{SIDE NOTE. Interestingly… while looking for those, I came across the following post: “The most useful iPhone app I can't release”. I honestly don’t know the Apple iPhone SDK, therefore can’t express myself on it and didn’t know that it “specifically prohibits using private framework API” (assuming that’s true, based on all the comments of that post): it’s simply interesting to know that WiFi Programmers have similar “non-technical” problems with iPhone platform as well. And I’m sure that Android ones benefit from the “oversights” of the other platforms! That’s the beauty of the competition in a market… (side-by-side note: remember that on Windows Mobile you can programmatically control Bluetooth through the Bluetooth CSP).}

Let’s go back to Windows Mobile and examine the 2 approaches above.

 

1. Use some undocumented APIs exposed by a DLL that is inside every \Windows folder of a WM device (this would also allow to interact with the other wireless radios of the device)

This is an incredibly powerful approach, because it gives you the power of controlling the power state of all the wireless “devices”: Bluetooth, WiFi and Radio (i.e. the RIL = “Radio Interface Layer”: it’s the driver responsible for radio data connection (GPRS\EDGE\UMTS\..)). And it’s quite well known in the Mobile Community, we all know that \Windows\ossvcs.dll exposes some APIs (GetWirelessDevices, ChangeRadioState, etc, namely the “Wireless Device Power Management Functions”) that allow to develop your own Comm Manager (or Wireless Manager). And using those functions from within a managed application is even easier than on native applications, where you have to dynamically load the library and get the address of the function exposed with a particular ordinal: with managed apps, you can simply use the EntryPoint element of the DllImportAttribute.

Now, let’s pose the usual question: IS THIS SUPPORTED BY MICROSOFT TECHNICAL SUPPORT? The answer is simple here… Is it documented in the SDK? No: ergo it’s not officially supported… Or not? Those APIs are there: if the Dev Team didn’t want ISVs to use them then it wouldn’t develop a DLL which exposes them. Since the solution is well known in the Community and proved to work correctly in most cases, my personal opinion is that the problem is with the documentation here, which is simply missing for those APIs. I’ve asked the Product Group to consider adding the documentation in the public WM SDK… let’s see what that’ll sort out.

Just to be clear, even if one day those Wireless Device Power Management APIs will be fully documented and ready to be used by ISVs, what more can Microsoft Technical Support do after verifying that an ISV used the functions correctly? If those Wireless Device Power Management Functions don’t work correctly with one of the wireless drivers, chances are that the problem is with the driver, not with the functions (considering that the same APIs work correctly in other cases). And who develops the driver and technically has the ability to give support about it? Only the OEM…

 

2. Use Power Manager APIs with NDIS Miniport driver (valid for WiFi only)

If you look at pm.h header file of the WM5\6 SDKs, you’ll find a constant, which is is even public for 5.0 here and for 6.x here:

#define PMCLASS_NDIS_MINIPORT           TEXT("{98C5250D-C29A-4985-AE5F-AFE5367E5006}")

If you examine the registry contents at [HKLM\System\CurrentControlSet\Control\Power\State], you’ll see “devices” whose power-state can be controlled thru Power Manager APIs, accordingly to the SDK Documentation. One of those devices is the WiFi driver, described as “{98C5250D-C29A-4985-AE5F-AFE5367E5006}\<device name chosen by the OEM>” (The name can be seen also for example in the IPConfig section of the log generated by the Windows Mobile Network Analyzer PowerToy). Since the device appears under that registry key, an ISV can programmatically control its power state by using the documented Power Manager API SetDevicePower():

[DllImport("coredll.dll", SetLastError = true)] 
private static extern int SetDevicePower(string pvDevice, int dwDeviceFlags, DevicePowerState DeviceState); 

private enum DevicePowerState : int 
{ 
    Unspecified = -1, 
    D0 = 0, // Full On: full power, full functionality 
    D1, // Low Power On: fully functional at low power/performance 
    D2, // Standby: partially powered with automatic wake 
    D3, // Sleep: partially powered with device initiated wake 
    D4, // Off: unpowered 
}

private const int POWER_NAME = 0x00000001;

So, to turn the WiFi ON:

string driver = Utilities.WiFi.FindDriverKey(); 
SetDevicePower(driver, POWER_NAME, DevicePowerState.D0);

And OFF:

string driver = Utilities.WiFi.FindDriverKey(); 
SetDevicePower(driver, POWER_NAME, DevicePowerState.D4);

Utilities.WiFi.FindDriverKey() is simply a function that returns the whole registry key name of the key containing the NDIS MINIPORT class GUID defined in the SDK’s pm.h:

private static string FindDriverKey() 
{ 
     string ret = string.Empty; 

     //#define PMCLASS_NDIS_MINIPORT           TEXT("{98C5250D-C29A-4985-AE5F-AFE5367E5006}") 
     //(From "c:\Program Files (x86)\Windows Mobile 6 SDK\PocketPC\Include\Armv4i\pm.h") 
     string WiFiDriverClass= "{98C5250D-C29A-4985-AE5F-AFE5367E5006}";  

     foreach (string tmp in Registry.LocalMachine.OpenSubKey("System\\CurrentControlSet\\Control\\Power\\State", false).GetValueNames()) 
     { 
         if (tmp.Contains(WiFiDriverClass)) 
         { 
             ret = tmp; 
             break; 
         } 
     } 

     return ret; 
}

This proved to work on some devices I’ve tested it with, after reboots and also consistently with the Wireless Manager\Comm Manager of the device. I’ve found a forum post stating that on iPAQ devices this approach doesn’t work (and this is in line with the fact that HP provided a DLL within its private SDK exposing specific APIs to interact with their driver), however I tested this approach on an iPAQ Smartphone and it worked as well.

Now, the usual question: IS THIS SUPPORTED OR NOT BY MICROSOFT TECHNICAL SUPPORT? Every single detail an ISV uses in this approach is documented, hence supported. Cool! But… again, as before… what more can Microsoft Technical Support do after verifying that the ISV used the functions above correctly? If a Power Manager API doesn’t work correctly with a driver, chances are that the problem is with the driver, not with the PM API (considering that the same API works correctly in other cases). And who develops the driver and has technically the ability to give support about it? Again, the OEM…

 

I hope things are a bit clearer now… just let me know in case that’s not true!

 

Cheers,

~raffaele

 

P.S.: There’s an interesting point about NDIS Miniport drivers. On WM5, Microsoft's recommendation to OEMs was to implement the RIL Driver as PPP Adapter. Among other things, that allowed developers using IP Helper APIs to easily retrieve the related adapter information because the string "[Cellular Line]" was contained in the Adapter's name, for example thru the GetAdaptersInfo() API. Now, starting on WM6 Microsoft encourages OEMs to implement the RIL as NDIS Miniport driver, because this way the device will be ready to support simultaneous data calls (with PPP you can’t do that): "[...] One advantage of using WWAN-based GPRS connections is the ability to establish multiple GPRS connections simultaneously" (Establishing a WWAN-based GPRS Connection).

Note that not every single WM6.x device has already the RIL implemented as NDIS Miniport driver, demonstrating once again how OEMs have complete power on this. So, if you use IP Helper APIs or simply look the ipconfig report of the Windows Mobile Network Analyzer PowerToy, on WM6 devices where the OEM hasn’t yet “upgraded” the RIL you’ll still see

  • PPP Adapter [Cellular Line]:
    • Adapter Name ...... : Cellular Line

while on those WM6 devices where the RIL is already implemented as NDIS Miniport driver

  • Ethernet adapter Local Area Connection:
    • Adapter Name ...... : WWAN1-IBOX.TIM.IT-1 (for example)

therefore it's no longer distinguished from for example the WiFi connection:

  • Ethernet adapter Local Area Connection:
    • Adapter Name ...... : TNETW12511 (for example)

At TCP\IP Protocol level (e.g. by using IP Helper API GetAdaptersInfo), on those WM6 devices where the OEM followed Microsoft's recommendations for the RIL there’s no way to distinguish if a connection is WLAN (=WiFi) or WWAN (=RIL). In future I plan to post about another approach, probably based on NDISUIO (“NDIS User-mode I/O”)… I firstly need to be sure I’ll talk about something “SUPPORTED”… (that’s always my #1 priority! Or not? :-)

 

P.P.S.: Happy birthday to this blog!! THANK EVERYONE FOR READING\COMMENTING\CONTACTING ME, I couldn’t absolutely imagine the success it had in our Community (does it really deserve it? :-), so thanks again!! If you have any suggestion on anything, just let me know!

  • Advisory Services vs Problem\Resolution
  • Architectures for dynamic forms in NETCF

Many developers out there don’t know the full potential provided by the experts at Microsoft Technical Support… and I really want to do some marketing here!! For example, a typical scenario is as follows: you’ve just taken ownership of a project and don’t know where to start as it’s the first one targeting NETCF and Windows Mobile (just to use a beloved example…). A good investment at this point is to use some Advisory hours your Technical Support contract may have (you may need to be a Partner, probably – just check the //microsoft.com/support website).

Advisory Services are used to provide suggestions, guidelines, recommendations about how to possibly achieve a goal, so that ISVs can do a conscious choice; those services are something different from “My Visual Studio \ NETCF application doesn’t work as expected: help me on finding what’s wrong”-kind of requests (these are called “Problem\Resolution Services”. The 2 kinds of services are defined as follows:

"Problem Resolution Services" definition: Microsoft Problem Resolution Services provide assistance for problems with specific symptoms encountered while using a Microsoft product, where there is a reasonable expectation that the problem is caused by the Microsoft product. A Problem Resolution incident is defined as a single support issue and the reasonable effort needed to resolve it. A single support issue is a problem that cannot be broken down into subordinate issues. If a problem consists of subordinate issues, each shall be considered a separate incident. If a problem is determined by Microsoft to be the result of a defect in a Microsoft product, the customer will not be charged for that incident.

"Advisory Services" definition: Microsoft Advisory Services provides short-term advice and guidance for problems not covered with Problem Resolution Service and requests for consultative assistance for design, development and deployment issues.

So I really appreciated some time ago a .NET desktop developer asking for initial suggestions about how to develop his form-based NETCF application, where he needed to dynamically create forms and controls. I ended up with the following list of generic suggestions (just to start…, which may be useful for other NETCF Developers out there!

 

1. Shell Application

On large applications you might create a shell application: the main functionality is then created as separate applications. This minimizes the amount of memory being consumed by the active applications, which you know is a very scarce resource for NETCF apps. The shell would act as a guardian and kill background applications if it decided that too much memory is being consumed. Protect the shell and make sure it’s always available; then, make sure the foreground application the user is using is always running.

 

2. Microsoft Mobile Client Software Factory (July 2006) and Windows Mobile Accelerator (March 2008)

Microsoft Mobile Client Software Factory – This is a set of sample codes provided by Microsoft to Smart Devices developers: you would find various blocks (DataAccess, OrientationAware, and so on) that I would recommend you having at least on the dev machine to index the source files, so that when you want to search for a class’ usage you can use that sample. NOTE: some time ago I handled a case where the developer followed the pattern about dynamic form creation shown by the Mobile Client Software Factory, called *"Composite UI"*, and claimed that the perceived performances of dynamically creating\destroying controls (which is the base of the "Composite UI" pattern) were really bad. Indeed this was a pretty common complaint about the Mobile Client Factory (July 2006) and one of the reasons for the publishing of the

Mobile LOB Accelerator (March 2008) – where Composite UI block is NOT included.

Mobile Client Factory is a great idea once devices have lots of memory and processing power, as for desktops: but until then you are better off using Forms the way they were originally intended basically like the LOB Accelerator does.

So, pay much attention on dynamic creation of forms and controls.

 

3. MVC Pattern and NETCF

Just read all the MVC-related posts written by the NETCF guru Alex Yakhnin. He also posted the project to codeplex.

 

4. Mobile Application Architecture Pocket Guide

You must read “Mobile Application Architecture Pocket Guide”: as Rob Tiffany said, “[…] This is the first patterns & practices update to this guide since 2002 so it's a welcome sight to to have it out there for all our Windows Mobile developers.

 

HTH,

~raffaele

  • A possible approach valid on WM5\6\6.1, no longer using DTM_BROWSERDISPATCH
  • Appendix: sample code to find child windows (EnumChildWindows not available under Windows CE)

Undocumented doesn't mean "not achievable". Undocumented means that on future versions that particular detail may change. So if the application doesn't work on new platforms, that particular thing should be the first one to check. This was for example what happened to the ClassName of NETCF applications... see Daniel Moth's post about this: "#NETCF_AGL_".

Recently one developer I've worked with pointed me to this link, which describes a possible way to disable NETCF's WebBrowser's context-menu, which worked only on Windows Mobile 2003. He needed help on understanding why it no longer worked on WM5\6\6.1 and moreover on finding a suitable way to reach the same goal by other means, if that used in WM2003 really wasn't doable on later platforms.

Result of our analysis was that the aforementioned solution was based on DTM_BROWSERDISPATCH, which proved to work on WM2003 but is no longer working on WM5\6\6.1 devices, because the IBrowser interface was *deprecated* -- please see documentation here, specifically "IBrowser, IBrowser2 and IBrowser3 are deprecated and will not be supported in future versions. Instead use IWebBrowser2 and DWebBrowserEvents2. ".

A solution for current OSs may be found by subclassing the window of the native HTML control that is ultimately wrapped by the NETCF's WebBrowser: we used the same approach I described in Subclassing NETCF Applications. Subclassing means intercepting the Win32 messages sent to a window, by modifying its WndProc's address (which is the function that handles all messages, the so-called "message-pump"): once you intercept a message, you can deal with it or simply pass it to the old WndProc.

However, to be able to intercept messages you must be able to address the window you want to subclass: and here it comes the "undocumented" thing, which is simply the Class Name of the window associated to the native htmlview.dll control: this is not documented anywhere, but proved to be *PIEHTML* on every WM5\6\6.1 platforms I tested Remote Spy with. Furthermore, the undocumented detail has not been retrieved by looking at Microsoft's internal resources: it's publicly available to every developer who can use the "Remote Spy" tool available with VS2008. Here it is a screenshot:

image

Interestingly, we found out that there's an easy OEM-specific solution for Motorola devices, while for other other devices\emulators we had to analyze the message-chain to understand a possible pattern, and found one based on WM_NOTIFY and WM_LBUTTONUP, which should work on every scenario (to be thoroughly tested...).

So, basically we want to avoid the contextmenu to appear: being the WM_CONTEXTMENU the first message sent to the window while pressing&holding the stylus, the 1st test was to use a code like the following:

private IntPtr NewWndProc(IntPtr hWnd, uint msg, int wParam, int lParam)
{
    switch (msg)
    {
        case WM_CONTEXTMENU:
            return IntPtr.Zero;
            break;
    } 
    return CallWindowProc(oldWndProc, hWnd, msg, wParam, lParam); 
}

As I said, this worked only for Motorola devices: I can't state why since it may depend on whatever customization around the webview.dll native control done by the OEM (?). In any case, that didn't work on WM5\6\6.1 Emulators (so nothing OEM-dependent), and we had  to look at other patterns. An idea was to intercept WM_NOTIFY and pass it to the old WndProc only if the next WM_message was not one of WM_INITMENUPOPUP, WM_ENTERMENULOOP and WM_CONTEXTMENU: by doing so I could prevent the contextmenu to appear, however links on the page could not be clicked. Everything worked apart from this "detail"... Tongue out so, looking at the chain of messages (still by using simply Remote Spy!) I could notice that when user clicks on a link there's a WM_LBUTTONUP followed by a WM_NOTIFY. So I based the NewWndProc on the following and this worked:

private IntPtr NewWndProc(IntPtr hWnd, uint msg, int wParam, int lParam)
{
    switch (msg)
    {
        //when clicking on a link on the page, WM_NOTIFY is sent AFTER WM_LBUTTONUP
        //however, reset the bool variable if WM_NOTIFY is not received just after WM_LBUTTONUP
        case WM_LBUTTONUP:
            bLButtonUpHandled = true;
            break;
        case WM_NOTIFY:
            if (bLButtonUpHandled)
            {
                bLButtonUpHandled = false;
                break;
            }
            else
            {
                //block WM_NOTIFY
                return IntPtr.Zero;
            }
        case WM_CONTEXTMENU:
            //If you need to do something custom, do it here
            DialogResult dlg = MessageBox.Show("Close?", this.Text, MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button1);
            if (dlg == DialogResult.Yes)
            { 
                this.webBrowser1.Navigate(new Uri("http://blogs.msdn.com/raffael"));
                return IntPtr.Zero;
            }
            break;

        //for every other WM_x, if this is coming after WM_LBUTTONUP then reset bLButtonUpHandled
        default:
            if (bLButtonUpHandled)
                bLButtonUpHandled = false;

            break;
        
    }
    return CallWindowProc(oldWndProc, hWnd, msg, wParam, lParam);
}

Remember that the code provided is for didactic purposes only: it shows how you can reach the goal, however doesn't contain error-handling. Obviously I do recommend including try\catch and exception-handling!!

APPENDIX: FindWindow API can be used only for top-level windows. If you need the handle of a child window knowing its ClassName and having the handle of the parent, then you can use for example the following code:

private IntPtr FindChildWindowByParent(string strChildClassName, IntPtr hWndTopLevel)
{
    bool bFound = false;
    IntPtr hwndCur = IntPtr.Zero;
    IntPtr hwndCopyOfCur = IntPtr.Zero;
    char[] chArWindowClass = new char[32];

    do
    {
        // Is the current child null?
        if (IntPtr.Zero == hwndCur)
        {
            // get the first child
            hwndCur = GetWindow(hWndTopLevel, (uint)GetWindowFlags.GW_CHILD);
        }
        else
        {
            hwndCopyOfCur = hwndCur;
            // at this point hwndcur may be a parent of other windows
            hwndCur = GetWindow(hwndCur, (uint)GetWindowFlags.GW_CHILD);

            // in case it's not a parent, does it have "brothers"?
            if (IntPtr.Zero == hwndCur)
                hwndCur = GetWindow(hwndCopyOfCur, (uint)GetWindowFlags.GW_HWNDNEXT);
        }

        //if we found a window (child or "brother"), let's see if it's the one we were looking for
        if (IntPtr.Zero != hwndCur)
        {
            GetClassName(hwndCur, chArWindowClass, 256);
            string strWndClass = new string(chArWindowClass);
            strWndClass = strWndClass.Substring(0, strWndClass.IndexOf('\0'));

            bFound = (strWndClass.ToLower() == strChildClassName.ToLower());
        }
        else
            break;
    }
    while (!bFound);

    //found!
    return hwndCur;
}

If you don't have the handle of the parent or even don't know which top-level window it is, since we don't have EnumChildWindows under Windows CE, you need to use EnumWindows and basically invoke the function above for each of them, to be run only in case the child window hasn't been found yet -- I mean the following:

private void FindChildWindow(string strChildClassName, string strChildWindowName)
{
    //Enum all top-level windows
    //for each window, see if it has childs and if among them there's the window we're looking for
    EnumWindows(new EnumWindowsDelegate(EnumWindowsProc), 0);
    return;
}

private int EnumWindowsProc(IntPtr hWndParent, int lParam)
{
    IntPtr hwndCur = IntPtr.Zero;
    IntPtr hwndCopyOfCur = IntPtr.Zero;
    char[] chArWindowClass = new char[32];
    
    //for each window, see if it has childs and if among them there's the window we're looking for
    //if already found, don't search again
    if (!bFound)
    {
        do
        {
            // Is the current child null?
            if (IntPtr.Zero == hwndCur)
            {
                // get the first child
                hwndCur = GetWindow(hWndParent, (uint)GetWindowFlags.GW_CHILD);
            }
            else
            {
                hwndCopyOfCur = hwndCur;
                // at this point hwndcur may be a parent of other windows
                hwndCur = GetWindow(hwndCur, (uint)GetWindowFlags.GW_CHILD);

                // in case it's not a parent, does it have "brothers"?
                if (IntPtr.Zero == hwndCur)
                    hwndCur = GetWindow(hwndCopyOfCur, (uint)GetWindowFlags.GW_HWNDNEXT);
            }

            //if we found a window (child or "brother"), let's see if it's the one we were looking for
            if (IntPtr.Zero != hwndCur)
            {
                GetClassName(hwndCur, chArWindowClass, 256);
                string strWndClass = new string(chArWindowClass);
                strWndClass = strWndClass.Substring(0, strWndClass.IndexOf('\0'));

                bFound = (strWndClass.ToLower() == strChildClassName.ToLower());
            }
            else
                break;
        }
        while (!bFound);

        //found!
        hWndTarget = hwndCur;
    }
    return 1;
}

[DllImport("coredll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool EnumWindows(
    [MarshalAs(UnmanagedType.FunctionPtr)]EnumWindowsDelegate lpEnumFunc,
    int lParam);

internal delegate int EnumWindowsDelegate(IntPtr hwnd, int lParam); //NOTE THE RETURN VALUE!!!

Last hint: when invoking delegates of NATIVE functions, NETCF has a limitation about the type returned: it must be an integer, or in any case a blittable and integer-based datatype. I was using managed Boolean (bool in C#), but this is not blittable to a native BOOL because they are 1 Byte in managed code and 4 bytes in native code!! This limitation was a design decision on V2 because the registers for return values are 4 Byte integer registers on most platforms... and made me waste some time… it sufficed to use private int EnumWindowsProc() instead of private bool EnumWindowsProc() and Win32 and .NET started talking each other. Nerd

Cheers,

~raffaele

If you know my interest on troubleshooting memory issues on Windows Mobile applications (this was the topic of my very first post!), you'll forgive me if this time I simply link an article on CodeProject... Smile Visualizing the Windows Mobile Virtual Memory Monster. Maybe a candidate for Microsoft Windows Mobile Developer Contest 2008?

 

Cheers,

~raffaele

More Posts Next page »
 
Page view tracker