Thursday, December 29, 2005

I didn't expect it to take 8 hours straight to wire up a button press handler, but sure enough, it did! Here's how I was able to get a managed .NET Form to call my Javascript via listeners located in unmanaged code.

As is typical in GUI toolkits, the application registers a callback handler with the object or widget that generates the event. The list of events that the .NET Button class supports can be found by clicking here (scroll down to the bottom of the page, and then up a few clicks to find the section labeled "Public Events"). Of interest to me, of course, was hooking into the Button object's "Click" event.

To register a callback function with the Button object, I used the following code in WindowsButtonImpl::Create():

m_button->add_Click(new EventHandler(
static_cast<ButtonCallbackHelper*>(m_button),
&ButtonCallbackHelper::OnButtonClick));

Perhaps the most difficult part of the above was getting the first argument to EventHandler's constructor correct. To understand the problem, let's start with a look at the general pattern for registering a callback, which is:

obj1->add_EventName(new EventHandler(obj2, Method));

where:

-- obj1 is the .NET Form Control object that generates the event (e.g., Button).
-- EventName is the name of the event
-- obj2 is the object that will be notified by obj1 when EventName fires
-- Method is the name of the class member in obj2 that implements the callback, prefixed with an & operator.

My first attempt was to make the callback function a member of WindowsButtonImpl, which is compiled with /clr, but which is not managed because the class declaration is not prefixed with __gc, like this:

__gc class WindowsButtonImpl
{
...
};

My code, which omits the __gc prefix, caused the compiler to point out that the first argument to EventHandler must be a pointer to a managed object (i.e., it must be an object belonging to a class that was declared with the __gc modifier). The first obvious solution -- making WindowsButtonImpl a managed class -- was not possible, as doing so would require the class that embeds instances of WindowsButtonImpl to take on the gcroot template pattern that I described in the last post, bringing platform specific code above the level of the Windows-platform concrete classes. The right solution, I determined, was to create a class that inherits from .NET's Button, and instantiate it instead of Button as the concrete button widget implementation. That class would be declared using the gcroot template (just as was done with Button), and it would be extended to include a callback function that would be used to handle the Button Click events. That class, ButtonCallbackHelper, is defined simply as follows:

#include "windowsbuttonimpl.h"

__gc class ButtonCallbackHelper : public Button
{
public:
void OnButtonClick(Object *sender, System::EventArgs* e);
};

OnButtonClick is the name I gave the callback function. The first argument to this function specifies the object that generated the event, which I imagine can be dynamic_cast as needed (for example, to Button), although I didn't need to do this in my handler. The second argument contains data specific to the event. This argument is also something that I didn't need to look at in the handler, but for some events, it is critical. For example, it might contain information about what key was pressed in the case of an event sent to report a low-level keyboard press or release event. Button Click, however, is a very abstract event, and is independent of the mechanism used to cause the Click to fire -- it button click could have been triggered by a mouse press/release, or just as likely, by the user navigating the focus to the Button object with the Tab key and pressing Enter -- and so an application could care less about the details surrounding the Click event. Just knowing it fired is all we need.

The next stumbling block was getting the EventHandler CTOR to accept an instance of ButtonCallbackHelper as its first argument. As required, ButtonCallbackHelper had to be declared within the WindowsButtonImpl class using the gcroot template paradigm I described in the previous post. However, the EventHandler CTOR refuses to take an object declared this way. To get past this problem required a static_cast to ButtonCallbackHelper *, as is evident in the code presented at the start of this blog entry.

The next step was to arrange for the managed object to call unmanaged code in WindowsButtonImpl from within the Click callback function. In order to do this, we need to store in the ButtonCallbackHandler object a pointer or a reference to the WindowsButtonImpl object that contains that ButtonCallbackHandler instance. Then, in the callback function, we can issue a method of the WindowsButtonImpl, which knows how to publish the occurance of the button press back to the layout engine, which in turn will invoke the Javascript handler that is supposed to ultimately handle the event.

To do this, I coded a constructor for ButtonCallbackHandler that takes a pointer to the containing WindowsButtonImpl class. Here is the modified ButtonCallbackHelper definition:

#include "windowsbuttonimpl.h"

__gc class ButtonCallbackHelper : public Button
{
public:
ButtonCallbackHelper(WindowsButtonImpl *impl);
void OnButtonClick(Object *sender, System::EventArgs* e);
private:
WindowsButtonImpl *m_impl;
};

And, here is the code in the WindowsButtonImpl::Create function that creates the ButtonCallbackHelper instance that it manages, and passes itself to the constructor:

PRStatus WindowsButtonImpl::Create()
{
m_button = __gc new ButtonCallbackHelper(this);
if (m_button) {
...
return PR_SUCCESS;
}
return PR_FAILURE;
}

Notice that __gc is placed before the "new" keyword. This is because, once again, ButtonCallbackHelper must be implemented by managed code (for two reasons -- the callback must be implemented in managed code, and we are subclassing Button, which is implemented as managed code).

