Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
7a97078
Make IO FAT workspace selected-IED aware and protect completed evidence
masarray Jul 30, 2026
e5cfa46
Polish IO FAT card, grid, and selected IED workspace layout
masarray Jul 30, 2026
3f27b25
Cover selected IED context, grid layout, and evidence protection
masarray Jul 30, 2026
efa98ea
Fix all-passed IED card detection
masarray Jul 30, 2026
7c2a9ab
Simplify IO FAT PDF for customer readability
masarray Jul 30, 2026
ae8847f
Keep customer summary metrics inside report card
masarray Jul 30, 2026
75c00ef
Add customer report and all-pass badge regressions
masarray Jul 30, 2026
1d7c108
Bind IED PASS badge to total tested signal count
masarray Jul 30, 2026
83809ec
Preserve technical PDF metadata without visual report noise
masarray Jul 30, 2026
60e3992
Align PDF regressions with clean visible layout
masarray Jul 30, 2026
9ab6581
Make IO FAT PDF clean, lean, and timestamp focused
masarray Jul 30, 2026
3e8e247
Lock clean lean IO FAT report contract
masarray Jul 30, 2026
3de9648
Fix IO FAT preview text clipping and ellipsis
masarray Jul 30, 2026
55ab71a
Add regression guard against truncated FAT evidence text
masarray Jul 30, 2026
f98f6d8
Fit complete FAT evidence text without truncation
masarray Jul 30, 2026
3339867
Fix false ellipsis in IO FAT evidence rows
masarray Jul 30, 2026
b3252e2
Add regression guard against ellipsized FAT evidence
masarray Jul 30, 2026
df857ff
Preserve Rev3 document control and event-log traceability
masarray Jul 30, 2026
a60b19c
Import event-log-ready FAT workbook schema
masarray Jul 30, 2026
65acad5
Add document-controlled event-log FAT report layout
masarray Jul 30, 2026
392f102
Use executive FAT layout for native PDF
masarray Jul 30, 2026
c719f37
Preserve document control in selected IED preview
masarray Jul 30, 2026
79b638a
Use executive layout in native print preview
masarray Jul 30, 2026
73b49b9
Test Rev3 event-log-ready workbook import
masarray Jul 30, 2026
63f162f
Test executive FAT PDF traceability and signoff
masarray Jul 30, 2026
3df8132
Fix Rev3 IEC reference matching for IO FAT acquisition
masarray Jul 30, 2026
fbf58d0
Match Rev3 FAT points by exact event-log references
masarray Jul 30, 2026
387b762
Cover Rev3 Application and event-log reference matching
masarray Jul 30, 2026
769b858
Test Rev3 reference matching through public selection behavior
masarray Jul 30, 2026
d6032ec
Polish FAT evidence report hierarchy and status presentation
masarray Jul 30, 2026
b875f35
Use modern report font fallback stack
masarray Jul 30, 2026
18573e9
Lock final FAT report visual hierarchy and as-tested status
masarray Jul 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
325 changes: 325 additions & 0 deletions IoListTestingWindow.ContextUx.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
using System.ComponentModel;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Threading;
using ArIED61850Tester.Models.IoTesting;
using ArIED61850Tester.Services.IoTesting;

namespace ArIED61850Tester;

