-
Notifications
You must be signed in to change notification settings - Fork 4
feat: integrate SCL FAT workspace into MainWindow tab #273
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| 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(); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With multiple matching IEDs, assigning the first item here invokes
_selectionChanged, which immediately changesMainWindow.SelectedDeviceto that first IED.ShowIntegratedFatWorkspaceonly 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 👍 / 👎.