Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gray and ungray commands depending on conditions #428

Open
astrohart opened this issue May 7, 2023 · 3 comments
Open

Gray and ungray commands depending on conditions #428

astrohart opened this issue May 7, 2023 · 3 comments

Comments

@astrohart
Copy link

astrohart commented May 7, 2023

@madskristensen I am making a new extension with this toolkit. In my extension, I want to take a certain action but only if a Solution is currently loaded.

I have been following the tutorial:
https://www.vsixcookbook.com/getting-started/your-first-extension.html

Great tutorial by the way, and I am also thrilled that this package has been put together.

One recipe I think you forgot to add to your "cookbook" was graying out/ungraying commands, showing/hiding menu commands, and/or changing the text of a menu command. I also am struggling to implement those tips that I've Googled, with your package.

My command appears on the Edit menu in Visual Studio. I'd like to gray out my command if there is no solution currently open.

I am drawing a blank as to how to correctly implement this. Any assistance would be appreciated. Thank you.

@astrohart
Copy link
Author

astrohart commented May 7, 2023

@madskristensen See also:
https://social.msdn.microsoft.com/Forums/en-US/7bead8ae-a365-4dea-9c24-7fedf55fd58b/how-to-get-custom-context-menu-item-hiddenvisible-in-run-time?forum=vsx

UPDATE

I am referring to the post that is linked above.

I enabled my extension to just not be loaded unless a Solution is currently open, which, in principle, addressed the issue I was concerned about. However, the preferred UI/UX is to always show the command on the Edit menu, but just gray it out when it does not apply.

@astrohart
Copy link
Author

astrohart commented May 7, 2023

@madskristensen

UPDATE

I located this Stack Overflow post but I cannot find the OnBeforeQueryStatus method to override, that is mentioned.

I do see a BeforeQueryStatus method that I can override, defined in your BaseCommand class:

using Community.VisualStudio.Toolkit;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using System;
using Task = System.Threading.Tasks.Task;

namespace CopyActiveProjectFilename
{
    [Command(PackageIds.CopyActiveProjectFilenameCommand)]
    internal sealed class
        CopyActiveProjectFilenameCommand : BaseCommand<
            >
    {
        /// <summary>Override this method to control the commands visibility and other properties.</summary>
        protected override void BeforeQueryStatus(EventArgs e)
            => base.BeforeQueryStatus(e);

        /* ... */
    }
}

However, this does me no good, since -- and I am not trying to criticize -- but you hide the sender parameter in the BaseCommand implementation...basically you have a line

// Decompiled with JetBrains decompiler
// Type: Community.VisualStudio.Toolkit.BaseCommand
// Assembly: Community.VisualStudio.Toolkit, Version=16.0.430.0, Culture=neutral, PublicKeyToken=79441d341a79572c
// MVID: DE52B160-B802-48FF-B8F4-0A0035C0AB3F
// Assembly location: C:\Users\Brian Hart\.nuget\packages\community.visualstudio.toolkit.16\16.0.430\lib\net472\Community.VisualStudio.Toolkit.dll
// XML documentation location: C:\Users\Brian Hart\.nuget\packages\community.visualstudio.toolkit.16\16.0.430\lib\net472\Community.VisualStudio.Toolkit.xml

using Microsoft.VisualStudio.Shell;
using System;


#nullable enable
namespace Community.VisualStudio.Toolkit
{
  /// <summary>A base class that makes it easier to handle commands.</summary>
  /// <example>
  /// <code>
  /// [Command("489ba882-f600-4c8b-89db-eb366a4ee3b3", 0x0100)]
  /// public class TestCommand : BaseCommand&lt;TestCommand&gt;
  /// {
  ///     protected override Task ExecuteAsync(OleMenuCmdEventArgs e)
  ///     {
  ///         return base.ExecuteAsync(e);
  ///     }
  /// }
  /// </code>
  /// </example>
  public abstract class BaseCommand
  {
  
    /* ... */
    
    internal virtual void BeforeQueryStatus(object sender, EventArgs e) => this.BeforeQueryStatus(e);
    
    /// <summary>Override this method to control the commands visibility and other properties.</summary>
    protected virtual void BeforeQueryStatus(EventArgs e)
    {
    }

    /* ... */

  }
}

@madskristensen the overridable version of the method hides the sender parameter, but that is what I need, because I have to cast that sender to OleMenuCommand to then call its Visible, Enable, Text properties.

As an example, the code I'd write in my CopyActiveProjectFilenameCommand class, were I able to override the correct method, would be the following:

using Community.VisualStudio.Toolkit;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using System;
using Task = System.Threading.Tasks.Task;

namespace CopyActiveProjectFilename
{
    [Command(PackageIds.CopyActiveProjectFilenameCommand)]
    internal sealed class
        CopyActiveProjectFilenameCommand : BaseCommand<
            CopyActiveProjectFilenameCommand>
    {
        /// <summary>Override this method to control the commands visibility and other properties.</summary>
        protected override async Task BeforeQueryStatusAsync(object sender, EventArgs e)
        {
            base.BeforeQueryStatus(sender, e);

            if (sender is not OleMenuCommand menuCommand) return;

            menuCommand.Enabled = await VS.Solutions.IsOpenAsync();
        }

        /* ... */
    }
}

See my changes? First of all, make the internal virtual void version of the method the protected virtual void version that people can override -- so we can get at that sender variable.

Secondly, I suggest you append Async to the name of the method.

Thirdly, I suggest that you make the return type async Task not void.

The last change is needed so I can ask Visual Studio whether a solution is currently open using your VS.Solutions.IsOpenAsync() method.

Thank you for your consideration.

@reduckted
Copy link
Contributor

so we can get at that sender variable.

The sender variable is equivalent to this.Command, which is why it's hidden.

this.Command.Enabled = ...

Thirdly, I suggest that you make the return type async Task not void.

The problem with that is BeforeQueryStatus cannot be async. The method is basically an event handler, and as soon as the event has been raised, the menu will be displayed. If it becomes async, then the menu can end up being shown before the async operation completes, which means the menu is shown with the wrong state.

If you need to call async methods from a synchronous method (like BeforeQueryStatus), you should JoinableTaskFactory.Run like so:

protected override void BeforeQueryStatus(EventArgs e)
{
    Package.JoinableTaskFactory.Run(async () =>
    {
        Command.Enabled = await VS.Solutions.IsOpenAsync();
    });
}

Alternatively, you can avoid the asynchronous call completely. VS.Solutions.IsOpenAsync() is only async because it needs to get the IVsSolution service asynchronously. That can be done immediately after the command is created. Then BeforeQueryStatus just needs to call the IVsSolution.IsOpen() method.

private IVsSolution? _solution;

protected async override Task InitializeCompletedAsync()
{
    _solution = await VS.Services.GetSolutionAsync();
}

protected override void BeforeQueryStatus(EventArgs e)
{
    ThreadHelper.ThrowIfNotOnUIThread();
    Command.Enabled = _solution?.IsOpen() ?? false;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants