How to Turn a Burst of UI Events into One Useful Refresh

User interface events often arrive in bursts. A search box changes on every keystroke, a window fires a stream of resize events, and a data feed can update many times a second. Running an expensive refresh for each event wastes work, because only the state at the end of the burst matters.

TThrottledEvent in Sphere 10 Pascal Framework is a component that sits between the event source and the handler and decides when the handler actually runs. This article covers its delivery modes, how to wire it up, how to observe and cancel pending delivery, and the lifetime rules for its listeners.

A burst of interface activity gathered into one useful refresh.
A burst of interface activity gathered into one useful refresh.
💡 In short. Coalesce bursts of UI notifications into one handler call with TThrottledEvent, choose a delivery mode, and observe or cancel pending delivery.

1. Let TThrottledEvent own the delivery policy

Setting up the component takes four statements. The sample creates it without an owner, sets a 100 millisecond interval, selects the trailing mode, which delivers once after a burst has gone quiet, and registers one listener.

Pascal
constructor TInputHandler.Create;
begin
  inherited Create;
  FRefresh := TThrottledEvent.Create(nil);
  FRefresh.Interval := TTimeSpan.FromMilliseconds(100);
  FRefresh.Mode := temNotifyOnEventBurstFinished;
  FRefresh.Add(HandleLatest);
end;

Interval is a TTimeSpan and defaults to one second. Add accepts any TNotifyEvent, and several listeners can be registered. Its optional second parameter, ExecuteMainThread, dispatches that listener through the main thread.

The input handler stores whatever state the listener will need and then calls Notify.

Pascal
procedure TInputHandler.SetInput(const AValue: String);
begin
  FInput := AValue;
  FRefresh.Notify;
end;

The listener is an ordinary method that reads the latest state when it finally runs. Notify passes no arguments, so the listener always sees the current value rather than the value at the time of any particular notification.

Pascal
procedure TInputHandler.HandleLatest(Sender: TObject);
begin
  Inc(FDeliveries);
  FHandled := FInput;
end;

Three calls to SetInput within the interval produce one call to HandleLatest, which sees the third value.

Plain text
3 notifications, deliveries=1, handled=abc

Writing this by hand takes a TTimer that is restarted on every input, a Boolean that tracks whether a delivery is pending, and matching code to clear both on cancellation.

2. How TThrottledEvent delivers

The component lives in the UCommon.UI unit. The event source calls Notify as often as it likes. Listeners registered with Add are called according to the Mode and Interval properties, using a timer that the component owns.

The diagram shows the trailing mode. The mode determines which notifications reach the listener.

  • temNone calls the listener on every Notify. This is the default.
  • temNotifyEveryInterval calls it at most once per Interval while notifications keep arriving.
  • temNotifyOnEventBurstFinished calls it once, after Interval has passed with no further Notify.
  • temNotifyOnEventBurstStartAndFinished calls it immediately for the first Notify of a burst, and once more after the burst goes quiet if further notifications arrived.

The trailing mode suits input such as a search box, where intermediate values are discarded. The periodic mode suits a display that must keep updating during continuous input, because new notifications cannot postpone it indefinitely.

3. Observe and cancel pending delivery

In the two burst modes the component exposes the state of the current burst.

Member Purpose
Waiting True from the first Notify of a burst until it is delivered or cancelled.
OnWaitingChanged Raised when Waiting changes, for example to show a busy indicator.
OnBurstStarted, OnBurstFinished Raised at the beginning and at the quiet end of a burst.
Cancel Abandons the pending delivery and clears Waiting without raising OnBurstFinished.
Postpone Re-arms the pending delivery for another quiet interval.
SuppressedInvocations The number of notifications absorbed since the last delivery.
Pascal
procedure TInputHandler.Cancel;
begin
  FRefresh.Cancel;
end;

function TInputHandler.Waiting: Boolean;
begin
  Result := FRefresh.Waiting;
end;

Waiting in the diagram is the state the Waiting property reports, shown here for the trailing mode.

Changing Mode also cancels whatever was pending under the previous mode.

4. Keep pending input separate from running work

â„šī¸ Cancel stops a delivery that has not happened yet. It does not interrupt work that a listener has already started, such as a background request.

Waiting describes the input burst only. If a listener starts asynchronous work, that work needs its own busy state and its own cancellation.

The component is driven by a VCL or LCL TTimer, so it needs a running message loop and delivers on the thread that owns that loop. The interval is a minimum quiet time and not an exact deadline, because the operating system schedules timer messages.

The component does not keep its listeners' targets alive. Free the component, or call Remove, before destroying an object whose method is registered. Giving the component a form as its owner ties its lifetime to that form.

5. Choose a different delivery mode

Switching policy is a one-line change to Mode, and the notifying code and the listeners stay the same. The Framework's interactive timing example lets you try each mode and watch separate counters for notifications and deliveries.

6. Get the Framework and run the example

Get Sphere 10 Pascal Framework and follow its package instructions . The sample uses the UCommon and UCommon.UI units together with the VCL or LCL timer and forms units.

The sample sends three notifications inside one interval, waits for the single delivery, then schedules another and cancels it. It was built and run with Free Pascal 3.2.0 on Windows using the Lazarus Win32 widgetset, against Pascal Framework revision 2cc6558a91714086d02a358714bda01e0c06849d. Its Build.ps1 accepts the Framework, FPC, Lazarus and output paths.


Download the complete runnable sample.

sample.zip
Download the complete worked example and build instructions (ZIP).

For the broader Sphere 10 development toolkit, see the Sphere10 Framework overview .

Author: Herman Schoenfeld