diff --git a/FaultRecordUxBehavior.cs b/FaultRecordUxBehavior.cs
index d88b0c0f..127cebf2 100644
--- a/FaultRecordUxBehavior.cs
+++ b/FaultRecordUxBehavior.cs
@@ -80,18 +80,20 @@ private static void OnListBoxItemLoaded(object sender, RoutedEventArgs e)
{
Tag = CapabilityPanelMarker,
Orientation = Orientation.Horizontal,
- HorizontalAlignment = HorizontalAlignment.Left,
- VerticalAlignment = VerticalAlignment.Center,
- Margin = new Thickness(4, 4, 0, 0)
+ HorizontalAlignment = HorizontalAlignment.Right,
+ VerticalAlignment = VerticalAlignment.Top,
+ Margin = new Thickness(4, 2, 7, 0)
};
capabilityPanel.Children.Add(gooseButton);
capabilityPanel.Children.Add(smvButton);
capabilityPanel.Children.Add(fileButton);
- Grid.SetRow(capabilityPanel, Grid.GetRow(actionPanel));
- Grid.SetColumn(capabilityPanel, Grid.GetColumn(actionPanel));
- Grid.SetColumnSpan(capabilityPanel, Math.Max(1, Grid.GetColumnSpan(actionPanel)));
- Panel.SetZIndex(capabilityPanel, Math.Max(11, Panel.GetZIndex(actionPanel) + 1));
+ // Keep protocol capabilities on their own card row. The previous overlay shared
+ // the lifecycle-action row and could cover the Stop button when File Transfer was enabled.
+ cardGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+ Grid.SetRow(capabilityPanel, cardGrid.RowDefinitions.Count - 1);
+ Grid.SetColumn(capabilityPanel, 1);
+ Panel.SetZIndex(capabilityPanel, 10);
cardGrid.Children.Add(capabilityPanel);
var registration = new CardRegistration(
@@ -99,13 +101,39 @@ private static void OnListBoxItemLoaded(object sender, RoutedEventArgs e)
gooseButton,
smvButton,
fileButton,
- (_, _) => RefreshCapabilityState(currentDevice, gooseButton, smvButton, fileButton));
+ (_, _) => QueueCapabilityRefresh(item, currentDevice, gooseButton, smvButton, fileButton));
Registrations.Add(item, registration);
currentDevice.PropertyChanged += registration.PropertyChangedHandler;
RefreshCapabilityState(currentDevice, gooseButton, smvButton, fileButton);
}));
}
+ private static void QueueCapabilityRefresh(
+ ListBoxItem item,
+ Iec61850MonitorDevice device,
+ Button gooseButton,
+ Button smvButton,
+ Button fileButton)
+ {
+ void RefreshOnUiThread()
+ {
+ if (!item.IsLoaded || !ReferenceEquals(item.DataContext, device))
+ return;
+
+ RefreshCapabilityState(device, gooseButton, smvButton, fileButton);
+ }
+
+ if (item.Dispatcher.CheckAccess())
+ {
+ RefreshOnUiThread();
+ return;
+ }
+
+ // Discovery and connection state can be published from worker threads. WPF controls
+ // have thread affinity, so every capability-state update must be marshalled to the card UI.
+ item.Dispatcher.BeginInvoke(DispatcherPriority.DataBind, new Action(RefreshOnUiThread));
+ }
+
private static Button CreateCapabilityButton(string capability, string pathData, RoutedEventHandler click)
{
var icon = new System.Windows.Shapes.Path
@@ -273,4 +301,4 @@ private sealed record CardRegistration(
Button SmvButton,
Button FileButton,
PropertyChangedEventHandler PropertyChangedHandler);
-}
\ No newline at end of file
+}
diff --git a/FaultRecordWindow.xaml b/FaultRecordWindow.xaml
index 50b69de2..93c4a945 100644
--- a/FaultRecordWindow.xaml
+++ b/FaultRecordWindow.xaml
@@ -91,9 +91,9 @@
-
+
-
+
@@ -117,16 +117,16 @@
-
+ Style="{StaticResource PrimaryButtonStyle}" IsEnabled="{Binding IsNotBusy, Mode=OneWay}"/>
-
+ Style="{StaticResource SecondaryButtonStyle}" IsEnabled="{Binding IsNotBusy, Mode=OneWay}"/>
@@ -142,9 +142,9 @@
-
+
-
@@ -157,13 +157,13 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
@@ -184,14 +184,14 @@
-
-
+
+
+ Style="{StaticResource SecondaryButtonStyle}" IsEnabled="{Binding IsBusy, Mode=OneWay}"/>
+ Style="{StaticResource PrimaryButtonStyle}" IsEnabled="{Binding CanDownload, Mode=OneWay}"/>
-
+
\ No newline at end of file
diff --git a/FaultRecordWindow.xaml.cs b/FaultRecordWindow.xaml.cs
index 28c86578..b5cc61e4 100644
--- a/FaultRecordWindow.xaml.cs
+++ b/FaultRecordWindow.xaml.cs
@@ -128,7 +128,6 @@ private async Task ScanAsync()
var catalog = await _client.DiscoverAsync(
RemoteDirectory,
_operationCancellation.Token);
-
Records.Clear();
foreach (var record in catalog.Records)
{
diff --git a/scripts/verify-fault-record-bindings.ps1 b/scripts/verify-fault-record-bindings.ps1
new file mode 100644
index 00000000..2096e2e7
--- /dev/null
+++ b/scripts/verify-fault-record-bindings.ps1
@@ -0,0 +1,46 @@
+$ErrorActionPreference = 'Stop'
+
+$root = Split-Path -Parent $PSScriptRoot
+$xamlPath = Join-Path $root 'FaultRecordWindow.xaml'
+$xaml = Get-Content $xamlPath -Raw
+
+$requiredOneTimeBindings = @('DeviceName', 'EndpointText')
+foreach ($property in $requiredOneTimeBindings) {
+ $pattern = "\{Binding\s+$([regex]::Escape($property))\s*,[^}]*Mode=OneTime[^}]*\}"
+ if ($xaml -notmatch $pattern) {
+ throw "FaultRecordWindow immutable binding '$property' must explicitly use Mode=OneTime."
+ }
+}
+
+$requiredOneWayBindings = @(
+ 'IsNotBusy',
+ 'SelectionSummary',
+ 'Records',
+ 'RecordName',
+ 'ModifiedText',
+ 'SizeText',
+ 'FilesText',
+ 'Completeness',
+ 'StatusText',
+ 'ProgressValue',
+ 'IsIndeterminate',
+ 'IsBusy',
+ 'CanDownload'
+)
+
+foreach ($property in $requiredOneWayBindings) {
+ $pattern = "\{Binding\s+$([regex]::Escape($property))\s*,[^}]*Mode=OneWay[^}]*\}"
+ if ($xaml -notmatch $pattern) {
+ throw "FaultRecordWindow display binding '$property' must explicitly use Mode=OneWay."
+ }
+}
+
+$requiredTwoWayBindings = @('RemoteDirectory', 'DestinationDirectory', 'IsSelected')
+foreach ($property in $requiredTwoWayBindings) {
+ $pattern = "\{Binding\s+$([regex]::Escape($property))\s*,[^}]*Mode=TwoWay[^}]*\}"
+ if ($xaml -notmatch $pattern) {
+ throw "FaultRecordWindow editable binding '$property' must explicitly use Mode=TwoWay."
+ }
+}
+
+Write-Host 'Fault record binding modes are explicit and valid.' -ForegroundColor Green
diff --git a/scripts/verify-source-clean.ps1 b/scripts/verify-source-clean.ps1
index be5be771..49b9c6a8 100644
--- a/scripts/verify-source-clean.ps1
+++ b/scripts/verify-source-clean.ps1
@@ -151,4 +151,6 @@ if ($Problems.Count -gt 0) {
throw "ArIED source tree failed clean-room validation with $($Problems.Count) problem(s)."
}
+& (Join-Path $PSScriptRoot "verify-fault-record-bindings.ps1")
+
Write-Host "All Git-tracked ArIED content passed source, website, external-IP, and current-license checks." -ForegroundColor Green