Files
DP44/GLM5Analysis/PatternLibrary/ImportExportPattern.md

435 lines
16 KiB
Markdown
Raw Permalink Normal View History

2026-04-17 14:55:32 -04:00
# 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