Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I re-propagate an event after I initially set the handled property to true in an async handler?

I am trying to create a touch and hold event handler with a variable delay in a WPF application by calling a bool task which runs a timer. If the timer elapses, the task returns true. If another event such as touch leave or touch up occurs, the task immediately returns false. Below is my event handler code:

private static async void Element_PreviewTouchDown(object sender, TouchEventArgs e)
{
    // Set handled to true to avoid clicks
    e.Handled = true;

    var isTouchHold = await TouchHold((FrameworkElement)sender, variableTimespan);
    if (isTouchHold)
        TouchHoldCmd?.Execute(someParam);
    else
    {
        // Here is where I would like to re initiate bubbling up of the event.
        // This doesn't work:
        e.Handled = false;
    }
}

The reason I want it to propagate the event is because, for example, if the user wants to pan the scrollviewer that the element is part of and the panning gesture is started by touching my element, my touchhold works as intended in that the touch and hold command won't get triggered but neither will the scrollviewer start panning.

I tried raising the event manually but this also doesn't seem to work:

bool firedBySelf;
private static async void Element_PreviewTouchDown(object sender, TouchEventArgs e)
{
    if(firedBySelf) 
    {
        firedBySelf = false;   
        return;
    }

    ...
    else
    {
        firedBySelf = true;
        e.Handled = false;
        ((FrameworkElement)sender).RaiseEvent(e);
    }
}

How can I achieve my goal?

Edit: Here is the class containing the task:

public static class TouchHoldHelper
{
    private static DispatcherTimer _timer;
    private static TaskCompletionSource<bool> _task;
    private static FrameworkElement _element;

    private static void MouseUpCancel(object sender, MouseButtonEventArgs e) => CancelHold();
    private static void MouseLeaveCancel(object sender, System.Windows.Input.MouseEventArgs e) => CancelHold();
    private static void TouchCancel(object sender, TouchEventArgs e) => CancelHold();

    private static void AddCancellingHandlers()
    {
        if (_element == null) return;

        _element.PreviewMouseUp += MouseUpCancel;
        _element.MouseUp += MouseUpCancel;
        _element.MouseLeave += MouseLeaveCancel;

        _element.PreviewTouchUp += TouchCancel;
        _element.TouchUp += TouchCancel;
        _element.TouchLeave += TouchCancel;
    }

    private static void RemoveCancellingHandlers()
    {
        if (_element == null) return;

        _element.PreviewMouseUp -= MouseUpCancel;
        _element.MouseUp -= MouseUpCancel;
        _element.MouseLeave -= MouseLeaveCancel;

        _element.PreviewTouchUp -= TouchCancel;
        _element.TouchUp -= TouchCancel;
        _element.TouchLeave -= TouchCancel;
    }

    private static void CancelHold()
    {
        if (_timer != null)
        {
            _timer.Stop();
            _timer.Tick -= _timer_Tick;
            _timer = null;
        }
        if (_task?.Task.Status != TaskStatus.RanToCompletion)
            _task?.TrySetResult(false);

        RemoveCancellingHandlers();
    }

    private static void _timer_Tick(object sender, EventArgs e)
    {
        var timer = sender as DispatcherTimer;
        timer.Stop();
        timer.Tick -= _timer_Tick;
        timer = null;
        _task.TrySetResult(true);
        RemoveCancellingHandlers();
    }

    public static Task<bool> TouchHold(this FrameworkElement element, TimeSpan duration)
    {
        _element = element;

        _timer = new DispatcherTimer();
        _timer.Interval = duration;
        _timer.Tick += _timer_Tick;

        _task = new TaskCompletionSource<bool>();

        AddCancellingHandlers();

        _timer.Start();
        return _task.Task;
    }
}

Edit: to better explain my intended behavior, consider how icons on a smartphone's screen work. If I tap the icon, it starts the app the icon represents. If I touch and move on an icon, it pans the screen. If I touch and hold the icon, it allows me to move the icon so I can place it somewhere else without panning the screen. If I touch and hold the icon but I don't hold it long enough to trigger the moving of the icon, it acts as if I tapped it, starting the app. I am trying to replicate these last 2 behaviors.

I am not saying my current implementation is the right approach but it's what I was able to come up with. If there is any alternative approach, I would be glad to explore it.

like image 389
user1969903 Avatar asked Jul 30 '19 11:07

user1969903


1 Answers

Your workflow of setting e.Handled to true and then wanting to set it back to false again strikes me as odd.

From When to Mark Events as Handled

Another way to consider the "handled" issue is that you should generally mark a routed event handled if your code responded to the routed event in a significant and relatively complete way.

Seems like either you'd be using the wrong event or it's as if the folks at Microsoft had gotten it wrong ;)

// Set handled to true to avoid clicks

Nope, they even thought of that, ref Remarks.

You can set Stylus.IsPressAndHoldEnabled="False" to disable the 'click behavior'. Allowing you to fall back to the default WPF pattern of handling the event or letting it tunnel (in this case) forward.

private static async void Element_PreviewTouchDown(object sender, TouchEventArgs e)
{
    var isTouchHold = await TouchHold((FrameworkElement)sender, variableTimespan);
    if (isTouchHold)
    {
        TouchHoldCmd?.Execute(someParam);
        e.Handled = true;
    }
}

However, as you so aptly point out in the comments:

The issue is that the event handler (Element_PreviewTouchDown) is finished executing before the task is. By the time the task is finished, it doesn't make any difference if I change the e.Handled value.

Given the ship has already sailed and you don't want to interfere with the normal functioning of UI elements, we can remove the line that marks the event as handled all together.

private static async void Element_PreviewTouchDown(object sender, TouchEventArgs e)
{
    var isTouchHold = await TouchHold((FrameworkElement)sender, variableTimespan);
    if (isTouchHold)
    {
        TouchHoldCmd?.Execute(someParam);
    }
}
like image 90
Funk Avatar answered Oct 03 '22 04:10

Funk