32

In WPF most controls have MouseUp and MouseDown events (and the mouse-button-specific variations) but not a simple Click event that can be used right away. If you want to have a click-like behaviour using those events you need to handle both which i consider to be a bit of a pain.

The obvious problem is that you cannot simply omit the MouseDown event because if your click is started on another control and it is released over the control that only handles MouseUp your supposed click will fire while it really should not: Both MouseUp and MouseDown should occur over the same control.

So i would be interested in a more elegant solution to this general problem if there is any.


Notes: There are several good solutions to this as can be seen below, i chose to accept Rachel's answer because it seems to be well received, but additionally i'd like to add the following annotations:

Rachel's button answer is quite clean and straightforward, but you need to wrap your actual control in a button and in some cases you might not really consider your control to be a button just because it can be clicked (e.g. if it is more like a hyperlink), further you need to reference a template every time.

Rick Sladkey's behaviour answer more directly answers the original question of how to just simulate a click/make a control clickable, the drawback is that you need to reference System.Windows.Interactivity and like Rachel's solution it inflates the Xaml-code quite a bit.

My attached event answer has the advantage of being quite close to a normal click event in terms of Xaml-Markup which can be done with one attribute, the only problem i see with it is that the event-attachment in code is not cleanly encapsulated (if anyone knows a fix for that, please add a comment to the answer).

Community
  • 1
  • 1
H.B.
  • 166,899
  • 29
  • 327
  • 400
  • 1
    WPF control's are different from WinForms in that they are meant to be lookless. Instead of a control consisting of a UI and a behavior, it is just the behavior. The default UI for a button is based on what theme is used, however it is meant to be overwritten for anything that is meant to behave like a button but not necessarily look like a button. – Rachel Jan 31 '11 at 13:16
  • I know this is quite old but a problem I noticed with Rachel's answer is that controls can't be referenced very easily when they are within a `ControlTemplate`. Workaround was to create a `Loaded` event on each control and instantiate them in code then. for simplicity's sake, I'm going to stick with A.R.'s solution. – windowsgm Jun 29 '12 at 10:57
  • @killianmcc: You should not reference controls in control templates anyway, so you are probably doing something wrong. Your actual content should not be in the ControlTemplate, it's just the frame for your content which then will be placed where the ContentPresenter is. Further you can bind properties inside the template to the Button, one would normally do that for various brushes and layout properties. – H.B. Jun 29 '12 at 11:09
  • Placed where the ContentPresenter is, as in within the `ControlTemplate`? I can't bind all properties though (E.g. this is a child window to which I'm passing text that has to be displayed in a `TextBlock` that is within the `ControlTemplate`). – windowsgm Jun 29 '12 at 11:19
  • @killianmcc: I edited Rachel's answer, in the example the `Label` will be placed where the `ContentPresenter` is in the template. As the `Label` is not *inside* the template you should have no problem referencing it. Don't put the `TextBlock` inside the template unless you just want to set the `Text`, then you can use `Text="{TemplateBinding Content}"`, then everything you set as content of the Button ends up in the `TextBlock` (which may cause exceptions for non-text; just use a `ContentPresenter`, which binds to the `Content` automatically and creates a `TextBlock` for Content that is text). – H.B. Jun 29 '12 at 11:23
  • Oh, I understand now. My apologies, thanks! – windowsgm Jun 29 '12 at 11:24

5 Answers5

19

I would use a Button control and overwrite the Button.Template to just show the content directly.

<ControlTemplate x:Key="ContentOnlyTemplate" TargetType="{x:Type Button}">
    <ContentPresenter />
</ControlTemplate>

<Button Template="{StaticResource ContentOnlyTemplate}">
    <Label Content="Test"/>
</Button>
H.B.
  • 166,899
  • 29
  • 327
  • 400
