From e8f20e744076f4e29e24068297bdb2ef588af8cf Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Tue, 8 Sep 2026 22:03:50 +0700 Subject: [PATCH 1/2] fix: preserve legacy RCB ticks across DataGrid recycling --- ...terWindow.VirtualizedSelectionAuthority.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 RcbExportFilterWindow.VirtualizedSelectionAuthority.cs diff --git a/RcbExportFilterWindow.VirtualizedSelectionAuthority.cs b/RcbExportFilterWindow.VirtualizedSelectionAuthority.cs new file mode 100644 index 000000000..26520deb4 --- /dev/null +++ b/RcbExportFilterWindow.VirtualizedSelectionAuthority.cs @@ -0,0 +1,53 @@ +using System.Runtime.CompilerServices; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using ArIED61850Tester.Models; + +namespace ArIED61850Tester; + +/// +/// Keeps the P1 multi-RCB selection model authoritative when the legacy DataGrid virtualizes +/// and recycles checkbox containers. A recycled CheckBox re-applies IsChecked from the bound +/// RcbExportRow.IsSelected value, which raises Checked/Unchecked even though the operator did +/// not change selection. The original single-select instance handlers interpret that visual +/// rehydration as a user action and call SelectOnly, silently clearing the other selected RCBs. +/// +/// P1 pointer/keyboard handling already owns real operator selection changes, so these routed +/// state-change events are presentation-only for this grid and must not reach the stale +/// single-select handlers. +/// +public partial class RcbExportFilterWindow +{ + [ModuleInitializer] + internal static void RegisterLegacyRcbVirtualizedSelectionAuthority() + { + EventManager.RegisterClassHandler( + typeof(CheckBox), + ToggleButton.CheckedEvent, + new RoutedEventHandler(SuppressLegacyRcbSingleSelectStateHandler), + handledEventsToo: true); + EventManager.RegisterClassHandler( + typeof(CheckBox), + ToggleButton.UncheckedEvent, + new RoutedEventHandler(SuppressLegacyRcbSingleSelectStateHandler), + handledEventsToo: true); + } + + private static void SuppressLegacyRcbSingleSelectStateHandler(object sender, RoutedEventArgs e) + { + if (sender is not CheckBox checkBox || + checkBox.DataContext is not RcbExportRow || + Window.GetWindow(checkBox) is not RcbExportFilterWindow window || + FindP1LegacyRcbAncestor(checkBox) is not { } grid || + !ReferenceEquals(grid, window.RcbGrid)) + { + return; + } + + // Do not allow the original XAML Checked/Unchecked instance handlers to translate + // DataGrid container materialization/recycling into a single-selection mutation. + // RcbExportRow.IsSelected remains the durable selection authority. + e.Handled = true; + } +} From 991dc255c28d4bfe594fa2fa381a83ad795d6b8b Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Tue, 8 Sep 2026 22:04:11 +0700 Subject: [PATCH 2/2] test: guard RCB multi-selection through virtualization --- ...bSelectionVirtualizationRegressionTests.cs | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 tests/ARSAS.Tests/RcbSelectionVirtualizationRegressionTests.cs diff --git a/tests/ARSAS.Tests/RcbSelectionVirtualizationRegressionTests.cs b/tests/ARSAS.Tests/RcbSelectionVirtualizationRegressionTests.cs new file mode 100644 index 000000000..f88878589 --- /dev/null +++ b/tests/ARSAS.Tests/RcbSelectionVirtualizationRegressionTests.cs @@ -0,0 +1,81 @@ +using ArIED61850Tester; +using ArIED61850Tester.Models; + +namespace ARSAS.Tests; + +public sealed class RcbSelectionVirtualizationRegressionTests +{ + [Fact] + public void LegacyRcbGrid_RecycledCheckboxEventsCannotReachSingleSelectHandlers() + { + var xaml = Read("RcbExportFilterWindow.xaml"); + var authority = Read("RcbExportFilterWindow.VirtualizedSelectionAuthority.cs"); + + Assert.Contains( + "IsChecked=\"{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}\"", + xaml, + StringComparison.Ordinal); + + // These legacy handlers still exist for the original single-RCB window contract. The + // P1 multi-select authority must stop virtualization-generated Checked/Unchecked events + // before those instance handlers can translate visual rehydration into SelectOnly(). + Assert.Contains("Checked=\"RcbCheckBox_Checked\"", xaml, StringComparison.Ordinal); + Assert.Contains("Unchecked=\"RcbCheckBox_Unchecked\"", xaml, StringComparison.Ordinal); + Assert.Contains("ToggleButton.CheckedEvent", authority, StringComparison.Ordinal); + Assert.Contains("ToggleButton.UncheckedEvent", authority, StringComparison.Ordinal); + Assert.Contains("ReferenceEquals(grid, window.RcbGrid)", authority, StringComparison.Ordinal); + Assert.Contains("e.Handled = true", authority, StringComparison.Ordinal); + } + + [Fact] + public void MultiRcbSelectionAuthority_KeepsIndependentRowsSelected() + { + var rows = new List + { + NewRow("Buffer"), + NewRow("Unbuffer"), + NewRow("A_URCB"), + NewRow("A_URCB_1"), + NewRow("A_URCB_10") + }; + var anchorIndex = -1; + var anchorValue = false; + + for (var index = 0; index < 4; index++) + { + MainWindow.ApplyRcbSelectionForTest( + rows, + ref anchorIndex, + ref anchorValue, + index, + extendRange: false); + } + + Assert.Equal(4, rows.Count(row => row.IsSelected)); + Assert.All(rows.Take(4), row => Assert.True(row.IsSelected)); + Assert.False(rows[4].IsSelected); + } + + private static RcbExportRow NewRow(string name) + => new() + { + Name = name, + Reference = $"IED/LLN0.{name}", + SourceSelectionKey = name, + ExportName = name + }; + + private static string Read(string relativePath) + { + DirectoryInfo? directory = new(AppContext.BaseDirectory); + while (directory != null) + { + var candidate = Path.Combine(directory.FullName, relativePath); + if (File.Exists(candidate)) + return File.ReadAllText(candidate); + directory = directory.Parent; + } + + throw new FileNotFoundException(relativePath); + } +}