This commit is contained in:
2026-04-17 14:55:32 -04:00
commit bc3ac1d4c9
18017 changed files with 4371742 additions and 0 deletions

View File

@@ -0,0 +1,420 @@
# DataPRO Architecture Document
## System Overview
DataPRO is a WPF-based desktop application for data acquisition system (DAS) management and sensor data analysis. It provides:
- **Hardware Management**: Configuration and control of DTS data acquisition hardware (SLICE, TDAS, Ribeye devices)
- **Sensor Configuration**: Management of sensor databases, calibrations, and channel setups
- **Test Setup Management**: Creation and management of test configurations
- **Real-time Data Collection**: Live streaming and visualization of sensor data
- **Data Review & Analysis**: Post-test data visualization, filtering, and export
- **Reporting**: PSD (Power Spectral Density) reports and pedestrian/head impact reports
The application follows a modular architecture using Prism Framework with Unity dependency injection, enabling loose coupling between components and supporting feature extensibility.
---
## Solution Structure
| Project | Path | Purpose |
|---------|------|---------|
| **DataPRO** | `DataPRO/DataPRO/` | Main WPF application (shell, bootstrapper, main window) |
| **DASFactory** | `DataPRO/DASFactory/` | Hardware discovery and DAS device management factory |
| **IService** | `DataPRO/IService/` | Core service interfaces and SLICE/TDAS service implementations |
| **DbAPI** | `DataPRO/DbAPI/` | Database API layer for SQL Server operations |
| **SensorDB** | `DataPRO/SensorDB/` | Sensor database models and calibration data |
| **DASFactoryDb** | `DataPRO/DASFactoryDb/` | Database wrapper for DAS factory operations |
| **ICommand** | `DataPRO/ICommand/` | Command pattern interfaces for hardware communication |
| **SLICECommands** | `DataPRO/SLICECommands/` | SLICE device command implementations |
| **TDASCommands** | `DataPRO/TDASCommands/` | TDAS device command implementations |
| **Reports** | `DataPRO/Reports/` | Report generation functionality |
| **DTS.Viewer** | `DTS Viewer/DTS.Viewer/` | Data visualization and analysis modules |
| **DTS.Common** | `Common/DTS.Common/` | Core interfaces, events, base classes |
| **DTS.Common.DataModel** | `Common/DTS.Common.DataModel/` | Data models (Group, Sensor, TestGraph) |
| **DTS.Common.Storage** | `Common/DTS.Common.Storage/` | Storage and serialization utilities |
| **DTS.Common.DAS.Concepts** | `Common/DTS.Common.DAS.Concepts/` | DAS domain concepts and interfaces |
| **[Module Projects]** | `DataPRO/Modules/*/` | 30+ Prism modules for UI features |
---
## Architecture Patterns
### MVVM (Model-View-ViewModel)
All UI modules follow strict MVVM pattern:
```
View (XAML) ←→ ViewModel (INotifyPropertyChanged) ←→ Model (Business Logic)
```
Example from `SensorsListModule.cs:42-43`:
```csharp
_unityContainer.RegisterType<ISensorsListView, SensorsListView>();
_unityContainer.RegisterType<ISensorsListViewModel, SensorsListViewModel>();
```
### Prism Framework
The application uses Prism 8.x for:
- **Modularity**: `IModule` implementations for feature isolation
- **Region Management**: Dynamic view composition in UI regions
- **Event Aggregation**: Pub/Sub messaging via `IEventAggregator`
- **Dependency Injection**: Unity container integration
Bootstrapper configuration (`Bootstrapper.cs:179-220`):
```csharp
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
moduleCatalog.AddModule<StatusAndProgressBar.StatusAndProgressBarModule>();
moduleCatalog.AddModule<DatabaseServices.DatabaseServicesModule>();
moduleCatalog.AddModule<SensorsList.SensorsListModule>();
// ... 30+ modules registered
}
```
### Unity Dependency Injection
Services and ViewModels are registered with Unity:
```csharp
// Singleton registration
_unityContainer.RegisterType<IMainViewModel, MainViewModel>(
new ContainerControlledLifetimeManager());
// Transient registration
_unityContainer.RegisterType<ISensorsListView, SensorsListView>();
```
### Event Aggregation Pattern
Inter-module communication uses Prism's `PubSubEvent<T>`:
Defined in `Common/DTS.Common/Events/`:
- `TestEvent` - Test lifecycle events
- `SensorChangedEvent` - Sensor modifications
- `HardwareSavedEvent` - Hardware configuration saves
- `GroupChannelsChangedEvent` - Channel group updates
- `DASConfigurationEvent` - DAS configuration changes
Example event from `TestEvent.cs:11`:
```csharp
public class TestEvent : PubSubEvent<TestEventArg> { }
```
---
## Module Dependency Graph
```mermaid
graph TB
subgraph "Main Application"
DataPRO[DataPRO Shell]
end
subgraph "Core Services"
DASFactory[DASFactory]
IService[IService]
DbAPI[DbAPI]
SensorDB[SensorDB]
end
subgraph "Common Libraries"
DTS_Common[DTS.Common]
DTS_DataModel[DTS.Common.DataModel]
DTS_Storage[DTS.Common.Storage]
DTS_DASConcepts[DTS.Common.DAS.Concepts]
end
subgraph "Hardware Commands"
SLICECmds[SLICECommands]
TDASCmds[TDASCommands]
RibeyeCmds[RibeyeCommands]
end
subgraph "UI Modules"
SensorsList[SensorsList Module]
HardwareList[HardwareList Module]
GroupList[GroupList Module]
TestSetupsList[TestSetupsList Module]
RealtimeModule[RealtimeModule]
DatabaseServices[DatabaseServices Module]
DTSViewer[DTS.Viewer]
end
DataPRO --> DASFactory
DataPRO --> IService
DataPRO --> DbAPI
DataPRO --> SensorsList
DataPRO --> HardwareList
DataPRO --> GroupList
DataPRO --> TestSetupsList
DataPRO --> RealtimeModule
DataPRO --> DatabaseServices
DataPRO --> DTSViewer
DASFactory --> IService
DASFactory --> SLICECmds
DASFactory --> TDASCmds
IService --> DTS_DASConcepts
IService --> DTS_Storage
IService --> SLICECmds
IService --> TDASCmds
DbAPI --> SensorDB
SensorsList --> DTS_Common
SensorsList --> DTS_DataModel
HardwareList --> DTS_Common
GroupList --> DTS_DataModel
TestSetupsList --> DTS_Common
DTS_Common --> DTS_DataModel
DTS_DataModel --> DTS_Storage
DTS_Storage --> DTS_DASConcepts
```
---
## Data Flow
### 1. Hardware Discovery Flow
```
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ USB/ │────▶│ DASFactory │────▶│ IService │
│ Ethernet │ │ Discovery │ │ (Slice) │
└─────────────┘ └──────────────┘ └─────────────┘
│ │
▼ ▼
┌──────────────┐ ┌─────────────┐
│ IEventAggr. │────▶│ Modules │
│ Publish │ │ (Hardware) │
└──────────────┘ └─────────────┘
```
The `DASFactory` (`DASFactory.cs:110`) handles:
- USB device enumeration (WinUSB, CDC)
- Ethernet multicast discovery
- Serial port detection
### 2. Test Configuration Flow
```
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Sensors │────▶│ Groups │────▶│ TestSetup │
│ Module │ │ Module │ │ Module │
└─────────────┘ └──────────────┘ └─────────────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ SensorDB │ │ Group.cs │ │ DbAPI │
│ (Models) │ │ (DataModel) │ │ (Persist) │
└─────────────┘ └──────────────┘ └─────────────┘
```
### 3. Real-time Data Flow
```
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ DAS HW │────▶│ IService │────▶│ Realtime │
│ (SLICE) │ │ (Stream) │ │ Module │
└─────────────┘ └──────────────┘ └─────────────┘
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ UDP Stream │ │ Charts │ │ Storage │
│ (TMATs) │ │ (Display) │ │ (DASDB) │
└─────────────┘ └─────────────┘ └─────────────┘
```
### 4. Database Access Pattern
```
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Modules │────▶│ DbAPI │────▶│ SQL Server │
│ (Views) │ │ (Static API) │ │ (Database) │
└─────────────┘ └──────────────┘ └─────────────┘
```
`DbAPI.cs:237-373` exposes static properties for each domain:
```csharp
public static IConnections Connections { get; }
public static IDataRecorders DAS { get; }
public static ISensors Sensors { get; }
public static IGroups Groups { get; }
public static ITestSetups TestSetups { get; }
```
---
## Extension Points
### Adding a New Module
1. Create project in `DataPRO/Modules/[Category]/[ModuleName]/`
2. Create Module class implementing `IModule`:
```csharp
[Module(ModuleName = "MyModule")]
public class MyModule : IModule
{
public void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterSingleton<IMyView, MyView>();
containerRegistry.RegisterSingleton<IMyViewModel, MyViewModel>();
}
}
```
3. Register in `Bootstrapper.cs:ConfigureModuleCatalog()`
4. Define region in `eAssemblyRegion` enum
5. Add module to solution folder structure
### Adding New Hardware Support
1. Create commands in `DataPRO/[Device]Commands/`
2. Add device handling in `DASFactory.cs:[Device].cs` partial class
3. Create service class in `IService/Classes/[Device]Service.cs`
4. Implement `IDASCommunication` interface
### Adding New Events
1. Create event class in `Common/DTS.Common/Events/`:
```csharp
public class MyEvent : PubSubEvent<MyEventArgs> { }
```
2. Create args class:
```csharp
public class MyEventArgs
{
public string Data { get; set; }
}
```
3. Publish from ViewModel:
```csharp
_eventAggregator.GetEvent<MyEvent>().Publish(new MyEventArgs { Data = "value" });
```
4. Subscribe in target module:
```csharp
_eventAggregator.GetEvent<MyEvent>().Subscribe(OnMyEvent);
```
---
## Key Technologies
### Frameworks & Libraries
| Technology | Version | Purpose |
|------------|---------|---------|
| .NET Framework | 4.8 | Runtime |
| Prism | 8.x | MVVM framework, modularity |
| Unity | 2.x | Dependency injection container |
| Stateless | 4.2.1 | State machine for DAS operations |
| Newtonsoft.Json | Latest | JSON serialization |
| ComponentOne WPF | 4.0.20203 | Charts and UI controls |
| Xceed WPF Toolkit | 3.0 | Extended WPF controls |
### Communication Protocols
- **USB**: WinUSB, CDC for SLICE devices
- **Ethernet**: TCP/UDP, multicast discovery
- **Serial**: RS-232 for legacy TDAS
- **CAN**: CAN-FD for SLICE Pro
### Database
- **SQL Server**: Primary data storage
- **Stored Procedures**: Versioned SPs for DB compatibility
- **DASFactoryDb**: Local SQLite for device caching
---
## Gotchas and Tech Debt
### Architecture Issues
1. **Static DbAPI Access** (`DbAPI.cs:237-373`)
- Static properties make testing difficult
- No interface abstraction for database layer
- Recommendation: Convert to injected `IDbAPI` service
2. **App.xaml.cs God Class** (4620 lines)
- Contains licensing, test state, UI logic, database operations
- Violates Single Responsibility Principle
- Recommendation: Extract into separate services
3. **Mixed Module Initialization**
- Some modules use `Initialize()`, others `RegisterTypes()`
- Example: `SensorsListModule.cs:39` vs `DatabaseServicesModule.cs:38`
### Threading Concerns
1. **UI Thread Dependencies** (`App.xaml.cs:128-161`)
- `StartTest()` method called from UI, affects global state
- `RunTestVariables` static class for test configuration
2. **Device Queue Processing** (`DASFactory.cs:127`)
```csharp
private BlockingCollection<Tuple<QueueActions, DeviceHandling>> _queueActionPerDevice;
```
### Database Versioning
- Stored procedures are versioned (`DbAPI.cs:34-97`)
- Client and server DB versions must be negotiated
- SP version caching implemented but fragile
### State Machine Complexity
IService uses Stateless for DAS operations (`IService/StateMachine/`):
- States: Prepare, Configure, Arm, Realtime, Download, Diagnose
- Complex state transitions can be difficult to debug
### Legacy Code Patterns
1. **WinForms Interop** in some modules
2. **BackgroundWorker** instead of async/await in places
3. **Mixed logging** - custom `APILogger` and `TextLogger` classes
### Missing Unit Tests
Limited test coverage in:
- `DataPRO/UnitTest/` - only database tests present
- No ViewModel tests
- No service layer tests
---
## Key File References
| Component | File | Lines |
|-----------|------|-------|
| Bootstrapper | `DataPRO/DataPRO/Bootstrapper.cs` | 223 |
| Application Entry | `DataPRO/DataPRO/App.xaml.cs` | 4620 |
| DAS Factory | `DataPRO/DASFactory/DASFactory.cs` | 1712 |
| SLICE Service | `DataPRO/IService/Classes/SLICEService/SLICE Service.cs` | 480+ |
| Database API | `DataPRO/DbAPI/DbAPI.cs` | 375 |
| Sensor Database | `DataPRO/SensorDB/SensorDB.cs` | 983 |
| Group Model | `Common/DTS.Common.DataModel/Group.cs` | 2452 |
| Events | `Common/DTS.Common/Events/*.cs` | 129 events |
---
## Module Summary
| Category | Modules |
|----------|---------|
| **Prepare** | SensorsList, HardwareList, GroupList, GroupChannelList, TestSetupsList, CachedItemsList, ChannelCodes |
| **Configure** | SensorSettingsModule, SoftwareFilters, AddEditHardware, GroupImport, Diagnostics |
| **System** | DatabaseServices, DBImportExport, RealtimeSettings, TestSettings, ISOSettings, QASettings, UISettings, TablesSettings, PowerAndBattery |
| **Realtime** | RealtimeModule, StatusAndProgressBar |
| **Viewer** | DTS.Viewer, Graph, GraphList, Filter, ChartOptions, TestModification, TestSummaryList, Navigation, AddCalculatedChannel, ViewerSettings |
| **Reports** | PSDReport, PSDReportSettings, PSDReportResults, PedestrianAndHeadReports |
| **ISO** | ExtraProperties, RegionOfInterestChannels |

View File