Rachel
  • 130,264
  • 66
  • 304
  • 490
  • I am looking for a click-behaviour on controls that are not buttons (e.g. `TextBlocks`) – H.B. Jan 28 '11 at 17:22
  • I would make the clickable item a button and overwrite the ControlTemplate to display the label instead... I'll change my post to show an example – Rachel Jan 28 '11 at 17:26
  • I am aware of this approach and i even had it outlined in a draft of my question but deleted it again (it's better to have it as an answer anyway). It's not too bad since at least you do not need to use the events any more, maybe it is the best solution there but maybe someone else has thought of a better way to do this. – H.B. Jan 28 '11 at 17:38
  • 2
    @H.B.: This is the intended solution using WPF. Controls are "lookless" in that they can be templated in any way you choose - and so any control that needs to be "clickable" can be a button with a custom template. – Dan Puzey Jan 28 '11 at 17:46
  • @Rachel: I know it's just an example but i would remove the `Label` and just have a `ContentPresenter` there, then it's the general way of how to create a click-able control. The template name would of course be something like `ClickableControl`, or maybe `ContentOnlyTemplate` then. @Dan Puzey: Sounds reasonable. – H.B. Jan 28 '11 at 18:15
  • I noticed that for some reason there is some lag after the click when using a control templated in this manner (you can compare it with a control that uses the dual-event approach), it's rather odd. – H.B. Jan 28 '11 at 19:02
  • I have used this method in the past and have never encountered any lag. Might be an issue with the Template, would have to see code to determine if that is the case – Rachel Jan 28 '11 at 19:35
  • Accepted this as answer but i think some other solutions are quite good too, see the notes that i added to the question. – H.B. Jan 30 '11 at 13:03
14

Here is a behavior you can add to any element so that it will raise the ButtonBase.Click event using the normal button logic:

public class ClickBehavior : Behavior<FrameworkElement>
{
    protected override void OnAttached()
    {
        AssociatedObject.MouseLeftButtonDown += (s, e) =>
        {
            e.Handled = true;
            AssociatedObject.CaptureMouse();
        };
        AssociatedObject.MouseLeftButtonUp += (s, e) =>
        {
            if (!AssociatedObject.IsMouseCaptured) return;
            e.Handled = true;
            AssociatedObject.ReleaseMouseCapture();
            if (AssociatedObject.InputHitTest(e.GetPosition(AssociatedObject)) != null)
                AssociatedObject.RaiseEvent(new RoutedEventArgs(ButtonBase.ClickEvent));
        };
    }
}

Notice the use of mouse capture/release and the input hit test check. With this behavior in place, we can write click handlers like this:

<Grid>
    <Rectangle Width="100" Height="100" Fill="LightGreen" ButtonBase.Click="Rectangle_Click">
        <i:Interaction.Behaviors>
            <utils:ClickBehavior/>
        </i:Interaction.Behaviors>
    </Rectangle>
</Grid>

and the code behind:

    private void Rectangle_Click(object sender, RoutedEventArgs e)
    {
        Debug.WriteLine("Code-behind: Click");
    }

It's easy enough to convert this to all code-behind; the important part is the capture and click logic.

If you are not familiar with behaviors, install the Expression Blend 4 SDK and add this namespace:

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"

and add System.Windows.Interactivity to your project.

Edit:

Here's how to attach the click behavior to an element in code-behind and add a handler for the click event:

void AttachClickBehaviors()
{
    AttachClickBehavior(rectangle1, new RoutedEventHandler(OnAttachedClick));
}

void OnAttachedClick(object sender, RoutedEventArgs e)
{
    Debug.WriteLine("Attached: Click");
}

// Reusable: doesn't need to be in the code-behind.
static void AttachClickBehavior(FrameworkElement element, RoutedEventHandler clickHandler)
{
    Interaction.GetBehaviors(element).Add(new ClickBehavior());
    element.AddHandler(ButtonBase.ClickEvent, clickHandler);
}
Rick Sladkey
  • 33,988
  • 6
  • 71
  • 95
  • Tested it now and it has the same problem as my solution: It does not work for `Popups` it seems, this be annoying... – H.B. Jan 29 '11 at 18:41
  • Event handlers often don't work as expected for popups because they are not part of the visual tree. The workaround is to install the handlers (or behavior) on the element that is the child of the popup. You can use `VisualTreeHelper.GetChild(popup, 0)` to get that element or you might have a way to access it by name. – Rick Sladkey Jan 29 '11 at 19:26
  • I can't find the WPF documentation but this Silverlight documentation describes the problem: Routed Events Outside the Visual Tree: http://msdn.microsoft.com/en-us/library/cc189018(v=vs.95).aspx – Rick Sladkey Jan 29 '11 at 20:00
5

Ok, just thought i'd play around with attached events and see where i'll get with it, the following seems to work in most cases, could use some refinement/debugging:

public static class Extensions
{
    public static readonly RoutedEvent ClickEvent = EventManager.RegisterRoutedEvent(
        "Click",
        RoutingStrategy.Bubble,
        typeof(RoutedEventHandler),
        typeof(UIElement)
        );

    public static void AddClickHandler(DependencyObject d, RoutedEventHandler handler)
    {
        UIElement element = d as UIElement;
        if (element != null)
        {
            element.MouseLeftButtonDown += new MouseButtonEventHandler(element_MouseLeftButtonDown);
            element.MouseLeftButtonUp += new MouseButtonEventHandler(element_MouseLeftButtonUp);
            element.AddHandler(Extensions.ClickEvent, handler);
        }
    }

    public static void RemoveClickHandler(DependencyObject d, RoutedEventHandler handler)
    {
        UIElement element = d as UIElement;
        if (element != null)
        {
            element.MouseLeftButtonDown -= new MouseButtonEventHandler(element_MouseLeftButtonDown);
            element.MouseLeftButtonUp -= new MouseButtonEventHandler(element_MouseLeftButtonUp);
            element.RemoveHandler(Extensions.ClickEvent, handler);
        }
    }

    static void element_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        UIElement uie = sender as UIElement;
        if (uie != null)
        {
            uie.CaptureMouse();
        }
    }
    static void element_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        UIElement uie = sender as UIElement;
        if (uie != null && uie.IsMouseCaptured)
        {
            uie.ReleaseMouseCapture();
            UIElement element = e.OriginalSource as UIElement;
            if (element != null && element.InputHitTest(e.GetPosition(element)) != null) element.RaiseEvent(new RoutedEventArgs(Extensions.ClickEvent));
        }
    }
}

