Monday, December 1, 2008

Setting the Context Menu on an editable ComboBox

There's a bug in the .NET 3.5 SP1 ComboBox template which stops you setting the context menu on the TextBox part of an editable ComboBox using a Style. This post describes the problem and outlines two workarounds.

This code snippet recreates the issue:
<Window.Resources>
    <ContextMenu x:Key="contextMenu">
        <ContextMenu.Items>
            <MenuItem Header="A Menu Item"/>
        </ContextMenu.Items>
    </ContextMenu>

    <Style TargetType="{x:Type TextBox}">
        <Setter Property="ContextMenu" Value="{StaticResource contextMenu}"/>
    </Style>

    <Style TargetType="{x:Type ComboBox}">
        <Setter Property="ContextMenu" Value="{StaticResource contextMenu}"/>
    </Style>
</Window.Resources>

<ComboBox IsEditable="True"/>

The context menu on the button part of the ComboBox gets set correctly, but the TextBox part is left with its default context menu:



The reason for this is that there is a missing TemplateBinding between the ComboBox and the contained TextBox in the default template. The TextBox style in the code snippet also fails to set the context menu; this is because the ComboBox itself internally sets a style to the TextBox, and it not permissable to have two different styles set to any one instance of a FrameworkElement.

The first workaround to the problem is to subclass the ComboBox and add the required template binding:
class ExtendedComboBox : ComboBox
{
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        
        // Use Snoop to find the name of the TextBox part
        // http://wpfmentor.blogspot.com/2008/11/understand-bubbling-and-tunnelling-in-5.html
        TextBox textBox = (TextBox)Template.FindName("PART_EditableTextBox", this);
        
        // Create a template-binding in code
        Binding binding = new Binding("ContextMenu");
        binding.RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent);
        BindingOperations.SetBinding(textBox, 
            FrameworkElement.ContextMenuProperty, binding);
        
    }
}
This approach does the job. The TextBox's ContextMenu property is now bound to the parent ComboBox and displays correctly.

The obvious problem with this approach is that all the ComboBoxes that require the new context menu now need to be replaced with this new class. This might well not be acceptable, perhaps the ComboBox has been subclassed already - you would need to create new versions for those too.

This second workaround avoids subclassing by using an Attached Behavior. If you are unfamiliar with the Attached Behavior pattern, go here for a great article by Josh Smith.

The Attached Behavior uses an Attached Property which can be set by a Style to create the Template Binding.
In order to create this, first write a new static class with an attached property thus:
public static class ComboBoxContextMenuBehavior
{
    public static bool GetIsContextMenuBound(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsContextMenuBoundProperty);
    }

    public static void SetIsContextMenuBound(DependencyObject obj, bool value)
    {
        obj.SetValue(IsContextMenuBoundProperty, value);
    }

    public static readonly DependencyProperty IsContextMenuBoundProperty =
        DependencyProperty.RegisterAttached(
        "IsContextMenuBound",
        typeof(bool),
        typeof(ComboBoxContextMenuBehavior),
        new UIPropertyMetadata(false, IsContextMenuBoundChanged));
)]
...and add a method to handle changes in the attached property value:
private static void IsContextMenuBoundChanged(DependencyObject d, 
    DependencyPropertyChangedEventArgs e)
{
    ComboBox comboBox = d as ComboBox;

    bool oldValue = e.OldValue is bool && (bool)e.OldValue;
    bool newValue = e.NewValue is bool && (bool)e.NewValue;

    if (comboBox != null && oldValue != newValue)
    {
        // Use Dispatcher with Loaded priority to ensure template has been
        // applied before applying the TemplateBinding
        comboBox.Dispatcher.BeginInvoke(DispatcherPriority.Loaded, 
            (DispatcherOperationCallback)delegate
        {
            TextBox textBox = (TextBox)comboBox.Template.FindName(
                "PART_EditableTextBox", comboBox);

            if (textBox != null)
            {
                if (!oldValue && newValue)
                {
                    // Create TemplateBinding
                    Binding binding = new Binding("ContextMenu");
                    
                    binding.RelativeSource = new RelativeSource(
                        RelativeSourceMode.TemplatedParent);
                    
                    BindingOperations.SetBinding(textBox, 
                        FrameworkElement.ContextMenuProperty, 
                        binding);
                }
                else
                {
                    // Clear TemplateBinding
                    BindingOperations.ClearBinding(textBox, 
                        FrameworkElement.ContextMenuProperty);
                }
            }
            
            return null;
        }, null);
    }
}
The Attached Behavior can now be assigned to the ComboBox using a Style:
<Window.Resources>
    <ContextMenu x:Key="contextMenu">
        <ContextMenu.Items>
            <MenuItem Header="A Menu Item"/>
        </ContextMenu.Items>
    </ContextMenu>

    <Style TargetType="{x:Type ComboBox}">
        <Setter Property="ContextMenu" Value="{StaticResource contextMenu}"/>
        <Setter Property="local:ComboBoxContextMenuBehavior.IsContextMenuBound"
            Value="True"/>
    </Style>
</Window.Resources>

<ComboBox IsEditable="True"/>

...and that's all you need. You have now successfully added a TemplateBinding to the ComboBox without subclassing it, and without individually referencing each ComboBox as a Style is used to fix the problem.

So, in conclusion, this bug is annoying but can be easily worked around whether it is acceptable or not to subclass the ComboBox.

Download sample app with source code



2 comments:

Anonymous said...

The custom control portion doesn't work. It compiles okay, but the application blows up when the control is created with a really cryptic message.

Dan said...

That's wierd - it works fine for me. What's the message?