CCF: Listening to External Application Events
It is often necessary to listen to external application UI events when automating applications in CCF. For example, we may need to save a value in the Context if the user presses a button, or do something when an alert dialog appears. There are many ways we can handle events. In CCF 2008, the Hosted Application Toolkit (HAT) provides an easy way to listen to them. If not using HAT we can hook to events using the UIAutomation or MSAA (Microsoft Active Accessibility) APIs within our own application adapters.
Events in HAT
When we configure a hosted application for using an Automation Adapter, this adapter listens to the application events. We can hook and unhook to these events using the CCF's WF activities RegisterActionForEvent (see figure bellow) and UnregisterActionForEvent respectively.
It is the Data Driven Adapter (DDA) the one who throws the ControlChanged event (to which the activity subscribes). The arguments of the event consists of eventTypeName, controlName and controlValue.
For example, the WinDataDrivenAdapter that CCF provides supports the following event types:
- SetControlValue: When the DDA set's a control's value.
- ExecuteControlAction: When the DDA executes a control.
- ContextChanged: When the context changes.
- MenuShown: When a menu is shown
- WindowShown: When a window is shown
- WindowDisappeared: When a window is closed
- LostFocus
- SetFocus
- CheckBoxSet
- CheckBoxCleared
- RadioButtonSet
- RadioButtonCleared
- ButtonPressed
- ButtonReleased
There are some tricks about how to use this events, for example:
- 4-6: For menu and window events we must not register with the control's friendly name that we set in the databindings section of the AppInitString. We must specify the window or menu's caption instead.
- 7-14: For these events the related control must be previously found on the UI. You can use the FindControl activity to ensure that the control is successfully found. When the DDA finds the control it registers it in a KnownControls collection. Only for those registered controls the events are thrown.
Custom DDAs or Legacy Adapters
If we need to handle events from a custom DDA or from a legacy adapter we can use win32 or accessibility APIs. If we use a custom DDA we can throw a ControlChanged event in order to use the WF/Automation activities mentioned before.
WinEvents
A low level approach is to use the SetWinEventHook and UnhookWinEvent functions from user32.dll. When we hook for events we specify the callback function that will handle the events. You can see the list of supported events and its constants in the MSAA SDK documentation on msdn.
In order to show some sample code, here are the user32 functions imports:
[Flags]
internal enum SetWinEventHookFlags
{
WINEVENT_INCONTEXT = 4,
WINEVENT_OUTOFCONTEXT = 0,
WINEVENT_SKIPOWNPROCESS = 2,
WINEVENT_SKIPOWNTHREAD = 1
}
internal delegate void WinEventProc(IntPtr hWinEventHook, int iEvent, IntPtr hWnd, int idObject, int idChild, int dwEventThread, int dwmsEventTime);
[DllImport("user32.dll", SetLastError = true)]
internal static extern IntPtr SetWinEventHook(int eventMin, int eventMax, IntPtr hmodWinEventProc, WinEventProc lpfnWinEventProc, int idProcess, int idThread, SetWinEventHookFlags dwflags);
[DllImport("user32.dll", SetLastError = true)]
internal static extern int UnhookWinEvent(IntPtr hWinEventHook);
Here's how we can subscribe to win events from our custom adapter:
WinEventProc wep = new WinEventProc(this.EventCallback);
SetWinEventHook(1, 0x7fffffff, IntPtr.Zero, wep, 0, threadId, SetWinEventHookFlags.WINEVENT_OUTOFCONTEXT);
And finally how to react to events in the event handler method:
private void EventCallback(IntPtr hWinEventHook, int iEvent, IntPtr hWnd, int idObject, int idChild, int dwEventThread, int dwmsEventTime)
{
//get accObj from handler
//get accObj's name, role, etc...
...
switch (iEvent)
{
case 0x10: //EVENT_SYSTEM_DIALOGSTART
if (role.Equals(localize.ACC_ROLE_TEXT_WINDOW))
{
if (name.Length > 0)
{
//throw ControlChangedEvent
this.ControlChanged(accObj, new ControlChangedEventArgs("DialogStart", controlName, string.Empty));
}
}
break;
...
}
}
UIAutomation
Another option is to use the UIAutomation accessibility API, included on .Net framework 3.0 to hook to events from custom adapters. The Automation element provides the following static methods to hook to different event on different controls:
//Registers a method that handles UI Automation events.
public static void AddAutomationEventHandler(AutomationEvent eventId, AutomationElement element, TreeScope scope, AutomationEventHandler eventHandler);
//Registers a method that will handle focus-changed events.
public static void AddAutomationFocusChangedEventHandler(AutomationFocusChangedEventHandler eventHandler);
//Registers a method that will handle property-changed events.
public static void AddAutomationPropertyChangedEventHandler(AutomationElement element, TreeScope scope, AutomationPropertyChangedEventHandler eventHandler, params AutomationProperty[] properties);
//Registers the method that will handle structure-changed events.
public static void AddStructureChangedEventHandler(AutomationElement element, TreeScope scope, StructureChangedEventHandler eventHandler);
The UIAutomation API is much more friendly to use, but some times it takes too long for the event to reach the handler. You should be very precise when setting the element and scope to subscribe the event to. You can find more information about this API and sample code on my previous post about WPF Accessibility.