3

I've made a custom button to bind a command to a (custom, routed) IsPressedChanged event so that the command is executed both when the button is pressed AND when it is released:

<local:CustomButton xmlns:i="http://schemas.microsoft.com/xaml/behaviors" x:Name="MyButton">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="CustomIsPressedChanged">
            <i:InvokeCommandAction Command="{Binding Path=SomeCommand}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</local:CustomButton>

With the custom button implementation:

public partial class CustomButton : Button
    {
        /* Register a custom routed event using the bubble routing strategy. */
        public static readonly RoutedEvent CustomIsPressedChangedEvent = EventManager.RegisterRoutedEvent(
            name: "CustomIsPressedChanged",
            routingStrategy: RoutingStrategy.Bubble,
            handlerType: typeof(RoutedEventHandler),
            ownerType: typeof(CustomButton));

        /* Provide CLR accessors for assigning an event handler. */
        public event RoutedEventHandler CustomIsPressedChanged
        {
            add { AddHandler(CustomIsPressedChangedEvent, value); }
            remove { RemoveHandler(CustomIsPressedChangedEvent, value); }
        }

        public CustomButton() { InitializeComponent(); }

        /* Custom Event handling of the IsPressedChanged event */
        protected override void OnIsPressedChanged(System.Windows.DependencyPropertyChangedEventArgs e)
        {
            /* Call the base class OnIsPressedChanged() method so IsPressedChanged event subscribers are notified. */
            base.OnIsPressedChanged(e);

            /* Raise custom event */
            RaiseEvent(new RoutedEventArgs(routedEvent: CustomIsPressedChangedEvent));
        }
    }

This works perfectly as it should.

And now comes the Problem:

When I try to propagate the value of the IsPressed property to the command like so:

<i:InvokeCommandAction Command="{Binding Path=SomeCommand}"
                       CommandParameter="{Binding ElementName=MyButton, Path=IsPressed}"/>

the propagated value will (seemingly) allways be the old value of IsPressed. When I press the button, the command called with the parameter beeing false, when I release the button the parameter is true. But when I check the value of IsPressed inside the event handler CustomButton.OnIsPressedChanged(), it represents the new value as expected.

My Question is: How should I propagate the value of IsPressed to get the correct value? Is it guaranteed that the command will always be called with the old value? In that case I could simply invert the value but that seems a bit shady to me and I really would not want to do this unless I know it will allways yield the correct result.

