107 lines
6.7 KiB
Markdown
107 lines
6.7 KiB
Markdown
|
|
---
|
||
|
|
source_files:
|
||
|
|
- DataPRO/Modules/DatabaseImporter/DatabaseImport/App/WaitCursor.cs
|
||
|
|
- DataPRO/Modules/DatabaseImporter/DatabaseImport/App/App.cs
|
||
|
|
generated_at: "2026-04-16T04:29:09.171697+00:00"
|
||
|
|
model: "Qwen/Qwen3-Coder-Next-FP8"
|
||
|
|
schema_version: 1
|
||
|
|
sha256: "85d98116aaeec020"
|
||
|
|
---
|
||
|
|
|
||
|
|
# App
|
||
|
|
|
||
|
|
## Documentation: `WaitCursor` and `App` Module (DatabaseImporter)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 1. Purpose
|
||
|
|
|
||
|
|
This module provides UI-level cursor and application state management during long-running operations in a WPF-based database import workflow. Specifically, the `WaitCursor` class offers a lightweight, `IDisposable` wrapper to temporarily override the mouse cursor to a wait state and restore it upon disposal. The `App` class (a partial class for `Application`) extends this functionality at the application level by coordinating global busy/idle states: it disables the main window and manages a singleton `WaitCursor` instance across thread boundaries using the WPF `Dispatcher`, ensuring safe, re-entrant, and exception-safe busy/idle transitions—typically used before/after data-intensive operations (e.g., database refreshes or imports).
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 2. Public Interface
|
||
|
|
|
||
|
|
#### `WaitCursor` class (`DataImport.WaitCursor`)
|
||
|
|
|
||
|
|
- **`public WaitCursor()`**
|
||
|
|
Constructor. Saves the current `Mouse.OverrideCursor`, sets `Mouse.OverrideCursor = Cursors.Wait`, and prepares for later restoration via `Dispose()`.
|
||
|
|
|
||
|
|
- **`public void Dispose()`**
|
||
|
|
Restores the previously saved cursor (`_previousCursor`) to `Mouse.OverrideCursor`. Must be called exactly once per instance (per `using` or manual disposal pattern). No-op if called multiple times (but cursor may be incorrectly restored if disposed more than once).
|
||
|
|
|
||
|
|
#### `App` class (`DatabaseImport.App`, partial)
|
||
|
|
|
||
|
|
- **`public ISO13499FileDb IsoDb { get; }`**
|
||
|
|
Lazy-initialized singleton accessor for the `ISO13499FileDb` database instance. On first access, instantiates `_isoDb`, calls `_isoDb.RefreshAllData()`, and returns it. Thread-safety is *not* guaranteed (no locking around initialization or `RefreshAllData()`).
|
||
|
|
|
||
|
|
- **`public void SetAppBusy()`**
|
||
|
|
Sets the application into a *busy* state:
|
||
|
|
- Marshals to the UI thread via `Dispatcher.BeginInvoke` if called from a non-UI thread.
|
||
|
|
- Disposes any existing `_wc` (if not null).
|
||
|
|
- Creates a new `WaitCursor` instance and assigns it to `_wc`.
|
||
|
|
- Sets `MainWindow.IsEnabled = false`.
|
||
|
|
- Uses `lock (WaitCursorLock)` to serialize concurrent calls.
|
||
|
|
- *No-op* if `MainWindow` is null.
|
||
|
|
|
||
|
|
- **`public void SetAppAvailable()`**
|
||
|
|
Sets the application into an *available* (idle) state:
|
||
|
|
- Marshals to the UI thread via `Dispatcher.BeginInvoke` if called from a non-UI thread.
|
||
|
|
- Disposes and nullifies `_wc` (if not null).
|
||
|
|
- Sets `MainWindow.IsEnabled = true`.
|
||
|
|
- Uses `lock (WaitCursorLock)` to serialize concurrent calls.
|
||
|
|
- *No-op* if `MainWindow` is null.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 3. Invariants
|
||
|
|
|
||
|
|
- **Cursor restoration guarantee**: After a `WaitCursor` instance is disposed, `Mouse.OverrideCursor` is restored to the value it held at the time of construction.
|
||
|
|
- **Application busy/idle state consistency**:
|
||
|
|
- `MainWindow.IsEnabled` is `false` *only* when `_wc` is non-null (i.e., during a `SetAppBusy()` call with no intervening `SetAppAvailable()`).
|
||
|
|
- `_wc` is `null` *only* when the application is not busy (i.e., after `SetAppAvailable()` or before any `SetAppBusy()` call).
|
||
|
|
- **Thread-safety of state transitions**:
|
||
|
|
- `SetAppBusy()` and `SetAppAvailable()` are thread-safe *with respect to each other* via `lock (WaitCursorLock)`.
|
||
|
|
- Both methods *always* marshal to the UI thread via `Dispatcher.BeginInvoke` if invoked off-thread, ensuring UI mutations occur on the correct thread.
|
||
|
|
- **Singleton `IsoDb` initialization**: `_isoDb` is initialized at most once, and only upon first access to `IsoDb`. `RefreshAllData()` is called exactly once per instance creation.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 4. Dependencies
|
||
|
|
|
||
|
|
#### This module depends on:
|
||
|
|
- `System.Windows.Input.Cursors` (for `Cursors.Wait`)
|
||
|
|
- `System.Windows.Input.Mouse` (for `Mouse.OverrideCursor`)
|
||
|
|
- `System.Windows.Threading.Dispatcher` (for `Dispatcher.CheckAccess`, `BeginInvoke`)
|
||
|
|
- `System.Windows.Application` (base class `App : Application`)
|
||
|
|
- `System.Windows.Window` (via `MainWindow`)
|
||
|
|
- `DatabaseImport.ISO13499FileDb` (referenced in `App.IsoDb` property; defined elsewhere in the codebase)
|
||
|
|
|
||
|
|
#### This module is depended on by:
|
||
|
|
- Any code that needs to wrap long-running operations in a wait cursor (e.g., `using (new WaitCursor()) { ... }`).
|
||
|
|
- Code that triggers database refreshes or imports (uses `App.IsoDb`).
|
||
|
|
- UI or service layers that call `App.SetAppBusy()` before and `App.SetAppAvailable()` in `finally` blocks (as recommended in comments).
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 5. Gotchas
|
||
|
|
|
||
|
|
- **`IsoDb` initialization is not thread-safe**: Multiple concurrent first-time accesses to `IsoDb` may result in multiple `ISO13499FileDb` instances being created and `RefreshAllData()` being called multiple times. No locking or double-check pattern is used.
|
||
|
|
- **`WaitCursor` is not re-entrant**: If `WaitCursor` is constructed multiple times without disposal (e.g., nested `using` blocks), only the *outermost* cursor state is preserved in `_previousCursor`. Inner disposals will overwrite the cursor prematurely.
|
||
|
|
*Example*:
|
||
|
|
```csharp
|
||
|
|
using (new WaitCursor()) // Saves cursor A, sets Wait
|
||
|
|
{
|
||
|
|
using (new WaitCursor()) // Saves cursor Wait, sets Wait again
|
||
|
|
{
|
||
|
|
// ...
|
||
|
|
} // Restores cursor Wait → incorrect!
|
||
|
|
} // Restores cursor A
|
||
|
|
```
|
||
|
|
- **`SetAppBusy()` / `SetAppAvailable()` may silently no-op**: If `MainWindow` is `null` at the time of call (e.g., during early startup or after window close), the methods exit early with no side effects—no exception is thrown.
|
||
|
|
- **`WaitCursorLock` is static and shared across all `App` instances**: Since `App` is a singleton in WPF, this is expected—but if multiple `App` instances were ever created (e.g., in tests), locking would be incorrectly shared.
|
||
|
|
- **No exception handling in `SetAppBusy()`/`SetAppAvailable()`**: If `MainWindow.IsEnabled = false/true` or `WaitCursor.Dispose()` throws, the busy/idle state may be left inconsistent (e.g., `_wc` disposed but `MainWindow.IsEnabled` not updated).
|
||
|
|
- **`_wc` is not thread-local**: Though marshaled to the UI thread, `_wc` is a *single instance field*—concurrent calls via `BeginInvoke` are serialized by `WaitCursorLock`, but the timing of disposal/creation may cause transient cursor flicker if not used carefully (e.g., overlapping busy periods).
|
||
|
|
- **`WaitCursor` does not prevent user input beyond disabling `MainWindow`**: `Mouse.OverrideCursor` alone does not block input; the disabling of `MainWindow` is what prevents interaction. If `MainWindow` is hidden or replaced, the busy state may persist incorrectly.
|
||
|
|
|
||
|
|
None identified beyond the above.
|