Files
2026-04-17 14:55:32 -04:00

227 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
source_files:
- DataPRO/Modules/RegionOfInterest/RegionOfInterestChannels/ViewModel/RegionOfInterestChannelsViewModel.cs
generated_at: "2026-04-16T04:34:24.697647+00:00"
model: "Qwen/Qwen3-Coder-Next-FP8"
schema_version: 1
sha256: "2e798c0127d02e4a"
---
# ViewModel
**Documentation: RegionOfInterestChannelsViewModel**
---
### 1. **Purpose**
This module implements the `RegionOfInterestChannelsViewModel`, a core view model for managing and displaying channels associated with Regions of Interest (ROIs) in the DTS test configuration and analysis system. It serves as the data context for the ROI Channels UI, enabling users to view, filter, sort, and select channels for inclusion in one or more ROIs. The view model bridges UI interactions with underlying channel data (from `IGroupChannel`, `ITestChannel`, or test metadata), handles ROI channel assignment validation (e.g., scrubbing stale channel references), and coordinates with the event aggregator for cross-component communication (e.g., busy indicators, notifications). It is used in contexts such as ROI definition, CSV/HDF export, and data download workflows.
---
### 2. **Public Interface**
#### **Constructor**
```csharp
RegionOfInterestChannelsViewModel(
IRegionOfInterestChannelsView view,
Prism.Regions.IRegionManager regionManager,
Prism.Events.IEventAggregator eventAggregator,
Unity.IUnityContainer unityContainer)
```
- Initializes the view model, sets up data binding, registers event subscriptions (`RaiseNotification`, `BusyIndicatorChangeNotification`), and initializes `RegionsOfInterest` as a `BindingList<IRegionOfInterest>`.
#### **Properties**
- `bool IsDirty { get; private set; }`
Indicates whether the underlying data has unsaved changes. *Currently always `false` in source.*
- `bool IsBusy { get; set; }`
Binds to UI busy indicator state. Set via `OnBusyIndicatorNotification`.
- `bool IsMenuIncluded { get; set; }`
- `bool IsNavigationIncluded { get; set; }`
UI layout flags for optional navigation elements.
- `IRegionOfInterestChannelsView View { get; set; }`
Reference to the associated view.
- `InteractionRequest<Notification> NotificationRequest { get; }`
- `InteractionRequest<Confirmation> ConfirmationRequest { get; }`
Prism Interactivity triggers for modal dialogs.
- `BindingList<IRegionOfInterest> RegionsOfInterest { get; set; }`
List of ROI definitions. On setter, performs scrubbing of stale channel references (see *Gotchas*), then calls `ResetDataView()`.
- `List<DTS.Common.Classes.Groups.GroupChannel> AllChannelsUnfiltered { get; set; }`
Raw list of all channels (from `IGroupChannel[]`) before filtering.
- `ObservableCollection<GroupChannel> AllChannels { get; set; }`
Filtered list of `GroupChannel` instances displayed in the UI.
- `ObservableCollection<ITestChannel> AllTestChannels { get; set; }`
Filtered list of `ITestChannel` instances (used when loading from test summary/metadata).
- `string[] AllChannelSSNs { get; }`
Returns array of `Hardware\SerialNumber` strings for all *enabled* channels (used for ROI export). Handles both `GroupChannel` and `ITestChannel` sources.
- `BindingList<ChannelEnabler> ChannelList { get; set; }`
List of `ChannelEnabler` objects representing UI rows (one per channel), each containing ROI inclusion checkboxes (`ROIIncludes`). Setter wires `ListChanged` event.
- `ObservableCollection<ColumnDescriptor> Columns { get; set; }`
Defines grid column metadata (header, display member, type) for `ChannelList`.
- `IsoViewMode ISOViewMode { get; set; }`
Controls which channel name variant is used (e.g., ISO, User Code, or raw).
#### **Methods**
- `void SetParent(object o)`
Stores `Parent` object (used in event args for ROI channel selection events).
- `void SetTest(string path, IsoViewMode viewMode)`
Loads test metadata from `.dts` file at `path`, populates `_testSummary`, and triggers `Filter()` and `ResetDataView()`.
- `void SetGroups(ITestSetup testSetup, Dictionary<string, IDASHardware> serialNumberToHardware, IsoViewMode viewMode)`
Initializes channel data from `ITestSetup` (e.g., during ROI definition). Validates DAS assignments, logs phantom assignments, sorts channels, and calls `ProcessChannels()``Filter()``ResetDataView()`.
- `void Filter(string term)` / `void SetFilter(PossibleFilters bridgeFilter)` / `void Filter(object tag, string term)`
Set search term, filter type, or field-specific filter term, then invoke `Filter()`.
- `void ClearAllFilters()`
Clears `_filterByField` dictionary.
- `void Sort(object o, bool bColumnClick)`
Sorts `ChannelList` by field (e.g., `GroupName`, `SerialNumber`, or `ROIIncludes[i]`). Supports toggling sort direction on repeated clicks.
- `void SelectAll(int roiIndex, bool selection)`
Sets `Checked` state of all `ROIIncludes[roiIndex]` checkboxes to `selection`.
- `void Activated()` / `void Cleanup()` / `Task CleanupAsync()` / `void Initialize()` / `Task InitializeAsync()`
Lifecycle stubs (no-op in source).
- `bool Validate(ref List<string> errors)`
Currently always returns `true`.
- `void OnPropertyChanged(string propertyName)`
Raises `PropertyChanged` event.
---
### 3. **Invariants**
- **Channel scrubbing on ROI assignment change**: When `RegionsOfInterest` is set, stale channel references (channels no longer present in `AllChannelsUnfiltered`) are removed from `roi.ChannelNames` and `roi.ChannelIds`. This is done by comparing against `AllChannelHash` (computed from `AllChannelsUnfiltered` with hardware/serial number normalization) and `allChannelIdHash`.
- **Channel ID parsing**: Older `.dts` files may have composite `ChannelId` strings (e.g., `"H3-3ch_0_2"`). `ParseChannelId()` extracts the trailing numeric ID; if parsing fails, returns `-1`.
- **Digital outputs excluded**: In `BuildChannelListFromGroupChannels()`, channels with `sd.IsDigitalOutput()` or `sd.IsTestSpecificDigitalOutput()` are skipped.
- **Filtering semantics**:
- `ChannelFilter()` uses *AND* logic across multiple filter fields (a channel must match *all* active filters).
- `ChannelSearch()` uses *OR* logic (a channel matches if *any* field contains the search term).
- `ROIIncludes` is excluded from search (commented as "not meaningful").
- **Hardware/serial normalization**: Channel descriptors use `RegionOfInterest.RemoveParentDASName()` and `RegionOfInterest.RemoveAssignedByIDFromHardwareString()` to normalize hardware strings.
- **Calibration caching**: `BuildChannelListFromGroupChannels()` caches sensor calibrations (`cals`) to avoid repeated DB lookups.
---
### 4. **Dependencies**
#### **Imports/Usings (External Dependencies)**
- **Prism**: `IRegionManager`, `IEventAggregator`, `InteractionRequest<T>`, `PopupWindowAction` (via `NotificationRequest`).
- **Unity**: `IUnityContainer`.
- **DTS Common Libraries**:
- `DTS.Common.Classes.Groups`, `DTS.Common.Enums`, `DTS.Common.Events.*`, `DTS.Common.Interface.*`, `DTS.Common.Utils`, `DTS.Common.Utilities.Logging.APILogger`.
- `DTS.SensorDB` (for `SensorsCollection`, `SensorCalibrationList`).
- `DTS.Serialization.SliceRaw` (for `PersistentChannel.GetIsoCode`).
- **System**: `BindingList<T>`, `ObservableCollection<T>`, `INotifyPropertyChanged`, `IComparer<T>`.
#### **Internal Dependencies**
- **Interfaces**:
- `IRegionOfInterestChannelsView`, `IRegionOfInterest`, `IGroupChannel`, `ITestChannel`, `IDASHardware`, `IGroup`, `ITestSetup`, `ITestSummary`.
- **Helper Types**:
- `ChannelSerialNumber`, `HardwareConstants`, `RegionOfInterest`, `DTS.Common.Constants.CURRENT_SUFFIX`.
- `ChannelEnabler`, `State`, `ColumnDescriptor`, `ChannelEnablerComparer` (defined in same file).
- **Events**:
- `RaiseNotification`, `BusyIndicatorChangeNotification`, `RegionOfInterestChannelsSelectedEvent`, `PageErrorEvent`.
#### **Depended Upon By**
- UI layer (`IRegionOfInterestChannelsView` implementations).
- Event subscribers (e.g., `RegionOfInterestChannelsSelectedEvent` listeners).
- Export/download workflows (`SetTest()`, `SetGroups()`).
---
### 5. **Gotchas**
- **`AllChannels` null handling**: `AllChannelSSNs` and `BuildChannelListFromSummary()` guard against `AllChannels` being `null` (e.g., when loaded via export tile), but `BuildChannelListFromGroupChannels()` does *not* check `AllChannels` for `null` before iteration (relies on `?? new ObservableCollection<>()` in `SetGroups()`).
- **`ROIIncludes` index mismatch**: In `CheckboxesOnListChanged`, `LastIndexChanged` is used to infer the ROI index, but this relies on the `ListChanged` events `NewIndex` matching the ROI index *at the time of change*. If `ChannelList` is reordered, this index may become stale.
- **`ParseChannelId` edge case**: If `parsedChannelId` is non-numeric (e.g., `"f9f0bfe8-afc4-4730-8045-8f1e45340573_0_8533"``"8533"` is numeric, but `"f9f0bfe8-afc4-4730-8045-8f1e45340573"` is not), `long.TryParse` fails and returns `-1`. This may cause ROI channel assignment to silently drop channels.
- **`UpdateChannelList` heuristic**: This method replaces a channel in `channelList` if its serial number matches *any* channel in `AllChannelsUnfiltered`. This may incorrectly overwrite channels if multiple channels share the same serial number (e.g., same sensor type on different hardware).
- **`IsDirty` unused**: The property is defined but never set to `true`, making it unreliable for change tracking.
- **Calibration fallback**: In `BuildChannelListFromGroupChannels()`, if `sd.Calibration?.Records?.Records?[0]?.EngineeringUnits` is `null`, `"N/A"` is used. However, `ChannelFilter()` for `DisplayUnits` uses a *separate* `GetLatestCalibrationBySerialNumber(sd)` call, which may yield different results if calibrations change mid-session.
- **`ChannelSearch` redundancy**: `ChannelSearch(GroupChannel)` and `ChannelSearch(ITestChannel)` have overlapping logic, but `ChannelSearch(GroupChannel)` skips `Fields.DisplayName` (commented as "redundant"), while `ChannelSearch(ITestChannel)` does not.
- **`ChannelEnablerComparer` sort stability**: Sorting is case-insensitive but not culture-aware (`StringComparison.InvariantCultureIgnoreCase`). May produce unexpected orderings for non-ASCII characters.
- **`SetTest()` assumes single test**: `SetTest()` loads `tsl[0]` unconditionally; if `tsl` is empty, this will throw `IndexOutOfRangeException`.
- **`ROIChannelEnabler` unused**: A `ROIChannelEnabler` class is defined but never instantiated or used in the source.
- **`IsBusy` thread affinity**: `OnBusyIndicatorNotification` subscribes with `ThreadOption.PublisherThread`, but `IsBusy` setter calls `OnPropertyChanged("IsBusy")`, which may raise `PropertyChanged` on the publisher thread (UI thread). If `IsBusy` is set from a background thread elsewhere, this could cause cross-thread exceptions (though not evident in current usage).
- **`AllChannelSSNs` for CSV export**: The comment `//13477 Crash when clicking + to add second ROI for CSV export` implies a known race condition or null-state issue when dynamically adding ROIs during export. The guard (`null != AllChannels && AllChannels.Any()`) mitigates but does not eliminate all failure modes.
- **No validation of ROI channel count**: `SelectAll(int roiIndex, bool selection)` checks bounds on `roiIndex`, but no validation ensures `roiIndex` corresponds to a valid ROI in `_regionsOfInterest` *after* `RegionsOfInterest` is reassigned.
- **`ChannelEnabler.LastIndexChanged` reset**: `LastIndexChanged` is set on `ListChanged`, but never reset. If multiple changes occur rapidly, it may retain an outdated index.
- **`ChannelFilter` for `SampleRate`**: Uses `ch.TestSampleRate.ToString()` (no format specifier), while `ChannelSearch` uses both `ToString()` and `ToString("N")`. Inconsistent formatting may cause filter mismatches.
- **`ChannelFilter` for `DisplayUnits`**: Uses `GetLatestCalibrationBySerialNumber(sd)` *without* caching, potentially causing repeated DB lookups in `Filter()` loops.
- **`ChannelFilter` for `DASSerialNumber`**: Uses `ch.HardwareChannel?.GetParentDAS().SerialNumber ?? "N/A"`, but `ChannelFilter(ITestChannel)` uses `ch.HardwareChannelName` directly (no `GetParentDAS()`), leading to inconsistent behavior.
- **`ChannelEnabler.Descriptor` format**: `Descriptor = chHardware + "\\" + serialNumber` assumes `chHardware` and `serialNumber` are non-null; if either is `null`, this may produce `"\\serial"` or `"hardware\\"`.
- **`RegionOfInterestChannelsSelectedEvent` publish**: Published in `CheckboxesOnListChanged` *after* updating `_regionsOfInterest[roiChangedIndex]`, but the event args use `_regionsOfInterest[roiChangedIndex]` *after* the list has been modified. If `roiChangedIndex` is stale (see *index mismatch*), this may publish stale ROI data.
- **`SetGroups()` phantom DAS assignment**: Logs an error for `ch.DASChannelIndex < 0`, but continues processing (does not skip the channel). May lead to inconsistent channel sample rates.
- **`BuildChannelListFromGroupChannels()` sensor lookup**: If `sd` is `null` after lookup, logs and skips the channel. However, `ch.SensorData` is *not* assigned in this case, so subsequent calls may repeat the lookup.
- **`ChannelEnabler.GetChannelName()`**: Returns `SerialNumber` as fallback for missing `UserChannelName`/`ISOChannelName`, but `AllChannelSSNs` uses `Hardware + "\\" + serialNumber`. Inconsistent naming may cause mismatches in ROI channel matching.
- **`ChannelEnablerComparer` null safety**: Compares `aValue?.ToString()` and `bValue?.ToString()`, but if both are `null`, returns `0` (equal). If only one is `null`, `string.Compare(null, "x")` returns `-1`, which may not be intuitive.
- **`ChannelEnabler.ROIIncludes` event wiring**: `ROIIncludes.ListChanged` is wired in constructor, but `ChannelEnabler` instances are recreated on every `ChannelList = ...` assignment. The old `BindingList<State>` may not be disposed, potentially causing memory leaks if event handlers are not properly detached (though `ListChanged` is not explicitly unhooked in setter).
- **`ChannelEnabler.LastIndexChanged` property**: Used only to trigger `OnPropertyChanged("LastIndexChanged")`, but no UI binding or logic consumes this property. Likely tech debt.
- **`RegionOfInterestChannelsViewModel` `Parent` field**: Stored as `object Parent` (not typed), and passed in `RegionOfInterestChannelsSelectedEventArgs`. No validation ensures `Parent` is of expected type.
- **`ChannelFilter` for `ROIIncludes` field**: The `Fields.ROIIncludes` case in `ChannelFilter` throws `ArgumentOutOfRangeException`. This field is *not* filterable (as intended), but the enum value is included in `Enum.GetValues`, so the switch must handle it (currently via `default: throw`).
- **`ChannelSearch` for `SampleRate`**: Uses `ch.SampleRateHz.ToString()` and `ToString("N")`, but `ToString("N")` includes thousand separators (e.g., `"1,000"`), which may not match user input like `"1000"`.
- **`ChannelEnabler` `GuidString`**: Generated via `Guid.NewGuid()` on every `ChannelList` assignment. Not persisted across sessions or used for cross-reference (only in `CheckboxesOnListChanged` for lookup). Likely a temporary ID with no semantic meaning.
- **`ChannelEnabler` `ChannelId`**: Set to `Convert.ToInt64(ch.Id)` (from `IGroupChannel.Id`), but `ch.Id` is a `string`. If `ch.Id` is non-numeric, `Convert.ToInt64` throws `FormatException`. *This is a critical risk.* (Note: `ParseChannelId` is used only for `ITestChannel.ChannelId`.)
- **`ChannelEnabler` `ChannelId` for `ITestChannel`**: Uses `ParseChannelId(ch.ChannelId)`, which may return `-1` for invalid IDs. No validation ensures `-1` is not a valid channel ID elsewhere.
- **`ChannelEnabler` `ChannelId` for `IGroupChannel`**: Uses `Convert.ToInt64(ch.Id)`, which may throw `FormatException` if `ch.Id` is non-numeric (e.g., GUID-based IDs). *This is inconsistent with `ITestChannel` handling and a likely source of crashes.*
- **`ChannelEnabler` `ChannelId` for ROI matching**: In `CheckboxesOnListChanged`, `ChannelId` is used to build `checkedIds`, but ROI `ChannelIds` may contain IDs from `ParseChannelId` (which may be `-1`), while `IGroupChannel.Id` may throw. This mismatch could cause ROI channel assignments to fail silently.
- **`ChannelEnabler` `ChannelId` for `AllChannelSSNs`**: Uses `ch.Id` (from `GroupChannel`), but `AllChannelSSNs` does *not* use `ChannelId`—it uses `Hardware + "\\" + serialNumber`. Thus, ROI channel matching relies on `ChannelId`, but export relies on string-based descriptors. Inconsistency may cause mismatches.
- **`ChannelEnabler` `ChannelId` for `ParseChannelId`**: The `ParseChannelId` method is *only* used for `ITestChannel.ChannelId`, but `ChannelEnabler.ChannelId` is set from `IGroupChannel.Id` via `Convert.ToInt64(ch.Id)`. This means ROI channel IDs may be inconsistent between test metadata (`ParseChannelId`) and group channels (`Convert.ToInt64`).
- **`ChannelEnabler` `ChannelId` for `ChannelFilter`**: The `Channel