Felix
  • 1,066
  • 1
  • 5
  • 22
  • 2
    I would avoid Interaction.Triggers completely. Instead I would bind IsPressed to a view model property using [this solution](https://stackoverflow.com/a/3667609/1506454), and invoke required command method in property setter – ASh May 13 '22 at 16:32
  • 1
    Can't you bind the command directly to the Button.Command property? This would be the easiest solution. IsPressed is true only for a very brief moment when the button is activated. It appears like the trigger is evaluated after the event has finished its traversal. By the way, if you only need the pressed==true state, you can filter it and raise the event only in this case. This eliminates the requirement of the parameter. – BionicCode May 13 '22 at 17:08
  • 1
    If you need both states, then a better solution would be to implement an event for each state: Pressed and Released. This always better than communicating the state via event arguments. You filter the state in the OnIsPressedChanged method and then raise the corresponding event. – BionicCode May 13 '22 at 17:08
  • You can also use the eventTrigger for mouse up and mouse down events instead of implementing a custom button. Is the Command for mouse up different than command for mouse down? – XAMlMAX May 13 '22 at 21:10
  • @BionicCode, the `Button.Command` can only either call the command on pressing or on releasing which is why I can't use it here. - As for the behaviour of the `IsPressed` value: It may depend on the configuration of the button, but for my implementation, `IsPressed` is true as long as I hold down the mouse button. – Felix May 16 '22 at 07:30
  • @BionicCode, raising different events for pressed and released is a good idea. It encapsulates functionality and simplifies things outside the buton implementation. – Felix May 16 '22 at 07:32
  • @XAMlMAX, mouse up and down events unfortunaltey do not work when the button is triggered by keyboard which is not desirable in many cases. – Felix May 16 '22 at 07:34
  • @Felix Now I understand. You must know that the button has a `Button.ClickMode` property. This property (internal filtering) makes the Button only execute once on either Press, Release or Hover. That's why you experiencing a single click behavior. IsPressed is reset after the event cycle has completed. This means the button has already executed the click event. Let me post a simple solution for you. – BionicCode May 16 '22 at 09:03
  • Ah, you should add that information to your question. And it seems like you are using commands for statistics? I am not sure if I can give you a good advice here but end of command when command is triggered means that button is released. Unless its a toggle button? Bit more info why, you are trying to do this might give us better understanding. – XAMlMAX May 16 '22 at 20:25
  • I am also pretty sure that you can use can execute and execute of your command as entry and exit points. Just trying to make it easier for you, so less work is required. HTH – XAMlMAX May 16 '22 at 20:27
  • @XAMlMAX, I want to do some action as long as the button is pressed and immediately stop once the button is released. No statistics here. Also, I dont understand wht you mean by "end of command" now how the `CanExecute` might help here. – Felix May 17 '22 at 06:54
  • Now it all makes sense. In that case you can implement mouse service that is specifically monitoring mouse clicks. This way, once injected into your view model it will notify you when and how long that click was. Your command can then execute while your mouse service returns true. If that makes sense? It will also makes testing a lot easier. – XAMlMAX May 17 '22 at 19:17
  • 1
    @XAMlMAX I previously had an implementation using mouse services where i registered the mouse serevice once the button was pressed (to not have it running all the time). This solution had threading issues however. But the below solutions work perfectly now. – Felix May 18 '22 at 06:01

3 Answers3

3

You can pass the DependencyPropertyChangedEventArgs as a parameter of the RoutedEventArgs that is raised:

protected override void OnIsPressedChanged(DependencyPropertyChangedEventArgs e)
{
    base.OnIsPressedChanged(e);

    // you may want to pass e.NewValue here for simplicity.
    RaiseEvent(new RoutedEventArgs(CustomIsPressedChangedEvent, e));
}

Then ask the InvokeCommandAction to pass it to the command:

<i:InvokeCommandAction Command="{Binding Path=SomeCommand}"
                       PassEventArgsToCommand="True" />

And then, in the command you just need to cast the passed object to retrieve the new value of IsPressed:

SomeCommand = new ActionCommand(SomeCommandAction);

//...

private void SomeCommandAction(object o)
{
    if (o is not RoutedEventArgs routedEventArgs)
        return;

    if (routedEventArgs.OriginalSource is not DependencyPropertyChangedEventArgs eventArgs)
        return;

    if (eventArgs.NewValue is true)
        Count++;

    if (eventArgs.NewValue is false)
        Count--;

}

Working demo here.

Orace
  • 7,822
  • 30
  • 45
  • 2
    Great, this works. Unfortunately, this forces the viewmodel to handle a `RoutedEventArgs` object wich is a bit messy. I would like the viewmodel to only need to handle a boolean. – Felix May 16 '22 at 10:17
  • 1
    @Felix, you can use a converter (attribute `EventArgsConverter`) to extract the new value from the `RoutedEventArgs`. But at this point an attached property is cleaner. – Orace May 16 '22 at 11:43
  • @Felix. I added an attached property example [here](https://github.com/Orace/SO/commit/f2fb0c8632e9960d7f8f0914a42a5ed3bf666474) – Orace May 16 '22 at 12:04
  • thank you for that complete examle. I dont jet fully understand every tiny bit, but thats fine for now. – Felix May 17 '22 at 06:30
3

For reasons of convenience (for the usage of your control), you should not implement a parallel command. Instead modify the existing behavior.

The button has a Button.ClickMode property. The button's internal filtering of this property makes the Button execute only once on either ClickMode.Press, ClickMode.Release or ClickMode.Hover.
We need to bypass this filtering to execute the Button.Command and the Button.Click event on both MouseLeftButtonDown and MouseLeftButtonUp (to implement ClickMode.Press and ClickMode.Release) as well as MouseEnter and MouseLeave (to support ClickMode.Hover):

DoubleTriggerButton.cs

public class DoubleTriggerButton : Button
{
  public bool IsDoubleTriggerEnabled
  {
    get => (bool)GetValue(IsDoubleTriggerEnabledProperty);
    set => SetValue(IsDoubleTriggerEnabledProperty, value);
  }

  public static readonly DependencyProperty IsDoubleTriggerEnabledProperty = DependencyProperty.Register(
    "IsDoubleTriggerEnabled",
    typeof(bool),
    typeof(DoubleTriggerButton),
    new PropertyMetadata(true));

  protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
  {
    if (this.IsDoubleTriggerEnabled
      && this.ClickMode != ClickMode.Hover)
    {
      base.OnClick();
    }
    else
    {
      base.OnMouseLeftButtonDown(e);
    }
  }

  protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
  {
    if (this.IsDoubleTriggerEnabled
      && this.ClickMode != ClickMode.Hover)
    {
      base.OnClick();
    }
    else
    {
      base.OnMouseLeftButtonUp(e);
    }
  }

  protected override void OnMouseEnter(MouseEventArgs e)
  {
    if (this.IsDoubleTriggerEnabled
      && this.ClickMode == ClickMode.Hover)
    {
      base.OnClick();
    }
    else
    {
      base.OnMouseEnter(e);
    }
  }

  protected override void OnMouseLeave(MouseEventArgs e)
  {
    if (this.IsDoubleTriggerEnabled
      && this.ClickMode == ClickMode.Hover)
    {
      base.OnClick();
    }
    else
    {
      base.OnMouseLeave(e);
    }
  }
}
BionicCode
  • 1
  • 4
  • 28
  • 44
1

I have found another solution which barely needs changes compared to my original code:

Edit: As BionicCode pointed out in the comments, this is not a good design due to multiple reasons.

By adding a dependency property to the CustmButton which replaces the IsPressed property, one can assign the correct value inside the OnIsPressedChanged event handler. Binding to the new IsPressed property then works as I expected the original property to work:

public new static readonly DependencyProperty IsPressedProperty =
    DependencyProperty.Register("IsPressed", typeof(bool), typeof(CustomButton),
        new PropertyMetadata(false));

public new bool IsPressed
{
    get { return (bool)GetValue(IsPressedProperty); }
    set { SetValue(IsPressedProperty, value); }
}

protected override void OnIsPressedChanged(System.Windows.DependencyPropertyChangedEventArgs e)
{
    /* Call the base class OnIsPressedChanged() method so IsPressedChanged event subscribers are notified. */
    base.OnIsPressedChanged(e);

    /* Forward the value of the base.IsPressed property to the custom IsPressed property  */
    IsPressed = (bool)e.NewValue;

    /* Raise event */
    RaiseCustomRoutedEvent(new RoutedEventArgs(routedEvent: CustomIsPressedChangedEvent));
}

One can now bind the command parameter with the new value beeing forwarded:

<local:CustomButton xmlns:i="http://schemas.microsoft.com/xaml/behaviors" x:Name="MyButton">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="CustomIsPressedChanged">
            <i:InvokeCommandAction Command="{Binding Path=SomeCommand}"
                                   CommandParameter="{Binding ElementName=MyButton, Path=IsPressed}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</local:CustomButton>

Felix
  • 1,066
  • 1
  • 5
  • 22
  • This is not a good design. And its terrible to use. You can only use this button after reading its documentation to learn that it completely bypasses the original API (for example the Command property or Click event). You would always have to configure the CustomButton before every use with an interaction trigger, which only works with a very specific event. From a class design perspective, you have not extended the Button but implemented a completely new bypass behavior in parallel that makes the *complete!* original Button API useless. – BionicCode May 17 '22 at 09:02
  • You should keep the original API for the sake of usability. Then change the behavior by overriding the corresponding virtual members. This way, the Button can be used as usual. Like my example shows, you can even make the new behavior optional and still allow the use of the Click event (which is also raised twice). – BionicCode May 17 '22 at 09:02
  • @BionicCode thak you very much for your assessment. I have not yet thought about what happens when the CustomButton is to be reused somewhere else. Would this problem partly be solved if I renamed the new `IsPressed` dependency property to something like `CustomIsPressed` to not interfere with the original proerty? – Felix May 17 '22 at 09:09
  • @BionicCode also, how does this design break the original Command property and Click event (apart form messing around with the IsPressed property)? It appears to still be working for me. – Felix May 17 '22 at 09:46
  • Not really. The point is if I had to use your button, I would use it wrong. The feature is a nice one. But not well designed. If I get a `CustomButton`, which is in fact a `Button`, I would bind a command to the `Command` property or assign an event handler to the `Click` event. Just to realize that the `CustomButton` does not behave as advertised and won't execute the command or event handler twice. Even if I'm the author myself, I won't remember this twist in a few month. – BionicCode May 17 '22 at 09:48
  • Now, when maintaining old code I could struggle to understand why I used the complicated interaction trigger instead of the `Command` property. It's a bad designed API (bypass original API and in addition the requirement of interaction triggers) and a bad designed class (instead of overriding behavior it implements features that require to bypass the original behavior and API). Renaming a property does not change class and API design. If you manage to setup the interaction trigger internally (via C#) you can at least eliminate this inconvenient configuration requirement. – BionicCode May 17 '22 at 09:48
  • It's broken as `CustomButton` is a `Button` (inheritance). But the original API like the `Command` or `Click` have no functionality in the derived `CustomButton`. The user of `CustomButton` is forced to use a different API to execute a command. But since `CustomButton` is a `Button`, executing a command must still take place via the `Command`property. Only the behavior has changed e.g. the way or the moment this `Command` is invoked. Currently, casting the `CustomButton` to `Button` will break the control as the `Button.Command` property is not used. – BionicCode May 17 '22 at 09:52
  • That's why classes that are meant to be extended have virtual members. Those virtual members mark entry points or key operations that make the behavior of the class. You override this virtual members to change or extend the original base behavior. `Button` exposes such virtual members. You should always use and override them first, before thinking about introducing a new API that changes the usage of the extended class. – BionicCode May 17 '22 at 09:56
  • @BionicCode thank you very much for your feedback and the thorough explanation. It is very much apreciated! – Felix May 17 '22 at 10:51