---
title: SAIF.Presentation business rules
summary: Enumerated business rules with per-rule trigger, enforcement, and outcomes.
status: draft
owner: SAIF Doc Gen
last_reviewed: 2026-08-17
library: SAIF.Presentation
reference_type: package
---

# SAIF.Presentation business rules

This page captures business rules enforced by `SAIF.Presentation`'s sole public type, `clsPreferences`. All persistence is delegated to `SAIF.Framework.Preferences.PreferenceData`; no rule in this library involves SQL or database tables owned by this repository.

## BR-PRES-001: Preference store scoped to one application

| Field | Value |
| --- | --- |
| **Statement** | A preference store is always scoped to one application name supplied at construction. |
| **Trigger** | `clsPreferences` construction |
| **Enforcement** | `clsPreferences.vb` (`Initialize`) |
| **Outcome** | All reads and writes filter on that application. |
| **Source** | `SAIF.Presentation/clsPreferences.vb` |

---

## BR-PRES-002: Preference access uses the fixed Oracle user CLM

| Field | Value |
| --- | --- |
| **Statement** | Preference data is always read as the fixed Oracle user `CLM`, against the database named by the framework `OracleDatabase` configuration key. |
| **Trigger** | `Load` |
| **Enforcement** | `clsPreferences.vb` (`Load`) |
| **Outcome** | Preference access runs under a dedicated, environment-resolved account. |
| **Source** | `SAIF.Presentation/clsPreferences.vb` |

---

## BR-PRES-003: Effective user identity comes from the data portal

| Field | Value |
| --- | --- |
| **Statement** | The effective user identity for preference storage is the data portal's `UserID`, not the user ID passed to the constructor. |
| **Trigger** | `Load`, `Save` |
| **Enforcement** | `clsPreferences.vb` |
| **Outcome** | Preferences follow the authenticated data-portal identity. |
| **Source** | `SAIF.Presentation/clsPreferences.vb` |

---

## BR-PRES-004: All preferences loaded eagerly on construction

| Field | Value |
| --- | --- |
| **Statement** | All of a user's preferences (every application, every page) are loaded eagerly on construction. |
| **Trigger** | `Load` |
| **Enforcement** | `clsPreferences.vb` (`Load`) |
| **Outcome** | Subsequent reads incur no database round trips. |
| **Source** | `SAIF.Presentation/clsPreferences.vb` |

---

## BR-PRES-005: User ID trimmed and lower-cased for lookup

| Field | Value |
| --- | --- |
| **Statement** | The user ID used for the preference lookup is trimmed and lower-cased. |
| **Trigger** | `Load` |
| **Enforcement** | `clsPreferences.vb` (`Load`) |
| **Outcome** | Case and whitespace differences do not split a user's preferences. |
| **Source** | `SAIF.Presentation/clsPreferences.vb` |

---

## BR-PRES-006: A Nothing key is a no-op

| Field | Value |
| --- | --- |
| **Statement** | A `Nothing` key is a no-op: reads return `Nothing` and writes/removals do nothing. |
| **Trigger** | Any `Item`, `Add`, `Remove` call |
| **Enforcement** | `clsPreferences.vb` |
| **Outcome** | No null-reference failures from caller input. |
| **Source** | `SAIF.Presentation/clsPreferences.vb` |

---

## BR-PRES-007: Preference keys are case-insensitive

| Field | Value |
| --- | --- |
| **Statement** | Preference keys are case-insensitive — they are lower-cased on every read, add, and remove. |
| **Trigger** | Any keyed operation |
| **Enforcement** | `clsPreferences.vb` |
| **Outcome** | `ColNamesGrid1` and `colnamesgrid1` address the same preference. |
| **Source** | `SAIF.Presentation/clsPreferences.vb` |

---

## BR-PRES-008: Only PageLevel scope binds to a page

| Field | Value |
| --- | --- |
| **Statement** | Only `PrefScope.PageLevel` binds a preference to a page; `AppLevel` and `WebLevel` both resolve to an empty page key. |
| **Trigger** | `PageKey` |
| **Enforcement** | `clsPreferences.vb` (`PageKey`) |
| **Outcome** | `AppLevel` and `WebLevel` preferences currently share the same storage slot.* |
| **Source** | `SAIF.Presentation/clsPreferences.vb` |

