locked
Focus in an editor and menus RRS feed

  • Question

  • Hi,

    I have a question about focus in an editor within Visual Studio.  First, let me try to outline my goal.  I want to be able to enable/disable certain menu items based on focus within the editor.  My "real" editor makes use of the MS .NET 4 workflow designer (which is WPF based). 

    I started this thread in the WPF forum:

    http://social.msdn.microsoft.com/Forums/en/wpf/thread/9f219282-9ccf-4f81-81d4-869c74b39e5d

    and we got to a certain point where some of what I wanted was working a vanilla WPF application.  However, the same solution isn't translating to VS (in my case, a VS 2010 isolated shell application).

    I did some searching and came across a VS blog post that describes keyboard focus in VS 2010, and notes some differences between a vanilla WPF application:

    http://blogs.msdn.com/b/visualstudio/archive/2010/03/09/wpf-in-visual-studio-2010-part-3-focus-and-activation.aspx

    This was interesting reading, but I couldn't figure out how to apply what was discussed in the blog post to my scenario.  While ultimately I want to get this working with my 'real' editor that rehosts the MS workflow designer, I think the VS side of the problem can be seen with a simple generated package with a few tweaks.  So, assuming the reader doesn't have an appropriate editor package handy, these steps will likely get us to the same point.

    1. Generate a new Visual Studio Package using the wizard.  In the wizard, select to generate a new
    editor and menu command.
    2. In the generated code, find the package file class, typically <project name>Package.cs
    3. In the package file class, modify the generated Initialize() method, so it looks like this (just adding a BeforeQueryStatus handler):

        protected override void Initialize()
        {
          Trace.WriteLine (string.Format(CultureInfo.CurrentCulture, "Entering Initialize() of: {0}", this.ToString()));
          base.Initialize();
    
          //Create Editor Factory. Note that the base Package class will call Dispose on it.
          base.RegisterEditorFactory(new EditorFactory(this));
    
          // Add our command handlers for menu (commands must exist in the .vsct file)
          OleMenuCommandService mcs = GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
          if ( null != mcs )
          {
            // Create the command for the menu item.
            CommandID menuCommandID = new CommandID(GuidList.guidEditorVSPackageCmdSet, (int)PkgCmdIDList.cmdidMyCommand);
            //MenuCommand menuItem = new MenuCommand(MenuItemCallback, menuCommandID );
            OleMenuCommand menuItem = new OleMenuCommand(MenuItemCallback, menuCommandID );
            menuItem.BeforeQueryStatus += new EventHandler(menuItem_BeforeQueryStatus);
            mcs.AddCommand( menuItem );
          }
        }
    

    4. Create a new "menuItem_BeforeQueryStatus" routine that looks like this:

        void menuItem_BeforeQueryStatus(object sender, EventArgs e)
        {
          bool isFocusScope = System.Windows.Input.FocusManager.GetIsFocusScope(System.Windows.Application.Current.MainWindow);
          System.Diagnostics.Trace.Write("Application main window is focus scope: " + isFocusScope.ToString());
          System.Windows.IInputElement focusedElement = System.Windows.Input.FocusManager.GetFocusedElement(System.Windows.Application.Current.MainWindow);
          if (focusedElement != null)
            System.Diagnostics.Trace.WriteLine("Focused element in custom focus scope: " + focusedElement.ToString());
          else
            System.Diagnostics.Trace.WriteLine("No focused element in custom focus scope");
    
    
          OleMenuCommand command = (OleMenuCommand)sender;
          command.Enabled = true;
        }
    

    5. As the generated editor/menu package uses WinForms rather than a WPF based editor, add the following
    references to the project:

    PresentationCore
    PresentationFramework
    System.Xaml
    WindowsBase

    NOTE: My test not be accurate here, as the generated editor is WinForm based, not WPF based.  As noted previously, the .NET 4 workflow editor (the new one) is WPF based.

    6. Buld the project and perform the necessary steps to add it as a package of VS or an isolated shell app.

    7. Run VS/isolated shell app.

    8. Create a new file of type related to your custom editor.  This will eventually bring up a rich editor control.

    9. Attach VS as a debugger to VS/isolated shell app, if not already done.

    10. Make sure you can see the VS/isolated shell app plus the VS debugger's output window.

    11. Set keyboard focus within the rich edit control of the custom editor, then use the mouse to click on the Tools menu item, which will begin to show the custom menu item and will fire the menuItem_BeforeQueryStatus handler.  Take note of output in the VS output window.  The output indicates that the main window is a logical focus scope, but no focused element could be found.  (I'm worried, again, that using this WinForm based editor might confuse things, but I get the same with the WPF workflow editor).

    How should I go about checking what has (had?) keyboard focus when a menu item is to be shown?  Note also this observation from the WPF forum question:

    I did notice one thing, that might be relevant to the focus difference in VS vs. a vanilla WPF app.  In the WPF application, when I click on the menu item, the caret is still present in the text box control (not blinking) and when I close the menu, the caret is back in the textbox (and is blinking).  In VS, when I click on the VS menu item, the caret is not present in the text box, and when I close the VS menu, the caret appears back in the textbox (and is blinking).  Perhaps this is why GetFocusedElement is failing in VS?

    Thanks,

    Notre

    Thursday, March 10, 2011 9:49 PM

Answers

  • Focus is basically an unprotected, global variable that anyone can change, so relying on it heavily is asking for trouble. I believe we clear the main window focus scope on entry into menu mode because if focus is in a nested, Win32 control WPF will think that 'focus' for the main window is really on the hosting element, on exiting menu mode it will try to 'restore' focus but it will do it incorrectly as it won't restore it to the previously focus Win32 control that had focus. We had to write our own focus restoration logic to handle proper focus restoration as VS is a tremendously complex case and WPF's default behavior does not do 'the right/expected thing' in many, many scenarios.

    One solution that can work is to monitor PreviewGotKeyboardFocus in your subtree's root. This allows you to track keyboard focus inside your window, you can store the target element from the message as 'the last thing in my window to have keyboard focus'. If your handlers are at the window frame level then you can rely on that captured focus item in your QS handler because even if focus isn't currently in your window you know it must have been before the menu was clicked on or else your handlers would not be being called.

    This approach won't work at the package level as you don't want to monitor losing keyboard focus because that will occur when the user clicks outside your window, specifically on the menus/toolbars, and you DON'T want to consider having lost keyboard focus (temporarily) as interesting in that case. This is problematic as package level handlers can get invoked even when your editor is not activated, which would mean you could erronously enable your commands even when focus was in another toolwindow!

    You may also be able to use the value of WPF's IsInMenuMode property on ComponentDispatcher to help understand if focus is currently on the menu/toolbars, but I haven't tried using that property myself so I can't offer much help around when its state flips or if it will give you enough context to understand when you might have 'temporarily' lost focus.

    Ryan

    • Marked as answer by Notre Friday, March 11, 2011 9:53 PM
    Friday, March 11, 2011 7:22 PM

All replies

  • Relying on focus for command states is hazardous and perilous and will drive you mad, at least in these cases, I REALLY recommend against it :) 

    Editing to remove my suggestion around UI context, I think that is hampered because I believe we (VS) clear focus scopes on entry to menu mode and restore it on exit, it is a horribly, terribly confusing topic and not one you want to go near with a 200 foot pole, trust me.

    I will ask around for best approaches on something like this, but it is an ugly, ugly thing made MUCH worse by the fact that VS supports elements in WPF, WinForms and Win32, which means 2 distinct concepts of focus :(

    You handler here is at a package level right?  If so can you move it to be at the editor level so it is only called when your editor is active?

    Ryan

    Friday, March 11, 2011 12:42 AM
  • Hi Ryan,

    Yes, this focus thing is certainly driving me mad.  It was tough enough trying to wrap my mind around the WPF focus story (I think I only 1/2 understand it at this point) and it seems to be yet more confusing in VS :(

    I appreciate any feedback you can give about best practices!

    In my sample, the handler is at the package level.  In my real package, most of the handlers are at the editor level.  This only partially solves the problem, however, as even when my editor is active, I still only want a menu item to be selectively enabled.

    Thanks,

    Notre

    Friday, March 11, 2011 7:00 PM
  • Focus is basically an unprotected, global variable that anyone can change, so relying on it heavily is asking for trouble. I believe we clear the main window focus scope on entry into menu mode because if focus is in a nested, Win32 control WPF will think that 'focus' for the main window is really on the hosting element, on exiting menu mode it will try to 'restore' focus but it will do it incorrectly as it won't restore it to the previously focus Win32 control that had focus. We had to write our own focus restoration logic to handle proper focus restoration as VS is a tremendously complex case and WPF's default behavior does not do 'the right/expected thing' in many, many scenarios.

    One solution that can work is to monitor PreviewGotKeyboardFocus in your subtree's root. This allows you to track keyboard focus inside your window, you can store the target element from the message as 'the last thing in my window to have keyboard focus'. If your handlers are at the window frame level then you can rely on that captured focus item in your QS handler because even if focus isn't currently in your window you know it must have been before the menu was clicked on or else your handlers would not be being called.

    This approach won't work at the package level as you don't want to monitor losing keyboard focus because that will occur when the user clicks outside your window, specifically on the menus/toolbars, and you DON'T want to consider having lost keyboard focus (temporarily) as interesting in that case. This is problematic as package level handlers can get invoked even when your editor is not activated, which would mean you could erronously enable your commands even when focus was in another toolwindow!

    You may also be able to use the value of WPF's IsInMenuMode property on ComponentDispatcher to help understand if focus is currently on the menu/toolbars, but I haven't tried using that property myself so I can't offer much help around when its state flips or if it will give you enough context to understand when you might have 'temporarily' lost focus.

    Ryan

    • Marked as answer by Notre Friday, March 11, 2011 9:53 PM
    Friday, March 11, 2011 7:22 PM
  • Thanks for all the info.

    I think I can safely restrict any focus related disabling/enabling to QS handlers that are on the editor level, not the package level.

    I was leaning towards the PreviewGotKeyboardFocus approach; I'll start down that path and report back here on how that goes.

    Thanks,

    Notre

    Friday, March 11, 2011 7:52 PM
  • The PreviewGotFocus approach appears to be working - thanks for your help!

    Notre

    Friday, March 11, 2011 9:53 PM