Every developer who works with UI handles a lot of user input. And it’s not a rare situation that we don’t need to handle every incoming event because there might be just too many of them.
Consider this example, a user fills an email field on “Sign up” form in some application which works with a REST API. We want to validate entered email before a user actually will click “Submit” button. To do it, we make a call to hypothetical “/email/validate” REST endpoint and display validation status on the UI, which enables a user to adjust his input until he finishes working with the form.
Some people type really fast, so we don’t want to make a REST call for every entered character. Lots of APIs have request limits and in order not to exceed them it’s necessary to “throttle” events emitted by fast user typing.
When someone faces this issue for the first time, probably the first idea what comes to mind is to use a timer. When an event happens, we handle it and start a timer. When the next event happens, we don’t handle it until a timer completes waiting for a given interval. Besides the fact that this approach actually works, it’s usually not a very good design solution to mix event handling logic and timer manipulations, so let’s come up with a better approach.
Frontend developers often refer to an operation of limiting user input as “debouncing”. Let’s break it down. Have a look at the interactive demonstration below:
Every character typed to the input field emits a raw event, which is represented by the top bar. When a user types a character, we display this raw event by adding a colored tile into the bar. Note that when you type fast, basically every cell of the top bar gets filled, and our goal is to limit this excessive input by debouncing. The bottom bar demonstrates exactly that, no cell gets filled more often than once in 500 milliseconds. No matter how fast you type, no more than one cell from five subsequent cells will get filled and that’s exactly what we need.
Also, note that no user input gets ignored. Even if we don’t react to some raw input right away, we will eventually emit a debounced event as soon as delay interval passes.
Let’s implement this with Delphi. Basic Delphi event type is TNotifyEvent – a method with one parameter of TObject class. The most convenient solution would be to get a raw TNotifyEvent and return a debounced event as another TNotifyEvent which can be directly assigned to UI event, such as button click. We don’t want to pollute our form with excessive event handlers so let’s create a “wrapper” class exposing a method with the same definition as usual event handler which will effectively allow us to assign this method a TNotifyEvent handler.
Consider this method. This is where debouncing happens:
procedure TDebouncedEvent.DebouncedEvent(Sender: TObject); var Between: int64; begin Between := MilliSecondsBetween(Now, self.FLastcallTimestamp); // if timer is not enabled, it means that last call happened // earlier than <self.FInteval> milliseconds ago if Between >= self.FInterval then begin self.DoCallEvent(Sender); end else begin // adjusting timer, so interval between calls will never be more than <FInterval> ms self.FTimer.Interval := self.FInterval - Between; // reset the timer self.FTimer.Enabled := false; self.FTimer.Enabled := true; // remember last Sender argument value to use it in a delayed call self.FSender := Sender; end; end;
As you see, we call a method only if an interval of FInterval milliseconds passed. To count a delay we also use a timer, but this time all timer manipulations are encapsulated within a class and not mixed with any event handling logic. Note that DebouncedMethod definition looks exactly like a usual TNotifyEvent handler.
To make it easy to use, let’s implement a convenience method which will instantiate wrapper object and return a debounced event:
class function TDebouncedEvent.Wrap(ASourceEvent: TNotifyEvent; AInterval: integer; AOwner: TComponent): TNotifyEvent; begin Result := TDebouncedEvent.Create(AOwner, ASourceEvent, AInterval).DebouncedEvent; end;
Here is an example usage:
self.FEditEmail.OnChange := TDebouncedEvent.Wrap(self.DoOnEmailEditChange, 200, self);
And that’s it, we wrapped our event handler with actual logic into a debounced event handler and assigned into the TNotifyEvent in one line of code. A user may type as fast as he can, but DoOnEmailEditChange method won’t be executed more frequently than once in 200 milliseconds.
As I drawback of demonstrated class, I would like to note that it is compatible only with TNotifyEvents. Since every method has its own definition and we need to expose an appropriate method handler with this exact definition, there is no easy way to generalize TDebouncedEvent class to be compatible with generic events of arbitrary definitions.
The full source code for TDebouncedEvent and a sample project is available here.