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.

13
  • 2
    I would avoid Interaction.Triggers completely. Instead I would bind IsPressed to a view model property using this solution, and invoke required command method in property setter Commented May 13, 2022 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. Commented May 13, 2022 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. Commented May 13, 2022 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? Commented May 13, 2022 at 21:10
  • 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. Commented May 18, 2022 at 6:01

3 Answers 3

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.

Sign up to request clarification or add additional context in comments.

4 Comments

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, you can use a converter (attribute EventArgsConverter) to extract the new value from the RoutedEventArgs. But at this point an attached property is cleaner.
@Felix. I added an attached property example here
thank you for that complete examle. I dont jet fully understand every tiny bit, but thats fine for now.
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);
    }
  }
}

Comments

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>

9 Comments

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.
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 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?
@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.
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.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.