Let's take a look at the implementation of ButtonCallbackHelper:

#include "buttoncallbackhelper.h"

ButtonCallbackHelper::ButtonCallbackHelper(WindowsButtonImpl *impl)
{
m_impl = impl;
}

void ButtonCallbackHelper::OnButtonClick(Object *sender, System::EventArgs* e)
{
if (m_impl)
m_impl->HandleCommand();
}

As you can see, ButtonCallbackHelper's constructor stores the pointer to a WindowsButtonImpl object that was passed as an argument. This pointer allows OnButtonClick to call WindowsButtonImpl's HandleCommand() method. Since WindowButtonImpl is implemented as non-managed code, this call represents the transition from managed code to unmanaged code. From then on, the code behaves exactly as it does on all of the other platforms, with HandleCommand() calling a function that is implemented in the layout engine, designed to iterate and call all of the listeners that have been registered against the abstract widget.

For those of you who are so inclined, here is a call stack that illustrates the entire process at runtime of responding to the Button Click event, including the invocation of the Javascript function that is assigned in markup to handle the event:

js3250.dll!JS_CallFunction(JSContext * cx=0x003f8628, JSObject * obj=0x003f9fc8, JSFunction * fun=0x072e64e0, unsigned int argc=0x00000000, long * argv=0x00000000, long * rval=0x0012e9bc) Line 4116 + 0x22 C
layout.exe!Button::ButtonPressed() Line 124 + 0x1a C++
layout.exe!ButtonPressSubject::NotifyButtonPress() Line 14 C++
layout.exe!WindowsButtonImpl::HandleCommand() Line 21 C++

The code above represents those portions of the call stack that are executed within unmanaged code. The code below this line is all executed as managed code. The top of the managed portion of the stack (00146e38, see below) is likely some function that is used to transition from managed to unmanaged. I suspect that it is a part of the gcroot template.

00146e38()
layout.exe!ButtonCallbackHelper.OnButtonClick(System.Object sender = 0x0012f138, System.EventArgs e = 0x0012f148) Line 15 C++
system.windows.forms.dll!System.Windows.Forms.Control.OnClick(System.EventArgs e) + 0x54 bytes
system.windows.forms.dll!System.Windows.Forms.Button.OnClick(System.EventArgs e) + 0x2b bytes
system.windows.forms.dll!System.Windows.Forms.Button.OnMouseUp(System.Windows.Forms.MouseEventArgs mevent) + 0xd6 bytes
system.windows.forms.dll!System.Windows.Forms.Control.WmMouseUp(System.Windows.Forms.Message m, System.Windows.Forms.MouseButtons button, int clicks) + 0x1c3 bytes
system.windows.forms.dll!System.Windows.Forms.Control.WndProc(System.Windows.Forms.Message m) + 0x468 bytes
system.windows.forms.dll!System.Windows.Forms.ButtonBase.WndProc(System.Windows.Forms.Message m) + 0xdb bytes
system.windows.forms.dll!System.Windows.Forms.Button.WndProc(System.Windows.Forms.Message m) + 0x5d bytes
system.windows.forms.dll!ControlNativeWindow.OnMessage(System.Windows.Forms.Message m) + 0xb bytes
system.windows.forms.dll!ControlNativeWindow.WndProc(System.Windows.Forms.Message m) + 0xbc bytes
system.windows.forms.dll!System.Windows.Forms.NativeWindow.Callback(int hWnd, int msg, int wparam, int lparam) + 0x30 bytes
0018c752()
user32.dll!77d43a50()
user32.dll!77d43b1f()
user32.dll!77d43d79()
user32.dll!77d43c7d()
system.windows.forms.dll!System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods+IMsoComponentManager.FPushMessageLoop(int dwComponentID, int reason, int pvLoopData) + 0x382 bytes
system.windows.forms.dll!ThreadContext.RunMessageLoopInner(int reason, System.Windows.Forms.ApplicationContext context) + 0x15f bytes
system.windows.forms.dll!ThreadContext.RunMessageLoop(int reason, System.Windows.Forms.ApplicationContext context) + 0x45 bytes
system.windows.forms.dll!System.Windows.Forms.Application.Run() + 0x31 bytes
layout.exe!WindowsAppImpl::MainLoop() Line 36 C++
00146e38()
layout.exe!App::MainLoop() Line 24 C++
layout.exe!main(int argc=0x00000003, char * * argv=0x003f5d28) Line 365 C++
layout.exe!mainCRTStartup() Line 398 + 0x11 C

Well, there you have it -- managed code calling an unmanaged callback function via a wrapper class (after some trial and error). Next stop is to do the same thing for Window resize events, so that I can reflow my layout as the user resizes the window. I suspect that the very same pattern that I applied here for handling Button clicks will also apply to handling Form resizes. But I'm one step ahead already, since I already have a managed wrapper class that inherits fron Form. Details, and code, to follow, so stay tuned.

0 Comments:

Post a Comment

<< Home