10

I have a button in my application which is bound to a command. This button sits inside another control which also reacts to mouseclicks. When the button is enabled I get the behaviour that I expect - click on the button and the command is triggered, click outside the button but inside the container control and that is triggered instead.

Unfortunately when the button is disabled (through the CanExecute method of the command) the clicks on the button are bubbled up to the container control. I don't want this, I want the clicks to be swallowed - neither trigger the command nor bubble up.

I tried to overcome this by created a new class inheriting from Button but none of the following methods even seem to get called on a disabled button:

  • OnPreviewMouseDown
  • OnPreviewMouseUp
  • OnPreviewMouseLeftButtonDown
  • OnPreviewMouseLeftButtonUp
  • OnMouseDown
  • OnMouseUp
  • OnMouseLeftButtonDown
  • OnMouseLeftButtonUp
  • OnClick

Is a disabled control completely ignored by the WPF routed event system? And if so is there anyway that I can get the behaviour that I'm looking for?

Martin Harris
  • 28,277
  • 7
  • 90
  • 101

5 Answers5

11

RCGoforth's answer got me 90% of the way there, but the solution isn't to put a rectangle behind the button, because the bubbling event goes up the tree not across to siblings. In the end I surrounded the button with a ContentControl (since rectangle's can't have children) which swallowed the event before it could go up any further:

<ContentControl MouseDown="ContentControl_MouseDown">
    <Button Content="Click Test"
            Padding="2"
            Command="{Binding TestCommand}"/>
</ContentControl>

In the code behind:

private void ContentControl_MouseDown(object sender, MouseButtonEventArgs e)
{
    e.Handled = true;
}

Or to do this entirely in XAML (and to increase the hack level of the code...)

<Button>
    <Button.Template>
        <ControlTemplate>
            <Button Content="Click Test"
                    Command="{Binding TestCommand}"/>
        </ControlTemplate>
    </Button.Template>
</Button>
Martin Harris
  • 28,277
  • 7
  • 90
  • 101
2

It isn't really clean, but you could put a transparent rectangle behind your button that swallows click events.

RCGoforth
  • 146
  • 1
  • 7
0

What you want to do is register a routed event handler using the overload that takes the handledEventsToo parameter and specifying a value of true for that parameter. This way your outer handler will receive the event regardless of whether or not the button actually handles the event. That will look something like this:

this.AddHandler(Mouse.MouseUpEvent, this.MyMouseUpHandler, true);

And then in your handler you can always check what was clicked, whether it was handled, etc. via the MouseButtonEventArgs you are handed. For example, to check if another control actually handled the event already you can do:

if(!args.Handled)
{
     // handle it here instead
}
Drew Marsh
  • 33,111
  • 3
  • 82
  • 100
  • Thanks, but I've just been looking into this and I think you might have misinterpreted my question. I don't want the outer control to *always* receive the event, I want it to only receive the event if the click is outside of the button, even if the button is disabled. – Martin Harris Nov 06 '09 at 16:01
  • You're right, I did misunderstand your intent. I will revise. – Drew Marsh Nov 06 '09 at 18:08
0

A cleaner, more reusable solution would be implement this functionality as an attached property.

Using the service/action pattern:

namespace Control.Services
{

  public class UIElementService
  {
    public static readonly DependencyProperty HandleMouseEventsProperty = DependencyProperty.RegisterAttached("HandleMouseEvents",
      typeof(bool), typeof(UIElementService), new FrameworkPropertyMetadata(false, UIElementService.HandleMouseEventsPropertyChanged));

    static void HandleMouseEventsPropertyChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
      FrameworkElement element = sender as FrameworkElement;
      if (element == null)
        return;

      new HandleMouseEventsAction(element);
    }

    public static bool GetHandleMouseEvents(FrameworkElement target)
    {
      return (bool)target.GetValue(HandleMouseEventsProperty);
    }

    public static void SetHandleMouseEvents(FrameworkElement target, bool value)
    {
      target.SetValue(HandleMouseEventsProperty, value);
    }

    class HandleMouseEventsAction
    {
      UIElement m_Target;
      MouseButtonEventHandler m_Handler;

      internal HandleMouseEventsAction(FrameworkElement source)
      {
        m_Source = source;
        m_Handler = new MouseButtonEventHandler(PreviewMouseLeftButtonUp);

        m_Source.Loaded += OnSource_Loaded;
        m_Source.Unloaded += OnSource_Unloaded;
      }

