8

I have a control that allows the user to perform some heavy duty image processing on a specific part of an image and they have arrow buttons to move this area around the image.

as the process is very heavy duty (avg 800ms per run) I have used a repeat button which turns this are into a "Ghost" and only executes the process upon the mouse up event.

This works really well and solves most performance issues relating to this function

HOWEVER

A certain group of users are refusing to learn this method of holding and releasing and persist in tapping the button to move it rather than holding and releasing.

This means that the heavy duty method is being called every time the they tap and as it only moves a small increment each time the method fires, so they end up with a application hang whilst it tries to do > 100 of these 800ms + processes

MY QUESTION

How can I handle this tapping behaviour in the same way as holding and releasing?

I thought about a timer but cant work out how I would detect the difference between a normal tap and the last tap.

ΩmegaMan
  • 29,542
  • 12
  • 100
  • 122
Steven Wood
  • 2,675
  • 3
  • 26
  • 51
  • 7
    You could just have a `bool` flag that you set to true when the process is running and false when it completes. If the flag is true, have the button event return early without starting the process. – Abion47 Jan 31 '17 at 12:03
  • 1
    You can use timer to wait until there are no consecutive taps for a while to start heavy job. For series of taps timer will be restarted and event handler (containing heavy job) will not run. – Sinatr Jan 31 '17 at 12:16
  • Abion47 advised you do to it with a flag . Did you try it ? – Drag and Drop Feb 06 '17 at 15:58
  • You mentioned that your application hangs during this operation. Do you have specific reasons for doing that work on the UI thread? – Pieter Witvoet Feb 06 '17 at 16:05

6 Answers6

3

Quick, dirty solution: use a Timer.

Each time the user taps the button, stop the timer, increase the number of taps, start the timer. If the timer elapses before the user taps again, then it should do your big work method.

Is this prone to threading issues? Probably. I'm not a threading expert. I would love if a threading expert can come comment on it.

Is this the best solution? Hardly. But it will get you by for a while (until the threading issues come up).

private int _totalTaps = 0;
private const int _tapSequenceThreshold = 250; // Milliseconds
private Timer _tapTimer = new Timer(_tapSequenceThreshold);

private void InitializeTimer()
{
    _tapTimer.Elapsed += OnTapTimerElapsed;
}

private void OnTapTimerElapsed(object source, System.Timers.ElapsedEventArgs e)
{
    _tapTimer.Stop();

    // The `DoBigLogic` method should take the number of taps and
    // then do *something* based on that number, calculate how far
    // to move it, for example.
    DoBigLogic(_totalTaps);

    _totalTaps = 0;
}

// Call this each time the user taps the button
private void Tap()
{
    _tapTimer.Stop();
    _totalTaps++;
    _tapTimer.Start();
}

Best solution: this method plus moving this work off the GUI thread. Then you don't have to worry about taps or click-and-hold, you won't block the GUI thread.

If you have to do work that doesn't update the UI (redraw the image, for example) then send the image to the UI, you can make a new thread, then you'll hit an error about 'accessing a UI element from a non-UI thread', just drop the UI code in a Marshal for it.

await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(
    Windows.UI.Core.CoreDispatcherPriority.Normal,
    () => { UpdateDisplay(); }
);
Der Kommissar
  • 5,848
  • 1
  • 29
  • 43
2

Consider monitoring mouse activity and start your heavy duty process after a short period of inactivity.

Consider running the process on a separate thread - this might mean cloning (part of) the image in memory.

Consider preventing the process from being ran multiple times concurrently (if that is possible ie. the process is async).

Leo
  • 5,013
  • 1
  • 28
  • 65
  • 1
    Generally, you can make the 'short period' 250-500ms, somewhere in there usually works. People can generally hit a button 4-6 times per second with ease. – Der Kommissar Feb 06 '17 at 19:15
2

Consider one of these options:

Disable the button until the process completes, causing that 800ms delay. Users will soon learn to use the hold down method. This would involve the smallest amount of code and put the onus on humans. It also ensures you are not holding up the app with clicks in the buffer or over-using resources.

Put a timer in your button click event: 'Ghost area' Timer Start ( or reset to zero)

Then the code to call your main work is in the timer elapsed event which will be set to whatever pause you wish. (ie If a user has not clicked again within a second or so) Then stop the timer Execute code

Joe C
  • 3,925
  • 2
  • 11
  • 31
2

You could try using reactive extensions which is available on nuget.

using System;
using System.Windows.Controls;
using System.Windows.Input;
using System.Linq;
using System.Reactive.Linq;

