Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
131 changes: 131 additions & 0 deletions IntegratedFatWorkspaceControl.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<UserControl x:Class="ArIED61850Tester.IntegratedFatWorkspaceControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<Grid x:Name="WorkspaceView">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="230"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Style="{StaticResource WorkspaceCard}" Padding="10">
<DockPanel>
<StackPanel DockPanel.Dock="Top" Margin="2,1,2,9">
<TextBlock Text="FAT IED" Style="{StaticResource SectionTitle}"/>
<TextBlock Text="Shared SCL and live connection from Engineering" Style="{StaticResource Caption}" Margin="0,3,0,0" TextWrapping="Wrap"/>
</StackPanel>
<ListBox ItemsSource="{Binding EngineeringIeds}" SelectedItem="{Binding SelectedEngineeringIed, Mode=TwoWay}"
IsEnabled="{Binding Session.CanSelectIed}"
BorderThickness="0" Background="Transparent" HorizontalContentAlignment="Stretch"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling">
<ListBox.ItemTemplate>
<DataTemplate>
<Border Padding="9" Margin="0,0,0,7" Background="#F8FAFC" BorderBrush="#D8E1EE" BorderThickness="1" CornerRadius="10">
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="42"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
<Viewbox Width="38" Height="38">
<Control Width="50" Height="50" Template="{StaticResource IedRelayFrontPanelTemplate}">
<Control.Style>
<Style TargetType="Control">
<Setter Property="Foreground" Value="#EF4444"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsConnected}" Value="True">
<Setter Property="Foreground" Value="#20B26B"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Control.Style>
</Control>
</Viewbox>
<StackPanel Grid.Column="1" Margin="7,0,0,0" VerticalAlignment="Center">
<TextBlock Text="{Binding Name}" FontSize="12" FontWeight="SemiBold" Foreground="{StaticResource Ink}" TextTrimming="CharacterEllipsis"/>
<TextBlock Text="{Binding EndpointText}" FontSize="9.8" Foreground="{StaticResource Muted}"/>
<TextBlock Text="{Binding ActivityText}" FontSize="9.2" FontWeight="SemiBold" Foreground="{StaticResource Accent}" Margin="0,2,0,0"/>
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</Border>
<Border Grid.Column="2" Style="{StaticResource WorkspaceCard}" Padding="10">
<Grid>
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="8"/><RowDefinition Height="*"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
<StackPanel>
<TextBlock Text="{Binding SelectedIed.IedName, StringFormat=FAT · {0}}" Style="{StaticResource WorkspaceTitle}"/>
<TextBlock Text="{Binding Session.StatusText}" Style="{StaticResource WorkspaceSubtitle}" Margin="0,2,0,0" TextTrimming="CharacterEllipsis"/>
</StackPanel>
<WrapPanel Grid.Column="1">
<Button Content="Start FAT" Style="{StaticResource PrimaryButton}" Click="StartFat_Click" Margin="0,0,6,0"
IsEnabled="{Binding Session.CanStart}"/>
<Button Content="Stop" Style="{StaticResource SoftButton}" Click="StopFat_Click" Margin="0,0,6,0"
IsEnabled="{Binding Session.CanStop}"/>
<Button Content="Value 1" Style="{StaticResource SoftButton}" Click="CaptureValue1_Click" Margin="0,0,6,0"/>
<Button Content="Value 2" Style="{StaticResource SoftButton}" Click="CaptureValue2_Click" Margin="0,0,6,0"/>
<Button Content="Print Preview" Style="{StaticResource SoftButton}" Click="OpenPreview_Click"/>
</WrapPanel>
</Grid>
<DataGrid x:Name="FatSignalsGrid" Grid.Row="2" ItemsSource="{Binding SelectedIed.TestPoints}"
AutoGenerateColumns="False" SelectionMode="Extended" SelectionUnit="FullRow"
Style="{StaticResource ModernDataGrid}" EnableRowVirtualization="True" EnableColumnVirtualization="True"
VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling"
ScrollViewer.CanContentScroll="True" ScrollViewer.HorizontalScrollBarVisibility="Auto">
<DataGrid.Columns>
<DataGridCheckBoxColumn Header="TEST" Binding="{Binding TestEnabled, Mode=TwoWay}" Width="52"/>
<DataGridTextColumn Header="SIGNAL" Binding="{Binding DisplaySignalName}" Width="1.2*" MinWidth="145" IsReadOnly="True"/>
<DataGridTextColumn Header="IEC REFERENCE" Binding="{Binding ReportIecReference}" Width="1.65*" MinWidth="210" IsReadOnly="True"/>
<DataGridTextColumn Header="TYPE" Binding="{Binding SignalKind}" Width="74" IsReadOnly="True"/>
<DataGridTemplateColumn Header="LIVE VALUE" Width="108" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate><DataTemplate>
<StackPanel><TextBlock Text="{Binding Runtime.CurrentValue}" FontSize="11.8" FontWeight="SemiBold" Foreground="#1E3C64"/>
<TextBlock Text="{Binding Runtime.CurrentQuality}" FontSize="8.8" Foreground="#69798F"/></StackPanel>
</DataTemplate></DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="VALUE 1" Width="116" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate><DataTemplate>
<StackPanel><TextBlock Text="{Binding Value1Text}" FontSize="11.4" FontWeight="SemiBold"/>
<TextBlock Text="{Binding Value1RelayTimestampText}" FontSize="8.2" Foreground="#707E91"/></StackPanel>
</DataTemplate></DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="VALUE 2" Width="116" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate><DataTemplate>
<StackPanel><TextBlock Text="{Binding Value2Text}" FontSize="11.4" FontWeight="SemiBold"/>
<TextBlock Text="{Binding Value2RelayTimestampText}" FontSize="8.2" Foreground="#707E91"/></StackPanel>
</DataTemplate></DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="STATUS" Binding="{Binding FatStatusText}" Width="104" IsReadOnly="True"/>
<DataGridTextColumn Header="RESULT" Binding="{Binding FatResultText}" Width="98" IsReadOnly="True"/>
</DataGrid.Columns>
</DataGrid>
<Grid Grid.Row="3" Margin="0,8,0,0">
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
<TextBlock Text="{Binding StatusText}" Style="{StaticResource Caption}"/>
<TextBlock Grid.Column="1" Text="{Binding Session.ProgressText}" FontSize="10.5" FontWeight="SemiBold" Foreground="{StaticResource Accent}"/>
</Grid>
</Grid>
</Border>
</Grid>
<Border x:Name="PreviewView" Visibility="Collapsed" Style="{StaticResource WorkspaceCard}" Padding="0">
<Grid>
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
<Border Background="#F3F7FC" BorderBrush="#DDE6F2" BorderThickness="0,0,0,1" Padding="10,7">
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<Button Content="← Back to FAT" Style="{StaticResource SoftButton}" Click="ClosePreview_Click" Margin="0,0,8,0"/>
<Button Content="Print" Style="{StaticResource PrimaryButton}" Click="PrintPreview_Click" Margin="0,0,8,0"/>
<Button Content="Refresh" Style="{StaticResource SoftButton}" Click="RefreshPreview_Click"/>
</StackPanel>
<TextBlock Grid.Column="1" Text="{Binding SelectedIed.IedName, StringFormat=Print Preview · {0}}" FontSize="11" FontWeight="SemiBold"
Foreground="{StaticResource Muted}" VerticalAlignment="Center"/>
</Grid>
</Border>
<DocumentViewer x:Name="PreviewDocument" Grid.Row="1" Background="#EEF3F9" BorderThickness="0" ShowPageBorders="True"/>
</Grid>
</Border>
</Grid>
</UserControl>
237 changes: 237 additions & 0 deletions IntegratedFatWorkspaceControl.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Threading;
using ArIED61850Tester.Models;
using ArIED61850Tester.Models.IoTesting;
using ArIED61850Tester.Services.IoTesting;