---

## BR-PRES-009: Web page name derived from request path

| Field | Value |
| --- | --- |
| **Statement** | In a web context the page name is the last path segment of `Request.Path`. |
| **Trigger** | `Page` getter with a non-null `HttpContext.Current` |
| **Enforcement** | `clsPreferences.vb` (`Page`) |
| **Outcome** | Page-scoped preferences are keyed by the `.aspx` page name. |
| **Source** | `SAIF.Presentation/clsPreferences.vb` |

---

## BR-PRES-010: Non-web page name derived from override or call stack

| Field | Value |
| --- | --- |
| **Statement** | Outside a web context the page name is `OverridePage` when set, otherwise the declaring type name reflected from the call stack (skipping four frames). |
| **Trigger** | `Page` getter with a null `HttpContext.Current` |
| **Enforcement** | `clsPreferences.vb` (`Page`) |
| **Outcome** | Desktop callers get a form-name-based page key; `OverridePage` must be set before `Save`. |
| **Source** | `SAIF.Presentation/clsPreferences.vb` |

---

## BR-PRES-011: Preference record identified by application and page

| Field | Value |
| --- | --- |
| **Statement** | A preference record is identified by the pair (application name, page name). |
| **Trigger** | Any scoped read/write |
| **Enforcement** | `clsPreferences.vb` |
| **Outcome** | One JSON item bag exists per application + page per user. |
| **Source** | `SAIF.Presentation/clsPreferences.vb` |

---

## BR-PRES-012: Explicit three-argument lookup matches lower-cased fields

| Field | Value |
| --- | --- |
| **Statement** | The explicit three-argument lookup matches on lower-cased application and page names and treats `Nothing` arguments as empty strings. |
| **Trigger** | `Item(inApplication, inPage, inKey)` |
| **Enforcement** | `clsPreferences.vb` |
| **Outcome** | Cross-application/page reads are possible without changing scope. |
| **Source** | `SAIF.Presentation/clsPreferences.vb` |

---

## BR-PRES-013: Adding a preference creates a missing record

| Field | Value |
| --- | --- |
| **Statement** | Adding a preference for an application/page combination that has no record creates the record on demand. |
| **Trigger** | `Add` with no matching `Preference` |
| **Enforcement** | `clsPreferences.vb` (`Add`) |
| **Outcome** | New pages get a preference bag automatically. |
| **Source** | `SAIF.Presentation/clsPreferences.vb` |

---

## BR-PRES-014: Adding an existing key replaces its value

| Field | Value |
| --- | --- |
| **Statement** | Adding an existing key replaces the previous value (remove-then-add). |
| **Trigger** | `Add` with a key already present |
| **Enforcement** | `clsPreferences.vb` (`Add`) |
| **Outcome** | Keys are unique within a preference record. |
| **Source** | `SAIF.Presentation/clsPreferences.vb` |

---

## BR-PRES-015: Preference values stored as strings

| Field | Value |
| --- | --- |
| **Statement** | Preference values are stored as strings (`inValue.ToString()`). |
| **Trigger** | `Add` |
| **Enforcement** | `clsPreferences.vb` (`Add`) |
| **Outcome** | Callers must parse values back to their intended type. |
| **Source** | `SAIF.Presentation/clsPreferences.vb` |

---

## BR-PRES-016: Removing an absent key is a no-op

| Field | Value |
| --- | --- |
| **Statement** | Removing a key that is not present is a silent no-op. |
| **Trigger** | `Remove` |
| **Enforcement** | `clsPreferences.vb` (`Remove`) |
| **Outcome** | No exception for an absent key. |
| **Source** | `SAIF.Presentation/clsPreferences.vb` |

---

## BR-PRES-017: Clear empties all items in a scope's record

| Field | Value |
| --- | --- |
| **Statement** | `Clear(scope)` empties every item in the current application's record for that scope. |
| **Trigger** | `Clear` |
| **Enforcement** | `clsPreferences.vb` (`Clear`) |
| **Outcome** | A whole page's preferences are reset in one call. |
| **Source** | `SAIF.Presentation/clsPreferences.vb` |

