Click here to Skip to main content
15,867,568 members
Articles / Desktop Programming / Win32

Closing Microsoft Dynamics GP Report Destination window

Rate me:
Please Sign up or sign in to vote.
4.88/5 (6 votes)
22 Aug 2014CPOL7 min read 23.5K   139   4   9
This article will show how to close the Microsoft Dynamics GP Report Destination window using both a .NET Add-in library and a stand-alone Windows Forms application.

Introduction

While creating integrations for Microsoft Dynamics GP, oftentimes we have to automate existing processes involving reports. The Report Destination window is a modal dialog that shows up by default whenever a Dexterity report is executed without having a destination specified in the run report statement call. We cannot alter the call and Dexterity development system fails to offer a consistent way of closing the Report Destination window. The automation is interrupted and the end-user has to manually close the dialog.

The solution presented here gives one full control over the closing of the Report Destination window. It was initially developed and subsequently deployed as a 32-bit Add-in integration library for Microsoft Dynamics GP 2010, running on Windows 7 and .NET 3.5.

The solution

In order to close the Report Destination window, we need a modality to detect when this window is displayed. The Report Destination window acts like a standard Windows modal dialog. When opened, this dialog will sit on top of our Dynamics GP application windows waiting for the user to manually close it. If the Dynamics GP application is the active application, the Report Destination window becomes the topmost window on our screen.

Windows offers, by means of a Win32 API event hook, an application-defined cross-process callback function that the system calls in response to events generated every time the topmost window has changed. Every such call comes with a handle of the window that generated the event. Using this handle we can obtain the caption of the topmost window and we can compare it with the known Report Destination window’s caption. When we have a match, we know the Report Destination dialog is the topmost window on our screen.

After the Report Destination dialog is detected, we need a way to close it. One approach to closing a modal dialog is by sending an Escape sequence to cancel it, and this is the solution chosen for this article.

Lastly, we want to be able to enable or disable the “closing” as needed. The closing code is encapsulated in a Closer class that exposes two public methods for this purpose: EnableClosing() and DisableClosing().

Step1: Detecting the Report Destination window

Our code sets up a Win32 API event hook function to start “listening” to all foreground window changes of the Dynamics GP application:

C#
Process[] localProcesses = Process.GetProcesses();
int DynamicsPID = 0;
foreach (Process p in localProcesses)
{
    if (p.ProcessName == DYNAMICS_GP_PROCESS_NAME)
    {
        DynamicsPID = p.Id;
        break;
    }
}
hHook = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND,IntPtr.Zero, callbackProc, (uint) DynamicsPID, 0, WINEVENT_OUTOFCONTEXT);

We are registering the hook for the Dynamics GP process only. If we fail to detect the Dynamics GP application, we will be listening to all foreground changes for all processes across the system. The code will work either way, although less noise does help. When listening with the event hook set from out-of-process, the timing becomes a critical issue as we will see later in the article.

The event hook function callbackProc gets called every time the Dynamics GP topmost window has changed. We acquire the caller’s caption and we compare it with REPORT_DESTINATION_CAPTION looking for a match:

C#
//the method for the callback delegate
private static void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hWnd,int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{
    [...]
    SendMessage(hWnd, WM_GETTEXT, MAX_CAPTION_SIZE, captionDlg);
    if (captionDlg.ToString() == REPORT_DESTINATION_CAPTION)
    {
        System.Diagnostics.Debug.Print("Detected [...]");
        [...]
    }
}

//the callback delegate
private delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hWnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);

//the callback delegate instance
private static WinEventDelegate callbackProc = WinEventProc;

It is important to notice that the event hook function runs in the same process as the one where the SetWinEventHook() call was made. If the call was made from an Add-in library, callbackProc will run in the same process as Dynamics GP. If the call was made from a stand-alone application, callbackProc will run in the application’s process.

To stop detecting we unregister the event hook with another Win32 call: UnhookWinEvent(hHook).

Step2: Closing the Report Destination window.

It appears that the Report Destination window is closer to a standard Windows modal dialog than it is to a genuine Dexterity window. Assuming it is a standard Windows modal dialog, we then know it has its own message processor. This message processor takes control over Dynamics GP application message processing during the life of the dialog. Incidentally this also explains why a Dexterity trigger registered against the Report Destination window cannot ever get activated.

If we want to close the dialog by sending an escape keystroke sequence we may have to follow different paths depending from where we’re sending these keystrokes. To send the keystrokes we will be using the .NET provided SendKeys class.

Let’s assume we have just received control of the execution inside the event hook function callbackProc and we want to close the dialog.

Closing from an Add-in library

When using an Add-in, we don’t have our own message loop. We are running in the same process as Dynamics GP, and all our Add-in operations are governed by the modal dialog message processor. The SendKeys class provides us with two methods for sending keystrokes: Send() and SendWait(). Send() needs a message loop and as such we cannot use it. SendWait() does not need a message loop but will disrupt the flow of messages due to its own synchronization object. If we would call it inside the callback we would be freezing the entire Dynamics GP. The solution is to run SendWait() on the Elapsed event of a timer running in a different thread, alike the Timer component provided by  the System.Timers namespace. Inside the event hook function we perform no other processing, we just enable the timer. The control of execution goes back to the dialog window.

C#
private static void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hWnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{
    [...]
    // in-process integration
    closerTimer.Enabled = true; //start timer
}

