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:
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:
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 [...]");
[...]
}
}
private delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hWnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
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.
private static void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hWnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{
[...]
closerTimer.Enabled = true; }
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:
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()
:
private static void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hWnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{
[...]
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:
public class CloserGPAddIn : IDexterityAddIn
{
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
- Developing integrations using Visual Studio Tools for Microsoft Dynamics GP
- Automating or Customizing the Report Destination Window, David Musgrave
- MSDN
- StackOverflow
- Pinvoke
- Understanding The COM Single-Threaded Apartment, Code Project, Lim Bio Liong
- Guarding Against Reentrancy in Hook Functions