namespace ArIED61850Tester;

public partial class IntegratedFatWorkspaceControl : UserControl, INotifyPropertyChanged, IDisposable
{
private readonly Action<Iec61850MonitorDevice?> _selectionChanged;
private IoTestIedPlan? _selectedIed;
private Iec61850MonitorDevice? _selectedEngineeringIed;
private string _statusText = "Select an IED and start FAT when its Engineering monitor is live.";
private bool _disposed;

public IntegratedFatWorkspaceControl(
IoTestWorkspaceLaunchResult launch,
IEnumerable<Iec61850MonitorDevice> engineeringIeds,
Action<Iec61850MonitorDevice?> selectionChanged)
{
InitializeComponent();
Project = launch.Project;
Session = launch.Session;
Storage = launch.Workspace;
EngineeringIeds = engineeringIeds;
_selectionChanged = selectionChanged;
Session.PropertyChanged += Session_PropertyChanged;
DataContext = this;
SelectedEngineeringIed = EngineeringIeds.FirstOrDefault();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the active Engineering IED when constructing FAT

With multiple matching IEDs, assigning the first item here invokes _selectionChanged, which immediately changes MainWindow.SelectedDevice to that first IED. ShowIntegratedFatWorkspace only tries to synchronize the previously selected device after construction, by which time that selection has already been overwritten, so opening FAT while a later IED is active unexpectedly switches both workspaces to the first IED. Initialize from the current Engineering selection or suppress the callback during this default assignment.

Useful? React with 👍 / 👎.

SelectedIed ??= Project.Ieds.FirstOrDefault();
}

public IoTestProject Project { get; }
public IoTestSessionController Session { get; }
public IoTestWorkspacePersistence Storage { get; }
public IEnumerable<Iec61850MonitorDevice> EngineeringIeds { get; }

public Iec61850MonitorDevice? SelectedEngineeringIed
{
get => _selectedEngineeringIed;
set
{
if (ReferenceEquals(_selectedEngineeringIed, value)) return;
_selectedEngineeringIed = value;
Raise(nameof(SelectedEngineeringIed));
SelectedIed = ResolveFatIed(value);
_selectionChanged(value);
}
}

public IoTestIedPlan? SelectedIed
{
get => _selectedIed;
set
{
if (ReferenceEquals(_selectedIed, value)) return;
_selectedIed = value;
Raise(nameof(SelectedIed));
ApplyPointScope();
if (PreviewView.Visibility == Visibility.Visible) RefreshPreview();
}
}

public string StatusText
{
get => _statusText;
private set
{
if (_statusText == value) return;
_statusText = value;
Raise(nameof(StatusText));
}
}

public event PropertyChangedEventHandler? PropertyChanged;

public void SelectEngineeringIed(string? deviceId, string? name, string? ipAddress)
{
if (!Session.CanSelectIed)
return;

var match = Project.Ieds.FirstOrDefault(ied =>
!string.IsNullOrWhiteSpace(deviceId) &&
ied.LiveDeviceId.Equals(deviceId, StringComparison.OrdinalIgnoreCase))
?? Project.Ieds.FirstOrDefault(ied =>
ied.IedName.Equals(name, StringComparison.OrdinalIgnoreCase) &&
(string.IsNullOrWhiteSpace(ipAddress) ||
ied.IpAddress.Equals(ipAddress, StringComparison.OrdinalIgnoreCase)));
var engineeringMatch = EngineeringIeds.FirstOrDefault(ied =>
!string.IsNullOrWhiteSpace(deviceId) &&
ied.DeviceId.Equals(deviceId, StringComparison.OrdinalIgnoreCase))
?? EngineeringIeds.FirstOrDefault(ied =>
ied.Name.Equals(name, StringComparison.OrdinalIgnoreCase) &&
(string.IsNullOrWhiteSpace(ipAddress) ||
ied.IpAddress.Equals(ipAddress, StringComparison.OrdinalIgnoreCase)));
if (engineeringMatch != null)
SelectedEngineeringIed = engineeringMatch;
else if (match != null)
SelectedIed = match;
}

private IoTestIedPlan? ResolveFatIed(Iec61850MonitorDevice? device)
=> device == null
? null
: Project.Ieds.FirstOrDefault(ied =>
ied.LiveDeviceId.Equals(device.DeviceId, StringComparison.OrdinalIgnoreCase))
?? Project.Ieds.FirstOrDefault(ied =>
ied.IedName.Equals(device.Name, StringComparison.OrdinalIgnoreCase) &&
ied.IpAddress.Equals(device.IpAddress, StringComparison.OrdinalIgnoreCase));

private void ApplyPointScope()
{
if (SelectedIed == null) return;
var view = CollectionViewSource.GetDefaultView(SelectedIed.TestPoints);
view.Filter = item => item is IoTestPointPlan point &&
point.WorkspaceSelected && point.IsIncludedInFat;
view.Refresh();
}

private void StartFat_Click(object sender, RoutedEventArgs e)
=> ShowResult(Session.Start(SelectedIed));

private void StopFat_Click(object sender, RoutedEventArgs e)
=> ShowResult(Session.Stop("Stopped from integrated FAT tab."));

private void CaptureValue1_Click(object sender, RoutedEventArgs e)
=> CaptureSelected(FatValueSlot.Value1);

private void CaptureValue2_Click(object sender, RoutedEventArgs e)
=> CaptureSelected(FatValueSlot.Value2);

private void CaptureSelected(FatValueSlot slot)
{
var points = FatSignalsGrid.SelectedItems.OfType<IoTestPointPlan>().ToArray();
if (points.Length == 0 && FatSignalsGrid.SelectedItem is IoTestPointPlan current)
points = new[] { current };
if (points.Length == 0)
{
StatusText = "Select at least one FAT row first.";
return;
}

var failures = new List<string>();
foreach (var point in points)
{
var result = Session.CaptureOperatorSnapshot(point, slot);
if (!result.Succeeded)
failures.Add($"{point.DisplaySignalName}: {result.Message}");
}
Storage.ScheduleSave();
StatusText = failures.Count == 0
? $"{slot}: captured for {points.Length} selected signal(s)."
: string.Join(" • ", failures.Take(3));
}

private void ShowResult(IoTestSessionActionResult result)
{
StatusText = result.Message;
if (result.Succeeded)
Storage.ScheduleSave();
}

private void OpenPreview_Click(object sender, RoutedEventArgs e)
{
if (SelectedIed == null)
{
StatusText = "Select an IED before opening Print Preview.";
return;
}
WorkspaceView.Visibility = Visibility.Collapsed;
PreviewView.Visibility = Visibility.Visible;
RefreshPreview();
}

private void ClosePreview_Click(object sender, RoutedEventArgs e)
{
PreviewView.Visibility = Visibility.Collapsed;
WorkspaceView.Visibility = Visibility.Visible;
}

private void RefreshPreview_Click(object sender, RoutedEventArgs e)
=> RefreshPreview();

private void RefreshPreview()
{
if (SelectedIed == null)
return;
try
{
var scoped = IoFatReportPreviewService.CreateIedScopedProject(Project, SelectedIed);
var draft = Session.IsSessionActive && ReferenceEquals(Session.ActiveIed, SelectedIed);
PreviewDocument.Document = IoFatReportPreviewDocumentBuilder.Build(scoped, draft);
PreviewDocument.UpdateLayout();
Dispatcher.BeginInvoke(new Action(PreviewDocument.FitToWidth), DispatcherPriority.Loaded);
}
catch (Exception ex)
{
StatusText = $"Print Preview could not be rendered: {ex.Message}";
}
}

private void PrintPreview_Click(object sender, RoutedEventArgs e)
{
if (PreviewDocument.Document != null &&
ApplicationCommands.Print.CanExecute(null, PreviewDocument))
{
ApplicationCommands.Print.Execute(null, PreviewDocument);
}
}

private void Session_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
Raise(nameof(Session));
if (PreviewView.Visibility == Visibility.Visible &&
e.PropertyName is nameof(IoTestSessionController.State) or
nameof(IoTestSessionController.EvidenceRecordCount))
{
Dispatcher.BeginInvoke(new Action(RefreshPreview), DispatcherPriority.Background);
}
}

private void Raise(string propertyName)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

public void Dispose()
{
if (_disposed) return;
_disposed = true;
Session.PropertyChanged -= Session_PropertyChanged;
Storage.ScheduleSave();
}
}
Loading
Loading