locked
Hiding Commands with QueryStatus RRS feed

  • Question

  • I am having trouble getting my Commands to only display in the correct context.  It works perfectly when I am debugging and using the test Add In, but when I build it in Release mode and apply it to Visual Studio, it works differently.

    I essentially want the to be visible and enabled when available and invisible when unavailable.  To accomplish this, I have code that looks like this

      bool isTestXMLFile = DetermineIfSelectedItemIsTestXMLFile(applicationObject);
    
          if (isTestXMLFile)
          {
            return (vsCommandStatus)vsCommandStatus.vsCommandStatusSupported | vsCommandStatus.vsCommandStatusEnabled;
          }
          else
          {
            return vsCommandStatus.vsCommandStatusInvisible | vsCommandStatus.vsCommandStatusUnsupported;
          }
    

    When I am debugging this it works properly.  If I right-click in the correct context I will see the command in the menu and it will be selectable.  If right-click in the inccorect context I will not see the command at all.

    When I have actually compile the AddIn and deploy it to Visual Studio as a finished product, though, it behaves differently.  When I right-click in the correct context, I see the command in the menu and it works fine.  The problem is that when I right-click in the inccorect context, the command is disabled but visible.  I do not want the command to show at all if you cant click on it. 

    I have tried simply retruning vsCommandStatus.vsCommandStatusInvisible, but it does not appear to do the trick. 

    The code to create the command looks like this

          Command runTestFromXMLCommand = _Commands.AddNamedCommand2(
           _addInInstance,
           "RunXMLTest",
           "Run Tests From XML",
           "Executes the command for RunXMLTest",
           false,
           1,
           ref contextGUIDS,
           (int)vsCommandStatus.vsCommandStatusSupported + (int)vsCommandStatus.vsCommandStatusEnabled,
           (int)vsCommandStyle.vsCommandStylePictAndText,
           vsCommandControlType.vsCommandControlTypeButton);
    

    The AddIn behavior looks like this

    		<LoadBehavior>1</LoadBehavior>
    		<CommandPreload>0</CommandPreload>
    		<CommandLineSafe>0</CommandLineSafe>
    


    Any idea why this would not be consistent? 

    Thursday, December 22, 2011 6:47 PM

