CustomTable
Wraps one Excel ListObject. ColumnExists, CellRange, ValueRange and Value read the table and SetValue writes into it. InsertRowsAt, DeleteRowsAt, AddRows and RemoveRows change its shape, RenameColumn rewrites a header, Sort orders the rows, SetValidation puts a dropdown behind a column, and Clean empties the data. Import and Export move values between this table and another table or a DataSheet.
WHAT THE INSTANCE HOLDS
A column lookup reads the header row once as a block and matches the name in memory for the rest of that call. Nothing about the table shape is held between calls, so an instance that stays alive answers from the worksheet as it stands at each call.
THE IDENTIFIER COLUMN
AddIds fills an identifier column, and IdValue and PrefixValue say which column carries it and what its values start with. A row is then reached by its identifier.
MOVING VALUES IN AND OUT
Import records the table before it writes, and RestoreTableSnapshot puts that state back. Source headers with no match here leave through ImportColumnsNotFound, headers an export was handed and could not write leave through ExportColumnsNotFound, and HasColumnsNotImported answers over the last import. Both routines unhide every column before they write and hide the same columns again after.
Depends on: BetterArray, Checking, HiddenNames, DropdownLists
Version: 1.2 (2026-08-24)
StateManagement
RestoreTableSnapshot #
restore-table-snapshot
Restore the table from a previously saved snapshot
Signature:
Public Sub RestoreTableSnapshot()
Reapplies the cached headers and rows when an import needs to roll back. Resizes the ListObject, writes the backup data, resets state, and re-hides previously hidden columns.
Factory
Create #
create
Create a CustomTable wrapper around a ListObject
Signature:
Public Function Create(ByVal Lo As ListObject, _
Optional ByVal idCol As String = vbNullString, _
Optional ByVal idPrefix As String = vbNullString) As CustomTable
Entry point for creating CustomTable instances.
Wraps the supplied ListObject with convenience methods for column lookup, row manipulation, import/export, and diagnostic logging. An optional ID column enables dictionary-style keyed row access. When adding rows, the ID column is automatically numbered in sequence, optionally prefixed (e.g. "line 1", "line 2").
Parameters:
Lo: ListObject. The Excel table to wrap.idCol: Optional String. Name of the ID column. Defaults to vbNullString.idPrefix: Optional String. Prefix for auto-generated ID values. Defaults to vbNullString.
Returns: CustomTable. A fully initialised CustomTable instance.
Throws:
- ProjectError.ObjectNotInitialized When Lo is Nothing.
Depends on:
- BetterArray
- Checking
Elements
IdValue #
id-value
ID column name
Signature:
Public Property Get IdValue() As String
Returns the name of the column used as a unique row identifier.
Returns: String. The ID column name.
HeaderRange #
header-range
Header row Range of the CustomTable
Signature:
Public Property Get HeaderRange() As Range
Returns the Range covering the header row of the underlying ListObject.
Returns: Range. The header row range.
ColumnExists #
column-exists
Check whether a column exists in the table
Signature:
Public Function ColumnExists(ByVal colName As String, _
Optional ByVal strictSearch As Boolean = False, _
Optional ByVal matchCase As Boolean = False) As Boolean
Delegates to ColumnIndex and returns True when the index is positive.
Parameters:
colName: String. Column name to search.strictSearch: Optional Boolean. When True, uses exact matching. Defaults to False.matchCase: Optional Boolean. When True, matching is case-sensitive. Defaults to False.
Returns: Boolean. True when the column is found.
DataRange #
data-range
Range of a column or the entire data body
Signature:
Public Property Get DataRange(Optional ByVal colName As String = "__all__", _
Optional ByVal includeHeaders As Boolean = False, _
Optional ByVal strictSearch As Boolean = False, _
Optional ByVal matchCase As Boolean = False) As Range
Returns a data Range for the specified column, or the entire data body when colName is "all". When strictSearch is False, the search uses partial matching; the first partial match is returned. Use strictSearch when column names share common text to avoid unexpected output. Returns Nothing when the column has no data rows or is not found. Answering Nothing is part of the contract: four callers read it as "this table does not have that column", so it can never become a raise or an empty range.
Parameters:
colName: Optional String. Column header to retrieve. Defaults to "all".includeHeaders: Optional Boolean. When True, includes the header row. Defaults to False.strictSearch: Optional Boolean. When True, uses exact whole-string matching. Defaults to False.matchCase: Optional Boolean. When True, matching is case-sensitive. Defaults to False.
Returns: Range. The requested data range, or Nothing when unavailable.
Name #
name
Name of the attached ListObject
Signature:
Public Property Get Name() As String
Reads the name from the live ListObject. It is deliberately not kept on the instance: the user can rename a table from the Excel interface at any time, and a name stored at creation would then be wrong for the rest of the object's life.
Returns: String. The table name.
CellRange #
cell-range
A specific cell in the CustomTable
Signature:
Public Property Get CellRange(ByVal colName As String, _
ByVal lineNum As Long) As Range
Returns the cell at the intersection of the given column and worksheet row number. Returns Nothing when the column does not exist or the row is out of bounds.
Parameters:
colName: String. Column header identifying the column.lineNum: Long. Worksheet row number (1-based).
Returns: Range. The target cell, or Nothing when not found.
ValueRange #
value-range
Locate the cell Range by column and key
Signature:
Public Function ValueRange(ByVal colName As String, ByVal keyName As String) As Range
Returns the Range at the intersection of the specified column and the row matching keyName in the ID column. Returns Nothing when the key or column is not found, or when no ID column is configured.
Parameters:
colName: String. Column header identifying the column.keyName: String. Key value identifying the row.
Returns: Range. The target cell range, or Nothing when not found.
Value #
value
Retrieve a cell value by column and key
Signature:
Public Property Get Value(ByVal colName As String, _
ByVal keyName As String) As String
For tables with a key column, returns the cell value at the intersection of the specified column and the row matching keyName. Returns an empty string when the key or column is not found.
Parameters:
colName: String. Column header to read from.keyName: String. Key value identifying the row.
Returns: String. The cell value converted to string, or vbNullString.
Modify
InsertRowsAt #
insert-rows-at
Insert rows at the selected position
Signature:
Public Sub InsertRowsAt(ByVal targetCell As Range, _
Optional ByVal insertShift As Boolean = False, _
Optional ByVal includeIds As Boolean = True)
Inserts rows matching the height of targetCell. Uses worksheet row insertion when insertShift is True to protect stacked tables, otherwise adds ListRows at the selection anchor. Renumbers the ID column when includeIds is True.
Parameters:
targetCell: Range. Selection anchoring the insertion point.insertShift: Optional Boolean. When True, inserts worksheet rows. Defaults to False.includeIds: Optional Boolean. When True, renumbers the ID column. Defaults to True.
DeleteRowsAt #
delete-rows-at
Delete rows intersecting the selection
Signature:
Public Sub DeleteRowsAt(ByVal targetCell As Range, _
Optional ByVal includeIds As Boolean = True, _
Optional ByVal forceShift As Boolean = False)
Removes data rows that intersect targetCell. Uses worksheet row deletion when forceShift or the stored shift tracker is active, otherwise removes ListRows directly. Adds a placeholder row when all rows are deleted. Renumbers the ID column when includeIds is True.
Parameters:
targetCell: Range. Selection identifying which rows to delete.includeIds: Optional Boolean. When True, renumbers the ID column. Defaults to True.forceShift: Optional Boolean. When True, forces worksheet row deletion. Defaults to False.
AddIds #
add-ids
Fill the ID column with sequential values
Signature:
Public Sub AddIds()
Writes sequential numbers (optionally prefixed) to every cell in the ID column. Silently exits when no ID column is configured or when the column does not exist. The ID column is typically locked and not modified by the user directly.
AddRows #
add-rows
Add rows to the CustomTable
Signature:
Public Sub AddRows(Optional ByVal nbRows As Long = 5, _
Optional ByVal insertShift As Boolean = False, _
Optional ByVal includeIds As Boolean = True)
Appends the requested number of rows, either by inserting worksheet rows (to protect stacked tables) or by extending the ListObject range directly. Insertion is useful when multiple ListObjects share a worksheet and overlaps must be avoided. Optionally renumbers the ID column afterwards.
Parameters:
nbRows: Optional Long. Number of rows to add. Defaults to 5.insertShift: Optional Boolean. When True, inserts worksheet rows. Defaults to False.includeIds: Optional Boolean. When True, renumbers the ID column. Defaults to True.
RemoveRows #
remove-rows
Remove empty rows from the CustomTable
Signature:
Public Sub RemoveRows(Optional ByVal totalCount As Long = 0, _
Optional ByVal includeIds As Boolean = True, _
Optional ByVal forceShift As Boolean = False)
Deletes rows whose non-empty cell count falls at or below totalCount. In the linelist, formula cells count as non-empty, so totalCount should match the number of formula columns. When totalCount is left at 0 the class counts the formula columns itself. Optionally renumbers the ID column afterwards.
Parameters:
totalCount: Optional Long. Maximum non-empty cells for a row to be considered empty. Defaults to 0.includeIds: Optional Boolean. When True, renumbers the ID column. Defaults to True.forceShift: Optional Boolean. When True, forces worksheet row deletion. Defaults to False.
SetValidation #
set-validation
Set dropdown validation on a column
Signature:
Public Sub SetValidation(ByVal colName As String, _
ByVal drop As DropdownLists, _
ByVal dropName As String, _
Optional ByVal alertType As String = "info", _
Optional ByVal message As String = vbNullString)
Applies data validation from the supplied DropdownLists to every cell in the specified column. Silently exits when the column does not exist or has no data range.
Parameters:
colName: String. Column header to validate.drop: DropdownLists. Dropdown source providing the validation list.dropName: String. Name of the dropdown list within the source.alertType: Optional String. Alert severity ("info", "warning", "error"). Defaults to "info".message: Optional String. Message shown when validation fails. Defaults to vbNullString.
SetValue #
set-value
Change the value of one specific cell
Signature:
Public Sub SetValue(ByVal colName As String, ByVal keyName As String, ByVal newValue As String)
Writes newValue to the cell identified by column name and key. Silently exits when the key or column is not found. Use with caution as it modifies data inside the ListObject.
Parameters:
colName: String. Column header identifying the cell.keyName: String. Key value identifying the row.newValue: String. The new value to assign.
RenameColumn #
rename-column
Rename a column header within the table
Signature:
Public Sub RenameColumn(ByVal colName As String, _
ByVal newName As String, _
Optional ByVal strictSearch As Boolean = True, _
Optional ByVal matchCase As Boolean = False)
Replaces the header text of the matched column with newName. Raises an error when newName is empty. The strict default here is the opposite of the loose default on ColumnIndex and DataRange, and that is on purpose: it is what stops a caller asking for "Label" from renaming "Formula Label".
Parameters:
colName: String. Current column header to find.newName: String. New header text to assign.strictSearch: Optional Boolean. When True, uses exact matching. Defaults to True.matchCase: Optional Boolean. When True, matching is case-sensitive. Defaults to False.
Throws:
- ProjectError.InvalidArgument When newName is empty.
Clean #
clean
Clear all non-formula, non-key columns
Signature:
Public Sub Clean()
Iterates every column in the table and clears the contents of those that do not contain formulas. The key column is preserved: it holds plain values, and clearing it wiped every ID whenever Clean was called outside Import. Each column is taken from its own position in the loop. Looking the header text back up cost about nine round trips and a worksheet search for a column whose place the loop already knew.
Sort #
sort
Sort the table on one or more columns
Signature:
Public Sub Sort(Optional ByVal colName As String = vbNullString, _
Optional ByVal colList As Object = Nothing, _
Optional ByVal directSort As Boolean = True, _
Optional ByVal strictSearch As Boolean = True)
colList holds the sort keys in DECREASING significance: its first item ends up the primary key, its last item the weakest of the list, and colName is the final tiebreak of them all. That reads backwards until you see how it works. Each pass is a full single-key Range.Sort over the whole table, so the LAST pass applied is the one that decides the order. colName is therefore sorted first and colList is walked from its last item to its first. Because the keys are applied one at a time, the precedence between them rests on Excel leaving rows that tie on a key in the order it found them. Excel behaves that way, but it does not promise to. A caller that cannot live with that should issue one Range.Sort carrying key1/key2/key3 instead, the way LLChoices does.
Parameters:
colName: Optional String. Column header sorted first, so it ends up the weakest key. Defaults to vbNullString.colList: Optional Object. BetterArray of column headers, most significant first. Defaults to Nothing.directSort: Optional Boolean. When True, sorts directly; when False, groups rows by first occurrence of each distinct value. Defaults to True.strictSearch: Optional Boolean. When True, uses exact column matching. Defaults to True.
DataExchange
Import #
import
Import data from a DataSheet or CustomTable
Signature:
Public Sub Import(ByVal impTab As Object, _
Optional ByVal pasteAtBottom As Boolean = False, _
Optional ByVal strictColumnSearch As Boolean = False, _
Optional ByVal insertShift As Boolean = True, _
Optional ByVal formatHeaders As Object = Nothing, _
Optional ByVal keepSourceHeaders As Boolean = False)
Import, export, and snapshot operations.
Copies column-matched data from the source object into this table. When keepSourceHeaders is True, replaces all headers and data entirely. Creates a snapshot before import and rolls back on error. Unhides hidden columns during the operation and restores their state afterwards. Note on size: with keepSourceHeaders False and the default insertShift True, this GROWS the table to fit the source but never shrinks it. A three-row table importing two rows stays three rows, with the third row merely blanked by Clean. Only the insertShift False branch trims.
Parameters:
impTab: Object. A DataSheet or CustomTable to import from.pasteAtBottom: Optional Boolean. When True, appends below existing data. Defaults to False.strictColumnSearch: Optional Boolean. When True, uses exact column matching. Defaults to False.insertShift: Optional Boolean. When True, inserts worksheet rows during resize. Defaults to True.formatHeaders: Optional Object. BetterArray of headers whose formatting to preserve. Defaults to Nothing.keepSourceHeaders: Optional Boolean. When True, replaces destination headers. Defaults to False.
ImportColumnsNotFound #
import-columns-not-found
Columns not matched during the last import
Signature:
Public Property Get ImportColumnsNotFound() As BetterArray
Returns a clone of the BetterArray containing column headers from the source that had no match in this table. Only available after a successful import; logs a diagnostic and exits when no import has occurred.
Returns: BetterArray. Unmatched column headers, or Nothing.
ExportColumnsNotFound #
export-columns-not-found
Headers the last export was asked for and could not write
Signature:
Public Property Get ExportColumnsNotFound() As BetterArray
Export skips a header it cannot resolve against this table and leaves its output column where it was, so the block it writes is narrower than the header list it was handed. A caller that writes a second row from that same list -- the label row of a data export is the case -- has to take these entries out first, or every label after the first gap sits one column left of its data.
The array is emptied at the start of every export, so it always describes the last one. It is empty when every header matched.
Returns: BetterArray. The headers that were skipped.
HasColumnsNotImported #
has-columns-not-imported
Whether the last import had unmatched columns
Signature:
Public Property Get HasColumnsNotImported() As Boolean
Returns True when the most recent import left some source columns unmatched against this table.
Returns: Boolean. True when unmatched columns exist.
Export #
export
Export the table to a worksheet
Signature:
Public Sub Export(ByVal sh As Worksheet, _
Optional ByVal headersTable As Object = Nothing, _
Optional ByVal startLine As Long = 1, _
Optional ByVal startColumn As Long = 1, _
Optional ByVal addListObject As Boolean = False, _
Optional ByVal clearSheet As Boolean = True)
Writes the requested columns (or all columns when headersTable is not a BetterArray) to the target worksheet, optionally creating a ListObject on the output range. Unhides hidden columns during the operation and restores their state afterwards, on the failure path too.
Parameters:
sh: Worksheet. Target worksheet to write to.headersTable: Optional Object. BetterArray of column headers to export. Defaults to Nothing (all columns).startLine: Optional Long. Row to begin writing at. Defaults to 1.startColumn: Optional Long. Column to begin writing at. Defaults to 1.addListObject: Optional Boolean. When True, creates a ListObject on the output. Defaults to False.clearSheet: Optional Boolean. When True, clears the target worksheet first. Defaults to True.
Checkings
HasCheckings #
has-checkings
Whether the table has logged diagnostic messages
Signature:
Public Property Get HasCheckings() As Boolean
Returns True when at least one diagnostic entry has been recorded.
Returns: Boolean. True when diagnostic entries exist.
CheckingValues #
checking-values
Retrieve the diagnostic log entries
Signature:
Public Property Get CheckingValues() As Object
Returns the internal Checking object containing all logged messages. Returns Nothing when no entries have been recorded.
Returns: Object. An Checking instance, or Nothing.
Internal members (not exported)
StateManagement
ResetCaches #
reset-caches
Drop the state a structural change can make wrong
Signature:
Private Sub ResetCaches()
Backup and guard helpers that keep internal state consistent.
Column lookups read the header row from the sheet on every call, so there is no lookup cache to drop here any more. The shift flag is the one piece of state a resize can leave saying the wrong thing.
ResetImportState #
reset-import-state
Reset import state so that imports start fresh
Signature:
Private Sub ResetImportState()
Clears the import flag, hidden-column list, not-imported-columns list, and any saved backup so the next import begins from a clean slate.
ClearBackup #
clear-backup
Release backup buffers
Signature:
Private Sub ClearBackup()
Sets the backup header and row arrays to Nothing and clears the hasBackup flag once the operation completes successfully.
SaveTableSnapshot #
save-table-snapshot
Save a snapshot of the current table state
Signature:
Private Sub SaveTableSnapshot()
Captures the current headers and data rows into BetterArray buffers so RestoreTableSnapshot can revert the table if an import fails. Unhides hidden columns before reading, and leaves them unhidden: the caller puts them back once the whole operation is over.
EnsureTableInitialized #
ensure-table-initialized
Guard against calls before Create has run
Signature:
Private Sub EnsureTableInitialized()
Raises an error when the ListObject reference is Nothing, preventing all table operations from executing on an uninitialised instance.
Throws:
- ProjectError.ObjectNotInitialized When the ListObject is Nothing.
Elements
Wksh #
wksh
Worksheet hosting the ListObject
Signature:
Private Property Get Wksh() As Worksheet
Properties that expose the table structure and cell access.
Returns the parent worksheet of the underlying ListObject.
Returns: Worksheet. The host worksheet.
SheetBlock #
sheet-block
Build a Range on the host worksheet from four numbers
Signature:
Private Function SheetBlock(ByVal firstRow As Long, ByVal firstColumn As Long, _
ByVal lastRow As Long, ByVal lastColumn As Long) As Range
The "With sh ... .Range(.Cells(a, b), .Cells(c, d))" shape was written out in five places. One small builder replaces all of them. It is deliberately bound to this table's own worksheet, so it must never be used to build a range on an export target sheet.
Parameters:
firstRow: Long. Top row.firstColumn: Long. Left column.lastRow: Long. Bottom row.lastColumn: Long. Right column.
Returns: Range. The block.
RowValues #
row-values
Read a one-row Range as a 1-based list of values
Signature:
Private Function RowValues(ByVal blockRng As Range) As Variant
Range.Value hands back a plain value when the Range is one cell, and a two-dimensional array otherwise. This flattens both cases so callers see one shape and do not repeat the test.
Parameters:
blockRng: Range. A single-row block.
Returns: Variant. A 1-based one-dimensional array of cell values.
ColumnValues #
column-values
Read a one-column Range as a 1-based list of values
Signature:
Private Function ColumnValues(ByVal blockRng As Range) As Variant
The column twin of RowValues, and for the same reason: a one-cell Range hands back a plain value where a taller one hands back a two-dimensional array.
Parameters:
blockRng: Range. A single-column block.
Returns: Variant. A 1-based one-dimensional array of cell values.
CellText #
cell-text
Read one cell value as text without raising
Signature:
Private Function CellText(ByVal cellValue As Variant) As String
A cell holding an error value (#N/A and the like) makes CStr raise a type mismatch. Such a header can never match a column name, so it reads as empty text instead of stopping the caller.
Parameters:
cellValue: Variant. The raw cell value.
Returns: String. The value as text, or empty text.
Table #
table
Underlying ListObject reference
Signature:
Public Property Get Table() As ListObject
Returns the ListObject wrapped by this CustomTable. Raises an error when the reference has not been set.
Returns: ListObject. The wrapped table.
Throws:
- ProjectError.ObjectNotInitialized When the ListObject is Nothing.
Table #
table-set
Assign the ListObject reference
Signature:
Public Property Set Table(ByVal Lo As ListObject)
Stores the ListObject and resets state and backup. Raises an error when Lo is Nothing.
Parameters:
Lo: ListObject. The table to assign.
Throws:
- ProjectError.ObjectNotInitialized When Lo is Nothing.
PrefixValue #
prefix-value
ID prefix string
Signature:
Public Property Get PrefixValue() As String
Returns the prefix prepended to sequential ID values.
Returns: String. The prefix text.
IdValue #
id-value-set
Assign the ID column name
Signature:
Public Property Let IdValue(ByVal idCol As String)
Parameters:
idCol: String. Column name to use as the row key.
PrefixValue #
prefix-value-set
Assign the ID prefix string
Signature:
Public Property Let PrefixValue(ByVal idPrefix As String)
Parameters:
idPrefix: String. Prefix to prepend to sequential ID values.
FindColumn #
find-column
Find one column in the header row, reading that row once
Signature:
Private Function FindColumn(ByVal headerRng As Range, _
ByVal colName As String, _
ByVal strictSearch As Boolean, _
ByVal matchCase As Boolean) As Long
This is the single lookup behind ColumnIndex, and so behind every column name this class is given. The header row is read once as a block and matched in memory: Range.Find used to cost a worksheet search plus five property reads, and a name that matched nothing repeated the whole search on every call because misses were never remembered. The two match rules are kept exactly as Range.Find applied them: strictSearch True is whole-string equality (xlWhole) and False is a substring test (xlPart); matchCase True compares binary and False compares text. The leftmost match wins, which several callers rely on. Two Range.Find behaviours are deliberately gone. It read "*", "?" and "~" in the search text as wildcards even under xlWhole, and it reused whatever LookIn and SearchOrder the last search anywhere in Excel had used, including the user's own Ctrl+F. Both were faults. The block is NOT kept on the instance: callers write to these sheets without going through this object, so it is read again on the next call.
Parameters:
headerRng: Range. The header row to search.colName: String. Column header to look for.strictSearch: Boolean. True for whole-string matching.matchCase: Boolean. True for case-sensitive matching.
Returns: Long. The 1-based position within the header row, or 0 when there is none.
ColumnIndex #
column-index
Resolve the 1-based column index for a header name
Signature:
Private Function ColumnIndex(ByVal colName As String, _
Optional ByVal strictSearch As Boolean = False, _
Optional ByVal matchCase As Boolean = False, _
Optional ByVal required As Boolean = False) As Long
Searches the header row for colName and returns its position within that row. Returns 0 when not found. When required is True, raises an error instead of returning 0. The 0-for-not-found answer is deliberate and stays: DataSheet answers -1, and lining the two up would make a "< 0" test written for a DataSheet silently pass for a CustomTable miss.
Parameters:
colName: String. Column name to look up.strictSearch: Optional Boolean. When True, uses exact whole-string matching. Defaults to False.matchCase: Optional Boolean. When True, matching is case-sensitive. Defaults to False.required: Optional Boolean. When True, raises an error if not found. Defaults to False.
Returns: Long. The 1-based column index, or 0 when not found.
Throws:
- ProjectError.InvalidArgument When colName is empty and required is True.
- ProjectError.ObjectNotInitialized When the header range is Nothing and required is True.
- ProjectError.ElementNotFound When the column is not found and required is True.
ResolveListColumn #
resolve-list-column
Retrieve the ListColumn object for a header name
Signature:
Private Function ResolveListColumn(ByVal colName As String, _
Optional ByVal strictSearch As Boolean = False, _
Optional ByVal matchCase As Boolean = False, _
Optional ByVal required As Boolean = False) As ListColumn
Centralises ListColumn retrieval so every caller benefits from the same guards. Returns Nothing when not found unless required is True.
Parameters:
colName: String. Column header to resolve.strictSearch: Optional Boolean. When True, uses exact matching. Defaults to False.matchCase: Optional Boolean. When True, matching is case-sensitive. Defaults to False.required: Optional Boolean. When True, raises an error if not found. Defaults to False.
Returns: ListColumn. The resolved column, or Nothing.
Throws:
- ProjectError.ElementNotFound When the column is not found and required is True.
Modify
AddShiftTracker #
add-shift-tracker
Record that worksheet row insertion was used
Signature:
Private Sub AddShiftTracker(Optional ByVal value As String = "Yes")
Methods that add, remove, or reorganise table rows and columns.
Persists a hidden name on the worksheet so future operations know to delete entire worksheet rows instead of just ListRows.
Parameters:
value: Optional String. Tracker value ("Yes" or "No"). Defaults to "Yes".
HasShiftTracker #
has-shift-tracker
Check whether worksheet row insertion was previously used
Signature:
Private Function HasShiftTracker() As Boolean
Reads the hidden name from the worksheet to determine whether prior operations used worksheet row insertion.
Returns: Boolean. True when the shift tracker is active.
Resize #
resize
Resize the table by adding or removing rows
Signature:
Private Sub Resize(Optional ByVal shouldAdd As Boolean = False, _
Optional ByVal insertShift As Boolean = True, _
Optional ByVal totalRowCount As Long = 0, _
Optional ByVal nbRows As Long = 5, _
Optional ByVal includeIds As Boolean = True, _
Optional ByVal forceShift As Boolean = False)
Dispatches to AddTableRows or RemoveEmptyDataRows depending on the shouldAdd flag, then optionally renumbers the ID column.
Parameters:
shouldAdd: Optional Boolean. When True, adds rows; when False, removes empty rows. Defaults to False.insertShift: Optional Boolean. When True, inserts worksheet rows. Defaults to True.totalRowCount: Optional Long. Non-empty cell threshold for row removal. Defaults to 0.nbRows: Optional Long. Number of rows to add. Defaults to 5.includeIds: Optional Boolean. When True, renumbers the ID column. Defaults to True.forceShift: Optional Boolean. When True, forces worksheet row deletion. Defaults to False.
AddTableRows #
add-table-rows
Physically add rows to the ListObject
Signature:
Private Sub AddTableRows(ByVal lo As ListObject, ByVal nbRows As Long, ByVal insertShift As Boolean)
Inserts worksheet rows below the table when insertShift is True (protecting stacked tables), otherwise extends the ListObject range directly. Resets state after the operation.
Parameters:
lo: ListObject. The table to extend.nbRows: Long. Number of rows to add.insertShift: Boolean. When True, inserts worksheet rows.
EnsureSelectionOnTableSheet #
ensure-selection-on-table-sheet
Ensure the provided range belongs to the table worksheet
Signature:
Private Sub EnsureSelectionOnTableSheet(ByVal targetCell As Range)
Validates that targetCell is not Nothing and resides on the same worksheet as the wrapped ListObject.
Parameters:
targetCell: Range. The range to validate.
Throws:
- ProjectError.InvalidArgument When targetCell is Nothing or belongs to a different sheet.
SelectionRowNumbers #
selection-row-numbers
Resolve worksheet row numbers intersecting the table
Signature:
Private Function SelectionRowNumbers(ByVal targetCell As Range, _
ByVal restrictToDataRows As Boolean) As BetterArray
Returns a BetterArray of worksheet row numbers where targetCell intersects the table scope. When restrictToDataRows is True, only data rows are considered; otherwise the full table range is used.
Parameters:
targetCell: Range. The selection to intersect.restrictToDataRows: Boolean. When True, limits to data rows only.
Returns: BetterArray. Row numbers, or Nothing when no intersection.
GetTotalFormulaColumns #
get-total-formula-columns
Count the number of columns containing formulas
Signature:
Private Function GetTotalFormulaColumns() As Long
Scans the first data row and counts cells that have formulas. Used to determine the baseline non-empty cell count for row removal. This used to be a Sub with a ByRef argument, and its only caller wrapped that argument in brackets, which passes a copy: the answer never came back and the count was always zero. A Function cannot be called that way.
Returns: Long. The number of columns whose first data cell holds a formula.
RemoveEmptyDataRows #
remove-empty-data-rows
Delete data rows that are effectively empty
Signature:
Private Sub RemoveEmptyDataRows(ByVal lo As ListObject, _
Optional ByVal totalRowCount As Long, _
Optional ByVal forceShift As Boolean)
A row is empty when its non-empty cell count is at or below totalRowCount. The first data row is always kept: it carries the formulas and the formats every new row is built from.
The body is read once and every row is answered once, into a flag array both branches below read.
WHICH BRANCH RUNS
With the shift tracker on, or forceShift, the empty rows go as ENTIRE worksheet rows, so a table stacked under this one is pulled up with them. Without it the table closes over its own rows and nothing outside its columns moves. That difference is the promise each branch carries and the two stacked table tests pin it.
WHY THE TABLE BRANCH LOOKS AT THE BOTTOM FIRST
The rows a resize drops sit at the bottom of the table nearly every time: Add rows pads 199 at a time and a resize takes the pad back. Shrinking the table over that block and clearing it is one operation. Deleting the same block a ListRow at a time is one Excel call per row, measured at about 2.5 ms a row on macOS -- 1.2 s for 500 rows, and a pad of a few thousand was the whole of the wait a user felt on the linelist Resize button. Column count, formulas, dropdowns, conditional formatting and other sheets reading the table all left that figure where it was, so the per-row calls were the cost and nothing else was. Rows left empty between filled ones still go one at a time, and there are seldom many.
Parameters:
lo: ListObject. The table to clean.totalRowCount: Optional Long. Non-empty cell threshold.forceShift: Optional Boolean. When True, forces worksheet row deletion.
RowIsEmpty #
row-is-empty
Whether one row of the block counts as empty
Signature:
Private Function RowIsEmpty(ByRef bodyValues As Variant, ByVal rowIndex As Long, _
ByVal tabColumnCount As Long, _
ByVal rowCount As Long) As Boolean
Counts the filled cells of the row and compares the count with the threshold. The count stops as soon as it passes the threshold, so a filled row costs a few cells rather than the whole width.
What counts as filled is exactly what CountA counted: a formula that returns "" reads back from a block as "", not as Empty, so it counts on both sides.
Parameters:
bodyValues: Variant. The body block read by the caller.rowIndex: Long. Which row of that block to answer for.
THE BLOCK COMES IN BY REFERENCE ON PURPOSE
A Variant holding an array is COPIED when it is passed ByVal, and this is called once per row. Measured with ByVal, a trim of 597 rows over 60 columns took 0.160 s where the same work takes 0.016 s here, and the whole-row branch came out slower than the row-by-row delete it replaced. Every routine that reads the block takes it ByRef for that reason.
tabColumnCount: Long. Columns in the block.rowCount: Long. Filled-cell threshold at or under which a row is empty.
Returns: Boolean. True when the row counts as empty.
LastKeptRowOf #
last-kept-row
The last row of the block that stays
Signature:
Private Function LastKeptRowOf(ByRef bodyValues As Variant, ByVal tabRowCount As Long, _
ByVal tabColumnCount As Long, _
ByVal rowCount As Long) As Long
Answers the highest row that does not count as empty, and 1 when every row under the first is empty. Everything after it is the trailing block.
Parameters:
bodyValues: Variant. The body block read by the caller.tabRowCount: Long. Rows in the block.tabColumnCount: Long. Columns in the block.rowCount: Long. Filled-cell threshold.
Returns: Long. A row index between 1 and tabRowCount.
DropTrailingRows #
drop-trailing-rows
Shrink the table over its trailing empty rows in one operation
Signature:
Private Function DropTrailingRows(ByVal lo As ListObject, ByVal firstBodyRow As Long, _
ByVal lastKeptRow As Long, ByVal tabRowCount As Long, _
ByVal tabColumnCount As Long) As Boolean
Resizes the ListObject to the rows that stay and clears the block it let go. The cells keep their values, their formats and their dropdowns until they are cleared, and the row-by-row delete this replaces took all three, so the clear is what makes the two the same. Nothing outside the table columns moves, which is the promise the table branch carries.
Answers False when Excel refuses the resize, and the caller then walks the rows one at a time exactly as before. A refused clear does not turn the answer back: the table has already let the rows go by then, and walking them again would delete rows that are no longer inside it.
Parameters:
lo: ListObject. The table to shrink.firstBodyRow: Long. Worksheet row of the first data row.lastKeptRow: Long. Last data row that stays, 1-based in the block.tabRowCount: Long. Rows in the block.tabColumnCount: Long. Columns in the block.
Returns: Boolean. True when the table was shrunk.
DeleteWorksheetRows #
delete-worksheet-rows
Delete the empty rows as entire worksheet rows, in one call
Signature:
Private Sub DeleteWorksheetRows(ByVal lo As ListObject, ByVal sh As Worksheet, _
ByRef bodyValues As Variant, _
ByVal tabRowCount As Long, ByVal tabColumnCount As Long, _
ByVal rowCount As Long, ByVal firstBodyRow As Long)
The empty rows are gathered and deleted in ONE call. Deleting them one at a time made Excel shift the whole sheet up once per row, and a sheet holding a few thousand spare rows spent minutes on it. A multi-area range costs a single shift however many areas it carries.
What goes into the Union is a RUN of touching rows rather than a row. Union grows dearer as the range it already holds grows, and the rows a resize drops are one run nearly every time, so the old shape paid thousands of Union calls to build a single area.
The walk runs from the bottom so a deletion does not move the rows still to come. When Excel refuses the delete, the fallback walks the table itself, which is what a stacked layout sometimes needs. It is slow and it only runs when the one-shot delete was refused.
Parameters:
lo: ListObject. The table being trimmed.sh: Worksheet. The sheet holding it.bodyValues: Variant. The body block, for the fallback.tabRowCount: Long. Rows in that block.tabColumnCount: Long. Columns in that block.rowCount: Long. Filled-cell threshold, for the fallback.firstBodyRow: Long. Worksheet row of the first data row.
AppendRowRun #
append-row-run
Add one run of touching worksheet rows to the range being gathered
Signature:
Private Sub AppendRowRun(ByRef deadRows As Range, ByVal sh As Worksheet, _
ByVal firstRow As Long, ByVal lastRow As Long)
Parameters:
deadRows: Range. The range gathered so far, Nothing on the first call.sh: Worksheet. The sheet holding the rows.firstRow: Long. First worksheet row of the run.lastRow: Long. Last worksheet row of the run.
DeleteRowsThroughTable #
delete-rows-through-table
Delete the empty rows one at a time through the ListObject
Signature:
Private Sub DeleteRowsThroughTable(ByVal lo As ListObject, ByRef bodyValues As Variant, _
ByVal tabRowCount As Long, ByVal tabColumnCount As Long, _
ByVal rowCount As Long)
The fallback of RemoveEmptyDataRows. It walks from the bottom so an index stays good after the row below it goes, and it raises when the table refuses a delete, which is the signal a caller reads as stacked tables.
Parameters:
lo: ListObject. The table to trim.bodyValues: Variant. The body block already read by the caller.tabRowCount: Long. Rows in that block.tabColumnCount: Long. Columns in that block.rowCount: Long. Filled-cell threshold at or under which a row is empty.
Throws:
- ProjectError.SomethingWentWrong When the table refuses a delete.
DataRowCount #
data-row-count
Count the number of data rows in the table
Signature:
Private Function DataRowCount() As Long
Returns: Long. The data row count (excluding headers), or 0 when empty.
DataColumnCount #
data-column-count
Count the number of columns in the table
Signature:
Private Function DataColumnCount() As Long
Returns: Long. The column count.
EnsureRowCapacity #
ensure-row-capacity
Guarantee the table has at least the requested data rows
Signature:
Private Sub EnsureRowCapacity(ByVal requiredRows As Long, ByVal insertShift As Boolean)
Parameters:
requiredRows: Long. Minimum number of data rows needed.insertShift: Boolean. When True, inserts worksheet rows.
EnsureColumnCapacity #
ensure-column-capacity
Guarantee the table has exactly the requested column count
Signature:
Private Sub EnsureColumnCapacity(ByVal requiredColumns As Long)
Adds or removes columns until the table matches requiredColumns. Trims excess columns using a descending loop so index shifts do not skip columns.
Parameters:
requiredColumns: Long. Target number of columns.
TrimTableRows #
trim-table-rows
Shrink the table to the requested number of data rows
Signature:
Private Sub TrimTableRows(ByVal targetRows As Long)
Resizes the ListObject so only targetRows data rows remain. Exits when the table already has fewer rows than the target.
Parameters:
targetRows: Long. Desired data row count.
SourceRowCount #
source-row-count
Derive the data row count from an import source
Signature:
Private Function SourceRowCount(ByVal impTab As Object) As Long
Reads the DataRange from the supplied CustomTable or DataSheet and returns the number of data rows. Returns 0 when the source is empty.
Parameters:
impTab: Object. A CustomTable or DataSheet to measure.
Returns: Long. The number of source data rows.
FormatIdValue #
format-id-value
Build an ID string from a prefix and numeric part
Signature:
Private Function FormatIdValue(ByVal prefixText As String, ByVal numericPart As Long) As String
Parameters:
prefixText: String. Prefix text, or empty for numeric-only IDs.numericPart: Long. The sequential number.
Returns: String. The formatted ID value.
SortHelper #
sort-helper
Dispatch to the appropriate sort strategy
Signature:
Private Sub SortHelper(ByVal colName As String, _
Optional ByVal directSort As Boolean = True, _
Optional ByVal strictSearch As Boolean = True)
Parameters:
colName: String. Column header to sort on.directSort: Optional Boolean. When True, sorts directly. Defaults to True.strictSearch: Optional Boolean. When True, uses exact column matching. Defaults to True.
SortOnFirst #
sort-on-first
Sort by grouping rows by first occurrence of each value
Signature:
Private Sub SortOnFirst(ByVal colName As String, Optional ByVal strictSearch As Boolean = True)
Adds a temporary "__number" column, assigns ascending numbers based on first-seen order of each distinct value, sorts the table on that column, then removes it. This groups identical values together while preserving the original order of first appearance.
Parameters:
colName: String. Column header to sort on.strictSearch: Optional Boolean. When True, uses exact column matching. Defaults to True.
RemoveHelperColumn #
remove-helper-column
Drop the temporary sort column whatever happened
Signature:
Private Sub RemoveHelperColumn(ByVal lo As ListObject, ByVal helperColumn As Long)
The helper column must go on every path out of SortOnFirst. A failure to remove it must not replace the error that brought us here, so the attempt is protected.
Parameters:
lo: ListObject. The table carrying the helper column.helperColumn: Long. Position of the helper column.
SortSimple #
sort-simple
Sort the table on a single column in ascending order
Signature:
Private Sub SortSimple(ByVal colName As String, Optional ByVal strictSearch As Boolean = True)
Parameters:
colName: String. Column header to sort on.strictSearch: Optional Boolean. When True, uses exact column matching. Defaults to True.
DataExchange
ImportObject #
import-object
Import column-matched data from a source object
Signature:
Private Sub ImportObject(ByVal impTab As Object, _
Optional ByVal pasteAtBottom As Boolean = False, _
Optional ByVal strictColumnSearch As Boolean = False, _
Optional ByVal insertShift As Boolean = True, _
Optional ByVal formatHeaders As Object = Nothing)
Iterates the source headers, finds matching columns in this table, and copies data column by column. Columns not found are recorded and read back through ImportColumnsNotFound. Optionally preserves formatting for specified headers. Trims the table when insertShift is False, and only then: on the default True branch the table grows to fit the source but never shrinks.
Parameters:
impTab: Object. A DataSheet or CustomTable to import from.pasteAtBottom: Optional Boolean. When True, appends below existing data. Defaults to False.strictColumnSearch: Optional Boolean. When True, uses exact column matching. Defaults to False.insertShift: Optional Boolean. When True, inserts worksheet rows during resize. Defaults to True.formatHeaders: Optional Object. BetterArray of headers whose formatting to preserve. Defaults to Nothing.
ImportFormat #
import-format
Copy formatting from a source range to a destination range
Signature:
Private Sub ImportFormat(ByVal impRng As Range, ByVal currRng As Range)
Transfers comments, comment threads, interior colour, font colour, bold, and italic from each cell in impRng to the corresponding cell in currRng. Uses error suppression for Excel versions that do not support CommentThreaded. This is deliberately NOT a straight format copy: the interior colour is only carried over when the source is not white and the font colour only when the source is not black. xlPasteFormats would be faster and would not do the same thing, so the routine stays a per-cell copy. What it stopped doing is paying for each cell more than once.
Parameters:
impRng: Range. Source range with formatting to copy.currRng: Range. Destination range in this table.
ImportAll #
import-all
Replace the entire table with imported data
Signature:
Private Sub ImportAll(ByVal impTab As Object)
Replaces all headers and data with the content of the supplied object, discarding the current structure entirely. Trims the table to one row, adjusts column count to match the source, then overwrites headers and data body.
Parameters:
impTab: Object. A DataSheet or CustomTable to import from.
UnhideHiddenColumns #
unhide-hidden-columns
Unhide all hidden columns before import/export
Signature:
Private Sub UnhideHiddenColumns()
Import and export both write across the whole table, and a hidden column has to take its share, so every column is made visible first. The indexes of the columns that were hidden are recorded so ReturnBackHiddenColumns can put them back afterwards. The whole header band is tested first: EntireColumn.Hidden answers False when nothing is hidden, True when everything is and Null when it is mixed, so the common case costs one round trip instead of three per column.
ReturnBackHiddenColumns #
return-back-hidden-columns
Restore previously hidden columns after import/export
Signature:
Private Sub ReturnBackHiddenColumns()
Re-hides columns that were recorded by UnhideHiddenColumns. Uses error suppression as a safeguard against stale column indexes.
RestoreHiddenAndReset #
restore-hidden-and-reset
Put the hidden columns back and drop the transient state
Signature:
Private Sub RestoreHiddenAndReset()
The pair was written out three times, on both exits of Import and at the end of Export.
Checkings
LogInfo #
log-info
Log a diagnostic message
Signature:
Private Sub LogInfo(ByVal label As String, _
Optional ByVal scope As Byte = checkingNote)
Diagnostic logging and error handling.
Adds a message to the internal Checking object, creating it on first use. The checkCounter provides a unique key for each entry.
Parameters:
label: String. The message to record.scope: Optional Byte. Severity level from CheckingScope. Defaults to checkingNote.
LogAndTrace #
log-and-trace
Log a message and print the debug trace
Signature:
Private Sub LogAndTrace(ByVal label As String, _
Optional ByVal scope As Byte = checkingNote)
The "log, show the trace, then leave" tail was written out on the failure paths of Import, ImportColumnsNotFound and Export.
Parameters:
label: String. The message to record.scope: Optional Byte. Severity level from CheckingScope. Defaults to checkingNote.
ShowDebug #
show-debug
Print a debug trace to the Immediate window
Signature:
Private Sub ShowDebug()
ThrowError #
throw-error
Raise a ProjectError-based exception
Signature:
Private Sub ThrowError(ByVal errNumb As Long, ByVal errorMessage As String)
Wrapper around Err.Raise that standardises the source to "CustomTable" for consistent stack traces.
Parameters:
errNumb: Long. ProjectError code.errorMessage: String. Descriptive message.
Throws:
- ProjectError.
Always raises the specified error.
Seal #
seal
Seal the instance so setup setters can no longer be written
Signature:
Public Sub Seal()
GuardNotSealed #
guard-not-sealed
Guard a setup setter against writes after sealing
Signature:
Private Sub GuardNotSealed(ByVal propName As String)
Parameters:
propName: String. Name of the property being guarded.
Used in (33 file(s))
- Analysis.cls
- LLExporter.cls
- LLImporter.cls
- DesignerPreparation.cls
- LLdictionary.cls
- LLExport.cls
- DataSheet.cls
- LLChoices.cls
- LLFormat.cls
- Linelist.cls
- LLDataEntry.cls
- LLTranslation.cls
- DiseaseSheet.cls
- MasterSetupVariables.cls
- EventSetup.cls
- SetupErrors.cls
- SetupImport.cls
- SetupPreparation.cls
- SetupTranslationsTable.cls
- EventsDesignerMulti.bas
- RibbonDev.bas
- EventsLinelistButtons.bas
- MasterSetupHelpers.bas
- EventsGlobal.bas
- EventsRibbon.bas
- TestAnalysis.bas
- TestLLImporter.bas
- TestDesignerMulti.bas
- TestCustomTable.bas
- TestCustomTableTiming.bas
- TestLLTranslation.bas
- TestSetupErrors.bas
- TestSetupPreparation.bas