Search code examples
c#eventsconsoleunsubscribe

unsubscribe then subscribe to Console.CancelKeyPress


I noticed that subscribing then unsubscribing from Console.CancelKeyPress makes the next subscription fail.

Even stranger: starting of with an empty eventhandler fixes this.

Could anybody explain this behavior?

    class Program
    {
        static void Main(string[] args)
        {
            Console.TreatControlCAsInput = false;
// 2:       uncomment following for an unexpected fix...
            //Console.CancelKeyPress += (s, a) => { };

            Console.CancelKeyPress += Handler_1;
            Console.WriteLine("Press Ctr+C to prove that Handler_1 cancels key press.");
            Console.ReadLine();

            Application.DoEvents(); // make sure events are handled before unsubscribe
            Console.CancelKeyPress -= Handler_1;

// 1:       uncomment following to prove failure...
            //Console.CancelKeyPress += Handler_2;
            //Console.WriteLine("Press Ctr+C to prove that Handler_2 cancels key press.");
            //Console.ReadLine();

            Application.DoEvents(); // make sure events are handled
            Console.WriteLine("End of demo, type something to prove application is still responsive.");
            Console.ReadLine();
        }

        private static void Handler_1(object sender, ConsoleCancelEventArgs args)
        {
            args.Cancel = true;
            Console.WriteLine("Key press is canceled by Handler_1");
        }

        private static void Handler_2(object sender, ConsoleCancelEventArgs args)
        {
            args.Cancel = true;
            Console.WriteLine("Key press is canceled by Handler_2");
        }
    }

Solution

  • It is a bug in the CancelKeyPress implementation

    public static event ConsoleCancelEventHandler CancelKeyPress {
                    [System.Security.SecuritySafeCritical]  // auto-generated
                    [ResourceExposure(ResourceScope.Process)]
                    [ResourceConsumption(ResourceScope.Process)]
                    add {
                        new UIPermission(UIPermissionWindow.SafeTopLevelWindows).Demand();
    
                        lock(InternalSyncObject) {
                            // Add this delegate to the pile.
                            _cancelCallbacks += value;
    
                            // If we haven't registered our control-C handler, do it.
                            if (_hooker == null) {
                                _hooker = new ControlCHooker();
    
    // BUG: after you unsubscribe from CancelKeyPress it becomes null
    // and when you subscribe to CancelKeyPress again the call below will never be called. In the Remove part they will not set _hooker to null.
                            _hooker.Hook();
                        }
                    }
                }
                [System.Security.SecuritySafeCritical]  // auto-generated
                [ResourceExposure(ResourceScope.Process)]
                [ResourceConsumption(ResourceScope.Process)]
                remove {
                    new UIPermission(UIPermissionWindow.SafeTopLevelWindows).Demand();
    
                    lock(InternalSyncObject) {
                        // If count was 0, call SetConsoleCtrlEvent to remove cb.
                        _cancelCallbacks -= value;
                        Contract.Assert(_cancelCallbacks == null || _cancelCallbacks.GetInvocationList().Length > 0, "Teach Console::CancelKeyPress to handle a non-null but empty list of callbacks");
                        if (_hooker != null && _cancelCallbacks == null)
                            _hooker.Unhook();
    //BUG: It Unhooks but does not set _hooker to null.
                        }
                    }
                }