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,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