mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-19 15:20:25 +08:00
feat(GalleryShowCase): implement deferred loading control and diagnostics for showcase items
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
namespace AtomUIGallery.Controls;
|
||||
|
||||
public static class GalleryShowCaseRuntimeOptions
|
||||
{
|
||||
public const string DisableDeferredLoadingEnvironmentVariable = "ATOMUI_GALLERY_DISABLE_SHOWCASE_DEFERRED";
|
||||
|
||||
private static bool s_isDeferredLoadingDisabledOverride;
|
||||
|
||||
public static event EventHandler? DeferredLoadingDisabledChanged;
|
||||
|
||||
public static bool IsDeferredLoadingDisabled
|
||||
{
|
||||
get => s_isDeferredLoadingDisabledOverride || IsEnvironmentDeferredLoadingDisabled();
|
||||
set => SetDeferredLoadingDisabledOverride(value);
|
||||
}
|
||||
|
||||
public static void ResetDeferredLoadingDisabledOverride()
|
||||
{
|
||||
SetDeferredLoadingDisabledOverride(false);
|
||||
}
|
||||
|
||||
private static void SetDeferredLoadingDisabledOverride(bool value)
|
||||
{
|
||||
var oldValue = IsDeferredLoadingDisabled;
|
||||
s_isDeferredLoadingDisabledOverride = value;
|
||||
var newValue = IsDeferredLoadingDisabled;
|
||||
if (oldValue != newValue)
|
||||
{
|
||||
DeferredLoadingDisabledChanged?.Invoke(null, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsEnvironmentDeferredLoadingDisabled()
|
||||
{
|
||||
var value = Environment.GetEnvironmentVariable(DisableDeferredLoadingEnvironmentVariable);
|
||||
return value is not null &&
|
||||
(value.Equals("1", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.Equals("true", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.Equals("yes", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.Equals("on", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
@@ -150,14 +150,24 @@ public class ShowCaseItem : ContentControl
|
||||
change.Property == DeferredContentTemplateProperty ||
|
||||
change.Property == IsDeferredContentMaterializedProperty)
|
||||
{
|
||||
MaterializeDeferredContentIfDiagnosticsDisabled();
|
||||
UpdateDeferredPlaceholderVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
private void MaterializeDeferredContentIfDiagnosticsDisabled()
|
||||
{
|
||||
if (GalleryShowCaseRuntimeOptions.IsDeferredLoadingDisabled)
|
||||
{
|
||||
MaterializeDeferredContent();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateDeferredPlaceholderVisibility()
|
||||
{
|
||||
IsDeferredPlaceholderVisible =
|
||||
IsDeferredContentEnabled &&
|
||||
!GalleryShowCaseRuntimeOptions.IsDeferredLoadingDisabled &&
|
||||
DeferredContentTemplate is not null &&
|
||||
!IsDeferredContentMaterialized;
|
||||
}
|
||||
|
||||
@@ -125,7 +125,8 @@ public class ShowCasePanel : TemplatedControl
|
||||
if (_layoutPanel != null)
|
||||
{
|
||||
_nextProgressiveMountIndex = 0;
|
||||
if (OperatingSystem.IsBrowser())
|
||||
if (OperatingSystem.IsBrowser() &&
|
||||
!GalleryShowCaseRuntimeOptions.IsDeferredLoadingDisabled)
|
||||
{
|
||||
_nextProgressiveMountIndex = MountShowCaseItems(0, BrowserInitialShowCaseItemCount);
|
||||
StartProgressiveMountTimer();
|
||||
@@ -135,7 +136,7 @@ public class ShowCasePanel : TemplatedControl
|
||||
MountShowCaseItems(0, Children.Count);
|
||||
}
|
||||
|
||||
if (IsDeferredLoadingEnabled)
|
||||
if (IsDeferredLoadingEffectivelyEnabled)
|
||||
{
|
||||
MaterializeInitialDeferredContent();
|
||||
QueueDeferredViewportMaterialization();
|
||||
@@ -151,12 +152,18 @@ public class ShowCasePanel : TemplatedControl
|
||||
{
|
||||
base.OnAttachedToVisualTree(e);
|
||||
_isAttachedToVisualTree = true;
|
||||
GalleryShowCaseRuntimeOptions.DeferredLoadingDisabledChanged += HandleDeferredLoadingDisabledChanged;
|
||||
UpdateEffectiveViewportSubscription();
|
||||
if (GalleryShowCaseRuntimeOptions.IsDeferredLoadingDisabled)
|
||||
{
|
||||
MaterializeAllDeferredContent();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
_isAttachedToVisualTree = false;
|
||||
GalleryShowCaseRuntimeOptions.DeferredLoadingDisabledChanged -= HandleDeferredLoadingDisabledChanged;
|
||||
UpdateEffectiveViewportSubscription(false);
|
||||
StopProgressiveMountTimer();
|
||||
_lastEffectiveViewport = null;
|
||||
@@ -170,7 +177,7 @@ public class ShowCasePanel : TemplatedControl
|
||||
if (change.Property == IsDeferredLoadingEnabledProperty)
|
||||
{
|
||||
UpdateEffectiveViewportSubscription();
|
||||
if (IsDeferredLoadingEnabled)
|
||||
if (IsDeferredLoadingEffectivelyEnabled)
|
||||
{
|
||||
MaterializeInitialDeferredContent();
|
||||
QueueDeferredViewportMaterialization();
|
||||
@@ -218,7 +225,7 @@ public class ShowCasePanel : TemplatedControl
|
||||
if (Children[index] is ShowCaseItem item)
|
||||
{
|
||||
_layoutPanel.Children.Add(item);
|
||||
if (!IsDeferredLoadingEnabled)
|
||||
if (!IsDeferredLoadingEffectivelyEnabled)
|
||||
{
|
||||
item.MaterializeDeferredContent();
|
||||
}
|
||||
@@ -275,7 +282,7 @@ public class ShowCasePanel : TemplatedControl
|
||||
|
||||
private void UpdateEffectiveViewportSubscription(bool? shouldSubscribe = null)
|
||||
{
|
||||
var subscribe = shouldSubscribe ?? (IsDeferredLoadingEnabled && _isAttachedToVisualTree);
|
||||
var subscribe = shouldSubscribe ?? (IsDeferredLoadingEffectivelyEnabled && _isAttachedToVisualTree);
|
||||
if (subscribe == _isEffectiveViewportSubscribed)
|
||||
{
|
||||
return;
|
||||
@@ -312,7 +319,7 @@ public class ShowCasePanel : TemplatedControl
|
||||
|
||||
private void MaterializeInitialDeferredContent()
|
||||
{
|
||||
if (!IsDeferredLoadingEnabled)
|
||||
if (!IsDeferredLoadingEffectivelyEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -335,7 +342,7 @@ public class ShowCasePanel : TemplatedControl
|
||||
|
||||
private void QueueDeferredViewportMaterialization()
|
||||
{
|
||||
if (!IsDeferredLoadingEnabled ||
|
||||
if (!IsDeferredLoadingEffectivelyEnabled ||
|
||||
_deferredViewportMaterializationQueued ||
|
||||
!_isAttachedToVisualTree)
|
||||
{
|
||||
@@ -352,7 +359,7 @@ public class ShowCasePanel : TemplatedControl
|
||||
|
||||
private void MaterializeDeferredContentInViewport()
|
||||
{
|
||||
if (!IsDeferredLoadingEnabled ||
|
||||
if (!IsDeferredLoadingEffectivelyEnabled ||
|
||||
_layoutPanel is null ||
|
||||
_lastEffectiveViewport is not { } viewport)
|
||||
{
|
||||
@@ -410,6 +417,25 @@ public class ShowCasePanel : TemplatedControl
|
||||
return viewport.Intersects(itemBounds);
|
||||
}
|
||||
|
||||
private bool IsDeferredLoadingEffectivelyEnabled =>
|
||||
IsDeferredLoadingEnabled && !GalleryShowCaseRuntimeOptions.IsDeferredLoadingDisabled;
|
||||
|
||||
private void HandleDeferredLoadingDisabledChanged(object? sender, EventArgs e)
|
||||
{
|
||||
UpdateEffectiveViewportSubscription();
|
||||
if (IsDeferredLoadingEffectivelyEnabled)
|
||||
{
|
||||
MaterializeInitialDeferredContent();
|
||||
QueueDeferredViewportMaterialization();
|
||||
}
|
||||
else
|
||||
{
|
||||
StopProgressiveMountTimer();
|
||||
_nextProgressiveMountIndex = MountShowCaseItems(_nextProgressiveMountIndex, Children.Count);
|
||||
MaterializeAllDeferredContent();
|
||||
}
|
||||
}
|
||||
|
||||
internal virtual void NotifyAboutToActive()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using AtomUI.Controls;
|
||||
using AtomUI.Desktop.Controls;
|
||||
using AtomUIGallery.Controls;
|
||||
using AtomUIGallery.ShowCases.Alert;
|
||||
using AtomUIGallery.ShowCases.AutoComplete;
|
||||
using AtomUIGallery.ShowCases.Avatar;
|
||||
@@ -234,6 +235,7 @@ public class CaseNavigationViewModel : ReactiveObject, IActivatableViewModel
|
||||
|
||||
private void DoTestNavigatePages(TimeSpan interval)
|
||||
{
|
||||
GalleryShowCaseRuntimeOptions.IsDeferredLoadingDisabled = true;
|
||||
_dispatcherTimer.Stop();
|
||||
_dispatcherTimer.Interval = interval;
|
||||
_dispatcherTimer.Start();
|
||||
@@ -242,5 +244,6 @@ public class CaseNavigationViewModel : ReactiveObject, IActivatableViewModel
|
||||
private void DoStopTestNavigatePages()
|
||||
{
|
||||
_dispatcherTimer.Stop();
|
||||
GalleryShowCaseRuntimeOptions.IsDeferredLoadingDisabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,6 +273,7 @@ Panel 层负责控制一批 `ShowCaseItem` 何时 materialize:
|
||||
- `ShowCasePanel` 只允许使用一个 panel 级 `EffectiveViewportChanged` 监听来判断 viewport,不允许给每个 `ShowCaseItem` 单独挂监听。
|
||||
- 已 materialize 的内容不回收。ShowCase 是文档式页面,不做无限列表虚拟化,避免状态、焦点、Popup/Flyout 生命周期被破坏。
|
||||
- 旧页面在未迁移前可以保留默认 `IsDeferredLoadingEnabled="False"`;一旦纳入本轮 ShowCase 迁移,或后续新增 ShowCase,就必须开启。
|
||||
- 内存泄漏或全量创建压测不能修改单个 ShowCase 的 XAML。使用 `GalleryShowCaseRuntimeOptions` 统一关闭延迟创建:`F5` 启动自动切页压测时会设置 `IsDeferredLoadingDisabled=True`,`F6` 停止压测时恢复运行时开关。命令行全程压测可设置 `ATOMUI_GALLERY_DISABLE_SHOWCASE_DEFERRED=1`,该环境变量优先于 `F6` 的运行时恢复。
|
||||
|
||||
Item 层负责真正延迟创建演示控件。所有有演示内容的 `ShowCaseItem` 必须使用下面的写法:
|
||||
|
||||
|
||||
@@ -56,32 +56,98 @@ public class ShowCasePanelStructureTests
|
||||
public void ShowCaseItem_Deferred_Content_Template_Builds_Only_When_Materialized()
|
||||
{
|
||||
AvaloniaTestApp.EnsureInitialized();
|
||||
var buildCount = 0;
|
||||
var item = new ShowCaseItem
|
||||
var oldValue = SetDeferredLoadingEnvironment(null);
|
||||
GalleryShowCaseRuntimeOptions.ResetDeferredLoadingDisabledOverride();
|
||||
try
|
||||
{
|
||||
IsDeferredContentEnabled = true,
|
||||
DataContext = "demo data",
|
||||
DeferredContentTemplate = new FuncDataTemplate<object?>((data, _) =>
|
||||
var buildCount = 0;
|
||||
var item = new ShowCaseItem
|
||||
{
|
||||
buildCount++;
|
||||
return new TextBlock
|
||||
IsDeferredContentEnabled = true,
|
||||
DataContext = "demo data",
|
||||
DeferredContentTemplate = new FuncDataTemplate<object?>((data, _) =>
|
||||
{
|
||||
Text = data?.ToString()
|
||||
};
|
||||
}, true)
|
||||
};
|
||||
buildCount++;
|
||||
return new TextBlock
|
||||
{
|
||||
Text = data?.ToString()
|
||||
};
|
||||
}, true)
|
||||
};
|
||||
|
||||
buildCount.ShouldBe(0);
|
||||
item.IsDeferredContentMaterialized.ShouldBeFalse();
|
||||
buildCount.ShouldBe(0);
|
||||
item.IsDeferredContentMaterialized.ShouldBeFalse();
|
||||
|
||||
item.MaterializeDeferredContent();
|
||||
item.MaterializeDeferredContent();
|
||||
|
||||
buildCount.ShouldBe(1);
|
||||
item.IsDeferredContentMaterialized.ShouldBeTrue();
|
||||
item.Content.ShouldBeOfType<TextBlock>().Text.ShouldBe("demo data");
|
||||
buildCount.ShouldBe(1);
|
||||
item.IsDeferredContentMaterialized.ShouldBeTrue();
|
||||
item.Content.ShouldBeOfType<TextBlock>().Text.ShouldBe("demo data");
|
||||
|
||||
item.MaterializeDeferredContent();
|
||||
buildCount.ShouldBe(1);
|
||||
item.MaterializeDeferredContent();
|
||||
buildCount.ShouldBe(1);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetDeferredLoadingEnvironment(oldValue);
|
||||
GalleryShowCaseRuntimeOptions.ResetDeferredLoadingDisabledOverride();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShowCaseItem_Materializes_Deferred_Template_Immediately_When_Diagnostics_Disables_Defer()
|
||||
{
|
||||
AvaloniaTestApp.EnsureInitialized();
|
||||
var oldValue = SetDeferredLoadingEnvironment(null);
|
||||
GalleryShowCaseRuntimeOptions.ResetDeferredLoadingDisabledOverride();
|
||||
try
|
||||
{
|
||||
GalleryShowCaseRuntimeOptions.IsDeferredLoadingDisabled = true;
|
||||
|
||||
var buildCount = 0;
|
||||
var item = new ShowCaseItem
|
||||
{
|
||||
IsDeferredContentEnabled = true,
|
||||
DataContext = "diagnostic data",
|
||||
DeferredContentTemplate = new FuncDataTemplate<object?>((data, _) =>
|
||||
{
|
||||
buildCount++;
|
||||
return new TextBlock
|
||||
{
|
||||
Text = data?.ToString()
|
||||
};
|
||||
}, true)
|
||||
};
|
||||
|
||||
buildCount.ShouldBe(1);
|
||||
item.IsDeferredContentMaterialized.ShouldBeTrue();
|
||||
item.Content.ShouldBeOfType<TextBlock>().Text.ShouldBe("diagnostic data");
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetDeferredLoadingEnvironment(oldValue);
|
||||
GalleryShowCaseRuntimeOptions.ResetDeferredLoadingDisabledOverride();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShowCase_Runtime_Options_Do_Not_Let_Runtime_Stop_Override_Environment_Disable()
|
||||
{
|
||||
GalleryShowCaseRuntimeOptions.ResetDeferredLoadingDisabledOverride();
|
||||
var oldValue = SetDeferredLoadingEnvironment("1");
|
||||
try
|
||||
{
|
||||
GalleryShowCaseRuntimeOptions.IsDeferredLoadingDisabled.ShouldBeTrue();
|
||||
|
||||
GalleryShowCaseRuntimeOptions.IsDeferredLoadingDisabled = false;
|
||||
|
||||
GalleryShowCaseRuntimeOptions.IsDeferredLoadingDisabled.ShouldBeTrue();
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetDeferredLoadingEnvironment(oldValue);
|
||||
GalleryShowCaseRuntimeOptions.ResetDeferredLoadingDisabledOverride();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -101,12 +167,17 @@ public class ShowCasePanelStructureTests
|
||||
CountOccurrences(panelSource, "EffectiveViewportChanged +=").ShouldBe(1);
|
||||
panelSource.ShouldContain("MaterializeDeferredContentInViewport");
|
||||
panelSource.ShouldContain("MaterializeAllDeferredContent");
|
||||
panelSource.ShouldContain("GalleryShowCaseRuntimeOptions.IsDeferredLoadingDisabled");
|
||||
panelSource.ShouldContain("!GalleryShowCaseRuntimeOptions.IsDeferredLoadingDisabled");
|
||||
panelSource.ShouldContain("GalleryShowCaseRuntimeOptions.DeferredLoadingDisabledChanged += HandleDeferredLoadingDisabledChanged");
|
||||
panelSource.ShouldContain("GalleryShowCaseRuntimeOptions.DeferredLoadingDisabledChanged -= HandleDeferredLoadingDisabledChanged");
|
||||
|
||||
itemSource.ShouldContain("IsDeferredContentEnabledProperty");
|
||||
itemSource.ShouldContain("DeferredContentTemplateProperty");
|
||||
itemSource.ShouldContain("DeferredContentProperty");
|
||||
itemSource.ShouldContain("DeferredPlaceholderHeightProperty");
|
||||
itemSource.ShouldContain("public void MaterializeDeferredContent()");
|
||||
itemSource.ShouldContain("GalleryShowCaseRuntimeOptions.IsDeferredLoadingDisabled");
|
||||
itemTheme.ShouldContain("PART_DeferredPlaceholder");
|
||||
itemTheme.ShouldContain("DeferredPlaceholderHeight");
|
||||
itemTheme.ShouldContain("ShowCaseItemTokenResource DeferredPlaceholderHeight");
|
||||
@@ -153,4 +224,14 @@ public class ShowCasePanelStructureTests
|
||||
startIndex = matchIndex + value.Length;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? SetDeferredLoadingEnvironment(string? value)
|
||||
{
|
||||
var oldValue = Environment.GetEnvironmentVariable(
|
||||
GalleryShowCaseRuntimeOptions.DisableDeferredLoadingEnvironmentVariable);
|
||||
Environment.SetEnvironmentVariable(
|
||||
GalleryShowCaseRuntimeOptions.DisableDeferredLoadingEnvironmentVariable,
|
||||
value);
|
||||
return oldValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using AtomUIGallery.Controls;
|
||||
using AtomUIGallery.Workspace.ViewModels;
|
||||
using ReactiveUI;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace AtomUIGallery.Tests.Workspace;
|
||||
|
||||
public class CaseNavigationViewModelDiagnosticsTests
|
||||
{
|
||||
[Fact]
|
||||
public void Test_Navigate_Pages_Command_Disables_ShowCase_Deferred_Loading_Until_Stopped()
|
||||
{
|
||||
AvaloniaTestApp.EnsureInitialized();
|
||||
var oldValue = Environment.GetEnvironmentVariable(
|
||||
GalleryShowCaseRuntimeOptions.DisableDeferredLoadingEnvironmentVariable);
|
||||
Environment.SetEnvironmentVariable(
|
||||
GalleryShowCaseRuntimeOptions.DisableDeferredLoadingEnvironmentVariable,
|
||||
null);
|
||||
GalleryShowCaseRuntimeOptions.ResetDeferredLoadingDisabledOverride();
|
||||
var viewModel = new CaseNavigationViewModel(new TestScreen());
|
||||
|
||||
try
|
||||
{
|
||||
viewModel.TestNavigatePagesCommand.Execute(TimeSpan.FromMilliseconds(300))
|
||||
.Subscribe();
|
||||
|
||||
GalleryShowCaseRuntimeOptions.IsDeferredLoadingDisabled.ShouldBeTrue();
|
||||
|
||||
viewModel.StopTestNavigatePagesCommand.Execute()
|
||||
.Subscribe();
|
||||
|
||||
GalleryShowCaseRuntimeOptions.IsDeferredLoadingDisabled.ShouldBeFalse();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable(
|
||||
GalleryShowCaseRuntimeOptions.DisableDeferredLoadingEnvironmentVariable,
|
||||
oldValue);
|
||||
GalleryShowCaseRuntimeOptions.ResetDeferredLoadingDisabledOverride();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestScreen : IScreen
|
||||
{
|
||||
public RoutingState Router { get; } = new();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user