October, 2011

  • The Old New Thing

    My least effective Hallowe'en costume

    • 18 Comments

    One year I decided to dress as a nerd for Hallowe'en. I took an old pair of glasses with large lenses, "repaired" it with some masking tape, and combined it with a plaid dress shirt with a pocket protector and courduroy pants that were too short.

    Nobody at work realized I was wearing a costume.

    The best Hallowe'en work costume I saw was someone who came dressed as a Christmas tree. Complete with Christmas lights. She had to make sure to stand near an electrical outlet so she could plug herself in.

    I wish I had a picture: One of my friends spotted somebody at a work Hallowe'en party dressed as Bill Gates. But not the Bill Gates of today. He was dressed as Bill Gates from the arrest photo.

  • The Old New Thing

    Why isn't my transparent static control transparent?

    • 27 Comments

    A customer reported that their application uses transparent static controls positioned over a bitmap image control, but even though they set the Transparent property on the static control, the static control isn't transparent. The customer was kind enough to provide clear steps to illustrate the problem:

    • Open Visual Studio 2005 or 2008.
    • From the menu, select File, New File, Visual C++, Resource Template File (RCT).
    • Right-click on the RCT file, select Add Resource, and add a bitmap named IDB_BITMAP1.
    • Open the dialog box (IDD_DIALOG1) and add a "Picture Control", specifying IDC_BITMAP_1 as its ID.
    • Change the IDC_BITMAP_1 type to Bitmap and change the value of the Image property to IDB_BITMAP1.
    • Add a "Static Text" control IDC_TEST_STATIC and set its caption to "This is a test".
    • Reposition the static control so it overlaps the IDC_BITMAP_1 control.
    • On the IDC_TEST_STATIC control, set the Transparent property to True.
    • Type Ctrl+T to test the dialog and observe that the static control is not transparent.
    Dialog
    This is a test

    The Transparent property in Visual Studio corresponds to the WS_EX_TRANSPARENT window style, and the documentation explains that

    WS_EX_TRANSPARENT: The window should not be painted until siblings beneath the window (that were created by the same thread) have been painted. The window appears transparent because the bits of underlying sibling windows have already been painted.

    The observed behavior, therefore, matches the documentation: The control underneath (the bitmap control) paints first, and then the static control paints on top of it. And a static text control paints by filling with the background brush and drawing the text on top of it. You can customize this behavior by responding to the WM_CTL­COLOR­STATIC message:

    HBRUSH CTestDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
    {
     HBRUSH hbr = __super::OnCtlColor(pDC, pWnd, nCtlColor);
     if (pWnd->GetExStyle() & WS_EX_TRANSPARENT) {
      pDC->SetBkMode(TRANSPARENT);
      hbr = GetStockBrush(HOLLOW_BRUSH);
      // even better would be to use a pattern brush, if the background is fixed
     }
     return hbr;
    }
    

    The customer appreciated the explanation but was puzzled as to why the Transparent is available if it doesn't work. "We understand that we can use the WS_EX_TRANSPARENT window style to create a transparent window and it will be painted after its underlying siblings, but the window style by itself doesn't make the static control transparent. But if we have to write the code above, why is the Transparent property available in the Properties box?" They included a screen shot from Visual Studio where the built-in help text for the Transparent property reads "Specifies that the control will have a transparent background."

    The WS_EX_TRANSPARENT style doesn't mean "transparent"; it means "paint over siblings." The style is called "transparent" not because it makes the window transparent but because it makes transparency possible. It is one of the steps (but not the only one) for making child controls render transparently. Another important step is ensuring that the control does not erase its background in its WM_ERASE­BKGND, and that's the step that the On­Ctl­Color override performs.

    Why is the Transparent property offered for static controls when it doesn't actually work? Shouldn't it be disabled for static controls?

    The reason why it is offered is that it is a general window style that can be set on any control. Visual Studio doesn't know which controls can render transparently and which ones don't, or what extra steps are necessary to get the ones who can render transparently to actually do so. It just exposes the WS_EX_TRANSPARENT style and hopes that you know what you're doing.

    In retrospect, it was a poor chose of name for the style. And the incorrect online help doesn't make things any better.

    Bonus chatter: Note that the WS_EX_TRANSPARENT extended style is overloaded. In addition to affecting painting, it also affects hit-testing.

  • The Old New Thing

    Why do the pinned items in the Jump List go on the top instead of the bottom?

    • 33 Comments

    When you pin items to the Jump List, they go to the top of the menu that appears when you right-click the Taskbar item. Why not put the pinned items at the bottom? After all, over 98% of users leave the taskbar at the bottom of the screen, so putting the pinned items at the bottom of the list maintains a consistent position relative to the Taskbar icon, permitting the development of muscle memory.

    The Taskbar folks tried out all sorts of ideas for ordering the Pinned items, the Frequent/Recent items, the Tasks, and the system commands on that one pop-up menu. And these ideas were put to the test: With real users.

    Usability tests are one of those things that every developer should go through. You think you've designed a system that is intuitive and easy to use, and then you are shocked back into reality as you watch user after user struggle with your masterpiece.

    In this case, the usability tests revealed that most people look for the important items at the top of the list. When they went looking for their pinned items, they started at the top. And often, if it wasn't there, they simply gave up.

    The resulting order of items (Pinned, Frequent/Recent, Tasks, system) reflects the results of these studies. Since Pinned items go at the top, that leaves the opportunity to put the system commands at the bottom so that they have a consistent location. For example, Close is always the last item. That's where your muscle memory can develop. The Tasks go next to the system commands since they act like application-specific extensions of the system commands. Tasks also do not change, so that permits muscle memory to extend further into the menu.

    Once the other three items are placed, the decision of where to put the Frequent or Recent items is forced: It goes beneath the Pinned items and above the Tasks.

    There you have it: The order of the items in the right-click pop-up menu for Taskbar icons. There's a method to its madness. And it was decided by you, the users.

    Pre-emptive hate: Yes we know that you think messing with the taskbar button right-click menu was the stupidest idea on the planet.

  • The Old New Thing

    How can I get notified when some other window is destroyed?

    • 18 Comments

    A customer wanted to know whether there was a method (other than polling) to monitor another window and find out when it gets destroyed. The goal was to automate some operation, and one of the steps was to wait until some program closed its XYZ window before moving on to the next step. Finding the XYZ window could be done with a Find­Window, but since the window belongs to another process, you can't subclass it to find out when it gets destroyed.

    Enter accessibility.

    The Set­Win­Event­Hook function lets you monitor accessibility events, and you can do it globally, for a particular process, or for a particular thread. Since we're interested in just one specific window, we can restrict our monitoring to a specific process and thread. (You don't want to monitor too much or you end up getting spammed with notifications you don't care about, which will annoy both you and the end users who are wondering why all their CPU is being consumed on pointless activity.)

    Let's take our scratch program and have it monitor an arbitrary window whose name is passed on the command line.

    HWND g_hwnd; /* our main window */
    HWND g_hwndTarget; /* the window we are monitoring */
    HWINEVENTHOOK g_hweh;
    
    void CALLBACK WinEventProc(
        HWINEVENTHOOK hWinEventHook,
        DWORD         event,
        HWND          hwnd,
        LONG          idObject,
        LONG          idChild,
        DWORD         idEventThread,
        DWORD         dwmsEventTime)
    {
     if (event == EVENT_OBJECT_DESTROY &&
         hwnd == g_hwndTarget &&
         idObject == OBJID_WINDOW &&
         idChild == INDEXID_CONTAINER) {
      PostMessage(g_hwnd, WM_CLOSE, 0, 0);
     }
    }
    

    The Win­Event­Hook function is where it all happens. If our callback is told that a window was destroyed, and the window handle matches the one we are monitoring, then post ourselves a WM_CLOSE message, which will close the window and exit the program.

    The rest is just scaffolding to get to the point where our Win­Event­Hook gets called.

    BOOL
    OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
    {
     DWORD dwProcessId;
     DWORD dwThreadId = GetWindowThreadProcessId(g_hwndTarget,
                                                &dwProcessId);
     if (dwThreadId)
     g_hweh = SetWinEventHook(
         EVENT_OBJECT_DESTROY, EVENT_OBJECT_DESTROY,
         NULL, WinEventProc,
         dwProcessId, dwThreadId, WINEVENT_OUTOFCONTEXT);
     return g_hweh != NULL;
    }
    

    To register the hook, we obtain the thread ID and process ID of the window we are interested in tracking, then use the Set­Win­Event­Hook function to register our callback function, saying that we want to receive only EVENT_OBJECT_DESTROY notifications by passing it as both the event­Min and event­Max. We give it our callback function, and since we ask for WIN­EVENT_OUT­OF­CONTEXT, we don't need to pass a module handle since we are not requesting injection.

    Notice that we restrict our hook as much as we can. We specify that we care only about one event, and we are interested in only one process and only one thread. It's generally a good idea to restrict the hook as much as possible.

    Of course, we also have to unregister the hook when we're done.

    void
    OnDestroy(HWND hwnd)
    {
     if (g_hweh) UnhookWinEvent(g_hweh);
     PostQuitMessage(0);
    }
    

    And finally, we use our command line to specify the title of the window we are monitoring.

    int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev,
                       LPSTR lpCmdLine, int nShowCmd)
    {
     ...
      g_hwndTarget = FindWindowA(lpCmdLine);
      g_hwnd =
      hwnd = CreateWindow(
     ...
    }
    

    With the Run dialog open, run this program with the command line argument Run. The program window opens, and when you click Cancel in the Run dialog, the program window closes. Wow that was exciting.

    Bonus chatter: Remember that the window manager needs a message pump in order to call you back unexpectedly.

    Exercise: Since we registered for only one thing, why did we have to perform the tests in Win­Event­Proc? Why not just simplify the function to this?

    void CALLBACK WinEventProc(
        HWINEVENTHOOK hWinEventHook,
        DWORD         event,
        HWND          hwnd,
        LONG          idObject,
        LONG          idChild,
        DWORD         idEventThread,
        DWORD         dwmsEventTime)
    {
     PostMessage(g_hwnd, WM_CLOSE, 0, 0);
    }
    

    Exercise: With the Run dialog open, run this program with the command line argument Run. Now instead of clicking Cancel in the Run dialog, type some garbage into the edit control and then click OK. The Run dialog goes away and an error message appears instead. Why is the scratch program still running?

  • The Old New Thing

    Raymond misreads restaurant names: Local 360

    • 12 Comments

    Okay, maybe this time I'm misreading it on purpose, but when I see the name of popular new restaurant Local 360, I think, "That doesn't look all that low-calorie to me. I bet their dishes have more than 360 calories."

    Or maybe it's "We serve high-calorie foods five days a year. Congratulations, you happen to have picked those five days."

  • The Old New Thing

    No good deed goes unpunished: Helping to redirect a question

    • 42 Comments

    It is a common occurrence that a question is sent to a mailing that is close, but not quite right. Usually somebody will provide information to help redirect the question to a more appropriate mailing list. But this effort does not always go unpunished.

    From: X

    A customer is encountering a problem with Product Q when they blah blah blah. Can somebody help?

    From: Y

    Support for Product Q is handled by Team R. Note that Product Q is out of mainstream support; you will need to have an extended support agreement.

    From: X

    Thank you. I have confirmed that the customer has an extended support agreement for Product Q. Please help me on how to proceed further with this case.

    Person Y fell into the trap of being too helpful. If they had stopped after the sentence "Support for Product Q is now handled by Team R," they might have gotten away clean. But no, they made the mistake of providing a tiny bit more information, and person X has now latched on.

    Here's another example, and by an amazing coincidence, it came from the same Person X.

    From: X

    A customer is encountering a problem with Product P when they blah blah blah. Can somebody help?

    From: Z

    For this particular problem, I'd contact Team P.

    From: X

    Thank you for your prompt response. I look forward to the next update from you.

    Person Z made the mistake of only implying the "If I were you..." before the sentence "I'd contact Team P." Person X therefore interpreted the "I'd contact Team P" as saying "I will contact Team P for you."

    The moral of the story is that when you are redirecting a question to a more appropriate mailing list, you need to be very explicit that you are telling the person what to do and are not actually assuming responsibility for doing it. Otherwise you run the risk of being punished for being helpful.

    • "Support for Product Q is handled by Team R. You need to send your questions to them."
    • "For this particular problem, you should contact Team P."

    Bonus chatter: Just last week I tried to employ the lesson from this message:

    From: Q

    A customer wants Feature X to behave like ZZ instead of YY. Can somebody help?

    From: Raymond

    In order to get it changed, you will have to file a Design Change Request with the X team.

    Apparently even a statement this direct was not correctly interpreted.

    From: Q

    Thanks. I wanted to check about the request which the customer has requested to change Feature X to behave like ZZ instead of YY.

    Not only did the person think that I had taken responsibility for resolving their issue, they thought I had written up the Design Change Request for them and submitted it to the X team!

  • The Old New Thing

    Squeezing the last bit of enjoyment out of the lost half-inning of a baseball game

    • 11 Comments

    A colleague of mine complained, "When the home team is winning, they don't bother playing the bottom half of the ninth inning. I'm getting ripped off! Make them finish the game!"

    This led to some speculation as to how the visiting team could manage to salvage a win out of that final half-inning, given that they had no further opportunity to score any runs. My proposal was that they could try to get as many players on the home team to be rendered ineligible to play (say, by injuring them or provoking fights and getting them thrown out of the game), until the home team had fewer than nine eligible players, at which point they would be forced to forfeit the game.

    Mind you, employing this technique would certainly earn retaliation the next time you faced that team, so it's not a viable long-term strategy. And the league would certainly crack down on this sort of behavior.

    It was merely a theoretical exercise.

    I consulted with my umpire colleague, and he said that the home team could respond by simply refusing to come to bat. Rule 6.02(c) states that if the batter refuses to take his position in the batter's box, the umpire shall call a strike on the batter. Do this three times, and the batter is out. Do this nine times, and the half-inning is over.

    Mind you, the standard way of implementing this rule is to instruct the pitcher to deliver a pitch, and to call it a strike no matter where the pitch lands. But what if the pitcher doesn't want the free strike? Rule 8.04 says that if the pitcher fails to deliver the pitch within 12 seconds, the umpire shall call a ball. Or maybe this is where the umpire can exercise his judgement and call the strike in a nonstandard way. Or the umpire could declare the fielding team to have forfeited for "refusing to continue play during a game", per rule 4.15(c).

    (Though there is an interesting conflict between rules 6.02(c) and 4.15(c). If the batter refuses to come to the plate, both rules apply, yet the penalties are different.)

    This is all very confusing, what with conflicting rules applying to the same situation, and the umpire will have to improvise, per rule 9.01(c). (My favorite example of umpires having to improvise is the case where a switch-hitter faced an ambidextrous pitcher. At the end of the season, they added a new rule to cover this situation.)

    Looking for another loophole in the official rules of baseball, I found another way the visiting team could induce a forfeit: Provoke the crowd into rioting for fifteen minutes, then request that the umpire declare a forfeit on the grounds of inadequate security, by rule 3.18. On the other hand, the declaration is at the umpire's discretion, and seeing as you are the ones who provoked the riot, the umpire is unlikely to grant you that one. (So you'll have to provoke the riot surreptitiously.)

  • The Old New Thing

    If the shell is written in C++, why not just export its base classes?

    • 22 Comments

    ton suggested that since the shell is written in C++, IShell­Folder should have been an abstract class, and then it could have used techniques like exceptions and Inversion of Control.

    Okay, first of all, I'm not sure how Inversion of Control is something that requires C++, so I'm going to leave that aside.

    Second of all, who says the shell is written in C++? As it happens, when IShell­Folder was introduced in Windows 95, the entire shell was written in plain C. That's right, plain C. Vtables were built up by hand, method inheritance was implemented by direct replacement in the vtable, method overrides were implemented by function chaining, multiple inheritance was implemented by manually moving the pointer around.

    const IShellFolderVtbl c_vtblMyComputerSF =
    {
     MyComputer_QueryInterfaceSF,
     MyComputer_AddRefSF,
     MyComputer_ReleaseSF,
     MyComputer_ParseDisplayName,
     ... you get the idea ...
    };
    
    const IPersistFolderVtbl c_vtblMyComputerPF =
    {
     MyComputer_QueryInterfacePF,
     MyComputer_AddRefPF,
     MyComputer_ReleasePF,
     MyComputer_Initialize,
    };
    
    struct MyComputer {
     IShellFolder sf;
     IShellFolder pf;
     ULONG cRef;
     ... other member variables go here ...
    }
    
    MyComputer *MyComputer_New()
    {
     MyComputer *self = malloc(sizeof(MyComputer));
     if (self) {
      self->sf.lpVtbl = &c_vtblMyComputerSF;
      self->pf.lpVtbl = &c_vtblMyComputerPF;
      self->cRef = 1;
      ... other "constructor" operations go here ...
     }
     return self;
    }
    
    // sample cast
    MyComputer *pThis;
    IPersistFolder *ppf =  &pThis->pf;
    
    // sample method call
    hr = IShellFolder_CompareIDs(psf, lParam, pidl1, pidl2);
    // which expands to
    hr = psf->lpVtbl->CompareIDs(psf, lParam, pidl1, pidl2);
    
    // sample forwarder for multiply-derived method
    HRESULT STDCALL MyComputer_QueryInterfacePF(
        IPersistFolder *selfPF, REFIID riid, void **ppv)
    {
     MyComputer *self = CONTAINING_RECORD(selfPF, MyComputer, pf);
     return MyComputer_QueryInterfaceSF(&self->sf, riid, ppv);
    }
    

    So one good reason why the shell didn't export its C++ base classes was that it didn't have any C++ base classes.

    Why choose C over C++? Well, at the time the Windows 95 project started, C++ was still a relatively new language for systems programming. While there were certainly people on the shell team capable of writing code in C++, the old-timers grew up with C as their native language, and the newcomers weren't taught C++ in their computer science classes. (Computer science departments still taught primarily C or Pascal, with maybe some Lisp if you took an AI class.) Also, the C++ compilers of the day did not provide fine control over automatic code generation,¹ and since even saving 4KB of memory had a perceptible impact on overall system performance, manually grouping rarely-used functions into the same region of memory of memory (so they could all remain paged out) was still a common practice.

    But even if the shell was originally written in C++, exporting the base classes wouldn't have been a good idea. COM is a language-neutral platform. People have written COM objects in C, C++, Visual Basic, C#, Delphi, you-name-it. If IShell­Folder were an exported C++ base class, then you have effectively said, "Sorry, only C++ code can implement IShell­Folder. Screw off, all you other languages!"

    But wait, it's worse than just that. Exporting a C++ base class ties you to a specific compiler vendor, because name decoration is not standardized. So it's not just "To implement IShell­Folder you must use C++" but "To implement IShell­Folder you must use the Microsoft Visual Studio C++ compiler."

    But wait, it's worse than just that. The name decoration algorithm can even change between compiler versions. Furthermore, the mechanism by which exceptions are thrown and caught is not merely compiler-specific but compiler-version specific. If an exception is thrown by code compiled by one version of the C++ compiler and reaches code compiled by a different version of the C++ compiler, the results are undefined. (For example, the older version of the C++ compiler may not have supported RTTI.) So it's not just "To implement IShell­Folder you must use C++" but "To implement IShell­Folder you must use Microsoft Visual C++ 2.0." (So maybe Bjarne was right after all.)

    But wait, it's worse than just that. Exporting a C++ base class means that the base class can never change, because various properties of the base class become hard-coded into the derived classes. The list of interfaces implemented by the base class becomes fixed. The size of the base class is fixed. Any inline methods are fixed. The precise layout of member variables is fixed. Exporting a C++ base class for IShell­Folder would have meant that the base class could never change. You want support for IShell­Folder2? Sorry, we can't add that without breaking everybody who compiled with the old header file.

    Exercise: If exporting base classes is so horrible, why does the CLR do it all over the place?

    Footnote

    ¹ Actually, I don't think even the C++ compilers of today give you fine control over automatic code generation, which is why Microsoft takes a conservative position on use of C++ in kernel mode, where the consequences of a poorly-timed page fault are much worse than simply poor performance. It will bluescreen your machine.

  • The Old New Thing

    The video of Microsoft Store employees dressed in Windows colors, revealed by a falling curtain, gee that looks familiar, somehow

    • 13 Comments

    One of my former colleagues tipped me off to this video of the Grand Opening of the newest Microsoft Store, specifically calling out this moment at timecode 0:48 of a curtain dropping, revealing cheering employees dressed in Windows colors, which seemed familiar, somehow.

    Oh right, because I did the same thing over fifteen years ago, at timecode 5:25 in this video.

  • The Old New Thing

    The PSN_SETACTIVE notification is sent each time your wizard page is activated

    • 5 Comments

    A customer had received a number of crashes via Windows Error Reporting and believed that they had found a bug in the tree view common control.

    In our UI, we have a tree view with checkboxes. The tree view displays a fixed item at the top, followed by a variable number of dynamic items. When the user clicks Next, we look at the tree view to determine what the user selected. The code goes like this (pseudo):

    htiRoot = GetTreeRoot();
    
    // First process the fixed item
    htiFixed = GetChild(htiRoot);
    if (IsTreeViewItemChecked(htiFixed)) {
        .. add the fixed item ...
    }
    
    // Now process the dynamic items
    hti = GetNextSibling(htiFixed);
    while (hti != NULL) {
      if (IsTreeViewItemChecked(hti)) {
        ... add the dynamic item ...
      }
      hti = GetNextSibling(hti);
    }
    

    In the crashes we receive, other variables in the program indicate that there should be only one dynamic item, but our loop iterates multiple times. Furthermore, the first time through the loop, the hItem is not the handle to the first dynamic item but is in fact the handle to the fixed item. This naturally results in a crash when we try to treat the fixed item as if it were a dynamic item.

    Another thing we noticed is that at the time of the crash, all three variables htiRoot htiFixed, and hti have the same value.

    Our attempts to reproduce the problem in-house have been unsuccessful. From our analysis, we believe that the tree view APIs used to obtain handles to children and sibling nodes are misbehaving.

    The customer included the crash bucket number, so we were able to connect to the same crash dumps that the customer was looking at.

    The first thing to dismiss was the remark that all three of the local variables had the same value. This is to be expected since they have non-overlapping lifetimes, and the compiler decided to alias them all to each other to save memory.

    ...
            lea     eax,[ebp+8]         ; htiRoot
            push    eax
            push    1                   ; some flag
            push    ebx                 ; some parameter
            call    00965fb9            ; GetTreeRoot
            mov     [ebp-2Ch],eax
            test    eax, eax
            jl      00971a49            ; failed - exit
    
            mov     edi, [_imp__SendMessageW]
            push    4                   ; TVGN_CHILD
            push    110Ah               ; TVM_GETNEXTITEM
            push    dword ptr [ebx+10h] ; window handle
            call    edi                 ; SendMessage
            mov     [ebp+8],eax         ; htiFixed
    
        ... eliding if (IsTreeViewItemChecked(...)) ...
            jmp     00971a1c            ; enter loop
    
    00971931:
        ... eliding if (IsTreeViewItemChecked(...)) ...
    
    00971a1c:
            push    dword ptr [ebp+8]   ; hti
            push    1                   ; TVGN_NEXT
            push    110Ah               ; TVM_GETNEXTITEM
            push    dword ptr [ebx+10h] ; window handle
            call    edi                 ; SendMessage
            mov     [ebp+8],eax         ; update hti
            test    eax, eax            ; hti == NULL?
            jne     00971931            ; N: continue loop
    

    I've removed code not directly relevant to the discussion. The point to see here is that the compiler combined all three variables into one physical memory location at [ebp+8] since there is no point in the program where more than one of the values is needed at a time. In other words, the compiler rewrote your code like this:

    hti = GetTreeRoot();
    
    hti = GetChild(hti);
    if (IsTreeViewItemChecked(hti)) {
        .. add the fixed item ...
    }
    
    while ((hti = GetNextSibling(hti)) != NULL) {
      if (IsTreeViewItemChecked(hti)) {
        ... add the dynamic item ...
      }
    }
    

    Not only did the compiler merge all your hti variables into one, it realized that once it did that, the two calls to Get­Next­Sibling could be folded together as well.

    Okay, one mystery solved. What about the others?

    From studying the crash dump, the shell team determined that the reason the first dynamic item appears to be the fixed item is that the tree view actually has two fixed items:

    003d06d8 Root
    + 003d0a38 "Configuration settings"
    + 003d0888 "Configuration settings"
    + 003d07b0 "Saved game from May 27, 2009 at 2:42 PM (playing as Thor)"
    + 003d0600 "Saved game from May 27, 2009 at 2:42 PM (playing as Thor)"
    

    "Configuration settings" is the fixed item, and the saved games are the dynamic items. (This isn't the actual scenario from the customer, but it gets the point across.) The customer was wrong to use the definite article when referring to the handle to the fixed item, since there are two fixed items here. In a sense, the customer's understanding that there is only one fixed item clouded their ability to debug the problem: When they saw another fixed item, they assumed not that they received another item that was fixed, but rather that they were getting the same fixed item twice.

    Seeing that the tree view was being populated twice directed the next step of the investigation: Why?

    The code that populates the tree view is called from the wizard page's PSN_SET­ACTIVE notification, and that one piece of information was the last piece of the puzzle.

    The PSN_SET­ACTIVE notification is sent each time the wizard or property sheet page is selected as the current page. If the page is activated twice, then you will get two PSN_SET­ACTIVE notifications. The solution was to populate the tree view only the first time the page was activated.

    Exercise: What was missing from the customer's testing that prevented them from reproducing the problem in their labs?

Page 1 of 3 (27 items) 123