@@ -0,0 +1,339 @@
# Data Access Pattern
## When to Use
- Retrieving or storing data in SQL Server database
- Implementing data layer for business entities
- Creating CRUD operations for domain objects
## Architecture Overview
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ ViewModel │────>│ DbAPI │────>│ SQL Server │
│ (UI Layer) │ │ (Data Layer) │ │ (Database) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
├── Connections/
├── Sensors/
├── Groups/
├── TestSetups/
└── Channels/
```
## Files to Create/Modify
### Structure
```
DbAPI/
├── DbAPI.cs (Main facade class)
├── Connections/
│ └── ConnectionManager.cs (Connection handling)
├── {Entity}/
│ └── {Entity}.cs (Data access implementation)
└── Errors/
└── ErrorCodes.cs (Error code constants)
```
## Code Templates
### 1. Interface Definition
**File:** `DTS.Common/Interface/{Entity}/I{Entity}.cs`
```csharp
using DTS.Common.Interface.Database;
namespace DTS.Common.Interface.{Entity}
{
public interface I{Entity}
{
/// <summary>
/// Gets all {entity} records
/// </summary>
/// <param name="user">User making the request</param>
/// <param name="connection">Database connection details</param>
/// <param name="records">Output array of records</param>
/// <returns>Error code (0 = success)</returns>
ulong {Entity}Get(IUserDbRecord user, IConnectionDetails connection,
out I{Entity}Record[] records);
/// <summary>
/// Adds a new {entity} record
/// </summary>
ulong {Entity}Add(IUserDbRecord user, IConnectionDetails connection,
I{Entity}Record record, out int newId);
/// <summary>
/// Updates an existing {entity} record
/// </summary>
ulong {Entity}Update(IUserDbRecord user, IConnectionDetails connection,
I{Entity}Record record);
/// <summary>
/// Deletes a {entity} record
/// </summary>
ulong {Entity}Delete(IUserDbRecord user, IConnectionDetails connection, int id);
}
}
```
### 2. Data Access Implementation
**File:** `DbAPI/{Entity}/{Entity}.cs`
```csharp
using DbAPI.Connections;
using DbAPI.Errors;
using DbAPI.Logging;
using DTS.Common.Interface.Database;
using DTS.Common.Interface.{Entity};
using DTS.Common.Utilities.Logging;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
namespace DbAPI.{Entity}
{
internal class {Entity} : I{Entity}
{
private const int DB_VERSION_{FEATURE} = 100;
public ulong {Entity}Get(IUserDbRecord user, IConnectionDetails connection,
out I{Entity}Record[] records)
{
records = new I{Entity}Record[0];
var list = new List<I{Entity}Record>();
if (!DbAPI.Connections.IsUserLoggedIn(user, connection))
{
return ErrorCodes.ERROR_ACCESS_DENIED;
}
var ret = ConnectionManager.GetSqlCommand(connection, out var cmd, "sp_{Entity}Get");
if (ret != ErrorCodes.ERROR_SUCCESS) return ret;
try
{
cmd.CommandType = CommandType.StoredProcedure;
var reader = cmd.ExecuteReader();
while (reader.Read())
{
list.Add(new {Entity}Record(reader));
}
records = list.ToArray();
return ErrorCodes.ERROR_SUCCESS;
}
catch (Exception ex)
{
LogManager.Log(TraceEventType.Error, LogManager.LogEvents.{Entity},
$"sp_{Entity}Get failed: {ex.Message}");
return ErrorCodes.ERROR_UNKNOWN;
}
finally
{
cmd.Connection.Dispose();
}
}
public ulong {Entity}Add(IUserDbRecord user, IConnectionDetails connection,
I{Entity}Record record, out int newId)
{
newId = 0;
if (!DbAPI.Connections.IsUserLoggedIn(user, connection))
return ErrorCodes.ERROR_ACCESS_DENIED;
var ret = ConnectionManager.GetSqlCommand(connection, out var cmd, "sp_{Entity}Add");
if (ret != ErrorCodes.ERROR_SUCCESS) return ret;
try
{
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("@Name", SqlDbType.NVarChar) { Value = record.Name });
var idParam = new SqlParameter("@Id", SqlDbType.Int)
{
Direction = ParameterDirection.Output
};
cmd.Parameters.Add(idParam);
cmd.ExecuteNonQuery();
newId = (int)idParam.Value;
return ErrorCodes.ERROR_SUCCESS;
}
catch (Exception ex)
{
LogManager.Log(TraceEventType.Error, LogManager.LogEvents.{Entity},
$"sp_{Entity}Add failed: {ex.Message}");
return ErrorCodes.ERROR_UNKNOWN;
}
finally
{
cmd.Connection.Dispose();
}
}
protected void AddNullableIntParameter(SqlCommand cmd, string paramName, int? value)
{
cmd.Parameters.Add(new SqlParameter(paramName, SqlDbType.Int)
{
Value = value.HasValue ? (object)value.Value : DBNull.Value
});
}
protected void AddNullableStringParameter(SqlCommand cmd, string paramName, string value)
{
cmd.Parameters.Add(new SqlParameter(paramName, SqlDbType.NVarChar)
{
Value = string.IsNullOrEmpty(value) ? (object)DBNull.Value : value
});
}
}
}
```
### 3. Register in DbAPI.cs Facade
**File:** `DbAPI/DbAPI.cs`
```csharp
private readonly {Entity}.{Entity} _{entity} = new {Entity}.{Entity}();
public static I{Entity} {Entity}
{
get => _instance._{entity};
}
```
## Examples from Codebase
### Example 1: DbAPI Facade
**File:** `DataPRO/DbAPI/DbAPI.cs:23`
```csharp
public class DbAPI
{
private static readonly DbAPI _instance = new DbAPI();
private readonly ConnectionManager _connectionManager = new ConnectionManager();
public static IConnections Connections => _instance._connectionManager;
private readonly Sensors.Sensors _sensors = new Sensors.Sensors();
public static ISensors Sensors => _instance._sensors;
private readonly TestSetups.TestSetups _testSetups = new TestSetups.TestSetups();
public static ITestSetups TestSetups => _instance._testSetups;
private readonly Groups.Groups _groups = new Groups.Groups();
public static IGroups Groups => _instance._groups;
}
```
### Example 2: Sensor Data Access
**File:** `DataPRO/DbAPI/Sensors/Sensors.cs:26`
```csharp
internal class Sensors : ISensors
{
public ulong SensorsAnalogDiagnosticsGet(IUserDbRecord user, IConnectionDetails connection,
long? Id, long? diagnosticRunId, int? sensorId, string sensorSerialNumber,
out IDiagnosticEntry[] records)
{
records = new IDiagnosticEntry[0];
var list = new List<IDiagnosticEntry>();
if (!DbAPI.Connections.IsUserLoggedIn(user, connection))
return ErrorCodes.ERROR_ACCESS_DENIED;
var ret = ConnectionManager.GetSqlCommand(connection, out var cmd, "sp_AnalogDiagnosticsGet");
if (ret != ErrorCodes.ERROR_SUCCESS) return ret;
try
{
AddNullableBigIntParameter(cmd, "@Id", Id);
AddNullableIntParameter(cmd, "@SensorId", sensorId);
var reader = cmd.ExecuteReader();
while (reader.Read())
{
list.Add(new DiagnosticEntry(reader));
}
records = list.ToArray();
return ErrorCodes.ERROR_SUCCESS;
}
catch (Exception ex)
{
LogManager.Log(TraceEventType.Error, LogManager.LogEvents.Sensors,
$"sp_AnalogDiagnosticsGet failed, {ex.Message}");
return ErrorCodes.ERROR_UNKNOWN;
}
finally
{
cmd.Connection.Dispose();
}
}
}
```
### Example 3: Connection Manager Usage
**File:** `DataPRO/DbAPI/DbAPI.cs:104`
```csharp
public static ulong GetDatabaseVersion(IConnectionDetails connection, out int serverDbVersion)
{
serverDbVersion = 0;
var ret = ConnectionManager.GetSqlCommand(connection, out var cmd, "sp_DbVersionGet");
if (ret != ErrorCodes.ERROR_SUCCESS) return ret;
try
{
cmd.CommandType = CommandType.StoredProcedure;
var reader = cmd.ExecuteReader();
var dbVersionsList = new List<int>();
while (reader.Read())
{
var version = Convert.ToInt32(reader["Version"]);
dbVersionsList.Add(version);
}
serverDbVersion = dbVersionsList.Max();
return ErrorCodes.ERROR_SUCCESS;
}
finally
{
cmd.Connection.Dispose();
}
}
```
### Example 4: Stored Procedure Version Check
**File:** `DataPRO/DbAPI/DbAPI.cs:57`
```csharp
public static ulong GetStoredProcedureToUseCached(IConnectionDetails connection,
string storedProcedure, int clientDbVersion, out int storedProcedureVersionToUse)
{
lock (StoredProcedureLock)
{
if (_spLookup.ContainsKey(storedProcedure))
{
var match = _spLookup[storedProcedure].Find(sp =>
sp.ClientVersion == clientDbVersion && sp.DbVersion == connection.ConnectionDbVersion);
if (null != match)
{
storedProcedureVersionToUse = match.StoredProcedureVersion;
return ErrorCodes.ERROR_SUCCESS;
}
}
}
// ... determine version and cache it
}
```
## Common Mistakes to Avoid
1. **Not disposing SqlCommand connection** - Memory leak; always use `finally { cmd.Connection.Dispose(); }`
2. **Missing authentication check** - Always verify `IsUserLoggedIn(user, connection)` first
3. **Hardcoded stored procedure names** - Consider versioning for DB migrations
4. **Not handling DBNull** - Use nullable parameters with DBNull.Value conversion
5. **Swallowing exceptions** - Always log errors before returning error code
6. **Using dynamic SQL** - Always use parameterized stored procedures
7. **Not checking DB version** - Some features require minimum DB version
8. **Returning null instead of empty array** - Initialize arrays as `new T[0]`
9. **Missing error output parameter** - Some SPs require `@ErrorNumber` output
10. **Not caching SP version lookups** - Use `GetStoredProcedureToUseCached()` for performance

View File

@@ -0,0 +1,434 @@
# Import/Export Pattern
## When to Use
- Importing sensor data from external files (CSV, XML, SIF, etc.)
- Importing test setups and group configurations
- Exporting data to external formats
- Migrating data between database versions
## Architecture Overview
```
┌────────────────────────────────────────────────────────────────────────┐
│ Import Wizard Flow │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Options │───>│ Preview │───>│ Import │ │
│ │ View │ │ View │ │ View │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ v v v │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Shared Import ViewModel │ │
│ │ - SourceFiles │ │
│ │ - ParseSourceFiles() │ │
│ │ - Validate() │ │
│ │ - Import() │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ EventAggregator Events │ │
│ │ - TTSImportReadFileFinishedEvent │ │
│ │ - TTSImportTestSetupChangedEvent │ │
│ │ - CustomChannelImportEvent │ │
│ └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────┘
```
## Files to Create/Modify
### Structure
```
Modules/{Module}/
├── {Module}Import/
│ ├── {Module}ImportModule.cs
│ ├── View/
│ │ ├── {Module}ImportOptionsView.xaml
│ │ ├── {Module}ImportPreviewView.xaml
│ │ └── {Module}ImportImportView.xaml
│ └── ViewModel/
│ └── {Module}ImportViewModel.cs
Common/DTS.CommonCore/
├── Events/{Module}Import/
│ └── {Event}Events.cs
└── Interface/{Module}/
└── I{Module}ImportViewModel.cs
```
## Code Templates
### 1. Import ViewModel
```csharp
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq;
using Prism.Events;
using Prism.Regions;
using Unity;
using DTS.Common.Interface.{Module};
using DTS.Common.Interactivity;
using DTS.Common.Events;
namespace {Module}Import
{
[Export(typeof(I{Module}ImportOptionsView))]
[Export(typeof(I{Module}ImportPreviewView))]
[Export(typeof(I{Module}ImportImportView))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class {Module}ImportViewModel : I{Module}ImportViewModel
{
#region Views
public I{Module}ImportOptionsView ImportOptionsView { get; set; }
public I{Module}ImportPreviewView ImportPreviewView { get; set; }
public I{Module}ImportImportView ImportView { get; set; }
#endregion
private IEventAggregator _eventAggregator { get; set; }
private IRegionManager _regionManager;
private IUnityContainer UnityContainer { get; set; }
public InteractionRequest<Notification> NotificationRequest { get; private set; }
public InteractionRequest<Confirmation> ConfirmationRequest { get; private set; }
#region Properties
public string[] SourceFiles { get; set; }
public List<ImportItem> ParsedItems { get; set; }
public List<ImportError> Errors { get; set; }
public bool HasErrors => Errors?.Any() == true;
#endregion
public {Module}ImportViewModel(
I{Module}ImportOptionsView optionsView,
I{Module}ImportPreviewView previewView,
I{Module}ImportImportView importView,
IRegionManager regionManager,
IEventAggregator eventAggregator,
IUnityContainer unityContainer)
{
ImportOptionsView = optionsView;
ImportOptionsView.DataContext = this;
ImportPreviewView = previewView;
ImportPreviewView.DataContext = this;
ImportView = importView;
ImportView.DataContext = this;
NotificationRequest = new InteractionRequest<Notification>();
ConfirmationRequest = new InteractionRequest<Confirmation>();
_eventAggregator = eventAggregator;
UnityContainer = unityContainer;
_regionManager = regionManager;
_eventAggregator.GetEvent<RaiseNotification>().Subscribe(OnRaiseNotification);
}
/// <summary>
/// Parse source files and extract data
/// </summary>
public void ParseSourceFiles()
{
ParsedItems = new List<ImportItem>();
Errors = new List<ImportError>();
foreach (var file in SourceFiles ?? new string[0])
{
try
{
var lines = File.ReadAllLines(file);
for (var i = 0; i < lines.Length; i++)
{
var line = lines[i];
if (string.IsNullOrWhiteSpace(line)) continue;
var item = ParseLine(line, file, i);
if (item != null)
{
ParsedItems.Add(item);
}
}
}
catch (Exception ex)
{
Errors.Add(new ImportError
{
File = file,
Message = ex.Message
});
}
}
OnPropertyChanged("ParsedItems");
OnPropertyChanged("HasErrors");
}
private ImportItem ParseLine(string line, string file, int lineNumber)
{
var fields = line.Split(',');
// Validate required fields
if (fields.Length < 3)
{
Errors.Add(new ImportError
{
File = file,
Line = lineNumber,
Message = "Insufficient fields"
});
return null;
}
return new ImportItem
{
Name = fields[0],
Value = fields[1],
Type = fields[2]
};
}
/// <summary>
/// Execute the import
/// </summary>
public void Import()
{
var successCount = 0;
var errors = new List<string>();
foreach (var item in ParsedItems)
{
try
{
// Call DbAPI to insert
var result = DbAPI.DbAPI.{Entity}.{Entity}Add(
CurrentUser.User,
ConnectionManager.CurrentConnection,
item.ToRecord(),
out int newId);
if (result == ErrorCodes.ERROR_SUCCESS)
{
successCount++;
}
else
{
errors.Add($"Failed to import {item.Name}: Error {result}");
}
}
catch (Exception ex)
{
errors.Add($"Exception importing {item.Name}: {ex.Message}");
}
}
if (errors.Any())
{
NotificationRequest.Raise(new Notification
{
Title = "Import Completed with Errors",
Content = $"{successCount} items imported successfully.\n{errors.Count} errors occurred."
});
}
else
{
NotificationRequest.Raise(new Notification
{
Title = "Import Successful",
Content = $"{successCount} items imported successfully."
});
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
// IBaseViewModel implementation...
public bool IsBusy { get; set; }
public bool IsDirty => false;
public bool IsMenuIncluded { get; set; }
public bool IsNavigationIncluded { get; set; }
public void Initialize() { }
public void Initialize(object parameter) { }
public Task InitializeAsync() => Task.CompletedTask;
public Task InitializeAsync(object parameter) => Task.CompletedTask;
public void Activated() { }
public void Cleanup() { }
public Task CleanupAsync() => Task.CompletedTask;
}
}
```
### 2. Import Event Definitions
**File:** `Common/DTS.CommonCore/Events/{Module}Import/{Event}Event.cs`
```csharp
using Prism.Events;
using DTS.Common.Base;
namespace DTS.Common.Events.{Module}Import
{
/// <summary>
/// Event fired when import file parsing is complete
/// </summary>
public class {Module}ImportFinishedEvent : PubSubEvent<IImportData> { }
/// <summary>
/// Event fired when an item is imported
/// </summary>
public class {Module}ItemImportedEvent : PubSubEvent<ImportedItemEventArgs> { }
public class ImportedItemEventArgs
{
public string Name { get; set; }
public int NewId { get; set; }
public bool Success { get; set; }
public string Error { get; set; }
}
}
```
### 3. File Parser Pattern
```csharp
namespace DTS.Common.Import.Parsers
{
public interface IParseImport
{
string[] SupportedExtensions { get; }
bool CanParse(string filePath);
IImportData Parse(string filePath);
}
public class CSVParser : IParseImport
{
public string[] SupportedExtensions => new[] { ".csv" };
public bool CanParse(string filePath) =>
Path.GetExtension(filePath).Equals(".csv", StringComparison.OrdinalIgnoreCase);
public IImportData Parse(string filePath)
{
var data = new ImportData();
var lines = File.ReadAllLines(filePath);
// Skip header if present
for (int i = 1; i < lines.Length; i++)
{
var fields = lines[i].Split(',');
// Parse fields...
}
return data;
}
}
}
```
## Examples from Codebase
### Example 1: GroupImportViewModel
**File:** `DataPRO/Modules/Groups/GroupImport/ViewModel/GroupImportViewModel.cs:36`
```csharp
[Export(typeof(IGroupImportOptionsView))]
[Export(typeof(IGroupImportPreviewView))]
[Export(typeof(IGroupImportImportView))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class GroupImportViewModel : IGroupImportViewModel
{
public IGroupImportOptionsView ImportOptionsView { get; set; }
public IGroupImportPreviewView ImportPreviewView { get; set; }
public IGroupImportImportView ImportView { get; set; }
public GroupImportViewModel(
IGroupImportOptionsView optionsView,
IGroupImportPreviewView previewView,
IGroupImportImportView importView,
IRegionManager regionManager,
IEventAggregator eventAggregator,
IUnityContainer unityContainer)
{
ImportView = importView;
ImportView.DataContext = this;
// ...
}
public void ParseSourceFiles(string userTags)
{
var groups = new List<GroupGRPImportGroup>();
foreach (var file in SourceFiles)
{
var lines = System.IO.File.ReadAllLines(file);
// Parse group data...
}
}
}
```
### Example 2: TTS Import Events
**File:** `Common/DTS.CommonCore/Events/TTSImport/TTSImportReadFileFinishedEvent.cs:13`
```csharp
public class TTSImportReadFileFinishedEvent : CompositePresentationEvent<ITTSSetup> { }
```
**File:** `Common/DTS.CommonCore/Events/TTSImport/TTSImportTestSetupChangedEvent.cs:13`
```csharp
public class TTSImportTestSetupChangedEvent : CompositePresentationEvent<ITTSSetup> { }
```
### Example 3: Sensor Importer Base Class
**File:** `DataPRO/DataPRO/Controls/Sensors and models/Classes/SensorTestSetupImporter.cs:40`
```csharp
public abstract class SensorTestSetupImporter : BasePropertyChanged, ISensorTestSetupImporter
{
public abstract string[] SupportedExtensions { get; }
public abstract string FileFilter { get; }
public abstract ISensorData[] ParseImport(string[] files, ImportOptions options);
public abstract void ImportSensors(ISensorData[] sensors, IConnectionDetails connection);
}
```
### Example 4: CSVImporter Implementation
**File:** `DataPRO/DataPRO/Controls/Sensors and models/Classes/CSVImporter.cs:30`
```csharp
public class CSVImporter : SensorTestSetupImporter
{
public override string[] SupportedExtensions => new[] { ".csv" };
public override string FileFilter => "CSV Files (*.csv)|*.csv";
public override ISensorData[] ParseImport(string[] files, ImportOptions options)
{
var sensors = new List<ISensorData>();
foreach (var file in files)
{
var lines = File.ReadAllLines(file);
for (int i = 1; i < lines.Length; i++) // Skip header
{
var fields = lines[i].Split(',');
sensors.Add(new SensorData
{
SerialNumber = fields[0],
Description = fields[1],
// ...
});
}
}
return sensors.ToArray();
}
}
```
## Common Mistakes to Avoid
1. **Not validating before import** - Always parse and preview before executing import
2. **Missing error collection** - Track errors per line/item, not just global exceptions
3. **Blocking UI during import** - Use async/await or background threads
4. **Not handling file encoding** - Use proper encoding (UTF-8, ANSI) for international files
5. **Hardcoded column indexes** - Use header row to find columns by name
6. **Not reporting progress** - For large imports, report progress percentage
7. **Missing transaction rollback** - Consider wrapping imports in transactions
8. **Not checking for duplicates** - Validate against existing database records
9. **Silent failures** - Always notify user of failed imports with specific error messages
10. **Not supporting cancel** - Allow user to cancel long-running imports

View File

@@ -0,0 +1,275 @@
# MVVM Pattern
## When to Use
- All UI development in DataPRO
- Creating views that need to be testable
- Separating business logic from presentation
## Files to Create/Modify
### Standard Structure
```
{Module}/
├── Interface/
│ ├── I{ViewModelName}.cs (in DTS.Common or DTS.CommonCore)
│ └── I{ViewName}.cs (in DTS.Common or DTS.CommonCore)
├── View/
│ ├── {ViewName}.xaml
│ └── {ViewName}.xaml.cs
└── ViewModel/
└── {ViewModelName}.cs
```
## Code Templates
### 1. View Interface
**File:** `DTS.CommonCore/Interface/{Module}/I{ViewName}.cs`
```csharp
using DTS.Common.Base;
namespace DTS.Common.Interface.{Module}
{
public interface I{ViewName} : IBaseView
{
// Add view-specific properties/methods if needed
}
}
```
### 2. ViewModel Interface
**File:** `DTS.CommonCore/Interface/{Module}/I{ViewModelName}.cs`
```csharp
using DTS.Common.Base;
namespace DTS.Common.Interface.{Module}
{
public interface I{ViewModelName} : IBaseViewModel
{
I{ViewName} View { get; set; }
// Add other properties/methods
}
}
```
### 3. ViewModel Implementation
```csharp
using System;
using System.ComponentModel;
using System.ComponentModel.Composition;
using Prism.Events;
using Prism.Regions;
using Unity;
using DTS.Common.Interface.{Module};
using DTS.Common.Interactivity;
using DTS.Common.Events;
namespace {ModuleName}
{
[PartCreationPolicy(CreationPolicy.Shared)]
public class {ViewModelName} : I{ViewModelName}
{
public I{ViewName} View { get; set; }
private IEventAggregator _eventAggregator { get; }
private readonly IRegionManager _regionManager;
private IUnityContainer UnityContainer { get; }
public InteractionRequest<Notification> NotificationRequest { get; }
public InteractionRequest<Confirmation> ConfirmationRequest { get; }
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public {ViewModelName}(
I{ViewName} view,
IRegionManager regionManager,
IEventAggregator eventAggregator,
IUnityContainer unityContainer)
{
View = view;
View.DataContext = this;
NotificationRequest = new InteractionRequest<Notification>();
ConfirmationRequest = new InteractionRequest<Confirmation>();
_eventAggregator = eventAggregator;
UnityContainer = unityContainer;
_regionManager = regionManager;
_eventAggregator.GetEvent<RaiseNotification>().Subscribe(OnRaiseNotification);
_eventAggregator.GetEvent<BusyIndicatorChangeNotification>()
.Subscribe(OnBusyIndicatorNotification, ThreadOption.PublisherThread, true);
}
private bool _isBusy = false;
public bool IsBusy
{
get => _isBusy;
set { _isBusy = value; OnPropertyChanged("IsBusy"); }
}
public bool IsDirty { get; private set; }
public bool IsMenuIncluded { get; set; }
public bool IsNavigationIncluded { get; set; }
public void Initialize() { }
public void Initialize(object parameter) { }
public Task InitializeAsync() => Task.CompletedTask;
public Task InitializeAsync(object parameter) => Task.CompletedTask;
public void Activated() { }
public void Cleanup() { }
public Task CleanupAsync() => Task.CompletedTask;
private void OnBusyIndicatorNotification(bool eventArg) => IsBusy = eventArg;
private void OnRaiseNotification(NotificationContentEventArgs eventArgs)
{
NotificationRequest.Raise(new Notification {
Content = eventArgs,
Title = eventArgs.Title
});
}
}
}
```
### 4. XAML View
```xml
<base:BaseView x:Class="{ModuleName}.{ViewName}"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:base="clr-namespace:DTS.Common.Base;assembly=DTS.Common"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:converters="clr-namespace:DTS.Common.Converters;assembly=DTS.Common">
<base:BaseView.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/DTS.Common;component/Themes/CommonStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<converters:BooleanToVisibilityConverter x:Key="BoolToVisibility" />
</ResourceDictionary>
</base:BaseView.Resources>
<Grid>
<!-- Controls bound to ViewModel properties -->
<ListView ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem" BasedOn="{StaticResource TTS_ListViewItemStyle}">
<Style.Triggers>
<DataTrigger Binding="{Binding Status}" Value="Error">
<Setter Property="Background" Value="{StaticResource Brush_ApplicationStatus_Failed}" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListView.ItemContainerStyle>
</ListView>
</Grid>
</base:BaseView>
```
### 5. View Code-Behind
```csharp
using System.Windows.Controls;
using DTS.Common.Base;
using DTS.Common.Interface.{Module};
namespace {ModuleName}
{
public partial class {ViewName} : BaseView, I{ViewName}
{
public {ViewName}()
{
InitializeComponent();
}
}
}
```
## Examples from Codebase
### Example 1: SensorsListViewModel
**File:** `DataPRO/Modules/SensorsList/SensorsList/ViewModel/SensorsListViewModel.cs:41`
```csharp
[PartCreationPolicy(CreationPolicy.Shared)]
public class SensorsListViewModel : ISensorsListViewModel
{
public ISensorsListView View { get; set; }
private IEventAggregator _eventAggregator { get; }
private readonly IRegionManager _regionManager;
private IUnityContainer UnityContainer { get; }
public SensorsListViewModel(
ISensorsListView view,
IRegionManager regionManager,
IEventAggregator eventAggregator,
IUnityContainer unityContainer)
{
View = view;
View.DataContext = this;
_eventAggregator = eventAggregator;
// ...
}
}
```
### Example 2: ShellViewModel (BindableBase)
**File:** `DataPRO/DataPRO/ViewModel/ShellViewModel.cs:26`
```csharp
[Export(typeof(IShellView))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class ShellViewModel : BindableBase, IShellViewModel
{
public IShellView View { get; private set; }
public ShellViewModel(
IShellView view,
IRegionManager regionManager,
IEventAggregator eventAggregator,
IUnityContainer unityContainer)
{
View = view;
View.DataContext = this;
// ...
}
}
```
### Example 3: ISensorsListViewModel Interface
**File:** `Common/DTS.CommonCore/Interface/Sensors/SensorsList/ISensorsListViewModel.cs:8`
```csharp
public interface ISensorsListViewModel : IBaseViewModel, IFilterableListView
{
ISensorsListView View { get; set; }
IAnalogSensor[] AnalogSensors { get; set; }
void GetSensors(int sensorCalWarningPeriodDays, bool included);
void Filter(string currentFilter);
}
```
### Example 4: XAML Binding Pattern
**File:** `DataPRO/Modules/SensorsList/SensorsList/View/SensorsListView.xaml:44`
```xml
<ListView ItemsSource="{Binding AnalogSensors, UpdateSourceTrigger=PropertyChanged}"
SelectedIndex="{Binding SelectedAnalogIndex, Mode=TwoWay}">
<i:Interaction.Behaviors>
<behaviors:MultiSelectionBehavior SelectedItems="{Binding SelectedAnalogItems}" />
</i:Interaction.Behaviors>
</ListView>
```
## Common Mistakes to Avoid
1. **Not setting DataContext in constructor** - View won't bind to ViewModel
2. **Forgetting INotifyPropertyChanged** - UI won't update when properties change
3. **Using concrete types instead of interfaces** - Breaks DI and testability
4. **Putting logic in code-behind** - Should be in ViewModel
5. **Not using InteractionRequest for dialogs** - Use `NotificationRequest.Raise()` instead of `MessageBox.Show()`
6. **Hardcoded strings in OnPropertyChanged** - Use `nameof()` when possible
7. **Not implementing IBaseViewModel fully** - Missing Initialize/Cleanup methods
8. **Blocking UI thread** - Use async/await for long operations

View File

@@ -0,0 +1,187 @@
# Prism Module Pattern
## When to Use
- Creating a new feature module that integrates with DataPRO shell
- Adding new UI components that need to be loaded at runtime
- Implementing isolated feature sets that can be developed independently
## Files to Create/Modify
### 1. Module Project Structure
```
DataPRO/Modules/{ModuleName}/
├── {ModuleName}Module.cs (Module entry point)
├── View/
│ └── {ViewName}.xaml(.cs) (XAML views)
├── ViewModel/
│ └── {ViewModelName}.cs (View models)
├── Model/ (Data models, if needed)
└── Resources/ (String resources, images)
```
### 2. Register Module in Bootstrapper
**File:** `DataPRO/DataPRO/Bootstrapper.cs`
Add to `ConfigureModuleCatalog()`:
```csharp
moduleCatalog.AddModule<{ModuleName}.{ModuleClass}>();
```
## Code Template
### Module Class (`{ModuleName}Module.cs`)
```csharp
using System;
using System.ComponentModel.Composition;
using System.Windows.Media.Imaging;
using DTS.Common;
using DTS.Common.Interface;
using Prism.Ioc;
using Prism.Modularity;
using Unity;
[assembly: {ModuleName}Name]
[assembly: {ModuleName}ImageAttribute]
namespace {ModuleName}
{
[Export(typeof(IModule))]
[Module(ModuleName = "{ModuleName}")]
public class {ModuleName}Module : IModule
{
private readonly IUnityContainer _unityContainer;
public {ModuleName}Module(IUnityContainer unityContainer)
{
_unityContainer = unityContainer;
}
public void Initialize()
{
_unityContainer.RegisterType<I{ViewName}, {ViewName}>();
_unityContainer.RegisterType<I{ViewModelName}, {ViewModelName}>();
}
public void OnInitialized(IContainerProvider containerProvider) { }
public void RegisterTypes(IContainerRegistry containerRegistry)
{
Initialize();
}
}
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
public class {ModuleName}NameAttribute : TextAttribute
{
public {ModuleName}NameAttribute() : this(null) { }
public {ModuleName}NameAttribute(string s)
{
AssemblyName = AssemblyNames.{ModuleName}.ToString();
}
public override string AssemblyName { get; }
public override Type GetAttributeType() => typeof(TextAttribute);
public override string GetAssemblyName() => AssemblyName;
}
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
public class {ModuleName}ImageAttribute : ImageAttribute
{
private BitmapImage _img;
public {ModuleName}ImageAttribute() : this(null) { }
public override BitmapImage AssemblyImage
{
get { _img = AssemblyInfo.GetImage(AssemblyNames.{ModuleName}.ToString()); return _img; }
}
public {ModuleName}ImageAttribute(string s)
{
_img = AssemblyInfo.GetImage(AssemblyNames.{ModuleName}.ToString());
}
public override Type GetAttributeType() => typeof(ImageAttribute);
public override BitmapImage GetAssemblyImage() => AssemblyImage;
private string _name;
public override string AssemblyName
{
get { _name = AssemblyNames.{ModuleName}.ToString(); return _name; }
}
public override string GetAssemblyName() => AssemblyName;
private string _group;
public override string AssemblyGroup
{
get { _group = eAssemblyGroups.{GroupType}.ToString(); return _group; }
}
public override string GetAssemblyGroup() => AssemblyGroup;
private eAssemblyRegion _region;
public override eAssemblyRegion AssemblyRegion
{
get { _region = eAssemblyRegion.{ModuleName}Region; return _region; }
}
public override eAssemblyRegion GetAssemblyRegion() => AssemblyRegion;
}
}
```
## Examples from Codebase
### Example 1: SensorsListModule
**File:** `DataPRO/Modules/SensorsList/SensorsList/SensorsListModule.cs:22`
```csharp
[Module(ModuleName = "SensorsListModule")]
public class SensorsListModule : IModule
{
private readonly IUnityContainer _unityContainer;
public SensorsListModule(IUnityContainer unityContainer)
{
_unityContainer = unityContainer;
}
public void Initialize()
{
_unityContainer.RegisterType<ISensorsListView, SensorsListView>();
_unityContainer.RegisterType<ISensorsListViewModel, SensorsListViewModel>();
}
public void RegisterTypes(IContainerRegistry containerRegistry) => Initialize();
}
```
### Example 2: GraphModule (DTS Viewer)
**File:** `DTS Viewer/DTS.Viewer.Modules/DTS.Viewer.Graph/GraphModule.cs:16`
```csharp
[Module(ModuleName = "Graph")]
public class GraphModule : IModule
{
private readonly IUnityContainer _unityContainer;
public GraphModule(IUnityContainer unityContainer)
{
_unityContainer = unityContainer;
}
public void Initialize()
{
_unityContainer.RegisterType<IGraphView, GraphView>();
_unityContainer.RegisterType<IGraphViewModel, GraphViewModel>();
}
}
```
### Example 3: Bootstrapper Registration
**File:** `DataPRO/DataPRO/Bootstrapper.cs:179-220`
```csharp
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
moduleCatalog.AddModule<StatusAndProgressBar.StatusAndProgressBarModule>();
moduleCatalog.AddModule<DatabaseServices.DatabaseServicesModule>();
moduleCatalog.AddModule<SensorsList.SensorsListModule>();
// ... more modules
}
```
## Common Mistakes to Avoid
1. **Forgetting to register module in Bootstrapper** - Module won't load
2. **Not implementing both `Initialize()` and `RegisterTypes()`** - Both are required for Prism 6+
3. **Missing assembly attributes** - Module won't appear in UI tiles/menu
4. **Wrong eAssemblyGroups value** - Module appears in wrong section
5. **Not using interface for View/ViewModel registration** - Breaks testability and DI
6. **Singleton vs Transient** - Use `ContainerControlledLifetimeManager` for singletons, otherwise default is transient
7. **Missing `[Export(typeof(IModule))]`** - Required for MEF discovery in some scenarios

View File

@@ -0,0 +1,376 @@
# Service Pattern (Background Services)
## When to Use
- Implementing hardware communication (SLICE, TDAS, Ribeye)
- Long-running background operations
- Real-time data acquisition
- Download operations from hardware devices
- Diagnostics and calibration services
## Architecture Overview
```
┌──────────────────────────────────────────────────────────────────────┐
│ ServiceBase │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Callback Pattern: ServiceCallback(CallbackData) │ │
│ │ - Progress: Report progress percentage │ │
│ │ - NewData: Real-time sample data │ │
│ │ - Success/Failure: Operation completion │ │
│ │ - Canceled: User cancellation │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ State Machine: Stateless-based state transitions │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────────┐ │
│ │ Prepare │───>│Configure│───>│ Arm │───>│ Realtime │ │
│ └─────────┘ └─────────┘ └─────────┘ └──────────┘ │
│ │ │ │ │ │
│ v v v v │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────────┐ │
│ │ Diagnose│ │ Download│ │ Disarm │ │ Stop │ │
│ └─────────┘ └─────────┘ └─────────┘ └──────────┘ │
└──────────────────────────────────────────────────────────────────────┘
```
## Files to Create/Modify
### Structure
```
IService/
├── Classes/
│ ├── ServiceBase.cs (Base class for all services)
│ ├── GenericServices.cs (Service orchestration)
│ ├── SLICEService/
│ │ ├── SLICE Service.cs
│ │ ├── SLICE Service.Configuration.cs
│ │ ├── SLICE Service.Realtime.cs
│ │ └── SLICE Service.Download.cs
│ └── TDAS Service/
├── Interfaces/
│ ├── IConfigurationActions.cs
│ ├── IRealTimeActions.cs
│ ├── IDownloadActions.cs
│ └── IDiagnosticsActions.cs
└── StateMachine/
├── States.cs
├── Triggers.cs
└── States/
├── Configure.cs
├── Realtime.cs
└── Download.cs
```
## Code Templates
### 1. Service Base Class Pattern
```csharp
using System;
using System.Threading;
using DTS.Common.Utilities.Logging;
namespace DTS.DASLib.Service
{
public abstract class ServiceBase : IDisposable
{
public bool AggregateProgress { get; set; }
public class CallbackData
{
public enum CallbackStatus
{
Progress,
NewData,
AllFinished,
Success,
Failure,
Canceled
}
public CallbackStatus Status { get; set; }
public IDASCommunication Target { get; set; }
public string ErrorMessage { get; set; }
public Exception ErrorException { get; set; }
public int ProgressValue { get; set; }
public SampleData[] Data { get; set; }
}
public delegate void Callback(CallbackData data);
public delegate void ServiceBaseEventHandler(object sender, CallbackData data);
public event ServiceBaseEventHandler ServiceAvailable;
public event ServiceCallbackErrorEventHandler ServiceCallbackError;
public abstract string ServiceName();
public virtual void Cancel()
{
APILogger.LogString($"Entering {ServiceName()}.Cancel");
DASServiceLock.Cancel(this);
}
protected void RunService(Callback userCallback, object userObject)
{
var glue = new ServiceGlueClass(userCallback, userObject);
// Service orchestration logic
}
protected void FireCallback(CallbackData data)
{
// Fire callback to user
}
}
}
```
### 2. Hardware Service Implementation
```csharp
using System;
using System.Threading;
using DTS.Common.DASResource;
using DTS.Common.Interface.DASFactory;
namespace DTS.DASLib.Service
{
public class SLICEService : ServiceBase,
IConfigurationActions,
IRealTimeActions,
IDownloadActions
{
public override string ServiceName() => "SLICE Service";
#region Configuration
public void Configure(
IDASCommunication[] targets,
IServiceConfiguration config,
Callback callback,
object userObject)
{
APILogger.LogString($"{ServiceName()}.Configure starting");
try
{
foreach (var target in targets)
{
var slice = target as ISLICE;
if (slice == null) continue;
ApplyConfiguration(slice, config);
}
callback?.Invoke(new CallbackData
{
Status = CallbackData.CallbackStatus.Success,
Target = null
});
}
catch (Exception ex)
{
callback?.Invoke(new CallbackData
{
Status = CallbackData.CallbackStatus.Failure,
ErrorException = ex,
ErrorMessage = ex.Message
});
}
}
#endregion
#region Realtime
public void StartRealtime(
IDASCommunication[] targets,
Callback callback,
object userObject)
{
APILogger.LogString($"{ServiceName()}.StartRealtime");
RunService(callback, userObject);
foreach (var target in targets)
{
var slice = target as ISLICE;
slice?.StartRealtime(OnRealtimeData, target);
}
}
private void OnRealtimeData(SampleData[] data, object context)
{
FireCallback(new CallbackData
{
Status = CallbackData.CallbackStatus.NewData,
Data = data,
Target = context as IDASCommunication
});
}
public void StopRealtime(IDASCommunication[] targets)
{
foreach (var target in targets)
{
var slice = target as ISLICE;
slice?.StopRealtime();
}
}
#endregion
#region Download
public void Download(
IDASCommunication[] targets,
IDownloadParameters parameters,
Callback callback,
object userObject)
{
APILogger.LogString($"{ServiceName()}.Download");
foreach (var target in targets)
{
var slice = target as ISLICE;
slice?.Download(
parameters,
(progress, data, error) =>
{
if (!string.IsNullOrEmpty(error))
{
callback?.Invoke(new CallbackData
{
Status = CallbackData.CallbackStatus.Failure,
ErrorMessage = error,
Target = target
});
}
else if (data != null)
{
callback?.Invoke(new CallbackData
{
Status = CallbackData.CallbackStatus.NewData,
Data = data,
ProgressValue = progress,
Target = target
});
}
});
}
callback?.Invoke(new CallbackData
{
Status = CallbackData.CallbackStatus.AllFinished,
Target = null
});
}
#endregion
}
}
```
### 3. Service Interface
```csharp
namespace DTS.DASLib.Service.Interfaces
{
public interface IRealTimeActions
{
void StartRealtime(IDASCommunication[] targets, ServiceBase.Callback callback, object userObject);
void StopRealtime(IDASCommunication[] targets);
}
public interface IDownloadActions
{
void Download(IDASCommunication[] targets, IDownloadParameters parameters,
ServiceBase.Callback callback, object userObject);
}
public interface IConfigurationActions
{
void Configure(IDASCommunication[] targets, IServiceConfiguration config,
ServiceBase.Callback callback, object userObject);
}
}
```
## Examples from Codebase
### Example 1: ServiceBase Callback Pattern
**File:** `DataPRO/IService/Classes/GenericServices.cs:35-122`
```csharp
public abstract class ServiceBase : IDisposable
{
public class CallbackData
{
public enum CallbackStatus { Progress, NewData, AllFinished, Success, Failure, Canceled }
public CallbackStatus Status { get; set; }
public IDASCommunication Target { get; set; }
public string ErrorMessage { get; set; }
public Exception ErrorException { get; set; }
public int ProgressValue { get; set; }
public SampleData[] Data { get; set; }
}
public delegate void Callback(CallbackData data);
public event ServiceBaseEventHandler ServiceAvailable;
public abstract string ServiceName();
public virtual void Cancel()
{
APILogger.LogString($"Entering {ServiceName()}.Cancel");
DASServiceLock.Cancel(this);
}
}
```
### Example 2: Service with Progress Reporting
**File:** `DataPRO/IService/Classes/GenericServices.cs:198`
```csharp
protected void ServiceIsAvailable(object sender, CallbackData data)
{
ServiceAvailable -= ServiceIsAvailable;
// Handle service completion
}
protected class ServiceGlueClass
{
public object UserObject { get; set; }
public Callback UserCallback { get; set; }
public ManualResetEvent AvailableEvent { get; set; }
public ServiceGlueClass(Callback userCallback, object userObject)
{
UserObject = userObject;
UserCallback = userCallback;
AvailableEvent = new ManualResetEvent(false);
}
}
```
### Example 3: DASModule Configuration
**File:** `DataPRO/IService/Classes/DASModule.cs`
```csharp
public class DASModule
{
public List<AnalogInputDASChannel> AnalogInputChannels { get; set; }
public List<DigitalOutputDASChannel> DigitalOutputChannels { get; set; }
public void Configure(IServiceConfiguration config)
{
// Apply configuration to all channels
}
}
```
## Common Mistakes to Avoid
1. **Not handling cancellation** - Always implement `Cancel()` to clean up resources
2. **Blocking the callback thread** - Use async patterns, don't block in callbacks
3. **Missing error handling in callbacks** - Always check for null callback before invoking
4. **Not disposing connections** - Implement `IDisposable` properly
5. **Fire-and-forget without error logging** - Always log errors even if no callback
6. **Cross-thread UI updates** - Use `Dispatcher.Invoke` for UI updates from service callbacks
7. **Not using ServiceAvailable event** - Must unsubscribe to prevent memory leaks
8. **Ignoring Target in CallbackData** - Needed to identify which unit responded
9. **Not checking AggregateProgress** - Affects how progress is reported for multiple units
10. **Missing ServiceName() override** - Required for logging identification

View File

@@ -0,0 +1,492 @@
# Add New Hardware Support - DataPRO Prompt Template
## Context
DataPRO supports various Data Acquisition System (DAS) hardware through the hardware abstraction layer in `Common/DTS.Common.DAS.Concepts/` and the `DataPRO/Modules/Hardware/` modules. Hardware support involves implementing interfaces for arm/disarm, data collection, real-time streaming, and trigger functionality.
## System Architecture
```
Common/DTS.Common.DAS.Concepts/
├── DAS/
│ ├── Channel/ # Channel abstraction
│ ├── DAS.Id.cs # Hardware identification
│ └── DAS.Channel.cs # Channel definitions
├── Interfaces/ # Hardware interfaces
├── IArmable.cs # Arming capability
├── IDataCollectionEnabled.cs # Data collection
├── IDownloadEnabled.cs # Data download
├── IRealtimeable.cs # Real-time streaming
├── ITriggerable.cs # Trigger capability
└── IGpioEnabled.cs # GPIO support
DataPRO/Modules/Hardware/
├── HardwareList/ # Hardware management UI
│ ├── Model/
│ ├── View/
│ └── ViewModel/
└── AddEditHardware/ # Hardware configuration
```
## Step-by-Step Instructions
### 1. Create Hardware Model
**File:** `Common/DTS.Common.DAS.Concepts/DAS/{HardwareName}.cs`
```csharp
using System;
using System.Collections.Generic;
namespace DTS.Common.DAS.Concepts
{
public class {HARDWARE_NAME} :
IArmable,
IDataCollectionEnabled,
IRealtimeable,
ITriggerable
{
public DASId Id { get; set; }
public string SerialNumber { get; set; }
public string FirmwareVersion { get; set; }
public int ChannelCount { get; set; }
public int SampleRate { get; set; }
public ArmStatus ArmStatus { get; private set; }
public bool IsDataCollectionEnabled { get; private set; }
public bool IsRealtimeEnabled { get; private set; }
public {HARDWARE_NAME}()
{
Id = new DASId();
ChannelCount = 8;
SampleRate = 100000;
ArmStatus = ArmStatus.Disarmed;
}
// IArmable implementation
public void Arm()
{
ValidateArmConditions();
SendArmCommand();
ArmStatus = ArmStatus.Armed;
}
public void Disarm()
{
SendDisarmCommand();
ArmStatus = ArmStatus.Disarmed;
}
public AvailableArmModes GetAvailableArmModes()
{
return new AvailableArmModes
{
Modes = new List<ArmMode>
{
ArmMode.Triggered,
ArmMode.Immediate
}
};
}
// IDataCollectionEnabled implementation
public void EnableDataCollection()
{
ConfigureDataCollection();
IsDataCollectionEnabled = true;
}
public void DisableDataCollection()
{
StopDataCollection();
IsDataCollectionEnabled = false;
}
// IRealtimeable implementation
public void StartRealtime()
{
ValidateRealtimeConditions();
StartRealtimeStreaming();
IsRealtimeEnabled = true;
}
public void StopRealtime()
{
StopRealtimeStreaming();
IsRealtimeEnabled = false;
}
// ITriggerable implementation
public void ConfigureTrigger(TriggerConfiguration config)
{
ValidateTriggerConfiguration(config);
ApplyTriggerSettings(config);
}
// Hardware-specific methods
private void ValidateArmConditions()
{
if (string.IsNullOrEmpty(SerialNumber))
throw new InvalidOperationException("Serial number required");
}
private void SendArmCommand()
{
// Hardware communication implementation
}
private void SendDisarmCommand()
{
// Hardware communication implementation
}
private void ConfigureDataCollection()
{
// Configure sample rate, duration, etc.
}
private void StopDataCollection()
{
// Stop collection
}
private void ValidateRealtimeConditions()
{
if (!IsDataCollectionEnabled)
throw new InvalidOperationException("Enable data collection first");
}
private void StartRealtimeStreaming()
{
// Start real-time data stream
}
private void StopRealtimeStreaming()
{
// Stop real-time data stream
}
private void ValidateTriggerConfiguration(TriggerConfiguration config)
{
if (config.Threshold < 0)
throw new ArgumentException("Invalid threshold");
}
private void ApplyTriggerSettings(TriggerConfiguration config)
{
// Apply trigger configuration to hardware
}
}
public class TriggerConfiguration
{
public double Threshold { get; set; }
public TriggerEdge Edge { get; set; }
public int Channel { get; set; }
}
public enum TriggerEdge
{
Rising,
Falling,
Both
}
}
```
### 2. Create Hardware Channel Definition
**File:** `Common/DTS.Common.DAS.Concepts/DAS/{HardwareName}Channel.cs`
```csharp
using System;
using DTS.Common.DAS.Concepts.Channel;
namespace DTS.Common.DAS.Concepts
{
public class {HARDWARE_NAME}Channel : DAS.Channel
{
public int PhysicalChannel { get; set; }
public string Label { get; set; }
public double FullScaleRange { get; set; }
public double ExcitationVoltage { get; set; }
public {HARDWARE_NAME}Channel(int index)
{
PhysicalChannel = index;
FullScaleRange = 10.0;
ExcitationVoltage = 0.0;
}
public void Configure(ChannelConfiguration config)
{
FullScaleRange = config.FullScaleRange;
ExcitationVoltage = config.ExcitationVoltage;
ApplyChannelSettings();
}
private void ApplyChannelSettings()
{
// Apply channel configuration to hardware
}
}
public class ChannelConfiguration
{
public double FullScaleRange { get; set; }
public double ExcitationVoltage { get; set; }
public CouplingMode Coupling { get; set; }
}
public enum CouplingMode
{
DC,
AC
}
}
```
### 3. Create Hardware Factory
**File:** `Common/DTS.Common.DAS.Concepts/DAS/{HardwareName}Factory.cs`
```csharp
using System;
using DTS.Common.DAS.Concepts.Interfaces;
namespace DTS.Common.DAS.Concepts
{
public class {HARDWARE_NAME}Factory : IDASFactory
{
public string HardwareType => "{HARDWARE_NAME}";
public DAS.Channel CreateChannel(int index)
{
return new {HARDWARE_NAME}Channel(index);
}
public {HARDWARE_NAME} CreateHardware(string serialNumber)
{
var hardware = new {HARDWARE_NAME}
{
SerialNumber = serialNumber
};
InitializeHardware(hardware);
return hardware;
}
private void InitializeHardware({HARDWARE_NAME} hardware)
{
// Hardware initialization
// Connect, discover channels, read firmware version
}
public bool CanCreate(string hardwareIdentifier)
{
// Check if this factory supports the given hardware
return hardwareIdentifier.StartsWith("{HARDWARE_PREFIX}");
}
}
}
```
### 4. Update Hardware List Module
**File:** `DataPRO/Modules/Hardware/HardwareList/Model/{HardwareName}Model.cs`
```csharp
using System;
using System.ComponentModel;
using DTS.Common.DAS.Concepts;
namespace HardwareList.Model
{
public class {HARDWARE_NAME}Model : INotifyPropertyChanged
{
private {HARDWARE_NAME} _hardware;
public string SerialNumber
{
get => _hardware?.SerialNumber;
set
{
if (_hardware != null)
_hardware.SerialNumber = value;
OnPropertyChanged(nameof(SerialNumber));
}
}
public string FirmwareVersion => _hardware?.FirmwareVersion;
public int ChannelCount => _hardware?.ChannelCount ?? 0;
public ArmStatus ArmStatus => _hardware?.ArmStatus ?? ArmStatus.Disarmed;
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public void SetHardware({HARDWARE_NAME} hardware)
{
_hardware = hardware;
OnPropertyChanged(nameof(SerialNumber));
OnPropertyChanged(nameof(FirmwareVersion));
OnPropertyChanged(nameof(ChannelCount));
OnPropertyChanged(nameof(ArmStatus));
}
}
}
```
### 5. Create Hardware View
**File:** `DataPRO/Modules/Hardware/HardwareList/View/{HardwareName}View.xaml`
```xml
<UserControl x:Class="HardwareList.View.{HARDWARE_NAME}View"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:resx="clr-namespace:HardwareList.Resources">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{x:Static resx:StringResources.{HARDWARE_NAME}_Header}"
Style="{StaticResource HardwareHeaderStyle}"/>
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="5">
<TextBlock Text="{x:Static resx:StringResources.SerialNumber}"/>
<TextBox Text="{Binding SerialNumber, Mode=TwoWay}" Width="150" Margin="5,0"/>
<TextBlock Text="{x:Static resx:StringResources.Firmware}"/>
<TextBlock Text="{Binding FirmwareVersion}" Margin="5,0"/>
</StackPanel>
<TabControl Grid.Row="2">
<TabItem Header="{x:Static resx:StringResources.Channels}">
<ListView ItemsSource="{Binding Channels}" SelectedItem="{Binding SelectedChannel}">
<ListView.View>
<GridView>
<GridViewColumn Header="Channel" DisplayMemberBinding="{Binding PhysicalChannel}"/>
<GridViewColumn Header="Label" DisplayMemberBinding="{Binding Label}"/>
<GridViewColumn Header="Range" DisplayMemberBinding="{Binding FullScaleRange}"/>
</GridView>
</ListView.View>
</ListView>
</TabItem>
<TabItem Header="{x:Static resx:StringResources.Settings}">
<!-- Hardware-specific settings -->
</TabItem>
</TabControl>
</Grid>
</UserControl>
```
### 6. Register Factory in Bootstrapper
**File:** Modify the bootstrapper or module initialization
```csharp
private void RegisterDASFactories()
{
// Register existing factories
_unityContainer.RegisterType<IDASFactory, SLICE6Factory>("SLICE6");
_unityContainer.RegisterType<IDASFactory, TDASFactory>("TDAS");
// Register new hardware factory
_unityContainer.RegisterType<IDASFactory, {HARDWARE_NAME}Factory>("{HARDWARE_NAME}");
}
```
### 7. Add Hardware Type to Enums
**File:** `Common/DTS.Common/Enums/HardwareTypes.cs`
```csharp
public enum HardwareType
{
Unknown,
SLICE6,
TDAS,
{HARDWARE_NAME} // Add new type
}
```
## Interface Reference
### IArmable
```csharp
public interface IArmable
{
ArmStatus ArmStatus { get; }
void Arm();
void Disarm();
AvailableArmModes GetAvailableArmModes();
}
```
### IDataCollectionEnabled
```csharp
public interface IDataCollectionEnabled
{
bool IsDataCollectionEnabled { get; }
void EnableDataCollection();
void DisableDataCollection();
}
```
### IRealtimeable
```csharp
public interface IRealtimeable
{
bool IsRealtimeEnabled { get; }
void StartRealtime();
void StopRealtime();
}
```
### ITriggerable
```csharp
public interface ITriggerable
{
void ConfigureTrigger(TriggerConfiguration config);
}
```
## Files to Create/Modify Summary
| File | Action |
|------|--------|
| `DTS.Common.DAS.Concepts/DAS/{HardwareName}.cs` | Create |
| `DTS.Common.DAS.Concepts/DAS/{HardwareName}Channel.cs` | Create |
| `DTS.Common.DAS.Concepts/DAS/{HardwareName}Factory.cs` | Create |
| `HardwareList/Model/{HardwareName}Model.cs` | Create |
| `HardwareList/View/{HardwareName}View.xaml` | Create |
| `HardwareList/View/{HardwareName}View.xaml.cs` | Create |
| `HardwareList/HardwareListModule.cs` | Modify (register) |
| `DTS.Common/Enums/HardwareType.cs` | Modify (add enum) |
## Validation Checklist
- [ ] Hardware class implements required interfaces (`IArmable`, etc.)
- [ ] Channel class inherits from `DAS.Channel`
- [ ] Factory implements `IDASFactory`
- [ ] All interface methods properly implemented
- [ ] Validation added for hardware commands
- [ ] Error handling for communication failures
- [ ] Factory registered in DI container
- [ ] Hardware type added to enum
- [ ] View properly data-bound to ViewModel
- [ ] Localization strings added
- [ ] Channel configuration supports hardware features
## Hardware Communication Patterns
1. **Synchronous Commands:** Use for arm/disarm operations
2. **Asynchronous Data:** Use for real-time streaming
3. **Status Polling:** Implement periodic status checks
4. **Error Recovery:** Handle disconnections gracefully
5. **Timeout Handling:** Set appropriate timeouts for operations
## Common Gotchas
- Always check connection status before sending commands
- Validate hardware state transitions (can't arm if already armed)
- Handle firmware version differences
- Consider backward compatibility with older firmware
- Implement proper disposal of resources

View File

@@ -0,0 +1,464 @@
# Add New Import Format - DataPRO Prompt Template
## Context
DataPRO supports importing test data from various file formats through the `Common/DTS.Common.Import/` library. The import system uses a parser-based architecture where each format has a dedicated parser class that converts external data into the internal `ImportObject` structure.
## System Architecture
```
Common/DTS.Common.Import/
├── Parsers/
│ ├── CSV/ # CSV format parsers
│ ├── EQX/ # Equipment Exchange format
│ │ ├── EQXSensorsParser.cs
│ │ ├── EQXTestSetupParser.cs
│ │ └── EQXGroupImport.cs
│ ├── DefaultParseImport.cs
│ ├── DTSXMLParseImport.cs
│ └── ParseVariantBase.cs # Base class for parsers
├── ImportObject.cs # Container for imported data
├── ImportError.cs # Error handling
├── ImportOptions/ # Format-specific options
├── Interfaces/ # Parser interfaces
├── Persist/ # Database persistence
└── XML/ # XML processing utilities
```
## Step-by-Step Instructions
### 1. Create the Parser Class
**File:** `Common/DTS.Common.Import/Parsers/{FormatName}/{FormatName}Parser.cs`
```csharp
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using DTS.Common.Enums;
using DTS.Common.Import.Enums;
using DTS.Common.Import.ImportOptions;
using DTS.Common.Import.Parsers;
using DTS.Common.Interface.Sensors;
using DTS.Common.Storage;
using DTS.SensorDB;
namespace DTS.Common.Import
{
public class {FORMAT_NAME}Parser : ParseVariantBase
{
private readonly User _currentUser;
private readonly IImportNotification _importNotification;
private readonly {FORMAT_NAME}ImportOptions _importOptions;
public {FORMAT_NAME}Parser(
IImportNotification importNotification,
User user,
{FORMAT_NAME}ImportOptions importOptions)
{
_currentUser = user;
_importNotification = importNotification;
_importOptions = importOptions;
}
public override void Parse(ref ImportObject importObject)
{
if (string.IsNullOrEmpty(FileName))
{
return;
}
if (importObject == null)
{
throw new ArgumentNullException(nameof(importObject),
"importObject can't be null");
}
importObject = ParseFile(importObject, FileName);
}
private ImportObject ParseFile(ImportObject importObject, string filePath)
{
try
{
// Validate file exists
if (!File.Exists(filePath))
{
importObject.AddError(new ImportError(
$"File not found: {filePath}"));
return importObject;
}
// Read and parse file content
var content = File.ReadAllText(filePath);
var parsedData = ParseContent(content);
// Populate import object
PopulateImportObject(importObject, parsedData);
// Set source format
importObject.SourceFormat = ImportFormats.{FORMAT_NAME};
_importNotification?.NotifyProgress(100, "Import complete");
}
catch (Exception ex)
{
importObject.AddError(new ImportError(
$"Parse error: {ex.Message}"));
}
return importObject;
}
private ParsedData ParseContent(string content)
{
var data = new ParsedData();
// Format-specific parsing logic here
// Example: Parse lines, extract sensors, test setups, etc.
return data;
}
private void PopulateImportObject(ImportObject importObject, ParsedData data)
{
// Add sensors
foreach (var sensor in data.Sensors)
{
importObject.AddSensor(ConvertToSensorData(sensor));
}
// Add test setups
foreach (var setup in data.TestSetups)
{
importObject.AddTestSetup(ConvertToTestTemplate(setup));
}
// Add hardware
foreach (var hardware in data.Hardware)
{
importObject.AddHardware(ConvertToDASHardware(hardware));
}
}
private SensorData ConvertToSensorData(ParsedSensor parsed)
{
var sensorData = new SensorData
{
Name = parsed.Name,
SerialNumber = parsed.SerialNumber,
ChannelCode = parsed.ChannelCode,
CalibrationFactor = parsed.CalibrationFactor,
EngineeringUnits = parsed.EngineeringUnits,
Sensitivity = parsed.Sensitivity
};
return sensorData;
}
private TestTemplate ConvertToTestTemplate(ParsedTestSetup parsed)
{
// Convert parsed test setup to TestTemplate
return new TestTemplate();
}
private DASHardware ConvertToDASHardware(ParsedHardware parsed)
{
// Convert parsed hardware to DASHardware
return new DASHardware();
}
}
internal class ParsedData
{
public List<ParsedSensor> Sensors { get; set; } = new List<ParsedSensor>();
public List<ParsedTestSetup> TestSetups { get; set; } = new List<ParsedTestSetup>();
public List<ParsedHardware> Hardware { get; set; } = new List<ParsedHardware>();
}
internal class ParsedSensor
{
public string Name { get; set; }
public string SerialNumber { get; set; }
public string ChannelCode { get; set; }
public double CalibrationFactor { get; set; }
public string EngineeringUnits { get; set; }
public double Sensitivity { get; set; }
}
internal class ParsedTestSetup { }
internal class ParsedHardware { }
}
```
### 2. Create Import Options Class
**File:** `Common/DTS.Common.Import/ImportOptions/{FormatName}ImportOptions.cs`
```csharp
using System;
namespace DTS.Common.Import.ImportOptions
{
public class {FORMAT_NAME}ImportOptions
{
public bool ImportSensors { get; set; } = true;
public bool ImportTestSetups { get; set; } = true;
public bool ImportHardware { get; set; } = false;
public bool CreateGroups { get; set; } = true;
public bool ValidateData { get; set; } = true;
// Format-specific options
public string DateTimeFormat { get; set; } = "yyyy-MM-dd HH:mm:ss";
public string Delimiter { get; set; } = ",";
public bool HasHeaderRow { get; set; } = true;
public int SkipRows { get; set; } = 0;
public void Validate()
{
if (string.IsNullOrEmpty(Delimiter))
throw new ArgumentException("Delimiter is required");
}
}
}
```
### 3. Update Import Formats Enum
**File:** `Common/DTS.Common.Import/Enums/ImportFormats.cs`
```csharp
namespace DTS.Common.Import.Enums
{
public enum ImportFormats
{
NOT_SPECIFIED,
CSV,
EQX,
DTS_XML,
{FORMAT_NAME} // Add new format
}
public enum ImportFileFormat
{
NoTestSetup,
SingleTestSetup,
MultipleTestSetup
}
}
```
### 4. Create Parser Factory Registration
**File:** `Common/DTS.Common.Import/Factories/{FormatName}ParserFactory.cs`
```csharp
using System;
using DTS.Common.Import.ImportOptions;
using DTS.Common.Import.Parsers;
using DTS.Common.Interface.Sensors;
using DTS.SensorDB;
namespace DTS.Common.Import.Factories
{
public class {FORMAT_NAME}ParserFactory
{
public static {FORMAT_NAME}Parser Create(
IImportNotification importNotification,
User user,
{FORMAT_NAME}ImportOptions options)
{
if (options == null)
{
options = new {FORMAT_NAME}ImportOptions();
}
return new {FORMAT_NAME}Parser(importNotification, user, options);
}
}
}
```
### 5. Add File Detection Logic
**File:** `Common/DTS.Common.Import/ImportObject.cs` (modify)
Add method to detect format from file:
```csharp
public static ImportFormats DetectFormat(string filePath)
{
var extension = Path.GetExtension(filePath).ToLowerInvariant();
switch (extension)
{
case ".csv":
return ImportFormats.CSV;
case ".eqx":
return ImportFormats.EQX;
case ".{FORMAT_EXTENSION}":
return ImportFormats.{FORMAT_NAME};
default:
// Check file content for format signature
return DetectFormatFromContent(filePath);
}
}
private static ImportFormats DetectFormatFromContent(string filePath)
{
// Read first few lines to detect format
var firstLine = File.ReadLines(filePath).FirstOrDefault();
if (firstLine != null)
{
// Check for format-specific signatures
if (firstLine.StartsWith("{FORMAT_SIGNATURE}"))
{
return ImportFormats.{FORMAT_NAME};
}
}
return ImportFormats.NOT_SPECIFIED;
}
```
### 6. Create Unit Tests
**File:** `Common/DTS.Common.Tests/{FormatName}ParserShould.cs`
```csharp
using DTS.Common.Import;
using DTS.Common.Import.ImportOptions;
using NUnit.Framework;
using System;
using System.IO;
namespace DTS.Common.Tests
{
[TestFixture]
public class {FORMAT_NAME}ParserShould
{
private {FORMAT_NAME}Parser _parser;
private {FORMAT_NAME}ImportOptions _options;
[SetUp]
public void Setup()
{
_options = new {FORMAT_NAME}ImportOptions();
_parser = new {FORMAT_NAME}Parser(null, null, _options);
}
[Test]
public void Parse_ShouldReturnEmptyImportObject_WhenFileNameIsNull()
{
// Arrange
var importObject = new ImportObject();
_parser.FileName = null;
// Act
_parser.Parse(ref importObject);
// Assert
Assert.IsNotNull(importObject);
}
[Test]
public void Parse_ShouldThrowArgumentNullException_WhenImportObjectIsNull()
{
// Arrange
ImportObject importObject = null;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => _parser.Parse(ref importObject));
}
[Test]
public void Parse_ShouldImportSensors_WhenValidFile()
{
// Arrange
var importObject = new ImportObject();
var testFile = CreateTestFile();
_parser.FileName = testFile;
// Act
_parser.Parse(ref importObject);
// Assert
// Add assertions for expected data
// Cleanup
File.Delete(testFile);
}
private string CreateTestFile()
{
var path = Path.GetTempFileName();
// Write test content
File.WriteAllText(path, "test content");
return path;
}
}
}
```
## Files to Create/Modify Summary
| File | Action |
|------|--------|
| `Parsers/{FormatName}/{FormatName}Parser.cs` | Create |
| `ImportOptions/{FormatName}ImportOptions.cs` | Create |
| `Factories/{FormatName}ParserFactory.cs` | Create |
| `Enums/ImportFormats.cs` | Modify (add enum value) |
| `ImportObject.cs` | Modify (add detection logic) |
| `DTS.Common.Tests/{FormatName}ParserShould.cs` | Create |
## Parser Base Class Reference
```csharp
public abstract class ParseVariantBase
{
public string FileName { get; set; }
public abstract void Parse(ref ImportObject importObject);
}
```
## ImportObject Key Methods
```csharp
// Adding data to import object
void AddSensor(SensorData sensor);
void AddTestSetup(TestTemplate template);
void AddHardware(DASHardware hardware);
void AddCalibration(SensorCalibration calibration);
// Error handling
void AddError(ImportError error);
IEnumerable<ImportError> Errors();
// Format detection
ImportFormats GetImportFileFormat();
```
## Validation Checklist
- [ ] Parser inherits from `ParseVariantBase`
- [ ] `Parse()` method validates null inputs
- [ ] File existence checked before parsing
- [ ] Errors added to `ImportObject` for failures
- [ ] Progress notification via `IImportNotification`
- [ ] Source format set on `ImportObject`
- [ ] Import options class with validation
- [ ] Format added to `ImportFormats` enum
- [ ] File detection logic implemented
- [ ] Unit tests for happy path and error cases
- [ ] Test file cleanup in tests
## Common Patterns
1. **Dependency Injection:** Parser receives notification and user objects
2. **Error Accumulation:** Add errors to ImportObject rather than throwing
3. **Progress Notification:** Call `NotifyProgress()` during long operations
4. **File Validation:** Check existence before reading
5. **Format Detection:** Check extension and content signature
## Supported Data Types
The `ImportObject` can hold:
- `SensorData` - Sensor configurations
- `TestTemplate` - Test setup definitions
- `DASHardware` - Data acquisition hardware
- `SensorCalibration` - Calibration data
- `Group` - Test object groups
- `ISO.CustomerDetails` - Customer information

View File

@@ -0,0 +1,451 @@
# Add New Report - DataPRO Prompt Template
## Context
DataPRO reports are implemented as Prism modules in two locations:
- `DataPRO/Modules/Reports/` - Full DataPRO reports
- `DTS Viewer/DTS.Viewer.Reports/` - Viewer-specific reports
Reports follow MVVM with separate Input and Output views for parameter collection and results display.
## System Architecture
```
DataPRO/Modules/Reports/PedestrianAndHeadReports/
├── Classes/ # Report generation logic
│ ├── ReportBase.cs # Base class for reports
│ ├── ExportBase.cs # Export functionality
│ └── {ReportName}Export.cs # Specific export logic
├── View/
│ ├── {ReportName}InputView.xaml # Parameter input UI
│ └── {ReportName}OutputView.xaml # Results display UI
├── ViewModel/
│ └── {ReportName}ViewModel.cs
├── Resources/ # Localization
└── {ReportName}Module.cs # Module registration
```
## Step-by-Step Instructions
### 1. Create the Report Module Class
**File:** `DataPRO/Modules/Reports/{ReportName}/{ReportName}Module.cs`
```csharp
using System;
using System.ComponentModel.Composition;
using System.Windows.Media.Imaging;
using DTS.Common;
using DTS.Common.Interface;
using Microsoft.Practices.Prism.Modularity;
using Microsoft.Practices.Unity;
namespace {REPORT_NAME}
{
[Export(typeof(IModule))]
[Module(ModuleName = "{REPORT_NAME}Module")]
public class {REPORT_NAME}Module : IModule
{
private readonly IUnityContainer _unityContainer;
public {REPORT_NAME}Module(IUnityContainer unityContainer)
{
_unityContainer = unityContainer;
}
public void Initialize()
{
_unityContainer.RegisterType<I{REPORT_NAME}InputView, {REPORT_NAME}InputView>();
_unityContainer.RegisterType<I{REPORT_NAME}OutputView, {REPORT_NAME}OutputView>();
_unityContainer.RegisterType<I{REPORT_NAME}ViewModel, {REPORT_NAME}ViewModel>();
}
}
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
public class {REPORT_NAME}ImageAttribute : ImageAttribute
{
private BitmapImage _img;
public {REPORT_NAME}ImageAttribute() : this(null) { }
public override BitmapImage AssemblyImage
{
get { _img = AssemblyInfo.GetImage(AssemblyNames.DB.ToString()); return _img; }
}
public {REPORT_NAME}ImageAttribute(string s)
{
_img = AssemblyInfo.GetImage(AssemblyNames.DB.ToString());
}
public override Type GetAttributeType() => typeof(ImageAttribute);
public override BitmapImage GetAssemblyImage() => AssemblyImage;
private string _name;
public override string AssemblyName
{
get { _name = AssemblyNames.{REPORT_GROUP}.ToString(); return _name; }
}
public override string GetAssemblyName() => AssemblyName;
private string _group;
public override string AssemblyGroup
{
get { _group = eAssemblyGroups.Administrative.ToString(); return _group; }
}
public override string GetAssemblyGroup() => AssemblyGroup;
public override eAssemblyRegion GetAssemblyRegion()
{
throw new NotImplementedException();
}
public override eAssemblyRegion AssemblyRegion => throw new NotImplementedException();
}
}
```
### 2. Create the Report Base Class
**File:** `DataPRO/Modules/Reports/{ReportName}/Classes/{ReportName}.cs`
```csharp
using System;
using System.Collections.Generic;
using DTS.Common.Storage;
namespace {REPORT_NAME}
{
public class {REPORT_NAME}Report
{
public string ReportTitle { get; set; }
public DateTime GeneratedDate { get; set; }
public List<ReportChannel> Channels { get; set; }
public {REPORT_NAME}Report()
{
Channels = new List<ReportChannel>();
GeneratedDate = DateTime.Now;
}
public void Generate(TestSetup testSetup, ReportParameters parameters)
{
// Validate inputs
if (testSetup == null)
throw new ArgumentNullException(nameof(testSetup));
// Generate report data
ProcessData(testSetup, parameters);
}
private void ProcessData(TestSetup testSetup, ReportParameters parameters)
{
// Implementation specific to report type
}
}
public class ReportChannel
{
public string ChannelName { get; set; }
public double PeakValue { get; set; }
public double Duration { get; set; }
}
public class ReportParameters
{
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public double Threshold { get; set; }
}
}
```
### 3. Create the Export Class
**File:** `DataPRO/Modules/Reports/{ReportName}/Classes/{ReportName}Export.cs`
```csharp
using System;
using System.Data;
using System.IO;
using DTS.Common.Storage;
namespace {REPORT_NAME}
{
public class {REPORT_NAME}Export : ExportBase
{
public {REPORT_NAME}Report Report { get; set; }
public void ExportToCSV(string filePath)
{
if (Report == null)
throw new InvalidOperationException("Report not generated");
using (var writer = new StreamWriter(filePath))
{
WriteHeader(writer);
WriteData(writer);
}
}
public void ExportToExcel(string filePath)
{
// Excel export implementation
}
private void WriteHeader(StreamWriter writer)
{
writer.WriteLine($"Report,{Report.ReportTitle}");
writer.WriteLine($"Generated,{Report.GeneratedDate:yyyy-MM-dd HH:mm:ss}");
writer.WriteLine();
writer.WriteLine("Channel,Peak Value,Duration");
}
private void WriteData(StreamWriter writer)
{
foreach (var channel in Report.Channels)
{
writer.WriteLine($"{channel.ChannelName},{channel.PeakValue},{channel.Duration}");
}
}
}
}
```
### 4. Create Input View (XAML)
**File:** `DataPRO/Modules/Reports/{ReportName}/View/{ReportName}InputView.xaml`
```xml
<UserControl x:Class="{REPORT_NAME}.{REPORT_NAME}InputView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:resx="clr-namespace:{REPORT_NAME}.Resources">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{x:Static resx:StringResources.ReportParameters}"
Style="{StaticResource HeaderStyle}"/>
<StackPanel Grid.Row="1" Orientation="Vertical" Margin="5">
<DatePicker SelectedDate="{Binding StartDate, Mode=TwoWay}"
Header="{x:Static resx:StringResources.StartDate}"/>
<DatePicker SelectedDate="{Binding EndDate, Mode=TwoWay}"
Header="{x:Static resx:StringResources.EndDate}"/>
<TextBox Text="{Binding Threshold, Mode=TwoWay}"
Header="{x:Static resx:StringResources.Threshold}"/>
</StackPanel>
<Button Grid.Row="2" Content="{x:Static resx:StringResources.Generate}"
Command="{Binding GenerateCommand}"
HorizontalAlignment="Right" Margin="5"/>
</Grid>
</UserControl>
```
### 5. Create Output View (XAML)
**File:** `DataPRO/Modules/Reports/{ReportName}/View/{ReportName}OutputView.xaml`
```xml
<UserControl x:Class="{REPORT_NAME}.{REPORT_NAME}OutputView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:resx="clr-namespace:{REPORT_NAME}.Resources">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding ReportTitle}"
Style="{StaticResource HeaderStyle}"/>
<DataGrid Grid.Row="1" ItemsSource="{Binding ReportData}"
AutoGenerateColumns="False" IsReadOnly="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Channel" Binding="{Binding ChannelName}"/>
<DataGridTextColumn Header="Peak Value" Binding="{Binding PeakValue}"/>
<DataGridTextColumn Header="Duration" Binding="{Binding Duration}"/>
</DataGrid.Columns>
</DataGrid>
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="{x:Static resx:StringResources.ExportCSV}"
Command="{Binding ExportCSVCommand}" Margin="5"/>
<Button Content="{x:Static resx:StringResources.ExportExcel}"
Command="{Binding ExportExcelCommand}" Margin="5"/>
</StackPanel>
</Grid>
</UserControl>
```
### 6. Create the ViewModel
**File:** `DataPRO/Modules/Reports/{ReportName}/ViewModel/{ReportName}ViewModel.cs`
```csharp
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ComponentModel.Composition;
using System.Windows.Input;
using DTS.Common.Interface;
using Microsoft.Practices.Prism.Commands;
using Microsoft.Practices.Unity;
namespace {REPORT_NAME}
{
public class {REPORT_NAME}ViewModel : I{REPORT_NAME}ViewModel, INotifyPropertyChanged
{
private readonly IUnityContainer _unityContainer;
public I{REPORT_NAME}InputView InputView { get; set; }
public I{REPORT_NAME}OutputView OutputView { get; set; }
private DateTime _startDate;
public DateTime StartDate
{
get => _startDate;
set { _startDate = value; OnPropertyChanged(nameof(StartDate)); }
}
private DateTime _endDate;
public DateTime EndDate
{
get => _endDate;
set { _endDate = value; OnPropertyChanged(nameof(EndDate)); }
}
private double _threshold;
public double Threshold
{
get => _threshold;
set { _threshold = value; OnPropertyChanged(nameof(Threshold)); }
}
public ObservableCollection<ReportChannel> ReportData { get; set; }
public ICommand GenerateCommand { get; }
public ICommand ExportCSVCommand { get; }
public ICommand ExportExcelCommand { get; }
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public {REPORT_NAME}ViewModel(
I{REPORT_NAME}InputView inputView,
I{REPORT_NAME}OutputView outputView,
IUnityContainer unityContainer)
{
InputView = inputView;
InputView.DataContext = this;
OutputView = outputView;
OutputView.DataContext = this;
_unityContainer = unityContainer;
ReportData = new ObservableCollection<ReportChannel>();
GenerateCommand = new DelegateCommand(OnGenerate);
ExportCSVCommand = new DelegateCommand(OnExportCSV);
ExportExcelCommand = new DelegateCommand(OnExportExcel);
InitializeDefaults();
}
private void InitializeDefaults()
{
StartDate = DateTime.Now.AddDays(-7);
EndDate = DateTime.Now;
Threshold = 0.0;
}
private void OnGenerate()
{
// Generate report logic
var report = new {REPORT_NAME}Report();
// ... generate data
}
private void OnExportCSV()
{
// Export to CSV
}
private void OnExportExcel()
{
// Export to Excel
}
}
}
```
### 7. Add Localization Resources
**File:** `DataPRO/Modules/Reports/{ReportName}/Resources/StringResources.resx`
Add required strings:
- `ReportParameters`
- `StartDate`
- `EndDate`
- `Threshold`
- `Generate`
- `ExportCSV`
- `ExportExcel`
## For DTS Viewer Reports
If creating a Viewer report, use this location:
```
DTS Viewer/DTS.Viewer.Reports/DTS.Viewer.{ReportName}/
```
The module class differs slightly:
```csharp
[Module(ModuleName = "{REPORT_NAME}")]
public class {REPORT_NAME}Module : I{REPORT_NAME}Module
{
public bool SessionStarted { get; private set; }
public void StartSession()
{
var eventAggregator = _unityContainer.Resolve<IEventAggregator>();
eventAggregator.GetEvent<LoadViewModulEvent>().Publish(new LoadViewModulArg());
SessionStarted = true;
}
}
```
## Files to Create Summary
| File | Action |
|------|--------|
| `{ReportName}/{ReportName}Module.cs` | Create |
| `{ReportName}/Classes/{ReportName}.cs` | Create |
| `{ReportName}/Classes/{ReportName}Export.cs` | Create |
| `{ReportName}/View/{ReportName}InputView.xaml` | Create |
| `{ReportName}/View/{ReportName}InputView.xaml.cs` | Create |
| `{ReportName}/View/{ReportName}OutputView.xaml` | Create |
| `{ReportName}/View/{ReportName}OutputView.xaml.cs` | Create |
| `{ReportName}/ViewModel/{ReportName}ViewModel.cs` | Create |
| `{ReportName}/Resources/StringResources.resx` | Create |
| `DTS.Common/Interface/{ReportName}Interfaces.cs` | Create |
## Validation Checklist
- [ ] Module registered with `[Module]` attribute
- [ ] Assembly image attribute defined
- [ ] Input/Output views follow naming convention
- [ ] ViewModel implements `INotifyPropertyChanged`
- [ ] Commands use `DelegateCommand` from Prism
- [ ] Export methods handle file I/O properly
- [ ] Localization strings for all UI text
- [ ] Report generation validates inputs
- [ ] Error handling implemented
- [ ] Assembly group set appropriately (`eAssemblyGroups.Administrative`)
## Common Patterns
1. **Two-View Pattern:** Reports use separate Input and Output views
2. **ExportBase Inheritance:** Export classes inherit from `ExportBase`
3. **ObservableCollection:** Use for data binding in ViewModels
4. **DelegateCommand:** Use Prism's `DelegateCommand` for ICommand implementation

View File

@@ -0,0 +1,244 @@
# Add New Sensor Type - DataPRO Prompt Template
## Context
DataPRO manages sensor configurations through the `DataPRO/Modules/SensorsList/` module. The system supports various sensor types (Analog, Digital I/O, Squib, UART, Stream) with a consistent MVVM architecture using Prism modularity and Unity dependency injection.
## System Architecture
```
DataPRO/Modules/SensorsList/
├── SensorsList/ # Main sensor list management
│ ├── Model/ # Sensor data models
│ ├── View/ # XAML views
│ ├── ViewModel/ # Business logic
│ ├── Resources/ # Localization
│ └── SensorsListModule.cs # Module registration
├── SensorSettingsModule/ # Sensor configuration UI
└── SoftwareFilters/ # Signal processing filters
```
## Step-by-Step Instructions
### 1. Create the Sensor Model
**File:** `DataPRO/Modules/SensorsList/SensorsList/Model/{SensorName}Setting.cs`
```csharp
using System;
using System.Collections.Generic;
using DTS.Common.Classes.Sensors;
using DTS.Common.Enums.Sensors;
namespace SensorsList.Model
{
public class {SENSOR_NAME}Setting : ISensorSetting
{
public int Id { get; set; }
public string Name { get; set; }
public string SerialNumber { get; set; }
public string ChannelCode { get; set; }
// Add sensor-specific properties
public double CalibrationFactor { get; set; }
public string EngineeringUnits { get; set; }
// Required interface members
public KnownChannelTypes ChannelType => KnownChannelTypes.{CHANNEL_TYPE};
public void Validate()
{
if (string.IsNullOrEmpty(Name))
throw new ArgumentException("Name is required");
}
}
}
```
### 2. Create the View (XAML)
**File:** `DataPRO/Modules/SensorsList/SensorsList/View/{SensorName}View.xaml`
```xml
<UserControl x:Class="SensorsList.View.{SENSOR_NAME}View"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:resx="clr-namespace:SensorsList.Resources"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Text="{x:Static resx:StringResources.{SENSOR_NAME}_Header}"
Style="{StaticResource HeaderStyle}"/>
<StackPanel Grid.Row="1" Orientation="Vertical">
<TextBox Text="{Binding {SENSOR_NAME}Name, Mode=TwoWay}"
Header="{x:Static resx:StringResources.Name}"/>
<!-- Add sensor-specific controls -->
</StackPanel>
</Grid>
</UserControl>
```
**Code-behind:** `DataPRO/Modules/SensorsList/SensorsList/View/{SensorName}View.xaml.cs`
```csharp
using System.Windows.Controls;
using DTS.Common.Interface.Sensors.SensorsList;
namespace SensorsList.View
{
public partial class {SENSOR_NAME}View : UserControl, I{SENSOR_NAME}View
{
public {SENSOR_NAME}View()
{
InitializeComponent();
}
}
}
```
### 3. Create the ViewModel
**File:** `DataPRO/Modules/SensorsList/SensorsList/ViewModel/{SensorName}ViewModel.cs`
```csharp
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ComponentModel.Composition;
using DTS.Common.Interface.Sensors.SensorsList;
using DTS.Common.Events.Sensors.SensorsList;
using Prism.Events;
using Prism.Regions;
using Unity;
namespace SensorsList
{
[PartCreationPolicy(CreationPolicy.Shared)]
public class {SENSOR_NAME}ViewModel : I{SENSOR_NAME}ViewModel, INotifyPropertyChanged
{
private readonly IEventAggregator _eventAggregator;
private readonly IRegionManager _regionManager;
private readonly IUnityContainer _unityContainer;
public I{SENSOR_NAME}View View { get; set; }
private ObservableCollection<{SENSOR_NAME}Setting> _items;
public ObservableCollection<{SENSOR_NAME}Setting> Items
{
get => _items;
set { _items = value; OnPropertyChanged(nameof(Items)); }
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public {SENSOR_NAME}ViewModel(
I{SENSOR_NAME}View view,
IRegionManager regionManager,
IEventAggregator eventAggregator,
IUnityContainer unityContainer)
{
View = view;
View.DataContext = this;
_regionManager = regionManager;
_eventAggregator = eventAggregator;
_unityContainer = unityContainer;
Initialize();
}
private void Initialize()
{
Items = new ObservableCollection<{SENSOR_NAME}Setting>();
// Subscribe to events
_eventAggregator.GetEvent<{SENSOR_NAME}UpdatedEvent>().Subscribe(On{SENSOR_NAME}Updated);
}
private void On{SENSOR_NAME}Updated({SENSOR_NAME}Setting setting)
{
// Handle update logic
}
}
}
```
### 4. Create Interface
**File:** `Common/DTS.Common/Interface/Sensors/SensorsList/I{SensorName}View.cs`
```csharp
namespace DTS.Common.Interface.Sensors.SensorsList
{
public interface I{SENSOR_NAME}View
{
object DataContext { get; set; }
}
public interface I{SENSOR_NAME}ViewModel
{
I{SENSOR_NAME}View View { get; set; }
}
}
```
### 5. Update Module Registration
**File:** `DataPRO/Modules/SensorsList/SensorsList/SensorsListModule.cs`
Add to the `Initialize()` method:
```csharp
_unityContainer.RegisterType<I{SENSOR_NAME}View, {SENSOR_NAME}View>();
_unityContainer.RegisterType<I{SENSOR_NAME}ViewModel, {SENSOR_NAME}ViewModel>();
```
### 6. Add Localization Strings
**File:** `DataPRO/Modules/SensorsList/SensorsList/Resources/StringResources.resx`
Add entries:
- `{SENSOR_NAME}_Header` - Display header
- `{SENSOR_NAME}_Description` - Description text
### 7. Add Channel Type Enum (if new)
**File:** `Common/DTS.Common/Enums/Sensors/KnownChannelTypes.cs`
```csharp
public enum KnownChannelTypes
{
// Existing types...
{SENSOR_TYPE_CODE} // Add new type
}
```
## Files to Create/Modify Summary
| File | Action |
|------|--------|
| `SensorsList/Model/{SensorName}Setting.cs` | Create |
| `SensorsList/View/{SensorName}View.xaml` | Create |
| `SensorsList/View/{SensorName}View.xaml.cs` | Create |
| `SensorsList/ViewModel/{SensorName}ViewModel.cs` | Create |
| `DTS.Common/Interface/Sensors/SensorsList/I{SensorName}View.cs` | Create |
| `SensorsList/SensorsListModule.cs` | Modify (register types) |
| `SensorsList/Resources/StringResources.resx` | Modify (add strings) |
## Validation Checklist
- [ ] Model implements `ISensorSetting` interface
- [ ] View implements corresponding interface
- [ ] ViewModel has `[PartCreationPolicy(CreationPolicy.Shared)]` attribute
- [ ] Types registered in module's `Initialize()` method
- [ ] Localization strings added for all UI text
- [ ] Property change notifications implemented
- [ ] Event subscriptions properly managed
- [ ] Constructor injection follows existing patterns
- [ ] XAML uses resource references for strings (`{x:Static resx:StringResources...}`)
- [ ] Channel type added to enum if new sensor category
## Common Patterns to Follow
1. **Naming Convention:** Use PascalCase for class names, camelCase for private fields with underscore prefix
2. **Dependency Injection:** All dependencies injected via constructor
3. **Events:** Use `IEventAggregator` for cross-module communication
4. **Regions:** Register views with appropriate region (`eAssemblyRegion.SensorsListRegion`)
5. **ReSharper Annotations:** Include `// ReSharper disable` comments at file top as needed

View File

@@ -0,0 +1,460 @@
# Add Unit Test - DataPRO Prompt Template
## Context
DataPRO uses NUnit for unit testing with test projects in `Common/DTS.Common.Tests/`. Tests follow the Arrange-Act-Assert (AAA) pattern and use `TestCaseSource` for parameterized tests. The testing framework emphasizes clear test naming and comprehensive coverage of edge cases.
## System Architecture
```
Common/DTS.Common.Tests/
├── ChannelTypeUtilityShould.cs # Example test file
├── FilterClassShould.cs
├── GroupChannelShould.cs
├── LinearizationFormulaShould.cs
├── NetworkUtilsShould.cs
├── DTS.Common.Tests.csproj
└── Properties/
└── AssemblyInfo.cs
```
## Step-by-Step Instructions
### 1. Create Test Class
**File:** `Common/DTS.Common.Tests/{ClassName}Should.cs`
```csharp
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace DTS.Common.Tests
{
[TestFixture]
public class {CLASS_NAME}Should
{
private {CLASS_NAME} _sut; // System Under Test
[SetUp]
public void Setup()
{
_sut = new {CLASS_NAME}();
}
[TearDown]
public void TearDown()
{
// Cleanup if needed
_sut = null;
}
[Test]
public void MethodName_ShouldReturnExpectedResult_WhenGivenValidInput()
{
// Arrange
var input = "valid input";
var expected = "expected output";
// Act
var result = _sut.MethodName(input);
// Assert
Assert.That(result, Is.EqualTo(expected));
}
}
}
```
### 2. Basic Test Patterns
#### Simple Assertion Test
```csharp
[Test]
public void CalculateTotal_ShouldReturnSum_WhenGivenValidNumbers()
{
// Arrange
var calculator = new Calculator();
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var expected = 15;
// Act
var result = calculator.CalculateTotal(numbers);
// Assert
Assert.That(result, Is.EqualTo(expected));
}
```
#### Null/Empty Input Test
```csharp
[Test]
public void ParseInput_ShouldReturnEmpty_WhenPassedNull()
{
// Arrange
// Act
var result = _sut.ParseInput(null);
// Assert
Assert.IsNotNull(result);
Assert.That(result, Is.EqualTo(string.Empty));
}
[Test]
public void ParseInput_ShouldReturnEmpty_WhenPassedEmptyString()
{
// Arrange
// Act
var result = _sut.ParseInput("");
// Assert
Assert.IsNotNull(result);
Assert.That(result, Is.EqualTo(string.Empty));
}
```
#### Exception Test
```csharp
[Test]
public void Divide_ShouldThrowDivideByZeroException_WhenDivisorIsZero()
{
// Arrange
var dividend = 10;
var divisor = 0;
// Act & Assert
Assert.Throws<DivideByZeroException>(() => _sut.Divide(dividend, divisor));
}
[Test]
public void SetName_ShouldThrowArgumentException_WhenNameIsNull()
{
// Arrange
string name = null;
// Act & Assert
Assert.Throws<ArgumentException>(() => _sut.SetName(name));
}
```
### 3. Parameterized Tests
#### Using TestCase
```csharp
[TestCase(1, 2, 3)]
[TestCase(10, 20, 30)]
[TestCase(-5, 5, 0)]
[TestCase(0, 0, 0)]
public void Add_ShouldReturnCorrectSum_WhenGivenTwoNumbers(int a, int b, int expected)
{
// Arrange
// Act
var result = _sut.Add(a, b);
// Assert
Assert.That(result, Is.EqualTo(expected));
}
```
#### Using TestCaseSource
```csharp
public static IEnumerable<TestCaseData> ChannelTypeTestCases
{
get
{
yield return new TestCaseData("AC087155.1", "AC");
yield return new TestCaseData("DC123456.2", "DC");
yield return new TestCaseData("TM987654.3", "TM");
}
}
[TestCaseSource(nameof(ChannelTypeTestCases))]
public void ParseChannelType_ShouldReturnCorrectType_WhenGivenValidName(
string sensorName, string expectedType)
{
// Arrange
// Act
var result = _sut.ParseChannelType(sensorName);
// Assert
Assert.That(result, Is.EqualTo(expectedType));
}
```
#### Generating Test Data from Enum
```csharp
public static Array GetKnownChannelTypes()
{
var testValuesFromEnum = new List<string>();
var values = Enum.GetValues(typeof(KnownChannelTypes))
.Cast<KnownChannelTypes>()
.Select(x => x.ToString())
.ToArray();
foreach (var value in values)
{
testValuesFromEnum.Add($"{value}087155.1");
}
return testValuesFromEnum.ToArray();
}
[TestCaseSource("GetKnownChannelTypes")]
public void ParseSensorKnownChannelType_ShouldReturnCorrectTag_WhenPassedSensorNameWithCorrectPrefix(
string sensorName)
{
// Arrange
// Act
var result = ChannelTypeUtility.ParseSensorKnownChannelType(sensorName);
// Assert
Assert.IsNotNull(result);
Assert.IsTrue(Enum.IsDefined(typeof(KnownChannelTypes), result));
}
```
### 4. Testing Async Methods
```csharp
[Test]
public async Task LoadDataAsync_ShouldReturnData_WhenDataExists()
{
// Arrange
var expectedCount = 5;
// Act
var result = await _sut.LoadDataAsync();
// Assert
Assert.IsNotNull(result);
Assert.That(result.Count, Is.EqualTo(expectedCount));
}
[Test]
public void ProcessAsync_ShouldThrowException_WhenInputInvalid()
{
// Arrange
var invalidInput = "";
// Act & Assert
Assert.ThrowsAsync<ArgumentException>(async () =>
await _sut.ProcessAsync(invalidInput));
}
```
### 5. Testing Events
```csharp
[Test]
public void ValueChanged_ShouldRaiseEvent_WhenValueIsSet()
{
// Arrange
var eventRaised = false;
_sut.ValueChanged += (sender, args) => eventRaised = true;
// Act
_sut.Value = 42;
// Assert
Assert.IsTrue(eventRaised, "ValueChanged event was not raised");
}
[Test]
public void PropertyChanged_ShouldBeRaised_WhenNameChanges()
{
// Arrange
var eventArgs = new List<string>();
_sut.PropertyChanged += (sender, e) => eventArgs.Add(e.PropertyName);
// Act
_sut.Name = "New Name";
// Assert
Assert.Contains("Name", eventArgs);
}
```
### 6. Testing Collections
```csharp
[Test]
public void GetItems_ShouldReturnEmptyCollection_WhenNoItemsAdded()
{
// Arrange
// Act
var result = _sut.GetItems();
// Assert
Assert.IsNotNull(result);
Assert.That(result, Is.Empty);
}
[Test]
public void AddItem_ShouldIncreaseCount_WhenItemIsValid()
{
// Arrange
var item = new Item { Id = 1, Name = "Test" };
var initialCount = _sut.ItemCount;
// Act
_sut.AddItem(item);
// Assert
Assert.That(_sut.ItemCount, Is.EqualTo(initialCount + 1));
}
[Test]
public void GetItems_ShouldReturnItemsInOrder_WhenItemsAdded()
{
// Arrange
_sut.AddItem(new Item { Id = 3 });
_sut.AddItem(new Item { Id = 1 });
_sut.AddItem(new Item { Id = 2 });
// Act
var result = _sut.GetItems().ToList();
// Assert
Assert.That(result[0].Id, Is.EqualTo(1));
Assert.That(result[1].Id, Is.EqualTo(2));
Assert.That(result[2].Id, Is.EqualTo(3));
}
```
### 7. Mocking Dependencies (if needed)
```csharp
using Moq;
[TestFixture]
public class SensorServiceShould
{
private Mock<ISensorRepository> _mockRepository;
private SensorService _sut;
[SetUp]
public void Setup()
{
_mockRepository = new Mock<ISensorRepository>();
_sut = new SensorService(_mockRepository.Object);
}
[Test]
public void GetSensor_ShouldReturnSensor_WhenSensorExists()
{
// Arrange
var sensorId = 123;
var expectedSensor = new Sensor { Id = sensorId, Name = "Test" };
_mockRepository.Setup(r => r.Find(sensorId)).Returns(expectedSensor);
// Act
var result = _sut.GetSensor(sensorId);
// Assert
Assert.That(result, Is.EqualTo(expectedSensor));
_mockRepository.Verify(r => r.Find(sensorId), Times.Once);
}
[Test]
public void SaveSensor_ShouldCallRepository_WhenSensorIsValid()
{
// Arrange
var sensor = new Sensor { Id = 1, Name = "Test" };
// Act
_sut.SaveSensor(sensor);
// Assert
_mockRepository.Verify(r => r.Save(sensor), Times.Once);
}
}
```
## Test Naming Convention
Follow this pattern: `MethodName_Scenario_ExpectedResult`
Examples:
- `ParseSensorName_ShouldReturnNull_WhenPassedNull`
- `ParseSensorName_ShouldReturnEmpty_WhenPassedEmptyString`
- `ParseSensorName_ShouldReturnCorrectTag_WhenPassedValidName`
- `CalculateTotal_ShouldThrowException_WhenListIsNull`
## Common Assertions
```csharp
// Equality
Assert.That(result, Is.EqualTo(expected));
Assert.That(result, Is.Not.EqualTo(wrongValue));
// Null checks
Assert.That(result, Is.Null);
Assert.That(result, Is.Not.Null);
// Boolean
Assert.That(result, Is.True);
Assert.That(result, Is.False);
// Collections
Assert.That(collection, Is.Empty);
Assert.That(collection, Is.Not.Empty);
Assert.That(collection, Has.Count.EqualTo(5));
Assert.That(collection, Does.Contain(item));
// Exceptions
Assert.Throws<ArgumentException>(() => method());
Assert.ThrowsAsync<InvalidOperationException>(async () => await method());
// String
Assert.That(result, Does.StartWith("prefix"));
Assert.That(result, Does.EndWith("suffix"));
Assert.That(result, Does.Contain("substring"));
Assert.That(result, Is.Empty);
// Range
Assert.That(value, Is.InRange(1, 10));
Assert.That(value, Is.GreaterThan(0));
Assert.That(value, Is.LessThan(100));
// Type checking
Assert.That(result, Is.TypeOf<ExpectedType>());
Assert.That(result, Is.InstanceOf<IBaseInterface>());
```
## Files to Create Summary
| File | Action |
|------|--------|
| `DTS.Common.Tests/{ClassName}Should.cs` | Create |
## Validation Checklist
- [ ] Test class has `[TestFixture]` attribute
- [ ] Test methods have `[Test]` attribute
- [ ] `[SetUp]` used for test initialization
- [ ] `[TearDown]` used for cleanup (if needed)
- [ ] Test names follow naming convention
- [ ] Each test has Arrange-Act-Assert sections
- [ ] Edge cases tested (null, empty, boundary values)
- [ ] Exception cases tested
- [ ] Test is isolated (doesn't depend on other tests)
- [ ] No external dependencies (use mocks/stubs)
- [ ] Assertions are specific and meaningful
## Running Tests
```bash
# Run all tests in project
dotnet test Common/DTS.Common.Tests/
# Run specific test class
dotnet test --filter "FullyQualifiedName~ChannelTypeUtilityShould"
# Run specific test method
dotnet test --filter "FullyQualifiedName~ChannelTypeUtilityShould.ParseSensorKnownChannelType_ShouldReturnEmpty_WhenPassedNull"
```
## Best Practices
1. **One assertion per test** (or logically related assertions)
2. **Test behavior, not implementation**
3. **Use meaningful test names that describe the scenario**
4. **Keep tests independent** - no shared state between tests
5. **Test edge cases** - null, empty, max values, boundary conditions
6. **Don't test private methods directly** - test through public API
7. **Use parameterized tests for similar test cases with different data**
8. **Mock external dependencies** - database, file system, network

View File

@@ -0,0 +1,452 @@
# Fix Bug in ViewModel - DataPRO Prompt Template
## Context
DataPRO uses the MVVM (Model-View-ViewModel) pattern with Prism framework and Unity dependency injection. ViewModels contain business logic and state management, binding to Views through XAML data binding. Common issues include property change notification problems, command binding failures, and event subscription memory leaks.
## System Architecture
```
ViewModel Pattern:
┌─────────────┐ Data Binding ┌─────────────┐
│ View │◄────────────────────►│ ViewModel │
│ (XAML) │ │ (.cs) │
└─────────────┘ └─────────────┘
│ │
│ Code-behind │
▼ ▼
┌─────────────┐ Services/Events ┌─────────────┐
│ IView │◄───────────────────────►│ Services │
│ Interface │ │ (DI) │
└─────────────┘ └─────────────┘
```
## Common ViewModel Issues and Solutions
### Issue 1: Property Change Not Reflected in UI
**Symptoms:**
- Property value changes but UI doesn't update
- No visual feedback on data changes
**Diagnosis:**
```csharp
// Problem: Missing PropertyChanged notification
public string Name
{
get => _name;
set => _name = value; // Missing notification!
}
```
**Solution:**
```csharp
private string _name;
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged(nameof(Name));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
```
### Issue 2: Command Not Executing
**Symptoms:**
- Button click doesn't trigger action
- Command's CanExecute always returns false
**Diagnosis:**
```csharp
// Problem: Command not properly initialized or CanExecute not raising
public ICommand SaveCommand { get; set; }
// In constructor - missing initialization
SaveCommand = new DelegateCommand(OnSave); // Missing CanExecute check
```
**Solution:**
```csharp
// Using Prism's DelegateCommand
public ICommand SaveCommand { get; }
// In constructor
SaveCommand = new DelegateCommand(OnSave, CanSave);
private void OnSave()
{
// Save logic
}
private bool CanSave()
{
return !string.IsNullOrEmpty(Name) && HasChanges;
}
// Call when conditions change
private void RaiseCanExecuteChanged()
{
(SaveCommand as DelegateCommand)?.RaiseCanExecuteChanged();
}
// Call after property changes that affect CanExecute
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged(nameof(Name));
RaiseCanExecuteChanged(); // Update command state
}
}
}
```
### Issue 3: Event Subscription Memory Leak
**Symptoms:**
- ViewModel not garbage collected
- Multiple event handlers executing
- Memory usage increasing
**Diagnosis:**
```csharp
// Problem: Not unsubscribing from events
public MyViewModel(IEventAggregator eventAggregator)
{
eventAggregator.GetEvent<DataChangedEvent>().Subscribe(OnDataChanged);
// Missing: Keep subscriber reference for unsubscribe
}
```
**Solution:**
```csharp
using Prism.Events;
private readonly IEventAggregator _eventAggregator;
private SubscriptionToken _dataChangedToken;
public MyViewModel(IEventAggregator eventAggregator)
{
_eventAggregator = eventAggregator;
// Keep subscription token
_dataChangedToken = _eventAggregator.GetEvent<DataChangedEvent>()
.Subscribe(OnDataChanged, ThreadOption.PublisherThread, false,
data => data != null);
}
// Implement IDisposable
public void Dispose()
{
if (_dataChangedToken != null)
{
_eventAggregator.GetEvent<DataChangedEvent>().Unsubscribe(_dataChangedToken);
}
}
```
### Issue 4: Thread Affinity Problems
**Symptoms:**
- "The calling thread cannot access this object" exception
- UI freezing during operations
- Data updated on wrong thread
**Diagnosis:**
```csharp
// Problem: Updating UI-bound property from background thread
Task.Run(() => {
Status = "Processing..."; // Cross-thread violation
});
```
**Solution:**
```csharp
using System.Windows.Threading;
// Option 1: Use Dispatcher
Task.Run(() => {
Application.Current.Dispatcher.Invoke(() => {
Status = "Processing...";
});
});
// Option 2: Use ThreadOption in event subscription
_eventAggregator.GetEvent<DataChangedEvent>()
.Subscribe(OnDataChanged, ThreadOption.UIThread);
// Option 3: Use async/await properly
public async Task LoadDataAsync()
{
Status = "Loading..."; // On UI thread
var data = await _service.GetDataAsync(); // Background
Items = new ObservableCollection<DataItem>(data); // Back on UI thread
Status = "Complete";
}
```
### Issue 5: Collection Changes Not Notifying
**Symptoms:**
- Items added to collection don't appear in UI
- ListView/DataGrid not updating
**Diagnosis:**
```csharp
// Problem: Using List instead of ObservableCollection
public List<SensorItem> Sensors { get; set; }
```
**Solution:**
```csharp
using System.Collections.ObjectModel;
// Use ObservableCollection for UI binding
public ObservableCollection<SensorItem> Sensors { get; }
public MyViewModel()
{
Sensors = new ObservableCollection<SensorItem>();
}
// To add items from background thread:
Application.Current.Dispatcher.Invoke(() => {
Sensors.Add(newItem);
});
// For bulk updates, clear and add range:
public void UpdateSensors(List<SensorItem> newItems)
{
Sensors.Clear();
foreach (var item in newItems)
{
Sensors.Add(item);
}
}
```
## Debugging Approach
### Step 1: Check Property Implementation
```csharp
// Add debug output to property setter
public string Name
{
get => _name;
set
{
System.Diagnostics.Debug.WriteLine($"Name: {_name} -> {value}");
if (_name != value)
{
_name = value;
OnPropertyChanged(nameof(Name));
System.Diagnostics.Debug.WriteLine($"OnPropertyChanged raised for Name");
}
}
}
```
### Step 2: Verify Binding in View
```xml
<!-- Add PresentationTraceSources for binding debug -->
<TextBox Text="{Binding Name, Mode=TwoWay,
diagnostics:PresentationTraceSources.TraceLevel=High}"
xmlns:diagnostics="clr-namespace:System.Diagnostics;assembly=WindowsBase"/>
```
### Step 3: Check DataContext
```csharp
// In View code-behind, verify DataContext
public partial class MyView : UserControl
{
public MyView()
{
InitializeComponent();
this.DataContextChanged += (s, e) =>
{
System.Diagnostics.Debug.WriteLine($"DataContext: {e.NewValue?.GetType().Name}");
};
}
}
```
### Step 4: Verify Event Subscriptions
```csharp
// Add debug output to event handlers
private void OnDataChanged(DataChangedEventArgs args)
{
System.Diagnostics.Debug.WriteLine($"OnDataChanged called: {args?.Data}");
// ... handler logic
}
```
### Step 5: Check Command Binding
```csharp
// Add debug output to command methods
private void OnExecute()
{
System.Diagnostics.Debug.WriteLine("OnExecute called");
}
private bool CanExecute()
{
var result = !string.IsNullOrEmpty(Name);
System.Diagnostics.Debug.WriteLine($"CanExecute: {result}");
return result;
}
```
## ViewModel Template Reference
```csharp
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ComponentModel.Composition;
using System.Windows.Input;
using DTS.Common.Interface;
using Prism.Commands;
using Prism.Events;
using Prism.Regions;
using Unity;
namespace {NAMESPACE}
{
[PartCreationPolicy(CreationPolicy.Shared)]
public class {VIEWMODEL_NAME} : I{VIEWMODEL_NAME}, INotifyPropertyChanged, IDisposable
{
private readonly IEventAggregator _eventAggregator;
private readonly IRegionManager _regionManager;
private readonly IUnityContainer _unityContainer;
public I{VIEW_NAME} View { get; set; }
#region Properties with Change Notification
private string _status;
public string Status
{
get => _status;
set
{
if (_status != value)
{
_status = value;
OnPropertyChanged(nameof(Status));
}
}
}
#endregion
#region Commands
public ICommand SaveCommand { get; }
public ICommand CancelCommand { get; }
#endregion
#region Collections
public ObservableCollection<Item> Items { get; }
#endregion
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public {VIEWMODEL_NAME}(
I{VIEW_NAME} view,
IRegionManager regionManager,
IEventAggregator eventAggregator,
IUnityContainer unityContainer)
{
View = view;
View.DataContext = this;
_regionManager = regionManager;
_eventAggregator = eventAggregator;
_unityContainer = unityContainer;
// Initialize commands
SaveCommand = new DelegateCommand(OnSave, CanSave);
CancelCommand = new DelegateCommand(OnCancel);
// Initialize collections
Items = new ObservableCollection<Item>();
// Subscribe to events
SubscribeToEvents();
}
private void SubscribeToEvents()
{
// Event subscriptions
}
private void OnSave()
{
// Save implementation
}
private bool CanSave()
{
return true; // Add validation logic
}
private void OnCancel()
{
// Cancel implementation
}
public void Dispose()
{
// Unsubscribe from events
// Dispose resources
}
}
}
```
## Validation Checklist
After fixing a ViewModel bug:
- [ ] PropertyChanged event raised for all bound properties
- [ ] Commands use DelegateCommand with proper CanExecute
- [ ] RaiseCanExecuteChanged called when command state changes
- [ ] Event subscriptions use subscription tokens
- [ ] Unsubscribe from events in Dispose
- [ ] ObservableCollection used for collections
- [ ] Dispatcher used for cross-thread updates
- [ ] Async/await used for long-running operations
- [ ] DataContext set correctly in constructor
- [ ] View implements corresponding interface
## Quick Reference: Common Patterns
| Pattern | Implementation |
|---------|---------------|
| Property Notification | `OnPropertyChanged(nameof(PropertyName))` |
| Command | `new DelegateCommand(Execute, CanExecute)` |
| Event Subscription | `_eventAggregator.GetEvent<T>().Subscribe(Handler)` |
| Thread-Safe Update | `Dispatcher.Invoke(() => Property = value)` |
| Collection | `ObservableCollection<T>` |
| Validation | `RaiseCanExecuteChanged()` in property setters |

96
GLM5Analysis/README.md Normal file
View File

@@ -0,0 +1,96 @@
# GLM-5 Analysis Artifacts
This folder contains pre-computed analysis artifacts generated by GLM-5 for use with cheaper inference models.
## Purpose
These artifacts enable cheaper models to work effectively on the DataPRO codebase by providing:
- High-level architecture understanding
- Reusable code patterns
- Task-specific prompt templates
- Test scaffolds
## Contents
### Architecture.md
Comprehensive system architecture document including:
- System overview
- Solution structure
- Architecture patterns (MVVM, Prism, Unity DI)
- Module dependency graph
- Data flow diagrams
- Extension points
- Key technologies
- Gotchas and tech debt
### PatternLibrary/
Reusable code patterns extracted from the codebase:
| File | Purpose |
|------|---------|
| `PrismModulePattern.md` | Creating new Prism modules |
| `MVVM_Pattern.md` | View/ViewModel implementation |
| `DataAccessPattern.md` | Database operations via DbAPI |
| `ServicePattern.md` | Background services and hardware communication |
| `ImportExportPattern.md` | Data import/export workflows |
### PromptTemplates/
Task-specific prompts for common development activities:
| File | Use Case |
|------|----------|
| `AddNewSensorType.md` | Adding a new sensor to the system |
| `AddNewReport.md` | Creating a new report module |
| `AddNewImportFormat.md` | Supporting a new data format |
| `AddNewHardwareSupport.md` | Adding new DAS hardware support |
| `FixBugInViewModel.md` | Debugging MVVM issues |
| `AddUnitTest.md` | Creating unit tests |
### TestScaffolds/
Test templates for common testing scenarios.
## Usage with Cheaper Models
When using a smaller/cheaper model for development tasks:
1. **For architecture questions**: Include `Architecture.md` in context
2. **For code generation**: Include relevant pattern from `PatternLibrary/`
3. **For specific tasks**: Include relevant prompt template from `PromptTemplates/`
4. **For testing**: Include relevant scaffold from `TestScaffolds/`
### Example Context Assembly
```
You are working on DataPRO, a .NET WPF sensor data acquisition system.
Architecture overview:
[Include Architecture.md relevant sections]
Relevant pattern:
[Include pattern from PatternLibrary/]
Task:
[Include prompt from PromptTemplates/]
```
## Regeneration
These artifacts were generated on 2026-04-16. To regenerate:
1. Ensure enrichment is complete in `enriched-qwen3-coder-next/`
2. Run GLM-5 analysis on the codebase
3. Update this folder
## Token Budget
| Artifact | Approximate Size |
|----------|------------------|
| Architecture.md | ~16KB / ~4,000 tokens |
| PatternLibrary/* | ~56KB / ~14,000 tokens |
| PromptTemplates/* | ~75KB / ~19,000 tokens |
| TestScaffolds/* | TBD |
Select artifacts based on your context window budget.

View File

@@ -0,0 +1,227 @@
using System.Data;
using NUnit.Framework;
using NSubstitute;
namespace YourNamespace.Tests
{
[TestFixture]
public class DatabaseShould
{
private IDbConnection _connection;
private IDbTransaction _transaction;
private IDbCommand _command;
[SetUp]
public void Setup()
{
_connection = Substitute.For<IDbConnection>();
_transaction = Substitute.For<IDbTransaction>();
_command = Substitute.For<IDbCommand>();
_connection.BeginTransaction().Returns(_transaction);
_connection.CreateCommand().Returns(_command);
}
[Test]
public void ExecuteQuery_ReturnExpectedResults()
{
var reader = Substitute.For<IDataReader>();
reader.Read().Returns(true, true, false);
reader[0].Returns("Value1", "Value2");
_command.ExecuteReader().Returns(reader);
var sut = CreateSut();
var results = sut.ExecuteQuery("SELECT * FROM Table");
Assert.That(results.Count, Is.EqualTo(2));
Assert.That(results[0], Is.EqualTo("Value1"));
Assert.That(results[1], Is.EqualTo("Value2"));
}
[Test]
public void CommitTransaction_WhenOperationSucceeds()
{
var sut = CreateSut();
sut.ExecuteInTransaction(() => { _command.ExecuteNonQuery(); });
_transaction.Received(1).Commit();
}
[Test]
public void RollbackTransaction_WhenOperationFails()
{
_command.When(c => c.ExecuteNonQuery()).Do(_ => throw new System.Exception("DB Error"));
var sut = CreateSut();
Assert.Throws<System.Exception>(() => sut.ExecuteInTransaction(() => _command.ExecuteNonQuery()));
_transaction.Received(1).Rollback();
}
[Test]
public void UseCorrectParameters_WhenExecutingCommand()
{
var parameter = Substitute.For<IDbDataParameter>();
_command.CreateParameter().Returns(parameter);
var sut = CreateSut();
sut.ExecuteWithParameter("INSERT INTO Table (Col) VALUES (@val)", "@val", "TestValue");
_command.Received(1).Parameters.Add(Arg.Is<IDbDataParameter>(p =>
p.ParameterName == "@val" && (string)p.Value == "TestValue"));
}
[Test]
public void ReturnRowCount_WhenExecutingNonQuery()
{
_command.ExecuteNonQuery().Returns(5);
var sut = CreateSut();
var count = sut.ExecuteNonQuery("DELETE FROM Table");
Assert.That(count, Is.EqualTo(5));
}
[Test]
public void HandleNullValues_InQueryResults()
{
var reader = Substitute.For<IDataReader>();
reader.Read().Returns(true, false);
reader.IsDBNull(0).Returns(true);
_command.ExecuteReader().Returns(reader);
var sut = CreateSut();
var results = sut.ExecuteQuery("SELECT Col FROM Table");
Assert.That(results.Count, Is.EqualTo(1));
Assert.That(results[0], Is.Null);
}
[Test]
public void CloseConnection_AfterOperation()
{
_connection.State.Returns(ConnectionState.Open);
var sut = CreateSut();
sut.CloseConnection();
_connection.Received(1).Close();
}
[Test]
public void ThrowException_WhenConnectionFails()
{
_connection.When(c => c.Open()).Do(_ => throw new System.Exception("Connection failed"));
var sut = CreateSut();
Assert.Throws<System.Exception>(() => sut.OpenConnection());
}
[Test]
public void ExecuteScalar_ReturnCorrectValue()
{
_command.ExecuteScalar().Returns(42);
var sut = CreateSut();
var result = sut.ExecuteScalar("SELECT COUNT(*) FROM Table");
Assert.That(result, Is.EqualTo(42));
}
private DatabaseService CreateSut()
{
return new DatabaseService(_connection);
}
[TearDown]
public void TearDown()
{
_connection?.Dispose();
_command?.Dispose();
}
}
public class DatabaseService
{
private readonly IDbConnection _connection;
public DatabaseService(IDbConnection connection)
{
_connection = connection;
}
public void OpenConnection() => _connection.Open();
public void CloseConnection()
{
if (_connection.State == ConnectionState.Open)
_connection.Close();
}
public List<object> ExecuteQuery(string sql)
{
var results = new List<object>();
using (var command = _connection.CreateCommand())
{
command.CommandText = sql;
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
results.Add(reader.IsDBNull(0) ? null : reader[0]);
}
}
}
return results;
}
public int ExecuteNonQuery(string sql)
{
using (var command = _connection.CreateCommand())
{
command.CommandText = sql;
return command.ExecuteNonQuery();
}
}
public object ExecuteScalar(string sql)
{
using (var command = _connection.CreateCommand())
{
command.CommandText = sql;
return command.ExecuteScalar();
}
}
public void ExecuteInTransaction(Action operation)
{
using (var transaction = _connection.BeginTransaction())
{
try
{
operation();
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
}
public void ExecuteWithParameter(string sql, string paramName, object value)
{
using (var command = _connection.CreateCommand())
{
command.CommandText = sql;
var parameter = command.CreateParameter();
parameter.ParameterName = paramName;
parameter.Value = value;
command.Parameters.Add(parameter);
command.ExecuteNonQuery();
}
}
}
}

View File

@@ -0,0 +1,111 @@
using NUnit.Framework;
using NSubstitute;
using Prism.Modularity;
using Prism.Ioc;
using Unity;
namespace YourNamespace.Tests
{
[TestFixture]
public class ModuleShould
{
private IUnityContainer _unityContainer;
private IContainerRegistry _containerRegistry;
private IContainerProvider _containerProvider;
[SetUp]
public void Setup()
{
_unityContainer = Substitute.For<IUnityContainer>();
_containerRegistry = Substitute.For<IContainerRegistry>();
_containerProvider = Substitute.For<IContainerProvider>();
}
[Test]
public void RegisterTypes_WhenRegisterTypesIsCalled()
{
var sut = CreateSut();
sut.RegisterTypes(_containerRegistry);
_containerRegistry.Received().Register(Arg.Any<Func<object>>());
}
[Test]
public void Initialize_WhenInitializeIsCalled()
{
var sut = CreateSut();
sut.Initialize();
_unityContainer.Received().RegisterType(Arg.Any<System.Type>(), Arg.Any<System.Type>());
}
[Test]
public void RegisterViewAndViewModel_AsSingletons()
{
var sut = CreateSut();
sut.Initialize();
_unityContainer.Received().RegisterType<ITestView, TestView>();
_unityContainer.Received().RegisterType<ITestViewModel, TestViewModel>();
}
[Test]
public void OnInitialized_DoesNotThrow()
{
var sut = CreateSut();
Assert.DoesNotThrow(() => sut.OnInitialized(_containerProvider));
}
[Test]
public void ImplementIModuleInterface()
{
var sut = CreateSut();
Assert.That(sut, Is.InstanceOf<IModule>());
}
private TestModule CreateSut()
{
return new TestModule(_unityContainer);
}
[TearDown]
public void TearDown()
{
}
}
public class TestModule : IModule
{
private readonly IUnityContainer _unityContainer;
public TestModule(IUnityContainer unityContainer)
{
_unityContainer = unityContainer;
}
public void Initialize()
{
_unityContainer.RegisterType<ITestView, TestView>();
_unityContainer.RegisterType<ITestViewModel, TestViewModel>();
}
public void OnInitialized(IContainerProvider containerProvider)
{
}
public void RegisterTypes(IContainerRegistry containerRegistry)
{
Initialize();
}
}
public interface ITestView { }
public class TestView : ITestView { }
public interface ITestViewModel { }
public class TestViewModel : ITestViewModel { }
}

View File

@@ -0,0 +1,158 @@
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using NSubstitute;
namespace YourNamespace.Tests
{
[TestFixture]
public class ServiceShould
{
private IServiceDependency _dependency;
[SetUp]
public void Setup()
{
_dependency = Substitute.For<IServiceDependency>();
}
[Test]
public async Task ReturnResult_WhenOperationCompletes()
{
var expected = new ServiceResult { Value = 42 };
_dependency.GetDataAsync().Returns(Task.FromResult(expected));
var sut = CreateSut();
var result = await sut.PerformOperationAsync();
Assert.That(result, Is.Not.Null);
Assert.That(result.Value, Is.EqualTo(42));
}
[Test]
public async Task CallDependency_WhenOperationIsPerformed()
{
var sut = CreateSut();
await sut.PerformOperationAsync();
await _dependency.Received(1).GetDataAsync();
}
[Test]
public async Task InvokeCallback_WhenOperationCompletes()
{
var callbackInvoked = false;
var sut = CreateSut();
sut.OnCompleted += () => callbackInvoked = true;
await sut.PerformOperationAsync();
Assert.That(callbackInvoked, Is.True);
}
[Test]
public async Task InvokeCallbackWithError_WhenOperationFails()
{
_dependency.GetDataAsync().Returns(Task.FromException<ServiceResult>(new System.Exception("Test error")));
var errorCallbackInvoked = false;
var sut = CreateSut();
sut.OnError += (ex) => errorCallbackInvoked = true;
try
{
await sut.PerformOperationAsync();
}
catch
{
}
Assert.That(errorCallbackInvoked, Is.True);
}
[Test]
public void ThrowException_WhenNotInitialized()
{
var sut = new TestService(null);
Assert.ThrowsAsync<System.InvalidOperationException>(() => sut.PerformOperationAsync());
}
[Test]
public async Task CancelOperation_WhenCancellationTokenIsCancelled()
{
var cts = new CancellationTokenSource();
cts.Cancel();
var sut = CreateSut();
Assert.ThrowsAsync<OperationCanceledException>(() => sut.PerformOperationAsync(cts.Token));
}
[Test]
public async Task SupportMultipleConcurrentOperations()
{
var sut = CreateSut();
var tasks = Enumerable.Range(0, 5)
.Select(_ => sut.PerformOperationAsync())
.ToArray();
var results = await Task.WhenAll(tasks);
Assert.That(results.Length, Is.EqualTo(5));
Assert.That(results.All(r => r != null), Is.True);
}
private TestService CreateSut()
{
return new TestService(_dependency);
}
[TearDown]
public void TearDown()
{
}
}
public interface IServiceDependency
{
Task<ServiceResult> GetDataAsync();
}
public class ServiceResult
{
public int Value { get; set; }
}
public class TestService
{
private readonly IServiceDependency _dependency;
public event Action OnCompleted;
public event Action<System.Exception> OnError;
public TestService(IServiceDependency dependency)
{
_dependency = dependency;
}
public async Task<ServiceResult> PerformOperationAsync(CancellationToken cancellationToken = default)
{
if (_dependency == null)
throw new System.InvalidOperationException("Service not initialized");
cancellationToken.ThrowIfCancellationRequested();
try
{
var result = await _dependency.GetDataAsync();
OnCompleted?.Invoke();
return result;
}
catch (System.Exception ex)
{
OnError?.Invoke(ex);
throw;
}
}
}
}

View File

@@ -0,0 +1,133 @@
using System.ComponentModel;
using NUnit.Framework;
using NSubstitute;
using Prism.Events;
using Prism.Regions;
using Unity;
namespace YourNamespace.Tests
{
[TestFixture]
public class ViewModelShould
{
private IEventAggregator _eventAggregator;
private IRegionManager _regionManager;
private IUnityContainer _unityContainer;
[SetUp]
public void Setup()
{
_eventAggregator = Substitute.For<IEventAggregator>();
_regionManager = Substitute.For<IRegionManager>();
_unityContainer = Substitute.For<IUnityContainer>();
}
[Test]
public void RaisePropertyChanged_WhenPropertyIsSet()
{
var sut = CreateSut();
var eventArgs = new List<PropertyChangedEventArgs>();
sut.PropertyChanged += (sender, e) => eventArgs.Add(e);
sut.SomeProperty = "NewValue";
Assert.That(eventArgs.Count, Is.EqualTo(1));
Assert.That(eventArgs[0].PropertyName, Is.EqualTo(nameof(sut.SomeProperty)));
}
[Test]
public void NotRaisePropertyChanged_WhenPropertyIsSetToSameValue()
{
var sut = CreateSut();
sut.SomeProperty = "Value";
var eventArgs = new List<PropertyChangedEventArgs>();
sut.PropertyChanged += (sender, e) => eventArgs.Add(e);
sut.SomeProperty = "Value";
Assert.That(eventArgs.Count, Is.EqualTo(0));
}
[Test]
public void ExecuteCommand_WhenCommandIsInvoked()
{
var sut = CreateSut();
sut.SomeCommand.Execute(null);
Assert.That(sut.SomeCommandWasExecuted, Is.True);
}
[Test]
public void PublishEvent_WhenActionIsPerformed()
{
var testEvent = Substitute.For<TestEvent>();
_eventAggregator.GetEvent<TestEvent>().Returns(testEvent);
var sut = CreateSut();
sut.PerformAction();
testEvent.Received(1).Publish(Arg.Any<TestPayload>());
}
[Test]
public void SubscribeToEvents_WhenInitialized()
{
var testEvent = Substitute.For<TestEvent>();
_eventAggregator.GetEvent<TestEvent>().Returns(testEvent);
var sut = CreateSut();
testEvent.Received(1).Subscribe(Arg.Any<Action<TestPayload>>());
}
private TestViewModel CreateSut()
{
return new TestViewModel(_regionManager, _eventAggregator, _unityContainer);
}
[TearDown]
public void TearDown()
{
}
}
public class TestViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _someProperty;
public string SomeProperty
{
get => _someProperty;
set
{
if (_someProperty != value)
{
_someProperty = value;
OnPropertyChanged(nameof(SomeProperty));
}
}
}
public DelegateCommand SomeCommand { get; }
public bool SomeCommandWasExecuted { get; private set; }
public TestViewModel(IRegionManager regionManager, IEventAggregator eventAggregator, IUnityContainer unityContainer)
{
SomeCommand = new DelegateCommand(() => SomeCommandWasExecuted = true);
}
public void PerformAction() { }
protected void OnPropertyChanged(string propertyName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public class TestEvent : PubSubEvent<TestPayload> { }
public class TestPayload { }
public class DelegateCommand
{
private readonly Action _execute;
public DelegateCommand(Action execute) => _execute = execute;
public void Execute(object parameter) => _execute();
}
}