---

## BR-PRES-018: Delete empties every loaded record

| Field | Value |
| --- | --- |
| **Statement** | `Delete()` empties every loaded preference record for the user, across all applications and pages. |
| **Trigger** | `Delete` |
| **Enforcement** | `clsPreferences.vb` (`Delete`) |
| **Outcome** | Full user preference reset. |
| **Source** | `SAIF.Presentation/clsPreferences.vb` |

---

## BR-PRES-019: Mutations are in-memory until Save

| Field | Value |
| --- | --- |
| **Statement** | Mutations are in-memory only until `Save()` is called. |
| **Trigger** | `Add`, `Remove`, `Clear`, `Delete` |
| **Enforcement** | `clsPreferences.vb` |
| **Outcome** | Callers must call `Save()` or changes are lost at the end of the request. |
| **Source** | `SAIF.Presentation/clsPreferences.vb` |

---

## BR-PRES-020: Save persists only the current application's records

| Field | Value |
| --- | --- |
| **Statement** | `Save()` persists only records belonging to the store's current application. |
| **Trigger** | `Save` |
| **Enforcement** | `clsPreferences.vb` (`Save`) |
| **Outcome** | Records for other applications loaded into memory are left untouched. |
| **Source** | `SAIF.Presentation/clsPreferences.vb` |

---

## BR-PRES-021: Preference items persisted as JSON

| Field | Value |
| --- | --- |
| **Statement** | Preference items are persisted as a JSON-serialized dictionary. |
| **Trigger** | `Save` with a non-empty item bag |
| **Enforcement** | `clsPreferences.vb` (`Save`) |
| **Outcome** | One JSON document per application + page per user. |
| **Source** | `SAIF.Presentation/clsPreferences.vb` |

---

## BR-PRES-022: Empty preference record persisted as empty string

| Field | Value |
| --- | --- |
| **Statement** | An emptied preference record is persisted as an empty string rather than an empty JSON object. |
| **Trigger** | `Save` with `PreferenceItems.Count = 0` |
| **Enforcement** | `clsPreferences.vb` (`Save`) |
| **Outcome** | Cleared preference bags are stored as blank. |
| **Source** | `SAIF.Presentation/clsPreferences.vb` |

---

## BR-PRES-023: Persistence always routed through PreferenceData

| Field | Value |
| --- | --- |
| **Statement** | Persistence is always routed through `PreferenceData.SavePreferences` — this library issues no SQL. |
| **Trigger** | `Save` |
| **Enforcement** | `clsPreferences.vb` (`Save`) |
| **Outcome** | Storage details remain the framework's concern. |
| **Source** | `SAIF.Presentation/clsPreferences.vb` |

---

## BR-PRES-024: Persistence failures wrapped in ApplicationException

| Field | Value |
| --- | --- |
| **Statement** | Any persistence failure is wrapped in an `ApplicationException` with the message "Unable to save preferences." |
| **Trigger** | Exception inside `Save` |
| **Enforcement** | `clsPreferences.vb` (`Save`) |
| **Outcome** | Callers get a consistent, contextualized error. |
| **Source** | `SAIF.Presentation/clsPreferences.vb` |

---

## Notes
- `Save()` always returns `False` even on success — the `result` local is never assigned `True` before the (unreachable) trailing trace call. Callers checking the boolean return value of `Save()` cannot rely on it to detect failure; failures are instead signaled by the `ApplicationException` described in [BR-PRES-024](#br-pres-024-persistence-failures-wrapped-in-applicationexception).*

## Related documentation
- [Index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Dependencies](dependencies.md)
- [Operations and processes](operations-processes.md)
- [Database tables](database-tables.md)
- [SQL catalog](sql/index.md)
- [API reference](api/index.md)

> **Note:** Statements marked with * were inferred from code structure, naming conventions, or contextual evidence where direct confirmation was not available in source artifacts. Statements marked with ** could not be confirmed; treat as unknown until verified.

## Sources
- `SAIF.Presentation/clsPreferences.vb`
