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
2 changes: 1 addition & 1 deletion MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,7 @@
</Grid.RowDefinitions>

<Grid Grid.Row="0" MinHeight="0" ClipToBounds="True">
<DataGrid x:Name="EquipmentCommandGrid" ItemsSource="{Binding SelectedDevice.CommandSignals}" Loaded="CommandGrid_Loaded" IsReadOnly="False" CanUserAddRows="False" CanUserDeleteRows="False"
<DataGrid x:Name="EquipmentCommandGrid" ItemsSource="{Binding SelectedDevice.CommandSignals}" Loaded="CommandGrid_Loaded" IsReadOnly="False" CanUserAddRows="False" CanUserDeleteRows="False" CanUserSortColumns="False"
Style="{StaticResource CommandDataGrid}" RowStyle="{StaticResource CommandDataGridRow}"
CellStyle="{StaticResource CommandDataGridCell}" ColumnHeaderStyle="{StaticResource CommandDataGridHeader}"
AutoGenerateColumns="False" MinHeight="56" RowHeight="43" MinRowHeight="42" ColumnHeaderHeight="35" FrozenColumnCount="1"
Expand Down
18 changes: 15 additions & 3 deletions MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ public Iec61850MonitorDevice? SelectedDevice
Raise(nameof(ActiveIedSubtitle));
RaiseWorkspaceCounts();
TryAutoExpandCommandPanelOnce(_selectedDevice);
// The persistent Dock may retain the previous IED's viewport.
// Reposition once on real IED selection, never on report ingress.
if (CommandPanelExpander?.IsExpanded == true)
ScrollVerifiedEquipmentCommandsIntoView();
ApplyExplorerLiveSearchFilter();
if (MainTabs?.SelectedIndex == IedBrowserWorkspaceIndex)
RefreshIedBrowser();
Expand Down Expand Up @@ -1475,9 +1479,17 @@ private void ScrollVerifiedEquipmentCommandsIntoView()
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Loaded,
new Action(() =>
{
if (ReferenceEquals(SelectedDevice, device) &&
device.CommandSignals.Contains(first))
EquipmentCommandGrid.ScrollIntoView(first);
if (!ReferenceEquals(SelectedDevice, device) ||
!device.CommandSignals.Contains(first))
return;

// ScrollIntoView alone only ensures visibility, so a Tap
// already visible on the LAST row would remain at the bottom.
// Reset the virtualized viewport on expand/device switch only.
if (EquipmentCommandGrid.Template?.FindName(
"DG_ScrollViewer", EquipmentCommandGrid) is ScrollViewer scroll)
scroll.ScrollToTop();
EquipmentCommandGrid.ScrollIntoView(first);
}));
}

Expand Down
15 changes: 6 additions & 9 deletions Services/IecNaturalReferenceOrder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,12 @@ public int Compare(object? a, object? b)
if (ReferenceEquals(a, b)) return 0;
if (a is not SignalDefinition left || b is not SignalDefinition right)
return Comparer.DefaultInvariant.Compare(a,b);
// WPF's ListCollectionView.CustomSort is the FINAL visual order.
// The underlying device.OperableCommandSignals is already ranked,
// but the view's alphabetical comparer used to override that rank:
// SG7_BCUMaster/GGIO2 sorted above SG7_BCUSystem/Bkr0XCBR.Pos.
// Match exactly the live-verified command role priority at the view
// boundary, while keeping natural instance order WITHIN each role.
// This is presentation only; no rank authorizes an MMS Operate.
var role = Iec61850ControlIdentity.CommandDockPriority(left)
.CompareTo(Iec61850ControlIdentity.CommandDockPriority(right));
// WPF CustomSort is the final visible order (not CommandSignals).
// Use the SAME presentation-only priority as the source projection;
// the former role-only sorter put StatusOnly CSWI above verified BSC
// tap control. Live ctlModel/CDC admission remains independent.
var role = Iec61850ControlIdentity.CommandDockDisplayPriority(left)
.CompareTo(Iec61850ControlIdentity.CommandDockDisplayPriority(right));
Comment on lines +73 to +74

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 Re-sort the command view when live verification changes priority

