From 5ae03b662adc88dc2c02a606b11e8412a7171a7f Mon Sep 17 00:00:00 2001 From: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com> Date: Tue, 30 Jun 2026 15:03:53 -0700 Subject: [PATCH 1/6] virtualize chat timeline rows --- .../Chat/OpenClawChatTimeline.cs | 15 +++--- src/OpenClawTray.FunctionalUI/FunctionalUI.cs | 54 +++++++++++++++++++ ...ChatTimelineVirtualizationContractTests.cs | 24 +++++++++ 3 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 tests/OpenClaw.Tray.Tests/ChatTimelineVirtualizationContractTests.cs diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs index 4eea1e3d1..c2b2da852 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs @@ -465,7 +465,7 @@ public override Element Render() var scrollViewRef = UseRef(null); var isFollowingRef = UseRef(true); - var contentRef = UseRef(null); + var contentRef = UseRef(null); var prevEntryCountRef = UseRef(0); var prevSessionIdRef = UseRef(null); var prevFirstEntryIdRef = UseRef(null); @@ -508,7 +508,10 @@ public override Element Render() if (prevShowToolCallsRef.Current != showToolCalls) { prevShowToolCallsRef.Current = showToolCalls; - contentRef.Current?.Children.Clear(); + if (contentRef.Current is ItemsRepeater repeater) + repeater.ItemsSource = Array.Empty(); + else if (contentRef.Current is StackPanel stackPanel) + stackPanel.Children.Clear(); } // Hover state — set of entry ids currently under the pointer. Used to @@ -2482,12 +2485,12 @@ bool BurstIsNestable(System.Collections.Generic.List b) Grid([GridSize.Star()], [GridSize.Auto, GridSize.Auto, GridSize.Auto, GridSize.Auto], loadMoreButton.Grid(row: 0, column: 0), Border(Empty()).Height(20).Grid(row: 1, column: 0), - VStack(2, timelineRows).Set(sp => + VirtualVStack(2, timelineRows).Set(host => { - if (contentRef.Current != sp) + if (contentRef.Current != host) { - contentRef.Current = (Microsoft.UI.Xaml.Controls.StackPanel)sp; - sp.SizeChanged += (_, _) => + contentRef.Current = host; + host.SizeChanged += (_, _) => { if (scrollViewRef.Current is not { } sv) return; diff --git a/src/OpenClawTray.FunctionalUI/FunctionalUI.cs b/src/OpenClawTray.FunctionalUI/FunctionalUI.cs index 9cd01035a..00d8c44a2 100644 --- a/src/OpenClawTray.FunctionalUI/FunctionalUI.cs +++ b/src/OpenClawTray.FunctionalUI/FunctionalUI.cs @@ -202,6 +202,7 @@ public sealed record ComboBoxElement(string[] Items, int SelectedIndex, Action Children) : Element; +public sealed record VirtualStackElement(Orientation Orientation, double Spacing, IReadOnlyList Children) : Element; public sealed record FlexRowElement(IReadOnlyList Children) : Element { public double ColumnGap { get; init; } @@ -597,6 +598,8 @@ public static ComboBoxElement ComboBox(string[] items, int selectedIndex = -1, A public static StackElement VStack(double spacing, params Element?[] children) => new(Orientation.Vertical, spacing, children); public static StackElement HStack(params Element?[] children) => new(Orientation.Horizontal, 0, children); public static StackElement HStack(double spacing, params Element?[] children) => new(Orientation.Horizontal, spacing, children); + public static VirtualStackElement VirtualVStack(double spacing, params Element?[] children) => new(Orientation.Vertical, spacing, children); + public static VirtualStackElement VirtualHStack(double spacing, params Element?[] children) => new(Orientation.Horizontal, spacing, children); public static GridElement Grid(string[] columns, string[] rows, params Element?[] children) => new(columns, rows, children); public static GridElement Grid(GridSize[] columns, GridSize[] rows, params Element?[] children) => new(columns.Select(c => c.ToString()).ToArray(), rows.Select(r => r.ToString()).ToArray(), children); @@ -955,6 +958,7 @@ private UIElement RenderElement(Element element, string path, List effec ImageElement e => ConfigureImage(GetOrCreate(path), e), BorderElement e => ConfigureBorder(GetOrCreate(path), e, path, effects), StackElement e => ConfigureStack(GetOrCreate(path), e, path, effects), + VirtualStackElement e => ConfigureVirtualStack(GetOrCreate(path), e, path), FlexRowElement e => ConfigureFlexRow(GetOrCreate(path), e, path, effects), GridElement e => ConfigureGrid(GetOrCreate(path), e, path, effects), ScrollViewElement e => ConfigureScrollView(GetOrCreate(path), e, path, effects), @@ -1266,6 +1270,56 @@ private Border ConfigureStack(Border wrapper, StackElement element, string path, return wrapper; } + private Border ConfigureVirtualStack(Border wrapper, VirtualStackElement element, string path) + { + var repeater = GetOrCreate(path + ".repeater"); + if (repeater.Layout is not StackLayout layout) + { + layout = new StackLayout(); + repeater.Layout = layout; + } + layout.Orientation = element.Orientation; + layout.Spacing = element.Spacing; + + repeater.ItemsSource = element.Children + .Select((child, index) => child is null ? null : new VirtualStackItem(index, child)) + .Where(item => item is not null) + .Cast() + .ToArray(); + repeater.ItemTemplate = new VirtualStackItemTemplate(this, path); + repeater.HorizontalAlignment = HorizontalAlignment.Stretch; + repeater.VerticalAlignment = VerticalAlignment.Stretch; + + SetChild(wrapper, repeater); + ApplyModifiers(wrapper, element); + ApplySetters(repeater, element); + return wrapper; + } + + private sealed record VirtualStackItem(int Index, Element Element); + + private sealed class VirtualStackItemTemplate(UiRenderer renderer, string path) : IElementFactory + { + public UIElement GetElement(ElementFactoryGetArgs args) + { + if (args.Data is not VirtualStackItem item) + return new Border(); + + var effects = new List(); + var control = renderer.RenderElement(item.Element, ChildPath(path, item.Index, item.Element), effects); + foreach (var effect in effects) + effect(); + return control; + } + + public void RecycleElement(ElementFactoryRecycleArgs args) + { + // The renderer cache owns element identity by path. ItemsRepeater + // detaches recycled rows; re-realization asks for the same path and + // gets a refreshed control without rebuilding the whole list. + } + } + private Border ConfigureFlexRow(Border wrapper, FlexRowElement element, string path, List effects) { var panel = GetOrCreate(path + ".panel"); diff --git a/tests/OpenClaw.Tray.Tests/ChatTimelineVirtualizationContractTests.cs b/tests/OpenClaw.Tray.Tests/ChatTimelineVirtualizationContractTests.cs new file mode 100644 index 000000000..a306b129d --- /dev/null +++ b/tests/OpenClaw.Tray.Tests/ChatTimelineVirtualizationContractTests.cs @@ -0,0 +1,24 @@ +namespace OpenClaw.Tray.Tests; + +public sealed class ChatTimelineVirtualizationContractTests +{ + [Fact] + public void ProductionTimeline_UsesVirtualizedItemsRepeaterRows() + { + var timeline = Read("src", "OpenClaw.Tray.WinUI", "Chat", "OpenClawChatTimeline.cs"); + var functionalUi = Read("src", "OpenClawTray.FunctionalUI", "FunctionalUI.cs"); + + Assert.Contains("VirtualVStack(2, timelineRows)", timeline); + Assert.DoesNotContain(" VStack(2, timelineRows)", timeline); + Assert.Contains("UseRef", timeline); + Assert.Contains("ItemsRepeater repeater", timeline); + + Assert.Contains("VirtualStackElement", functionalUi); + Assert.Contains("ItemsRepeater", functionalUi); + Assert.Contains("StackLayout", functionalUi); + Assert.Contains("IElementFactory", functionalUi); + } + + private static string Read(params string[] parts) + => File.ReadAllText(Path.Combine(new[] { TestRepositoryPaths.GetRepositoryRoot() }.Concat(parts).ToArray())); +} From 55f3b10ac30badb4e75dbd7c2549ea911bf3e235 Mon Sep 17 00:00:00 2001 From: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com> Date: Tue, 30 Jun 2026 15:16:45 -0700 Subject: [PATCH 2/6] add chat timeline virtualization proof --- .../ChatTimelineVirtualizationProofTests.cs | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 tests/OpenClaw.Tray.UITests/ChatTimelineVirtualizationProofTests.cs diff --git a/tests/OpenClaw.Tray.UITests/ChatTimelineVirtualizationProofTests.cs b/tests/OpenClaw.Tray.UITests/ChatTimelineVirtualizationProofTests.cs new file mode 100644 index 000000000..d35ec20cc --- /dev/null +++ b/tests/OpenClaw.Tray.UITests/ChatTimelineVirtualizationProofTests.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.UI.Xaml.Controls; +using OpenClaw.Chat; +using OpenClawTray.Chat; +using OpenClawTray.FunctionalUI; +using OpenClawTray.FunctionalUI.Hosting; +using Windows.Graphics; +using static OpenClawTray.FunctionalUI.Factories; +using static OpenClaw.Tray.UITests.TestSupport; + +namespace OpenClaw.Tray.UITests; + +[Collection(UICollection.Name)] +public sealed class ChatTimelineVirtualizationProofTests +{ + private const int InitialRows = 240; + + private readonly UIThreadFixture _ui; + + public ChatTimelineVirtualizationProofTests(UIThreadFixture ui) => _ui = ui; + + [Fact] + public async Task LargeNativeChatTimeline_VirtualizesRowsAndFollowsNewMessages() + { + await _ui.ResetContainerAsync(); + + var props = BuildProps(InitialRows, scrollToBottomToken: 0); + FunctionalHostControl? host = null; + + await _ui.RunOnUIAsync(() => + { + _ui.TestWindow.AppWindow.MoveAndResize(new RectInt32(-32000, -32000, 960, 720)); + _ui.Container.Width = 900; + _ui.Container.Height = 640; + + host = new FunctionalHostControl + { + Width = 860, + Height = 560, + SuppressAutoDispose = true, + }; + _ui.Container.Children.Add(host); + host.Mount(_ => Component(props)); + }); + + await DrainRenderQueueAsync(); + + await _ui.RunOnUIAsync(() => + { + var repeater = FindLogical(host!).Single(); + var layout = Assert.IsType(repeater.Layout); + Assert.Equal(Orientation.Vertical, layout.Orientation); + Assert.Equal(2, layout.Spacing); + Assert.Equal(InitialRows, CountItems(repeater.ItemsSource)); + + var scrollViewer = FindLogical(host!).Single(); + _ui.Container.UpdateLayout(); + Assert.True(scrollViewer.ScrollableHeight > 0, "large chat timeline should overflow and become scrollable"); + + scrollViewer.ChangeView(null, scrollViewer.ScrollableHeight, null, disableAnimation: true); + _ui.Container.UpdateLayout(); + Assert.True(scrollViewer.VerticalOffset > 0, "large chat timeline should scroll away from the top"); + }); + + await DrainRenderQueueAsync(); + + var appendedRows = InitialRows + 1; + props = BuildProps(appendedRows, scrollToBottomToken: 1); + await _ui.RunOnUIAsync(() => + { + host!.Mount(_ => Component(props)); + }); + + await DrainRenderQueueAsync(); + + await _ui.RunOnUIAsync(() => + { + var repeater = FindLogical(host!).Single(); + Assert.Equal(appendedRows, CountItems(repeater.ItemsSource)); + + var scrollViewer = FindLogical(host!).Single(); + _ui.Container.UpdateLayout(); + Assert.True( + scrollViewer.ScrollableHeight - scrollViewer.VerticalOffset <= 4, + $"chat follow should stay near the newest row; offset={scrollViewer.VerticalOffset:0.0}, height={scrollViewer.ScrollableHeight:0.0}"); + + var visibleText = FindDescendants(host!) + .Select(t => t.Text) + .Where(t => !string.IsNullOrWhiteSpace(t)) + .ToArray(); + Assert.Contains("User proof row 241", visibleText); + + Console.WriteLine( + "CHAT_TIMELINE_VIRTUALIZATION_PROOF " + + $"rows={appendedRows} " + + $"itemsRepeater={repeater.GetType().Name} " + + $"layout={((StackLayout)repeater.Layout).Orientation} " + + $"scrollableHeight={scrollViewer.ScrollableHeight:0.0} " + + $"verticalOffset={scrollViewer.VerticalOffset:0.0} " + + "newestRowVisible=true"); + }); + + await _ui.RunOnUIAsync(() => host!.Dispose()); + } + + private async Task DrainRenderQueueAsync() + { + await _ui.RunOnUIAsync(() => { }); + await Task.Delay(50); + await _ui.RunOnUIAsync(() => _ui.Container.UpdateLayout()); + await _ui.RunOnUIAsync(() => { }); + } + + private static OpenClawChatTimelineProps BuildProps(int rows, int scrollToBottomToken) => + new( + SessionId: "ui-proof-large-chat", + Entries: BuildEntries(rows), + HasMoreHistory: false, + OnLoadMoreHistory: null, + UserSenderLabel: "UI proof user", + AssistantSenderLabel: "UI proof assistant", + DefaultModel: "proof-model", + ShowToolCalls: true, + ScrollToBottomToken: scrollToBottomToken); + + private static IReadOnlyList BuildEntries(int rows) + { + var entries = new List(rows); + for (var i = 1; i <= rows; i++) + { + var isUser = i % 2 == 1; + entries.Add(new ChatTimelineItem( + Id: $"proof-{i:000}", + Kind: isUser ? ChatTimelineItemKind.User : ChatTimelineItemKind.Assistant, + Text: isUser + ? $"User proof row {i}" + : $"Assistant proof row {i}")); + } + + return entries; + } + + private static int CountItems(object? itemsSource) => + itemsSource is System.Collections.IEnumerable enumerable + ? enumerable.Cast().Count() + : 0; +} From 5f3fb7e14cba67bf0e648e59e05b987fb4eedc93 Mon Sep 17 00:00:00 2001 From: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com> Date: Tue, 30 Jun 2026 15:27:45 -0700 Subject: [PATCH 3/6] add chat ui proof resources --- tests/OpenClaw.Tray.UITests/TestApp.cs | 42 ++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/OpenClaw.Tray.UITests/TestApp.cs b/tests/OpenClaw.Tray.UITests/TestApp.cs index a07a31fdb..14e60c155 100644 --- a/tests/OpenClaw.Tray.UITests/TestApp.cs +++ b/tests/OpenClaw.Tray.UITests/TestApp.cs @@ -1,6 +1,8 @@ using System; +using Microsoft.UI; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Markup; +using Microsoft.UI.Xaml.Media; namespace OpenClaw.Tray.UITests; @@ -20,6 +22,27 @@ namespace OpenClaw.Tray.UITests; /// internal sealed class TestApp : Application { + private static readonly (string Key, Windows.UI.Color Color)[] FluentBrushFallbacks = + [ + ("SolidBackgroundFillColorBaseBrush", Colors.White), + ("LayerFillColorDefaultBrush", Colors.White), + ("CardBackgroundFillColorDefaultBrush", Colors.White), + ("CardStrokeColorDefaultBrush", ColorHelper.FromArgb(0x33, 0x00, 0x00, 0x00)), + ("ControlFillColorTertiaryBrush", ColorHelper.FromArgb(0x0F, 0x00, 0x00, 0x00)), + ("ControlStrokeColorDefaultBrush", ColorHelper.FromArgb(0x33, 0x00, 0x00, 0x00)), + ("SubtleFillColorSecondaryBrush", ColorHelper.FromArgb(0x0F, 0x00, 0x00, 0x00)), + ("SubtleFillColorTertiaryBrush", ColorHelper.FromArgb(0x14, 0x00, 0x00, 0x00)), + ("AccentFillColorDefaultBrush", ColorHelper.FromArgb(0xFF, 0x00, 0x66, 0xCC)), + ("AccentFillColorSecondaryBrush", ColorHelper.FromArgb(0xCC, 0x00, 0x66, 0xCC)), + ("TextFillColorPrimaryBrush", Colors.Black), + ("TextFillColorSecondaryBrush", ColorHelper.FromArgb(0xE3, 0x00, 0x00, 0x00)), + ("TextFillColorTertiaryBrush", ColorHelper.FromArgb(0x99, 0x00, 0x00, 0x00)), + ("TextOnAccentFillColorPrimaryBrush", Colors.White), + ("SystemFillColorSuccessBrush", ColorHelper.FromArgb(0xFF, 0x0F, 0x7B, 0x0F)), + ("SystemFillColorCautionBrush", ColorHelper.FromArgb(0xFF, 0x9D, 0x5D, 0x00)), + ("SystemFillColorCriticalBrush", ColorHelper.FromArgb(0xFF, 0xC4, 0x2B, 0x1C)), + ]; + /// /// Merge XamlControlsResources + the production App.xaml's custom keys /// (LobsterAccentBrush, AccentButtonStyle) so renderers that look them up @@ -48,6 +71,11 @@ public void MergeStandardResources() "" + "" + ""); + + foreach (var (key, color) in FluentBrushFallbacks) + { + TryAddBrushResource(key, color); + } } private void TryAddResource(string key, string xaml) @@ -62,4 +90,18 @@ private void TryAddResource(string key, string xaml) // best-effort; missing key just means renderers fall back. } } + + private void TryAddBrushResource(string key, Windows.UI.Color color) + { + try + { + if (!Resources.ContainsKey(key)) + Resources[key] = new SolidColorBrush(color); + } + // slopwatch-ignore: SW003 Test cleanup or fixture teardown is best-effort and must not hide the test outcome. + catch + { + // best-effort; missing key just means renderers fall back. + } + } } From c711fb8d12c359ff8a6c904a7ecfd6e567cf7fbf Mon Sep 17 00:00:00 2001 From: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com> Date: Tue, 30 Jun 2026 15:37:19 -0700 Subject: [PATCH 4/6] stabilize chat ui proof resources --- tests/OpenClaw.Tray.UITests/TestApp.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/OpenClaw.Tray.UITests/TestApp.cs b/tests/OpenClaw.Tray.UITests/TestApp.cs index 14e60c155..932dfd402 100644 --- a/tests/OpenClaw.Tray.UITests/TestApp.cs +++ b/tests/OpenClaw.Tray.UITests/TestApp.cs @@ -95,8 +95,7 @@ private void TryAddBrushResource(string key, Windows.UI.Color color) { try { - if (!Resources.ContainsKey(key)) - Resources[key] = new SolidColorBrush(color); + Resources[key] = new SolidColorBrush(color); } // slopwatch-ignore: SW003 Test cleanup or fixture teardown is best-effort and must not hide the test outcome. catch From 1fa1e0ad481fead47f096ae666c2fd8d6a65c160 Mon Sep 17 00:00:00 2001 From: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com> Date: Tue, 30 Jun 2026 15:47:28 -0700 Subject: [PATCH 5/6] seed chat ui proof resources --- .../ChatTimelineVirtualizationProofTests.cs | 2 ++ tests/OpenClaw.Tray.UITests/TestApp.cs | 11 ++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/OpenClaw.Tray.UITests/ChatTimelineVirtualizationProofTests.cs b/tests/OpenClaw.Tray.UITests/ChatTimelineVirtualizationProofTests.cs index d35ec20cc..138675047 100644 --- a/tests/OpenClaw.Tray.UITests/ChatTimelineVirtualizationProofTests.cs +++ b/tests/OpenClaw.Tray.UITests/ChatTimelineVirtualizationProofTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using OpenClaw.Chat; using OpenClawTray.Chat; @@ -32,6 +33,7 @@ public async Task LargeNativeChatTimeline_VirtualizesRowsAndFollowsNewMessages() await _ui.RunOnUIAsync(() => { + TestApp.EnsureFluentBrushFallbacks(Application.Current.Resources); _ui.TestWindow.AppWindow.MoveAndResize(new RectInt32(-32000, -32000, 960, 720)); _ui.Container.Width = 900; _ui.Container.Height = 640; diff --git a/tests/OpenClaw.Tray.UITests/TestApp.cs b/tests/OpenClaw.Tray.UITests/TestApp.cs index 932dfd402..0c333ab38 100644 --- a/tests/OpenClaw.Tray.UITests/TestApp.cs +++ b/tests/OpenClaw.Tray.UITests/TestApp.cs @@ -72,9 +72,14 @@ public void MergeStandardResources() "" + ""); + EnsureFluentBrushFallbacks(Resources); + } + + internal static void EnsureFluentBrushFallbacks(ResourceDictionary resources) + { foreach (var (key, color) in FluentBrushFallbacks) { - TryAddBrushResource(key, color); + TryAddBrushResource(resources, key, color); } } @@ -91,11 +96,11 @@ private void TryAddResource(string key, string xaml) } } - private void TryAddBrushResource(string key, Windows.UI.Color color) + private static void TryAddBrushResource(ResourceDictionary resources, string key, Windows.UI.Color color) { try { - Resources[key] = new SolidColorBrush(color); + resources[key] = new SolidColorBrush(color); } // slopwatch-ignore: SW003 Test cleanup or fixture teardown is best-effort and must not hide the test outcome. catch From ef95d1daa82b236204c16dab87858b0d06e4315b Mon Sep 17 00:00:00 2001 From: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com> Date: Tue, 30 Jun 2026 15:58:27 -0700 Subject: [PATCH 6/6] stabilize ui test resource setup --- tests/OpenClaw.Tray.UITests/TestApp.cs | 38 ++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/tests/OpenClaw.Tray.UITests/TestApp.cs b/tests/OpenClaw.Tray.UITests/TestApp.cs index 0c333ab38..844996a61 100644 --- a/tests/OpenClaw.Tray.UITests/TestApp.cs +++ b/tests/OpenClaw.Tray.UITests/TestApp.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using Microsoft.UI; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Markup; @@ -51,9 +52,14 @@ private static readonly (string Key, Windows.UI.Color Color)[] FluentBrushFallba /// public void MergeStandardResources() { + if (!TryGetResources(out var resources)) + { + return; + } + try { - Resources.MergedDictionaries.Add(new Microsoft.UI.Xaml.Controls.XamlControlsResources()); + resources.MergedDictionaries.Add(new Microsoft.UI.Xaml.Controls.XamlControlsResources()); } // slopwatch-ignore: SW003 Test cleanup or fixture teardown is best-effort and must not hide the test outcome. catch @@ -62,17 +68,17 @@ public void MergeStandardResources() // going — the renderers degrade gracefully without theme styles. } - TryAddResource("LobsterAccentBrush", + TryAddResource(resources, "LobsterAccentBrush", ""); - TryAddResource("AccentButtonStyle", + TryAddResource(resources, "AccentButtonStyle", ""); - EnsureFluentBrushFallbacks(Resources); + EnsureFluentBrushFallbacks(resources); } internal static void EnsureFluentBrushFallbacks(ResourceDictionary resources) @@ -83,11 +89,31 @@ internal static void EnsureFluentBrushFallbacks(ResourceDictionary resources) } } - private void TryAddResource(string key, string xaml) + private bool TryGetResources(out ResourceDictionary resources) + { + for (var attempt = 0; attempt < 5; attempt++) + { + try + { + resources = Resources; + return true; + } + // slopwatch-ignore: SW003 Test fixture resource setup is best-effort and must not hide the test outcome. + catch + { + Thread.Sleep(10); + } + } + + resources = null!; + return false; + } + + private static void TryAddResource(ResourceDictionary resources, string key, string xaml) { try { - Resources[key] = XamlReader.Load(xaml); + resources[key] = XamlReader.Load(xaml); } // slopwatch-ignore: SW003 Test cleanup or fixture teardown is best-effort and must not hide the test outcome. catch