Removing Routed Event Handlers through Reflection?
Asked Answered
D

3

5

Background: I'm using WPF and C# (3.5) and am working on an app that allows a user to view a form/window/usercontrol that's already part of a compiled assembly. When they view it, they should be able to click on any control (buttons, textboxes, even labels), a little popup editor should appear by the control where they can then type in a tooltip, helpID, etc., for that control.

The long and short of it: I need to imitate a basic design view in WPF. Which means I need to do at least the following:

  • Load the usercontrol/window from a given assembly (no problem)
  • Instantiate it the usercontrol/window (no problem)
  • Clear all subscribed EventHandlers for all its controls
  • Assign my own "ShowEditorPopup" EventHandler to each control (shouldn't be a problem)

First off, if anybody has suggestions on an easier or better route to take, please let me know. (Apparently there is no DesignHost kind of component for WPF (like I've read .NET 2 has), so that's out.)

I'm stuck on the bolded item - clearing any subscribed EventHandlers. After digging around some and getting into Reflector, I've come up with this cool chunk of dangerous code (here, I'm just trying to get all the EventHandlers for a single Button called someButton defined in the XAML):

<Button Name="someButton" Click="someButton_Click"/>

Here's the code (you can run it from the someButton_Click eventHandler if you want):

public void SomeFunction()
{
// Get the control's Type
Type someButtonType = ((UIElement)someButton).GetType();

// Dig out the undocumented (yes, I know, it's risky) EventHandlerStore
// from the control's Type
PropertyInfo EventHandlersStoreType =  
        someButtonType.GetProperty("EventHandlersStore",  
        BindingFlags.Instance | BindingFlags.NonPublic);

// Get the actual "value" of the store, not just the reflected PropertyInfo
Object EventHandlersStore = EventHandlersStoreType.GetValue(someButton, null);

// Get the store's type ...
Type storeType = EventHandlersStore.GetType();

// ... so we can pull out the store's public method GetRoutedEventHandlers
MethodInfo GetEventHandlers =  
        storeType.GetMethod("GetRoutedEventHandlers",  
        BindingFlags.Instance | BindingFlags.Public);

// Reflector shows us that the method's sig is this:
// public RoutedEventHandlerInfo[] GetRoutedEventHandlers(RoutedEvent routedEvent);

// So set up the RoutedEvent param
object[] Params = new object[] { ButtonBase.ClickEvent as RoutedEvent };
// I've also seen this for the param, but doesn't seem to make a difference:
// object[] Params = new object[] { someButton.ClickEvent };

// And invoke it ... and watch it crash!
GetEventHandlers.Invoke(someButton, Params);
}

It works up to the Invoke, which returns: Object does not match target type (ie, my params or target object are messed). I've found you can resolve this with:

GetEventHandlers.Invoke(Activator.CreateInstance(someButton.GetType()), Params);
// Also doesn't work...

When I set a watch on the GetEventHandlers MethodInfo, it looks great, it just doesn't like what I'm passing it when I call the Invoke.

I feel like I'm at the very last step of how to get a list of the RoutedEvent Handlers (like good old GetInvocationList(), which doesn't work for WPF RoutedEvents, apparently). From there, it'll be simple enough to remove those Handlers from each control and have an eventless form, which I can then add my own events to.

Any clues? Again, if there's a better/easier way to do the task overall, let me know :)

Dodgson answered 11/6, 2009 at 18:8 Comment(1)
Wow, you did a great work, I was just trying to do the same. You just gave me the starting point I needed.Breach
M
3

What if you take a different approach. You could call EventManager.RegisterClassHandler() for all events, and then in your handler (assuming the event is for a control on the design surface, and not part of your UI) mark the event as handled. This should prevent it from being forwarded on to the controls on your design surface, since class handlers are called before standard event handlers.

You'd still need to use reflection to get the list of events provided by a control, but at least this way you wouldn't be using reflection to remove the events. Also, if the assembly you load also registers a class handler (likely before your code does), theirs would be called first, but I would guess that this would be a rare occurrence.

Mudguard answered 12/6, 2009 at 11:11 Comment(4)
Great suggestion. I was wondering if it were possible to block another other events from firing by setting Handled = true, but I didn't know that "Class Handlers" are fired first. I'll give it a try. Thanks!Dodgson
Pure genius! After looking at switchonthecode.com/tutorials/wpf-snippet-class-event-handlers for a related example, my button do anything anymore. Exactly what I needed. Thanks!! Now I'm wondering just how many RoutedEvents I should block besides a simple Button.ClickEvent... maybe the safest thing would be to "pre-handle" all RoutedEvents returned from EventManager.GetRoutedEvents(). But that's another issue :) Thanks again!Dodgson
And the special cases you mentioned where it could potentially cause problems shouldn't be an issue in our case. What you've given me fits the situation great.Dodgson
I have a similar issue. I want to clear all the handlers (im talking about private handlers in .NET) for a TextBoxBase.TextChanged.Flyback
L
3

If you use GetEventHandlers.Invoke(EventHandlersStore , Params) it seems to work well and does not crash.

Lenorelenox answered 1/8, 2009 at 17:40 Comment(0)
B
1

Using your code from above I did this:

// Get the control's Type
Type controlViewType = ((UIElement)control).GetType();

// Dig out the undocumented (yes, I know, it's risky) EventHandlerStore
// from the control's Type
PropertyInfo EventHandlersStoreType =
controlViewType.GetProperty("EventHandlersStore",
BindingFlags.Instance | BindingFlags.NonPublic);

// Get the actual "value" of the store, not just the reflected PropertyInfo
Object EventHandlersStore = EventHandlersStoreType.GetValue(tree, null);
var miGetRoutedEventHandlers 
EventHandlersStore.GetType().GetMethod("GetRoutedEventHandlers", 
BindingFlags.Public | BindingFlags.Instance);
RoutedEventHandlerInfo[] res =   
(RoutedEventHandlerInfo[])miGetRoutedEventHandlers.Invoke(EventHandlersStore, 
new object[] { CheckedTreeViewItem.CheckedEvent });

Once you have that then the only problem is that you now have the method info, so you need to get an instance to the object that implements that method. Usually event handlers are defined on the Window or Page object. So to get it: var parent = VisualTreeHelper.GetParent(control); while (!(control is Window) && !(control is Page)) { parent = VisualTreeHelper.GetParent(parent); }

And with that instance you can then invoke the event wtih:

res.[0].Handler.Method.Invoke(parent, new object[] { control, new RoutedEventArgs() }
Breach answered 6/4, 2013 at 17:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.