diff --git a/AIDevGallery/Controls/OpacityMask.xaml.cs b/AIDevGallery/Controls/OpacityMask.xaml.cs index feb92c7e..b9d953b4 100644 --- a/AIDevGallery/Controls/OpacityMask.xaml.cs +++ b/AIDevGallery/Controls/OpacityMask.xaml.cs @@ -31,8 +31,13 @@ public partial class OpacityMaskView : ContentControl private const string RootGridTemplateName = "PART_RootGrid"; private readonly Compositor _compositor = CompositionTarget.GetCompositorForCurrentThread(); + + // Composition resources we create and need to tear down explicitly to avoid retaining composition references longer than necessary private CompositionBrush? _mask; private CompositionMaskBrush? _maskBrush; + private CompositionSurfaceBrush? _sourceBrush; + private Grid? _rootGrid; + private SpriteVisual? _redirectVisual; /// /// Initializes a new instance of the class. @@ -41,6 +46,9 @@ public partial class OpacityMaskView : ContentControl public OpacityMaskView() { DefaultStyleKey = typeof(OpacityMaskView); + + // Ensure composition resources are cleaned up when control unloads + Unloaded += OpacityMaskView_Unloaded; } /// @@ -57,22 +65,35 @@ protected override void OnApplyTemplate() { base.OnApplyTemplate(); + // Clean up any prior composition resources (e.g., when the template is re-applied) + CleanupComposition(); + Grid rootGrid = (Grid)GetTemplateChild(RootGridTemplateName); ContentPresenter contentPresenter = (ContentPresenter)GetTemplateChild(ContentPresenterTemplateName); Border maskContainer = (Border)GetTemplateChild(MaskContainerTemplateName); + _rootGrid = rootGrid; + + // Create mask brush and its sources _maskBrush = _compositor.CreateMaskBrush(); - _maskBrush.Source = GetVisualBrush(contentPresenter); + + // Source is the content we want to render through the mask + _sourceBrush = GetVisualBrush(contentPresenter); + _maskBrush.Source = _sourceBrush; + + // Mask is the opacity mask visual brush _mask = GetVisualBrush(maskContainer); _maskBrush.Mask = OpacityMask is null ? null : _mask; + // Create a sprite visual that draws with the mask brush, and redirect the control visual to it SpriteVisual redirectVisual = _compositor.CreateSpriteVisual(); redirectVisual.RelativeSizeAdjustment = Vector2.One; redirectVisual.Brush = _maskBrush; ElementCompositionPreview.SetElementChildVisual(rootGrid, redirectVisual); + _redirectVisual = redirectVisual; } - private static CompositionBrush GetVisualBrush(UIElement element) + private static CompositionSurfaceBrush GetVisualBrush(UIElement element) { Visual visual = ElementCompositionPreview.GetElementVisual(element); @@ -100,6 +121,51 @@ private static void OnOpacityMaskChanged(DependencyObject d, DependencyPropertyC } UIElement? opacityMask = (UIElement?)e.NewValue; + + // Switch to the mask brush if an opacity mask is set; otherwise remove the mask maskBrush.Mask = opacityMask is null ? null : self._mask; } + + // On control unload, ensure we tear down composition resources and clear the child visual + private void OpacityMaskView_Unloaded(object sender, RoutedEventArgs e) => CleanupComposition(); + + /// + /// Clears the ElementCompositionPreview child visual and disposes all composition resources created by this control. + /// This prevents composition resource retention across template reapplications or when the control is unloaded. + /// + private void CleanupComposition() + { + // Detach the child visual from the root grid if present + if (_rootGrid != null) + { + ElementCompositionPreview.SetElementChildVisual(_rootGrid, null); + } + + // Dispose the redirect visual + if (_redirectVisual != null) + { + _redirectVisual.Brush = null; + _redirectVisual.Dispose(); + _redirectVisual = null; + } + + // Dispose mask brush and its sources + if (_maskBrush != null) + { + _maskBrush.Source = null; + _maskBrush.Mask = null; + _maskBrush.Dispose(); + _maskBrush = null; + } + + // Dispose the source content brush explicitly + _sourceBrush?.Dispose(); + _sourceBrush = null; + + // Dispose the mask brush instance if we created one + _mask?.Dispose(); + _mask = null; + + _rootGrid = null; + } } \ No newline at end of file