Answers

  • This looks like a bug in 2010.  I don't think there is any work-around that I can see.  The problem is we are treating the control as a 'user added' control, the logic around those (i.e. controls added by the user using the customization dialog) is that they are always visible (to avoid the case where a user goes to the Customize dialog and adds a command that isn't currently context visible and then looks at the menu they added it to and it isn't shown there).

    I thought that perhaps you could use the DTE control element and just set its Visible/Enabled state in the QueryStatus callback but DTE needs to know if the state you are setting is permissable (i.e. it won't let you explicitly set a command to visible if QueryStatus would return it is not visible, therefore the call to set_Visible on the DTE object ends up calling back into your DTCommandTarget asking if the command is currently visible, of course if your IDTCommandTarget is trying to mutate the DTE control state we get into an infinite loop).

    I think you have two choices

    1:  Live with it being visible but disabled.

    2:  Move to a package and not an AddIn, describe your command UI via VSCT and do the normal QueryStatus/Exec work to control visibilit/enabled staet. AddIns are kind of the lesser supported of the two extensibility models. I will file a bug to try and get this fixed, but that won't help you now.

    Edit:  Actually a work around would be to monitor the events around the conditions that cause its status to change (so selection change, active document change) and in those events you can get the DTE control object and set its Visible/Enabled state.  You would need to eliminate the IDTCommandTarget off your Connect object.

    Ryan

    Tuesday, December 27, 2011 8:41 PM

All replies

  • Is your AddIn loaded?  We won't load your add in to perform QueryStatus (just like we won't load a package to perform QueryStatus).  I believe during debugging the default project is set up to pass command line params to VS that would force your AddIn to 'reset', which requires it be loaded.  Normally an AddIn is only loaded if a command from it is executed or it is marked to automatically start when the application starts.

    Ryan

    Thursday, December 22, 2011 8:48 PM
  • Yes, the AddIn is loaded.  It may come down to how they are loaded and maybe I dont understand something about it.  Maybe you can explain some of how this works to me?

    In the OnConnect method, I am currently adding the Commands when the ConnectMode is ext_cm_Startup.  Will Startup occur every time the IDE is started or only when you first start the AddIn?

    I saw that the AddIn is reset when you run it in Debug mode, so I started running Visual Studio using  resetaddin from the command prompt.  I do notice a difference, but it still isnt correct and I need it to work more consistently. 

    I tried setting it to UISetup, which I assume will happen every time the IDE is started, but I dont get the results that I am looking for there either. 

    Do the different load befaviors affect what happens OnConnect?

    I realize these questions may be a little disjointed.  Sorry about that!  Just thought I had an understanding of how AddIns worked but am quickly realizing that I dont.

    Edit: I also wanted to add to this that even if I hardcode the status in QueryStatus to be vsCommandStatus.vsCommandStatusSupported, I get the same result. The Command displays on the menu but is disabled.
    • Edited by bsayegh Tuesday, December 27, 2011 2:13 PM Missing Details
    Tuesday, December 27, 2011 2:06 PM
  • Carlos has a good explanation of the various 'modes' here.  In short, no UISetup happens ONE time, the first time your AddIn is ever loaded, or after a Reset.  ext_cm_Startup occurs on every launch when the AddIn is loaded, but you shouldn't be adding commands there are you don't need to do that over and over, only once, during UISetup, the shell will persist your added controls/commands.

    Have you tried putting in some Debug.WriteLine calls and running a Debug build of your AddIn but not under the debugger?  You can use DebugView to watch the output stream even without a debugger attached, since you said running under the debugger doesn't cause this to happen.

    When you say 'yes the AddIn is loaded', how are you determining that exactly?  Did you explicitly mark it to load on IDE startup in the AddIn wizard when you created it?

    Ryan

     

    Tuesday, December 27, 2011 6:03 PM
  • I have found that it DOES happen with the debugger attached, but was not a problem because the Start Options for debugging included the arugment "/resetaddin ".  I reset the Command once (to start fresh), then removed the command line arguments so that I could better emulate a real scenario.  The first time I start visual studio after resetting the AddIn, everything works appropriately. Once I shut it down and start it again, things go a little crazy. Even the Icon dissapears from the menu item. When I debug this I can see that the status that is being set is correct, but when I remove the break point and let it finish execution I can see that it has ignored the command status.

    I initially had it marked to load on startup.  I believe that is what a LoadBehavior of 1 is, though I could be wrong. 

    What I tried before was adding Message Boxes that would alert me of the command name and the vsCommandStatus when QueryStatus was called.  It appeared ot be setting the appropriate status value. 

    I will read through that link you mentioned.  I have also changed my ConnecitonMode to UISetup.

    Thanks again!

    Tuesday, December 27, 2011 6:19 PM
  • Also make sure there are no exceptions being thrown, they would be swallowed on the COM boundary and viewed as a failure of QueryStatus. Otherwise I would need a repro, I have never heard of any bugs here.

    edit:  Also I note you put this in a previous post:

     also wanted to add to this that even if I hardcode the status in QueryStatus to bevsCommandStatus.vsCommandStatusSupported, I get the same result. The Command displays on the menu but is disabled.

     there is a difference between supported and enabled, you need both, your status should be something like

     status = (vsCommandStatus)vsCommandStatus.vsCommandStatusSupported|vsCommandStatus.vsCommandStatusEnabled;

    Ryan

    Tuesday, December 27, 2011 6:45 PM
  • By repro do you mean a Solution containing the code (or similar code) that you can debug?  Sadly I dont think  I can provide that, though if it comes down to it I may have to anyway.

    Let me post what is essentially the code and configuration and maybe you will be able to see what is wrong.  Otherwise it may be awhile before I can give you an AddIn project. 

    Some of this code is abbreviated to save space on the page...

    OnConnect looks like this

     public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom)
        {
          _applicationObject = (DTE2)application;
          _addInInstance = (AddIn)addInInst;
          if (connectMode == ext_ConnectMode.ext_cm_UISetup)
          {
            object[] contextGUIDS = new object[] { };
            _Commands = (Commands2)_applicationObject.Commands;
    
            //This try/catch block can be duplicated if you wish to add multiple commands to be handled by your Add-in,
            //  just make sure you also update the QueryStatus/Exec method to include the new command names.
            try
            {
              contextGUIDS = AddTestCommands(contextGUIDS);
            }
            catch (System.ArgumentException)
            {
              //If we are here, then the exception is probably because a command with that name
              //  already exists. If so there is no need to recreate the command and we can 
              //  safely ignore the exception.
            }
          }
        }
    
      private object[] AddTestCommands(object[] contextGUIDS)
        {
       //Other code up here, essentially just creating similar commands.
    
          Command runTestFromXMLCommand = _Commands.AddNamedCommand2(
           _addInInstance,
           "RunXMLTest",
           "Run Tests From XML",
           "Executes the command for RunXMLTest",
           false,
           1,
           ref contextGUIDS,
           (int)vsCommandStatus.vsCommandStatusSupported + (int)vsCommandStatus.vsCommandStatusEnabled,
           (int)vsCommandStyle.vsCommandStylePictAndText,
           vsCommandControlType.vsCommandControlTypeButton);
    
    
          //Add a control for the command to the tools menu:
          AddCommands(runTestCommand, debugTestCommand, runTestsInFolderCommand, runTestFromXMLCommand);
          return contextGUIDS;
        }
    
        private void AddCommands(Command runTestCommand, Command debugTestCommand, Command runTestsInFolderCommand, Command runTestFromXMLCommand)
        {
         //Similar null checks adding other commands.
    
          if (runTestFromXMLCommand != null)
          {
            AddCommandToContextMenu(runTestFromXMLCommand, "Code Window", "RunXMLTest", "Run Tests From XML", 47, 1);
          }
        }
    
        private void AddCommandToContextMenu(Command command, string menuName, string commandName, string commandText, int iconId, int position)
        {
          CommandBar contextMenu = ((CommandBars)_applicationObject.CommandBars)[menuName];
          command.AddControl(contextMenu, position);
        }
    

    Then QueryStatus looks like this

     public void QueryStatus(string commandName, vsCommandStatusTextWanted neededText, ref vsCommandStatus status, ref object commandText)
        {
          try
          {
            if (neededText == vsCommandStatusTextWanted.vsCommandStatusTextWantedNone)
            {
              switch (commandName)
              {
               //Other Cases
                case "RunTest.Connect.RunXMLTest":
                  status = RunTestFromXMLCommand.UpdateCommandStatus(_applicationObject);
                  break;
              }
            }
          }
          catch (Exception e)
          {
           //Shows a message box with the error.
            ErrorHandler.HandleError(e, commandName);
          }
    
        }
    

    The RunTestFromXMLCommand code is

        public static vsCommandStatus UpdateCommandStatus(DTE2 applicationObject)
        {
          bool fileHasBeenSelected = applicationObject.SelectedItems != null && applicationObject.SelectedItems.Count > 0;
    
          if (fileHasBeenSelected)
          {
            return GetCommandStatus(applicationObject);
          }
          else
          {
    //I have also tried just returning invisible or unsupported.  I really 
    //Just want the Command to dissapear from the menu.
            return (vsCommandStatus)vsCommandStatus.vsCommandStatusSupported | vsCommandStatus.vsCommandStatusInvisible;
          }
        }
    
        private static vsCommandStatus GetCommandStatus(DTE2 applicationObject)
        {
          bool isTestXMLFile = DetermineIfSelectedItemIsTestXMLFile(applicationObject);
    
          if (isTestXMLFile)
          {
            return (vsCommandStatus)vsCommandStatus.vsCommandStatusSupported | vsCommandStatus.vsCommandStatusEnabled;
          }
          else
          {
            return (vsCommandStatus)vsCommandStatus.vsCommandStatusSupported | vsCommandStatus.vsCommandStatusInvisible;
          }
        }
    
        private static bool DetermineIfSelectedItemIsTestXMLFile(DTE2 applicationObject)
        {
          string projectFileName = applicationObject.SelectedItems.Item(1).ProjectItem.ContainingProject.Name;
          Window window = applicationObject.ActiveWindow;
          Document document = window.Document;
    
          bool isTestXMLFile = false;
    
          if (window.Document != null)
          {
            isTestXMLFile = projectFileName == "DomainModelTest" &&
                            window.Document.FullName.Contains(".xml");
          }
          return isTestXMLFile;
        }
    

    My AddIn file contains these settings

    		<FullClassName>RunTest.Connect</FullClassName>
    		<LoadBehavior>1</LoadBehavior>
    		<CommandPreload>1</CommandPreload>
    		<CommandLineSafe>0</CommandLineSafe>
    

    I know that is a lot to look through, but it should show you all of the code that is being executed to determine the status of my Command.  If you have some time please look it over.  I will continue poking around at it to see if I can figure out the issue.

    Thanks!

    Tuesday, December 27, 2011 7:13 PM
  • Edit:  Nevermind, forgot to toggle off the /resetAddIn :)  Once I did that it seems to repro, I will dig into what is happening exactly.

    It looks like your AddIn isn't being loaded, at least in my repro. I verified this by going to Tools->Add In Manager and noticing your AddIn didn't have the 'Startup' column checked. Once I checked that option and restarted VS it all started working as expected.

    Can you see if your AddIn Manager dialog is showing that column as checked?  Also, I noticed the lovely wizard generated code has that

    if (neededText == vsCommandStatusTextWanted.vsCommandStatusTextWantedNone)

    check, if you set a break point on that instead of inside that is it getting hit?  That check is unecessary.

    I can't repro the 'result being ignored', it isn't in my case.  Double check there are no exceptions being thrown (they wouldn't break in the debugger as they would be swallowed by the AddIn code).

    Ryan


    Tuesday, December 27, 2011 8:02 PM
  • Oh so you are able to reproduce the issue?  That at least confirms that I am not crazy!  I will take that as a step forward.

    All of the code in the QueryStatus method is wrapped in a Try-Catch, with the Catch code displaying a message box if there is an error.  I have confirmed that the message box will display correctly if there is an error. 

    I will try removing the neededText check.

    Tuesday, December 27, 2011 8:10 PM
  • This looks like a bug in 2010.  I don't think there is any work-around that I can see.  The problem is we are treating the control as a 'user added' control, the logic around those (i.e. controls added by the user using the customization dialog) is that they are always visible (to avoid the case where a user goes to the Customize dialog and adds a command that isn't currently context visible and then looks at the menu they added it to and it isn't shown there).

    I thought that perhaps you could use the DTE control element and just set its Visible/Enabled state in the QueryStatus callback but DTE needs to know if the state you are setting is permissable (i.e. it won't let you explicitly set a command to visible if QueryStatus would return it is not visible, therefore the call to set_Visible on the DTE object ends up calling back into your DTCommandTarget asking if the command is currently visible, of course if your IDTCommandTarget is trying to mutate the DTE control state we get into an infinite loop).

    I think you have two choices

    1:  Live with it being visible but disabled.

    2:  Move to a package and not an AddIn, describe your command UI via VSCT and do the normal QueryStatus/Exec work to control visibilit/enabled staet. AddIns are kind of the lesser supported of the two extensibility models. I will file a bug to try and get this fixed, but that won't help you now.

    Edit:  Actually a work around would be to monitor the events around the conditions that cause its status to change (so selection change, active document change) and in those events you can get the DTE control object and set its Visible/Enabled state.  You would need to eliminate the IDTCommandTarget off your Connect object.

    Ryan

    Tuesday, December 27, 2011 8:41 PM
  • Sounds reasonable enough.  Thanks for doing that research!

    This AddIn is explicitly for internal use, so it should be okay to have a minor usability issue like this.  If the project becomes bigger I may convert to a Package, but I think for now it is not necessary. 

    At least I know what the issue is and can stop beating my head over it.

    Much appreciated!

    Tuesday, December 27, 2011 8:50 PM
  • NP, sorry about the bug (I wrote the code in question, boo me :().

    Ryan

    Tuesday, December 27, 2011 8:59 PM
  • No worries. I will be sure to throw you under the bus as often as possible (even when the subject has nothing to do with this bug).
    Tuesday, December 27, 2011 9:02 PM