Tuesday, March 2, 2010

Implementing disabled WPF toolbar buttons for Visual Studio 2010


If you’ve ever wanted to implement a toolbar in WPF you may have come upon this limitation: you can easily set images for the toolbar buttons, but there is no default way of making these images appear grayed when the toolbar buttons are disabled.

To implement the toolbar buttons and menu items, Visual Studio 2010 uses a custom algorithm to create gray images from the original picture. This is publicly available  for consumption via a converter Microsoft.VisualStudio.PlatformUI.GrayscaleImageConverter, implemented in Microsoft.VisualStudio.Shell.10.0 assembly. The converter takes as input an ImageSource and converts to an Image element that uses for source the grayed out version of the image.

If you need to implement toolbars for your package/dialogs you should strongly consider using shell-implemented toolbars (defined by your application via *.vsct files) and used through IVsToolWindowToolbar/IVsToolWindowToolbarHost interfaces. That guarantees a consistent look of all the buttons, separators or split-buttons in your toolbar with the rest of the shell.

For instance, to display a shell-owned toolbar in a WPF dialog you can use IVsUIShell4.CreateToolbarTray() interface to obtain an IVsToolbarTrayHost, then call AddToolbar() on this object to add the toolbar using the guid of your package and the resource Id of the toolbar in vsct, then get the UIElement representing the toolbar using IVsToolbarTrayHost.GetToolbarTray() and position the element to your liking in the dialog.


However, there may be situations when you’ll want to implement your own WPF toolbar buttons. To maintain a consistent grayed out look of disabled buttons you may choose to reuse the GrayscaleImageConverter implemented by the shell. Below I describe a way to do this:

1)  Implement a ToolbarButtonImage class having a Source dependency property. You’ll use this to set the button’s image in the “normal” state.

2) The style of the ToolbarButtonImage class will use a trigger to switch the template of the button’s content when the button gets enabled/disabled between a control template (ToolbarButtonImage_Normal) containing the image in the normal state and a control template (ToolbarButtonImage_Disabled) containing the grayed out image produced by the  GrayscaleImageConverter.

3) Add the buttons to the toolbars as follows:

    <Button IsEnabled="{Binding ……….}">
       <local:ToolbarButtonImage Source="Resources\ButtonNormalImage.png"/>

Below is the sample code for the ToolbarButtonImage and its style:








    public class ToolbarButtonImage : UserControl
        static ToolbarButtonImage()

                                new FrameworkPropertyMetadata(typeof(ToolbarButtonImage)));

static readonly DependencyProperty SourceProperty = DependencyProperty.Register

                               ("Source", typeof(BitmapSource), typeof(ToolbarButtonImage)); 

                 return (BitmapSource)GetValue(SourceProperty);



<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"


<ui:GrayscaleImageConverter x:Key="GrayscaleImageConverter"/>

<ControlTemplate x:Key="ToolbarButtonImage_Normal" TargetType="{x:Type local:ToolbarButtonImage}">

     <Image Width="16" Height="16" Source="{TemplateBinding Property=Source}"/>


<ControlTemplate x:Key="ToolbarButtonImage_Disabled" TargetType="{x:Type local:ToolbarButtonImage}">

     <ContentPresenter Width="16" Height="16" Content="{TemplateBinding Property=Source, Converter={StaticResource GrayscaleImageConverter}}"/>


<Style x:Key="{x:Type local:ToolbarButtonImage}" TargetType="{x:Type local:ToolbarButtonImage}">
    <Setter Property="Template" Value="{StaticResource ToolbarButtonImage_Normal}"/>


        <DataTrigger Binding="{Binding IsEnabled, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Button}}}" Value="False">

            <Setter Property="Template" Value="{StaticResource ToolbarButtonImage_Disabled}"/>






Anonymous said...

Hi Alin,

Great post, have you figured out yet how to disable a WPF button in VS2012?

The GreyScaleImageConverter does not seem to give the embossed look and feel of a disabled button which is what disabled buttons in VS2012 look like. It just converts color to grey, and in VS2012 all buttons are already mostly grey!

Jezz Santos

Alin Constantin said...

Hi Jezz,

Here is what VS2012 is doing to display the disabled buttons: it uses a ThemedImageConverter from MPF



Alin Constantin said...

Goddamn Enter.

Images are themed like this:


<vsui:ThemedImageConverter x:Key="ThemedImageConverter" />
<!-- When used as a bias color of ThemedImageConverter for a disabled item, this produces a 75% translucent effect -->
<Color x:Key="TranslucentBiasColor">#40FFFFFF</Color>

<Setter Property="Icon">
<MultiBinding Converter="{StaticResource ThemedImageConverter}" ConverterParameter="{StaticResource TranslucentBiasColor}">
<Binding Path="Image" />
<Binding Path="(vsui:ImageThemingUtilities.ImageBackgroundColor)" RelativeSource="{RelativeSource Self}" />
<Binding Path="IsEnabled" />

Now, buttons/menu items can have text, when they are disabled they use a different theme color entry, CommandBarTextInactiveBrushKey, like so:

<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Text" Property="Foreground" Value="{DynamicResource {x:Static vsui:EnvironmentColors.CommandBarTextInactiveBrushKey}}"/>

The ThemedImageConverter is a MultiValueConverter. The converter does 2 things for images that are BitmapSource: blends the original image's halo with the background, and renders the image disabled if necessary.
There is also a ThemedImageSourceConverter : MultiValueConverter helper class in case you need to convert to an ImageSource.

Alin Constantin said...

That being said, I strongly recommend reconsidering why you'd want to implement your own themed buttons. Each time the shell will adjust its menu/toolbar styles, you'd have to change your code to match (best example is my code above - things that worked in VS2010 need to change in VS2012, and will likely change in future versions, too).
Instead, I advise using shell-owned toolbars and menus, by using IVsUIShell4.CreateToolbarTray() and letting the shell create the toolbar for you, and defining the toolbar buttons in the package's VSCT file.

Anonymous said...

Thanks Alin,

I am starting to come to the same conclusion as you and wanting to use VS toolbars instead to mitigate these UI differences.

My issue is that I am simply managing the conversion of our VS2010 codebase to VS2012. I am no XAML expert by any means, and that's why I am hesitant just knowing how to change our XAML to use the IVsUIShell4.CreateToolbarTray() method. It makes total sense to me.

We are an open source project for the community (http://vspat.codeplex.com), would you be keen to guide us into making that transition in the XAML?
Sorry, I dont have a google account, could you contact me directly through the project site?

Alin Constantin said...

Hi Jezz,

Here is how you'd add a shell-owned toolbar to your window:
- in the vsct file for your package, define a toolbar. This is identified by a command
- on your dialog, implement IOleCommandTarget interface and its QueryStatus/Exec methods to handle command enabling and execution for buttons in the toolbar
- in the dialog's initialization code, get the shell SVsUIShell service, query IVsUIShell4 interface and call CreateToolbarTray, passing in your command target implementation. The result is a toolbar tray host object
- On this IVsToolbarTrayHost call AddToolbar() passing in the command ui and id identifying the toolbar (as defined in vsct)
- On the toolbar host, call GetToolbarTray. It will return an IVsUIElement. On this, call GetUIObject, cast the result to IVsUIWpfElement, and call GetFrameworkElement. The result will be an object that can be cast to FrameworkElement
- Take the framework element and add it as a child to another element in your dialog, e.g. a Border placed where your current xmal toolbar is.

I hope this helps. Sorry I don't have tome to help more, I'm caught up really bad these days.