namespace SmoothOutButtonTapping
{
    public static class Filters
    {
        //  First we need to combine the pressed events and the released
        //  events into a single unfiltered stream.
        private static IObservable<MouseButtonState> GetUnfilteredStream(Button button)
        {
            var pressedUnfiltered = Observable.FromEventPattern<MouseButtonEventHandler, MouseButtonEventArgs>(
                x => button.PreviewMouseLeftButtonDown += x,
                x => button.PreviewMouseLeftButtonDown -= x);
            var releasedUnfiltered = Observable.FromEventPattern<MouseButtonEventHandler, MouseButtonEventArgs>(
                x => button.PreviewMouseLeftButtonUp += x,
                x => button.PreviewMouseLeftButtonUp -= x);

            return pressedUnfiltered
                .Merge(releasedUnfiltered)
                .Select(x => x.EventArgs.ButtonState);
        }

        //  Now we need to apply some filters to the stream of events.
        public static IObservable<MouseButtonState> FilterMouseStream(
            Button button, TimeSpan slidingTimeoutWindow)
        {
            var unfiltered = GetUnfilteredStream(button);

            //  Ironically, we have to separate the pressed and released events, even
            //  though we just combined them. 
            //  This is because we need to apply a filter to throttle the released events,
            //  but we don't need to apply any filters to the pressed events.
            var released = unfiltered

                //  Here we throttle the events so that we don't get a released event
                //  unless the button has been released for a bit.
                .Throttle(slidingTimeoutWindow)
                .Where(x => x == MouseButtonState.Released);

            var pressed = unfiltered
                .Where(x => x == MouseButtonState.Pressed);

            //  Now we combine the throttled stream of released events with the unmodified
            //  stream of pressed events.
            return released.Merge(pressed);
        }
    }
}

Now we have a stream that will respond immediately whenever a user presses, but will not fire a released event unless the button is released for long enough.

Here is an example of how you could consume the above method. This example simply changes the color of the control while the button is in the Pressed state, but you could easily do whatever you wanted.

using System;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Reactive.Linq;
using System.Threading;

namespace SmoothOutButtonTapping
{
    public partial class SmoothTapButtonControl : UserControl
    {
        public SmoothTapButtonControl()
        {
            InitializeComponent();

            _pressed = new SolidColorBrush(Colors.Lime);
            _released = Background;

            //  Don't forget to call ObserveOn() to ensure your UI controls
            //  only get accessed from the UI thread.
            Filters.FilterMouseStream(button, SlidingTimeoutWindow)
                .ObserveOn(SynchronizationContext.Current)
                .Subscribe(HandleClicked);
        }

        //  This property indicates how long the button must wait before 
        //  responding to being released.
        //  If the button is pressed again before this timeout window 
        //  expires, it resets.
        //  This is handled for us automatically by Reactive Extensions.
        public TimeSpan SlidingTimeoutWindow { get; set; } = TimeSpan.FromSeconds(.4);

        private void HandleClicked(MouseButtonState state)
        {
            if (state == MouseButtonState.Pressed)
                Background = _pressed;
            else
                Background = _released;
        }

        private Brush _pressed;
        private Brush _released;
    }
}

You can find the complete version of the above examples (project files and xaml included) on my github.

Newell Clark
  • 345
  • 2
  • 13
2

You can prevent program from executing your 800 ms function by setting simple flags(Preferably bool type).

You need to create three events. One Mouse Down, one Mouse Up event and other is Mouse Move event of the button. Set your flag false at declaration. When you click the button, make your flag true in Mouse Down event. And when you lift your mouse click i.e., Mouse Up event make your flag false.

Simple illustration of code.

bool click = false,run_process = true;

mouseDown_event()
{
    click = true;
}

mouseUp_event()
{
    click = false;
}

mouseMove_event()
{
    if(click == true && run_process == true)
    {
         click = false;
         run_process = false;
         //call your function
    }
}
Raj Kumar Mishra
  • 516
  • 4
  • 16
1

How can I handle this tapping behaviour in the same way as holding and releasing?

Create a formula which calculates a weight value based last tap frequency, current operation and time between last actual operation; with any other factors I may not be aware of. With the right formula it should be representational to the person who uses the system correctly verses someone who sends multiple clicks.

The weighted value should be passed to an alternate thread which is handling the actual operation to the screen and can handle a blizzard of taps or a single tap without missing a beat, per se.

ΩmegaMan
  • 29,542
  • 12
  • 100
  • 122