      void OnSource_Loaded(object sender, RoutedEventArgs e)
      {
        m_Source.AddHandler(Mouse.PreviewMouseUpEvent, m_Handler, true);
      }

      void OnSource_Unloaded(object sender, RoutedEventArgs e)
      {
        m_Source.RemoveHandler(Mouse.PreviewMouseUpEvent, m_Handler);
      }

      void PreviewMouseLeftUIElementUp(object sender, MouseUIElementEventArgs e)
      {
        e.Handled = true;
      }

    }

  }

}

Then to use, import the namespace.

<Button sv:UIElementService.HandleMouseEvents="True" />

or

<ContentControl sv:UIElementService.HandleMouseEvents="True">
    <Button Content="Click Test" Padding="2" Command="{Binding TestCommand}"/>
</ContentControl>

I have not tested this (no time at the moment). I believe the action will still get the mouse events even if disabled.

HTH,

Dennis

Dennis
  • 20,275
  • 4
  • 64
  • 80
  • This doesn't compile, and if you just refactor it enough to compile it doesn't work. – Kelly Elton Jan 26 '17 at 18:07
  • @KellyElton, it says in the answer "I have not tested this (no time at the moment)." What errors did you get? Perhaps I can help. – Dennis Jan 30 '17 at 00:10
  • Yes I was aware that you didn't test it. The errors are compile time errors...once you fix those and get it up and running, the corrected code doesn't solve the problem. – Kelly Elton Jan 30 '17 at 21:18
  • @KellyElton, the `Mouse.PreviewMouseUpEvent` isn't handled? – Dennis Jan 31 '17 at 00:37
  • I'm just letting you know in it's current state it doesn't compile, and that if you fix those bugs so it does compile and does mark the PreviewMouseUpEvent as handled, that doesn't solve the problem. – Kelly Elton Jan 31 '17 at 20:09
0

I created a behavior (that actually works) that inserts a ContentPresenter between the Button and its parent. The ContentPresenter swallows mouse clicks when the Button is disabled. The behavior uses a couple of extension methods based on code from this answer. Using it is very simple:

<Button ...>
    <i:Interaction.Behaviors>
        <behaviors:SwallowMouseClicksWhenDisabled />
    </i:Interaction.Behaviors>
</Button>

And here is the source:

// the behavior (could also be an attached behavior)
public class SwallowMouseClicksWhenDisabled : Behavior<FrameworkElement>
{
    protected override void OnAttached()
    {
        var oldParent = AssociatedObject.Parent;

        oldParent.RemoveChild(AssociatedObject);

        var newParent = new ContentPresenter { Content = AssociatedObject };

        oldParent.AddChild(newParent);

        newParent.PreviewMouseDown += OnPreviewMouseEvent;

        newParent.PreviewMouseUp += OnPreviewMouseEvent;
    }

    private void OnPreviewMouseEvent(object sender, MouseButtonEventArgs e)
    {
        e.Handled = AssociatedObject.IsEnabled == false;
    }
}

// the extension methods
public static class DependencyObjectExtensions
{
    public static void AddChild(this DependencyObject parent, UIElement child)
    {
        var panel = parent as Panel;
        if (panel != null)
        {
            panel.Children.Add(child);
            return;
        }

        var decorator = parent as Decorator;
        if (decorator != null)
        {
            decorator.Child = child;
            return;
        }

        var contentPresenter = parent as ContentPresenter;
        if (contentPresenter != null)
        {
            contentPresenter.Content = child;
            return;
        }

        var contentControl = parent as ContentControl;
        if (contentControl != null)
        {
            contentControl.Content = child;
            return;
        }

        // maybe more
    }

    public static void RemoveChild(this DependencyObject parent, UIElement child)
    {
        var panel = parent as Panel;
        if (panel != null)
        {
            panel.Children.Remove(child);
            return;
        }

        var decorator = parent as Decorator;
        if (decorator != null)
        {
            if (Equals(decorator.Child, child))
            {
                decorator.Child = null;
            }
            return;
        }

        var contentPresenter = parent as ContentPresenter;
        if (contentPresenter != null)
        {
            if (Equals(contentPresenter.Content, child))
            {
                contentPresenter.Content = null;
            }
            return;
        }

        var contentControl = parent as ContentControl;
        if (contentControl != null)
        {
            if (Equals(contentControl.Content, child))
            {
                contentControl.Content = null;
            }
            return;
        }

        // maybe more
    }
}
Community
  • 1
  • 1
Chris Staley
  • 2,370
  • 1
  • 20
  • 21