When live verification changes a command's priority without changing the backing collection order, the displayed order now becomes stale. For example, unresolved CSWI10.Pos and CSWI2.Pos are stored lexically as CSWI10 then CSWI2, but this view displays CSWI2 first. Verifying CSWI10 makes it the higher-priority command while leaving the source order unchanged, so RefreshCommandSignals() returns without collection notifications. The installed ListCollectionView has neither live sorting nor a priority-change refresh, so CSWI2 remains above the verified command. Add a bounded view re-sort on changes to the priority inputs and cover verification/revocation after the view is installed; the new test verifies everything before creating the view and therefore misses this case.

Useful? React with 👍 / 👎.

if (role != 0) return role;
var result = IecNaturalReferenceOrder.Compare(left.DisplayReference,right.DisplayReference);
return result != 0 ? result : IecNaturalReferenceOrder.Compare(left.ObjectReference,right.ObjectReference);
Expand Down
85 changes: 85 additions & 0 deletions tests/ARSAS.Tests/MultiGridNaturalSortRegressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,91 @@ static SignalDefinition Verified(string reference, string cdc = "DPC")
});
}

[Fact]
public void RealWpfZivDockShowsLiveTapBeforeFourStatusOnlyCswi_WithoutSourceMutation()
{
RunInSta(() =>
{
static SignalDefinition Control(string reference, string cdc, string model, bool verified)
{
var signal = new SignalDefinition
{
Name = reference,
ObjectReference = reference,
IsControlSignal = true,
IsSelected = true,
HasControlServiceEvidence = true,
FunctionalConstraint = "CO",
ControlCdc = cdc,
ControlModelText = model
};
if (verified)
{
signal.AcceptVerifiedLiveControlCdc(cdc);
signal.ControlSemanticVerified = true;
}
return signal;
}
var tap = Control("ZIVAVRLD1/ATCC1.TapChg", "BSC",
"Select Before Operate (SBO) • Normal security", true);
var cswi = Enumerable.Range(1, 4)
.Select(i => Control($"ZIVAVRLD1/CSWI{i}.Pos", "DPC", "Status only", false))
.ToArray();
var device = new Iec61850MonitorDevice();
device.Signals.ReplaceAll([..cswi, tap]);
device.RecountSelectedSignals();
Assert.True(tap.ControlSupportsOperate);
Assert.All(cswi, s => Assert.False(s.ControlSupportsOperate));
Assert.Same(tap, device.CommandSignals[0]);

// This is the actual WPF collection view that painted the
// reported field screenshot incorrectly despite source order.
var grid = new DataGrid
{
CanUserAddRows = false, CanUserDeleteRows = false,
CanUserSortColumns = false, ItemsSource = device.CommandSignals
};
var view = Assert.IsType<ListCollectionView>(
CollectionViewSource.GetDefaultView(grid.ItemsSource));
view.SortDescriptions.Clear();
view.CustomSort = IecNaturalCommandSort.Instance;
SignalDefinition[] Visual() => view.Cast<SignalDefinition>().ToArray();

Assert.Same(tap, Visual()[0]);
Assert.Equal(cswi, Visual().Skip(1));
Assert.Equal(5, Visual().Length);

// Report/value churn must not recycle rows or elevate read-only
// controls. Changing process values never changes ctlModel.
var changes = 0;
device.CommandSignals.CollectionChanged += (_, _) => changes++;
for (var i = 0; i < 100; ++i)
{
tap.ControlCurrentValue = (i % 2 + 1).ToString();
device.RefreshCommandSignalProjection();
}
Assert.Equal(0, changes);
Assert.Same(tap, Visual()[0]);
Assert.All(Visual().Skip(1), s => Assert.False(s.ControlSupportsOperate));
});
}

[Fact]
public void CommandGridForbidsHeaderSortingAndResetsViewportOnIedSwitch()
{
var xaml = Read("MainWindow.xaml");
var xml = XDocument.Parse(xaml);
XNamespace wpf = "http://schemas.microsoft.com/winfx/2006/xaml/presentation";
XNamespace x = "http://schemas.microsoft.com/winfx/2006/xaml";
var command = xml.Descendants(wpf + "DataGrid")
.Single(g => (string?)g.Attribute(x + "Name") == "EquipmentCommandGrid");
Assert.Equal("False", (string?)command.Attribute("CanUserSortColumns"));

var code = Read("MainWindow.xaml.cs");
Assert.Contains("ScrollVerifiedEquipmentCommandsIntoView();", code);
Assert.Contains("scroll.ScrollToTop();", code);
}

[Fact]
public void CommandComparatorDoesNotTreatUnverifiedNamesAsOperatePermission()
{
Expand Down
Loading