The timer stays disabled most of the time. When enabled, on closerTimer_Elapsed event we send out the keystrokes to close the Report Destination dialog:

C#
private void closerTimer_Elapsed(object sender, ElapsedEventArgs e)
{
    closerTimer.Enabled = false;
    System.Windows.Forms.SendKeys.SendWait("{TAB}");
    System.Windows.Forms.SendKeys.SendWait("{ESC}");
}

Closing from a stand-alone Windows Forms application

We have our own message loop, and we are running in a different process than Dynamics GP altogether. In theory we can use either Send() or SendWait() methods to generate our keystroke messages. Also we do not need a timer. Not only do we not need it, but using one could be problematic.

Because our processes run their own independent message loops, reentrancy on the event hook function becomes a problem. In our callback event we’re already issuing a SendMessage() to read back the caption of the window that generated the event. Referring to event hooks, Microsoft specifically mentions that: “Because event processing is interrupted, additional events might be received any time the hook function calls a function that causes the owning thread's message queue to get checked. This happens when any of the following are called within the hook function: SendMessage, PeekMessage...”.

To try avoiding this situation we’re sending the escaping sequence inside the callback and we use Send():

C#
private static void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hWnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{
    [...]
    // out-of-process integration
    System.Windows.Forms.SendKeys.Send("{TAB}");
    System.Windows.Forms.SendKeys.Send("{ESC}");
}

One will observe that we’re sending a {TAB} followed by an {ESC}. This is the result of numerous tests that showed this key sequence to be the one that will close the Report Destination window in all situations we’ve encountered.

To distinguish at run-time between our class being part of a library or an executable, we overload the Closer constructor looking for the assembly’s EntryPoint. Libraries (usually) don’t have one.

Sample code

The project compiles with VS 2010 and .NET 3.5.

For verbosity, some of code snippets presented in the article left out parts of the code that exists in the source.

Scope

For the sake of usability the Closer class in this demo is integrated as a stand-alone application. However the code was written with a Microsoft Dynamics GP Add-in integration library in mind.

Here is an example of how the Closer code can be attached to a Microsoft Dynamics GP Add-in:

C#
public class CloserGPAddIn : IDexterityAddIn
{
    // IDexterityAddIn interface

    private Closer cls1 = new Closer();

    public void Initialize()
    {
        ProjectAccounting.Forms.PaProjectMaintenance.PaProjectMaintenance.ActivateAfterOriginal +=
            new EventHandler(PaProjectMaintenance_ActivateAfterOriginal);
        ProjectAccounting.Forms.PaProjectMaintenance.PaProjectMaintenance.CloseAfterOriginal +=
            new EventHandler(PaProjectMaintenance_CloseAfterOriginal);
    }

    private void PaProjectMaintenance_ActivateAfterOriginal(object sender, EventArgs e)
    {
        cls1.StartClosing();
    }

    private void PaProjectMaintenance_CloseAfterOriginal(object sender, EventArgs e)
    {
        cls1.StopClosing();
    }
}

The Add-in library class CloserGPAddIn instantiates the Closer class and then enables the closing after the Dynamics GP Project Maintenance window gets opened. The disabling is triggered after the Project Maintenance window is closed. For as long as Project Maintenance window remains open, the Report Destination window is closed automatically.

Conclusion

The closing of the Report Destination window proved to be less trivial than expected both in Dexterity and C#. While the code in this article cannot guarantee that one will be able to close the Report Destination window in all situations, there is enough information that might help someone to avoid reinventing the wheel, at least on the specific path the article described.

Read more

I have added an alternative to this code where we’re using polling instead of event notification to detect the Report Destination window (see “Alternatives” on main page).

References

  1. Developing integrations using Visual Studio Tools for Microsoft Dynamics GP
  2. Automating or Customizing the Report Destination Window, David Musgrave
  3. MSDN
  4. StackOverflow
  5. Pinvoke
  6. Understanding The COM Single-Threaded Apartment, Code Project, Lim Bio Liong
  7. Guarding Against Reentrancy in Hook Functions

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Cogsdale
Canada Canada
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionThere is an easier way for an add-in Pin
James H7-Aug-23 1:45
James H7-Aug-23 1:45 
QuestionGood to see Dynamics GP stuff Pin
James McCullough29-Mar-16 10:32
professionalJames McCullough29-Mar-16 10:32 
AnswerRe: Good to see Dynamics GP stuff Pin
Paul Maxan31-Mar-16 9:20
Paul Maxan31-Mar-16 9:20 
You're most welcome. Thanks.
GeneralMy vote of 5 Pin
L111123-Jan-15 8:07
L111123-Jan-15 8:07 
GeneralRe: My vote of 5 Pin
Paul Maxan23-Jan-15 8:56
Paul Maxan23-Jan-15 8:56 
GeneralThanks! Pin
L111124-Nov-14 18:36
L111124-Nov-14 18:36 
GeneralRe: Thanks! Pin
Paul Maxan26-Nov-14 5:19
Paul Maxan26-Nov-14 5:19 
GeneralModification for disconnected RDP sessions. Pin
Member 1117301913-Nov-14 9:09
Member 1117301913-Nov-14 9:09 
GeneralRe: Modification for disconnected RDP sessions. Pin
Paul Maxan26-Nov-14 5:13
Paul Maxan26-Nov-14 5:13 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.