2

I need to create a RichTextBox subclass that works the same in all ways except that it does not subscribe to UserPreferenceChanged. This event is causing a hang in my app. I have to use a RichTextBox and can't swap it for a TextBox with MultiLine=True, or anything else like that.

This is where System.Windows.Forms.RichTextBox subscribes;

protected override void OnHandleCreated(EventArgs e)
{
    ...
    SystemEvents.UserPreferenceChanged += new UserPreferenceChangedEventHandler(this.UserPreferenceChangedHandler);
}

This is the signature of the handler;

private void UserPreferenceChangedHandler(object o, UserPreferenceChangedEventArgs e)

The handler is not virtual so I can't override it. The handler is private so I can't do a simple -= to unsubscribe. I've looked into using reflection to remove the handler but I can't get it to work - this is what i have so far;

public partial class MyRichTextBox : RichTextBox
{
    ...

private void UnsubscribeUserPreferenceChanged()
{
    FieldInfo fieldInfo = typeof(SystemEvents).GetField("OnUserPreferenceChangedEvent", BindingFlags.NonPublic | BindingFlags.Static);
        // fieldInfo.ToString() = "System.Object.OnUserPreferenceChangedEvent"
    object eventObj = fieldInfo.GetValue(this);
        // eventInfo.ToString() = "System.Object"
    PropertyInfo propInfo = typeof(RichTextBox).GetProperty("Events", BindingFlags.NonPublic | BindingFlags.Instance);
        // propInfo.ToString() = "System.ComponentModel.EventHandlerList Events"
    EventHandlerList list = (EventHandlerList)propInfo.GetValue(this, null);
        // list.ToString() = "System.ComponentModel.EventHandlerList"
    ...

Now at this point I could just call;

list.RemoveHandler(eventObj, list[eventObj]);

and there would be no exceptions but I believe it is silently failing, because if i try to access the delegate as so;

list[eventObj].ToString()

I get a NullReferenceException as there is no such object key in the EventHandlerList. I am calling UnsubscribeUserPreferenceChanged() after MyTextBox has become visible so the handler should be in the list by then as it is added in OnHandleCreated for the RichTextBox.

Anyone got any pointers on how to unsubscribe a SystemEvent hooked to a private event handler in a super class?

Paul MacGuiheen
  • 628
  • 5
  • 17
  • Events are stored in a dictionary, the field name is `_handlers`. Most likely outcome after fixing this is that you'll just run into the next control that causes the deadlock. Fix the *real* bug in your program, don't create UI on a worker thread. – Hans Passant Oct 11 '16 at 16:11
  • I'm simply using a RichTextBox in my application. It is added to a WindowsForm in the designer in Visual Studio. The hang occurs mostly when the user locks / unlocks their workstation. (I saw you pointed this out [here](http://stackoverflow.com/questions/17191389/onuserpreferencechanged-hang-dealing-with-multiple-forms-and-mutlipe-ui-thread) as a particularly prone scenario for this issue). I'm not sure how a worker thread could be creating the UI when it's all handled by VS – Paul MacGuiheen Oct 11 '16 at 17:55

4 Answers4

5

Once you know the method name, it's quite easy due to the following handy CreateDelegate overload:

public static Delegate CreateDelegate(
    Type type,
    object target,
    string method
)

so the code in question could be like this:

private void UnsubscribeUserPreferenceChanged()
{
    var handler = (UserPreferenceChangedEventHandler)Delegate.CreateDelegate(
        typeof(UserPreferenceChangedEventHandler), this, "UserPreferenceChangedHandler");
    SystemEvents.UserPreferenceChanged -= handler;
}
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • This works! Now I just need to find the appropriate time to call UnsubscribeUserPreferenceChanged(). I was calling it in the MyRichTextBox c'tor if Visible==true but that is too early. I'll look for some other event that happens later or else just setup a timer or similar to wait a few seconds before calling UnsubscribeUserPreferenceChanged(). Thanks! – Paul MacGuiheen Oct 11 '16 at 17:44
  • 1
    You should override `OnHandleCreated` and call it right after the `base` call. – Ivan Stoev Oct 11 '16 at 17:51
  • That's the perfect solution to this - Thanks! – Paul MacGuiheen Oct 11 '16 at 19:03
0

You should use Type.GetEvent instead of Type.GetField.

Only then you should be able to remove it at runtime:

private void UnsubscribeuserPreferenceChanged()
{
    MethodInfo handler = typeof(RichTextBox).GetMethod("UserPreferenceChangedHandler", BindingFlags.Instance | BindingFlags.NonPublic);

    EventInfo evt = typeof(SystemEvents).GetEvent("UserPreferenceChanged", BindingFlags.Static | BindingFlags.Public);
    MethodInfo remove = evt.GetRemoveMethod(true);

    remove.Invoke(null, new object[]
    {
        Delegate.CreateDelegate(evt.EventHandlerType, null, handler)
    });
}
Matias Cicero
  • 25,439
  • 13
  • 82
  • 154
  • "register the handler without the wrapper:" - I don't register it, this is how it is done in the RichTextBox class. And (perhaps this is due to how RichTextBox registers the handler) this line 'MethodInfo handler = this.GetType().GetMethod("UserPreferenceChangedHandler", BindingFlags.Instance | BindingFlags.NonPublic);' returns null – Paul MacGuiheen Oct 11 '16 at 15:23
  • @PaulGuiheen Actually, there is no need to change the registration logic. It should still work. I have also updated the `MethodInfo` initialization. – Matias Cicero Oct 11 '16 at 15:41
  • This does not remove the handler for some reason. Getting the MethodInfo and EventInfo looks good and the Invoke doesn't except but when I tested I found the handler was still working. FYI: To test I use Spy++ to log messages for MyRichTextBox while I change the Windows Theme. If the handler is working I see '<00066> 00040A0C S message:0x0443 [User-defined:WM_USER+67] wParam:00000000 lParam:00FFFFFF' which corresponds to this line in RichTextBox.UserPreferenceChangedHandler 'this.SendMessage(1091, 0, ColorTranslator.ToWin32(this.BackColor));' – Paul MacGuiheen Oct 11 '16 at 17:41
0

I had the same issue for a regular Control, not a RichtTextBox. The code of Ivan Stoev then becomes this:

private void UnsubscribeUserPreferenceChanged(System.Windows.Forms.Control control) 
{
  var handler = (UserPreferenceChangedEventHandler)Delegate.CreateDelegate(
            typeof(UserPreferenceChangedEventHandler), control, "UserPreferenceChanged");
  SystemEvents.UserPreferenceChanged -= handler;
}

It unsubscribes the Control.UserPreferenceChanged (private) method from SystemEvents.

Stijn
  • 69
  • 5
-1

I found a way to get this fixed by iterating through all the objects subscribing to systemevents and then unsubscribing during execution time. It is a combination of the above approache and another code I found that catch all system events registered by all objects in my app

public static void UnsubscribeSystemEvents()
{         
     try
        {
            var handlers = typeof(SystemEvents).GetField("_handlers", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null);
            var handlersValues = handlers.GetType().GetProperty("Values").GetValue(handlers);
            foreach (var invokeInfos in (handlersValues as IEnumerable).OfType<object>().ToArray())
                foreach (var invokeInfo in (invokeInfos as IEnumerable).OfType<object>().ToArray())
                {
                    var syncContext = invokeInfo.GetType().GetField("_syncContext", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(invokeInfo);
                    if (syncContext == null) 
                        throw new Exception("syncContext missing");
                    if (!(syncContext is WindowsFormsSynchronizationContext))
                        continue;
                    var threadRef = (WeakReference)syncContext.GetType().GetField("destinationThreadRef", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(syncContext);
                    if (!threadRef.IsAlive)
                        continue;
                    var thread = (System.Threading.Thread)threadRef.Target;
                    if (thread.ManagedThreadId == 1)
                            continue;  // Change here if you have more valid UI threads to ignore
                    var dlg = (Delegate)invokeInfo.GetType().GetField("_delegate", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(invokeInfo);
                    var handler = (UserPreferenceChangedEventHandler)Delegate.CreateDelegate(typeof(UserPreferenceChangedEventHandler), dlg.Target, dlg.Method.Name);
                    SystemEvents.UserPreferenceChanged -= handler;
                }
        }
        catch ()
        {                
            //trace here your errors
        }
    }
Luis Otero
  • 37
  • 3