public partial class IoListTestingWindow
{
private bool _selectedIedContextInstalled;

public bool SelectedCanStartWorkflow =>
SelectedIed != null && !IsPreparingIed && Session.CanStart;

public bool SelectedCanPause =>
IsSelectedSessionIed && Session.CanPause;

public bool SelectedCanResume =>
IsSelectedSessionIed && Session.CanResume;

public bool SelectedCanStop =>
IsSelectedSessionIed && Session.CanStop;

public string SelectedStartWorkflowText
{
get
{
if (SelectedIed == null)
return "Select IED";

if (_preparingIed != null)
{
return ReferenceEquals(_preparingIed, SelectedIed)
? $"Connecting {SelectedIed.IedName}…"
: $"{_preparingIed.IedName} connecting…";
}

if (Session.IsSessionActive)
{
return IsSelectedSessionIed
? "IED session active"
: $"{Session.ActiveIed?.IedName ?? "Another IED"} active";
}

var enabled = EnabledPoints(SelectedIed);
if (enabled.Count > 0 && enabled.All(point => point.Runtime.State == IoTestPointState.Passed))
return "Reconnect / Retest";

return enabled.Any(point => point.Runtime.IsComplete)
? "Connect & Continue IED"
: "Connect & Start IED";
}
}

public string SelectedFooterStatusText
{
get
{
if (SelectedIed == null)
return "Select an imported IED.";

if (ReferenceEquals(_preparingIed, SelectedIed))
return PreparationStatusText;

if (IsSelectedSessionIed && Session.State != IoTestSessionState.Idle)
return Session.StatusText;

var enabled = EnabledPoints(SelectedIed);
var complete = enabled.Count(point => point.Runtime.IsComplete);
var passed = enabled.Count(point => point.Runtime.State == IoTestPointState.Passed);

if (enabled.Count > 0 && passed == enabled.Count)
return $"{SelectedIed.IedName} complete · all {enabled.Count} enabled points PASS · evidence protected.";

if (complete > 0)
return $"{SelectedIed.IedName} selected · {complete}/{enabled.Count} complete · completed evidence will be preserved.";

return $"{SelectedIed.IedName} selected · {SelectedIed.LiveStatusText}";
}
}

public string SelectedProgressText
{
get
{
if (SelectedIed == null)
return "0 / 0 complete";

var enabled = EnabledPoints(SelectedIed);
var complete = enabled.Count(point => point.Runtime.IsComplete);
var passed = enabled.Count(point => point.Runtime.State == IoTestPointState.Passed);
var review = enabled.Count(point => point.Runtime.State == IoTestPointState.Review);
var failed = enabled.Count(point => point.Runtime.State == IoTestPointState.Failed);
return $"{complete} / {enabled.Count} complete · {passed} PASS · {review} review · {failed} fail";
}
}

public int SelectedEvidenceCount => SelectedIed?.TestPoints.Sum(point =>
(point.Runtime.OnEvidence == null ? 0 : 1) +
(point.Runtime.OffEvidence == null ? 0 : 1)) ?? 0;

private bool IsSelectedSessionIed =>
SelectedIed != null && ReferenceEquals(Session.ActiveIed, SelectedIed);

private void IoListTestingWindow_ContentRendered(object? sender, EventArgs e)
{
Dispatcher.BeginInvoke(
new Action(InstallSelectedIedContext),
DispatcherPriority.ContextIdle);
}

private void InstallSelectedIedContext()
{
AdoptWorkspacePreviewToggle();
if (_selectedIedContextInstalled)
{
RaiseSelectedIedContextProperties();
return;
}

foreach (var ied in Project.Ieds)
ied.PropertyChanged += ContextIed_PropertyChanged;

PropertyChanged += ContextWindow_PropertyChanged;
Session.PropertyChanged += ContextSession_PropertyChanged;
Closed += ContextWindow_Closed;
_selectedIedContextInstalled = true;
RaiseSelectedIedContextProperties();
}

private void AdoptWorkspacePreviewToggle()
{
if (_printPreviewToggle != null && !ReferenceEquals(_printPreviewToggle, WorkspacePreviewToggle))
{
if (LogicalTreeHelper.GetParent(_printPreviewToggle) is Panel parent)
parent.Children.Remove(_printPreviewToggle);
}

_printPreviewToggle = WorkspacePreviewToggle;
_printPreviewToggle.Content = _printPreviewActive ? "Signals View" : "Print Preview";
_printPreviewToggle.ToolTip = "Toggle the selected IED between signal evidence and native print preview";
}

private async void StartSelectedIedSafely_Click(object sender, RoutedEventArgs e)
{
if (IsPreparingIed)
return;

var selectedIed = SelectedIed;
var preflight = IoTestSessionPreflight.Validate(selectedIed);
if (!preflight.Succeeded)
{
ShowActionResult(preflight, "FAT session scope is not ready");
return;
}

var enabledReady = selectedIed!.TestPoints
.Where(point => point.TestEnabled && point.ImportReady)
.ToList();
var completedPoints = enabledReady
.Where(point => point.Runtime.IsComplete)
.ToList();
var incompletePoints = enabledReady
.Where(point => !point.Runtime.IsComplete)
.ToList();

var explicitRetest = false;
if (completedPoints.Count > 0 && incompletePoints.Count == 0)
{
var answer = MessageBox.Show(
this,
$"All {completedPoints.Count} enabled rows for {selectedIed.IedName} already contain completed evidence.\n\nA normal Connect click will not erase them. Choose Yes only when you intentionally want to retest every completed row and replace its ON/OFF evidence.",
"Retest completed evidence?",
MessageBoxButton.YesNo,
MessageBoxImage.Warning,
MessageBoxResult.No);
if (answer != MessageBoxResult.Yes)
{
PreparationStatusText = $"{selectedIed.IedName} evidence preserved · no retest started.";
RaiseSelectedIedContextProperties();
return;
}

explicitRetest = true;
}

SetPreparingIed(selectedIed, $"Connecting {selectedIed.IedName} · {selectedIed.IpAddress}:102");
try
{
if (Owner is MainWindow engineeringWindow)
{
var progress = new Progress<string>(message =>
{
selectedIed.SetPreparationState(true, message);
PreparationStatusText = message;
RaiseStatusProperties();
RaiseSelectedIedContextProperties();
});
var preparation = await engineeringWindow.PrepareIoTestIedForFatAsync(
Project,
selectedIed,
progress);
Comment on lines +203 to +206

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exclude completed rows before connection preparation

When a mixed IED contains a completed signal that is no longer available in the live model, this preparation call still includes it because completed points remain enabled until immediately before Session.Start. PrepareIoTestIedForFatAsync snapshots every enabled point (MainWindow.IoTesting.AutoConnect.cs:24-26) and fails unless all requested points become live (MainWindow.IoTesting.AutoConnect.cs:203-213), so the method returns before pending, otherwise-valid rows can resume. Apply the protected-row scope during preflight and preparation as well as during Session.Start.

Useful? React with 👍 / 👎.

RaiseStatusProperties();
RaiseSelectedIedContextProperties();
if (!preparation.Succeeded)
{
PreparationStatusText = preparation.Message;
ShowActionResult(preparation, "IED acquisition could not start");
return;
}
}

var protectedPoints = explicitRetest
? Array.Empty<IoTestPointPlan>()
: completedPoints.ToArray();
IoTestSessionActionResult result;
try
{
foreach (var point in protectedPoints)
point.TestEnabled = false;

result = Session.Start(selectedIed);
}
finally
{
foreach (var point in protectedPoints)
point.TestEnabled = true;
}

ShowActionResult(result, "FAT evidence session could not start");
RaiseStatusProperties();
RaiseSelectedIedContextProperties();
if (result.Succeeded)
{
PreparationStatusText = protectedPoints.Length > 0
? $"{selectedIed.IedName} live · {protectedPoints.Length} completed row(s) preserved · waiting for pending OFF → ON → OFF tests"
: $"{selectedIed.IedName} live · waiting for OFF → ON → OFF";
Storage?.ScheduleSave();
}
else
{
PreparationStatusText = result.Message;
}
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidOperationException or ArgumentException)
{
PreparationStatusText = ex.Message;
MessageBox.Show(
this,
ex.Message,
"Connect and start IED failed",
MessageBoxButton.OK,
MessageBoxImage.Error);
}
finally
{
selectedIed.SetPreparationState(false, selectedIed.LiveStatusText);
SetPreparingIed(null, string.Empty);
RaiseSelectedIedContextProperties();
}
}

private void ContextWindow_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName is nameof(SelectedIed) or nameof(IsPreparingIed) or nameof(PreparationStatusText))
RaiseSelectedIedContextProperties();
}

