Messenger

Every message box the R package can walk into is shown from here. While the messenger is armed it shows nothing: it writes the text down and answers the caller with the answer that caller named. While it is disarmed it calls MsgBox with exactly what it was given and answers the click. A person opening the workbook and pressing buttons sees the boxes they have always seen.

THE RECORD LIVES ON THE DEFAULT INSTANCE

Callers write Messenger.Show and this class has no Create. One Create per call site would give each caller its own instance and its own record, and Messages would come back empty at the end of the run. One instance, one record.

ARM AND DISARM COME IN PAIRS

A wrapper arms the messenger as its first act and disarms it in its exit path, in the same Excel session. Nothing is written to the file, so a run that dies part way cannot leave a delivered workbook mute.

Arm starts a run: it empties the record. Disarm ends the silence and keeps the record, because the summary a wrapper answers is read after the run is over.

THE FORCE STATE AND CarryOn

Some questions ask whether to push past a warning. A call site writes Messenger.CarryOn() as its silent answer to one of those: vbYes when the run was armed with force, vbNo otherwise. Force is the only thing CarryOn reads, and Disarm drops it.

THE STORED SWITCH COVERS THE OPEN PATH

A box that fires from Workbook_Open fires before any Application.Run can reach a wrapper, so nothing in process can arm the messenger in time. OBT__SILENT_OPERATIONS is read off the workbook itself, and the open path arms on what it answers:

If Messenger.ReadStoredSwitch(ThisWorkbook) Then Messenger.Arm ThisWorkbook

The read goes through HiddenNames.QuickValue, three COM crossings with no instance behind them. HiddenNames.Create walks the whole Names collection of the host, which is the largest cost that class has, and the open path reads one value once.

A missing name reads as No, so a workbook built before the switch existed behaves exactly as it did.

WHY A CLASS

Six classes show a box today, and a class calling a standard module is out of scope (conventions.md section 9). A class serves the modules and the classes both.

Depends on: ProjectError, BetterArray, HiddenNames, The switch is stored at workbook level as a string holding Yes or No. The, name is settled on both sides of the wire and this is where the VBA side, keeps it: the three workbooks seed the flag through Messenger.SwitchName., What separates a title from its message on a recorded line.

Version: 1.0 (2026-08-31)

Lifecycle

Reset #

reset

Put the messenger back to its opening state

Signature:

Public Sub Reset()

Empties the record, ends the silence, drops the force state and forgets the host. Arm calls this, so a new run never reads the messages of the run before.


Arming

Arm #

arm

Turn the boxes off for the length of this call

Signature:

Public Sub Arm(ByVal hostBook As Workbook, Optional ByVal force As Boolean = False)

Reads nothing from the file. The wrapper decides, and the wrapper disarms in its exit path. The record of any run before is emptied here.

Parameters:

  • hostBook: Workbook. The workbook this run is about.
  • force: Boolean. Optional, default False. True makes CarryOn answer vbYes, which is what pushes a run past a warning that would otherwise stop it.

Disarm #

disarm

Turn the boxes back on

Signature:

Public Sub Disarm()

The record is kept. A wrapper disarms and then reads Messages for the summary it answers.


Armed #

armed

Whether the boxes are off right now

Signature:

Public Property Get Armed() As Boolean

Returns: Boolean. True while the messenger is armed.


HostBook #

host-book

The workbook the current run is about

Signature:

Public Property Get HostBook() As Workbook

Returns: Workbook. What Arm was given, or Nothing.


CarryOn #

carry-on

The silent answer to a question that asks whether to push on

Signature:

Public Function CarryOn() As VbMsgBoxResult

Written at a call site as the silent answer of a warning the run may pass through: Messenger.Show(text, Messenger.CarryOn(), vbYesNo). A run armed with force carries on. Every other run stops, which is the safe reading of a warning nobody is there to read.

Returns: VbMsgBoxResult. vbYes when the run was forced, vbNo otherwise.


The stored switch

SwitchName #

switch-name

The name the switch is stored under

Signature:

Public Property Get SwitchName() As String

The three workbooks seed their flag through this, so the string lives in one place on the VBA side.

Returns: String. OBT__SILENT_OPERATIONS


SwitchOnValue #

switch-on-value

The stored value that means silent

Signature:

Public Property Get SwitchOnValue() As String

Returns: String. Yes


SwitchOffValue #

switch-off-value

The stored value that means the workbook behaves as it always has

Signature:

Public Property Get SwitchOffValue() As String

Returns: String. No


ReadStoredSwitch #

read-stored-switch

Read the silence switch off a workbook

Signature:

Public Function ReadStoredSwitch(ByVal hostBook As Workbook) As Boolean

The read costs three COM crossings through HiddenNames.QuickValue. A workbook that holds no such name answers False, so every workbook built before the switch existed behaves as it did.

This answers the value and changes nothing. The open path arms on it:

If Messenger.ReadStoredSwitch(ThisWorkbook) Then Messenger.Arm ThisWorkbook

Parameters:

  • hostBook: Workbook. The workbook holding the name.

Returns: Boolean. True when the stored value reads Yes.


Showing

Show #

show

Show one box, or write it down and answer for it

Signature:

Public Function Show(ByVal message As String, _
                     ByVal silentAnswer As Long, _
                     Optional ByVal boxStyle As Long = vbOKOnly, _
                     Optional ByVal title As String = vbNullString) As VbMsgBoxResult

Armed: nothing is shown, the text goes into the record and silentAnswer comes back. Disarmed: MsgBox is called with what this was given and the click comes back unchanged.

silentAnswer is the second parameter and it carries no default. A box that guards a destructive step and one that merely asks "carry on" want different answers, and only the call site knows which. VBA needs every optional parameter last, so boxStyle and title follow it, and a call site that forgets the answer fails to compile.

Parameters:

  • message: String. The text of the box.
  • silentAnswer: Long. What this answers while armed. vbOK for a box with one button, and for a question the answer that call site wants with nobody there. Messenger.CarryOn() for a warning a forced run may pass through.
  • boxStyle: Long. Optional, default vbOKOnly. The MsgBox style.
  • title: String. Optional. The box title.

Returns: VbMsgBoxResult. The click while disarmed, silentAnswer while armed.


The record

Messages #

messages

Everything the run swallowed, one line each

Signature:

Public Function Messages() As String

Returns: String. The recorded lines joined by a line break. Empty when nothing was swallowed.


HasMessages #

has-messages

Whether the run swallowed anything

Signature:

Public Function HasMessages() As Boolean

Returns: Boolean. True when the record holds at least one line.


Internal members (not exported)

Lifecycle

Class_Initialize #

class-initialize

Start with an empty record and the boxes on

Signature:

Private Sub Class_Initialize()

Private helpers

RecordMessage #

record-message

Write one swallowed box into the record

Signature:

Private Sub RecordMessage(ByVal message As String, ByVal title As String)

Parameters:


SingleLine #

single-line

Flatten a box text onto one line

Signature:

Private Function SingleLine(ByVal boxText As String) As String

A box text often carries line breaks. The record answers one line per message, so a reader can count what a run swallowed, and every break becomes a space.

Parameters:

Returns: String. The same text with no line break in it.


ThrowError #

throw-error

Raise a ProjectError-based exception

Signature:

Private Sub ThrowError(ByVal errNumber As Long, ByVal message As String)

Parameters:


Used in (19 file(s))