Usage:

<TextBlock local:Extensions.Click="TextBlock_Click" Text="Test"/>

Edit: Changed it to use mouse-capture instead of storing sender/source which did not get cleared out sufficiently. Remaining problem is that you cannot add the event in code using UIElement.AddHandler(Extensions.ClickEvent, handler) because that omits the attachments of the other mouse events which are needed for raising the click event (you need to use Extensions.AddClickHandler(object, handler) instead).

H.B.
  • 166,899
  • 29
  • 327
  • 400
4

Here is a simple solution that I use on a fairly regular basis. It is easy, and works in all sorts of scenarios.

In your code, place this.

private object DownOn = null;

private void LeftButtonUp(object sender, MouseButtonEventArgs e)
{
  if (DownOn == sender)
  {
    MessageBox.Show((sender as FrameworkElement).Name);
  }
  DownOn = null;
}

private void LeftButtonDown(object sender, MouseButtonEventArgs e)
{
  DownOn = sender;
}

Now all you have to do is setup handlers for all of the controls that you care about i.e.

<Border Background="Red"  MouseLeftButtonUp="LeftButtonUp" MouseLeftButtonDown="LeftButtonDown".../>
<Border Background="Blue" MouseLeftButtonUp="LeftButtonUp" MouseLeftButtonDown="LeftButtonDown".../>

If you have a ton of controls, and don't want to do it all in XAML, you can do it in your controls / window constructor programatically.

This will cover most of your scenarios, and it can easily be tweaked to handle special cases.

A.R.
  • 15,405
  • 19
  • 77
  • 123
  • This is the way i used to do it myself as noted in a comment on ChrisF's answer, not too bad but not very elegant either... – H.B. Jan 28 '11 at 17:42
  • Maybe not, but experience has taught me that having to make a bunch of templates for simple behavior quickly gets into the territory of overkill. It will be interesting to see what else people come up with. – A.R. Jan 28 '11 at 17:51
  • I came up with an interesting solution myself by now in case you are interested, it extends this approach. – H.B. Jan 28 '11 at 18:58
-1

First add a Mouse event click function:

/// <summary>
/// Returns mouse click.
/// </summary>
/// <returns>mouseeEvent</returns>
public static MouseButtonEventArgs MouseClickEvent()
{
    MouseDevice md = InputManager.Current.PrimaryMouseDevice;
    MouseButtonEventArgs mouseEvent = new MouseButtonEventArgs(md, 0, MouseButton.Left);
    return mouseEvent;
}

Add a click event to one of your WPF controls:

private void btnDoSomeThing_Click(object sender, RoutedEventArgs e)
{
    // Do something
}

Finally, Call the click event from any function:

btnDoSomeThing_Click(new object(), MouseClickEvent());

To simulate double clicking, add a double click event like PreviewMouseDoubleClick and make sure any code starts in a separate function:

private void lvFiles_PreviewMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
    DoMouseDoubleClick(e);
}

private void DoMouseDoubleClick(RoutedEventArgs e)
{
    // Add your logic here
}

To invoke the double click event, just call it from another function (like KeyDown):

private void someControl_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
    if (e.Key == Key.Enter)
        DoMouseDoubleClick(e);
}
  • Sorry, but the point of the question is not to actually completely simulate a click in code but rather to create the desired behavior for controls which do not expose a `Click` event. – H.B. Apr 24 '14 at 15:16