private void ContextSession_PropertyChanged(object? sender, PropertyChangedEventArgs e)
=> RaiseSelectedIedContextProperties();

private void ContextIed_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (!ReferenceEquals(sender, SelectedIed))
return;

Raise(nameof(SelectedIedSummary));
RaiseSelectedIedContextProperties();
}

private void RaiseSelectedIedContextProperties()
{
Raise(nameof(SelectedCanStartWorkflow));
Raise(nameof(SelectedCanPause));
Raise(nameof(SelectedCanResume));
Raise(nameof(SelectedCanStop));
Raise(nameof(SelectedStartWorkflowText));
Raise(nameof(SelectedFooterStatusText));
Raise(nameof(SelectedProgressText));
Raise(nameof(SelectedEvidenceCount));
}

private void ContextWindow_Closed(object? sender, EventArgs e)
{
foreach (var ied in Project.Ieds)
ied.PropertyChanged -= ContextIed_PropertyChanged;

PropertyChanged -= ContextWindow_PropertyChanged;
Session.PropertyChanged -= ContextSession_PropertyChanged;
Closed -= ContextWindow_Closed;
}

private static List<IoTestPointPlan> EnabledPoints(IoTestIedPlan ied)
=> ied.TestPoints.Where(point => point.TestEnabled).ToList();
}

public sealed class IoFatAllPassedVisibilityConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var selectedCount = values.Length > 0 && values[0] is int count ? count : 0;
var passed = values.Length > 1 && values[1] is int passedCount ? passedCount : 0;
var allPassed = selectedCount > 0 && passed >= selectedCount;
if (string.Equals(parameter?.ToString(), "Inverse", StringComparison.OrdinalIgnoreCase))
allPassed = !allPassed;
return allPassed ? Visibility.Visible : Visibility.Collapsed;
}

public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
=> targetTypes.Select(_ => Binding.DoNothing).ToArray();
}
Loading
Loading