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

A search box can produce a change event for every character. Refreshing a grid immediately after each one makes the application repeat work that the next keystroke will invalidate. Adding a timer helps only when the timing rule makes clear whether we are waiting for silence, delivering periodically, or allowing an immediate first update.

Sphere 10 Pascal Framework's TThrottledEvent makes those policies explicit. In this example, three input notifications arrive before the quiet interval finishes. One trailing callback runs after the burst. The test uses a real LCL message loop, counts deliveries and fails if waiting never ends or more than one callback appears.

A burst of interface activity gathered into one useful refresh.
A burst of interface activity gathered into one useful refresh.

Contents

Choose the Delivery Rule

temNotifyOnEventBurstFinished delivers after one full interval without another Notify. Each new input restarts that quiet period. temNotifyOnEventBurstStartAndFinished adds eligible leading delivery to the same burst lifecycle. temNotifyEveryInterval is a periodic policy rather than a continually postponed quiet deadline.

Select the rule from the product behavior you want. A search query usually benefits from waiting until typing pauses. A continuous status display may need periodic updates. A user-facing preview can justify an immediate leading response plus a later final response after the changes settle.

Keep the Timer with Its Owner

Create the event as an owned component where possible, set its interval and register a callback whose target outlives that component. Call Notify from actual UI input handlers. Polling a worker must not repeatedly call Notify unless a new input event actually happened.

The sample keeps the receiver alive outside the event's try/finally. Its small verification loop pumps UI messages until Waiting becomes false, with a finite failure deadline. A normal application uses its existing message loop rather than adding this test loop to every edit handler.


program ArticleSample;
{$IFDEF FPC}{$mode Delphi}{$H+}{$ENDIF}
uses
  {$IFDEF FPC}Interfaces, Forms,{$ELSE}Vcl.Forms,{$ENDIF}
  Classes, SysUtils, UCommon, UCommon.UI;
type
  TReceiver = class
  public
    Deliveries: Integer;
    procedure Refresh(Sender: TObject);
  end;

procedure TReceiver.Refresh(Sender: TObject);
begin
  Inc(Deliveries);
end;

var Receiver: TReceiver; RefreshEvent: TThrottledEvent; Deadline: QWord;
begin
  Application.Initialize;
  Receiver := TReceiver.Create;
  try
    RefreshEvent := TThrottledEvent.Create(nil);
    try
      RefreshEvent.Interval := TTimeSpan.FromMilliseconds(100);
      RefreshEvent.Mode := temNotifyOnEventBurstFinished;
      RefreshEvent.Add(Receiver.Refresh);
      RefreshEvent.Notify;
      RefreshEvent.Notify;
      RefreshEvent.Notify;
      if not RefreshEvent.Waiting then raise Exception.Create('Quiet period not started');
      Deadline := GetTickCount64 + 2000;
      while RefreshEvent.Waiting and (GetTickCount64 < Deadline) do begin
        Application.ProcessMessages;
        Sleep(1);
      end;
      if RefreshEvent.Waiting or (Receiver.Deliveries <> 1) then
        raise Exception.Create('Expected exactly one trailing refresh');
      WriteLn('Inputs=3; deliveries=1; waiting=False');
      RefreshEvent.Cancel;
    finally
      RefreshEvent.Free;
    end;
  finally
    Receiver.Free;
  end;
end.

Check the Coalesced Delivery


Inputs=3; deliveries=1; waiting=False

The three notifications result in one callback, and the waiting state ends. Operating-system scheduling and message processing affect when the callback actually executes after its deadline becomes eligible.

The Framework's interactive Refresh throttle demonstration is useful for seeing the distinction. Repeated Notify clicks reset the quiet countdown. Its counters separately show inputs, delivered refreshes and provider fetches, making it clear that a notification, a callback and a completed page are different events.

Let Busy Presentation Follow the Right State

Waiting describes the input burst's quiet-period lifecycle. OnWaitingChanged can drive a busy indicator when that is what the product needs to communicate. A callback's background fetch can continue after quiet waiting ends, so the fetch may need its own loading presentation.

Keep the two states independent. A slow network request should not reset the input quiet clock, and a finished input burst does not prove the network request completed. Combining them into one undocumented Boolean tends to create spinners that vanish too early or never finish.

ℹ️ An active LCL/VCL message loop is required. Creating a timer-backed component in a console process that never processes its UI messages does not establish that the callback will run.

Cancel and Reconfigure Intentionally

Cancel clears pending delivery and waiting without fabricating a successful quiet finish. Changing the mode also cancels its previous pending policy. Use the separate postponement API when delivery needs to be deferred, keeping that policy out of unrelated worker polling.

Remove listeners or destroy the owned event before freeing callback targets. For an asynchronous consumer, coordinate its worker lifetime independently. Coalescing prevents redundant admission, while provider disposal still depends on worker retirement.

Use the component source and timing walkthrough to explore the other policies. Together with the grid's refresh gate, this gives a desktop application a precise vocabulary for input bursts, admitted work and visible completion instead of an accumulation of ad hoc timers.

Get the Framework and Run the Example

Get Sphere 10 Pascal Framework and follow its package instructions . Use the units directly, or build the optional Lazarus .lpk or Delphi runtime package. Pascal uses these source/package workflows. The C# Framework is distributed separately through NuGet.

The attached application sample was compiled and executed with Free Pascal 3.2.0 on Windows against this public source revision , with range and overflow checks. Its Build.ps1 accepts the Framework, compiler, Lazarus and output paths so the example does not need machine-specific paths. Lazarus UI examples require the LCL Win32 widgetset. A successful FPC run does not claim a new Delphi build. Follow the repository's separate Delphi/VCL project and resource instructions there.

Download the complete runnable sample.

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

Continue the Series

How to Keep Old Background Results Out of a New View

How to Build a Grid That Adapts to Its Window

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