diff --git a/doc/articles/controls/GLCanvasElement.md b/doc/articles/controls/GLCanvasElement.md index 82e53797632e..04018b28b529 100644 --- a/doc/articles/controls/GLCanvasElement.md +++ b/doc/articles/controls/GLCanvasElement.md @@ -43,7 +43,7 @@ Additionally, `GLCanvasElement` has an `Invalidate` method that requests a redra ## Detecting errors -To detect errors in initializing the OpenGL environment, `GLCanvasElement` exposes an `IsGLInitialized` property that shows whether or nor the loading of the element and its OpenGL setup were successful. This property is only valid when the element is loaded, i.e. its `IsLoaded` property is true. When the element is not loaded, the value of `IsGLInitialized` will be null. `GLCanvasElement` implements `INotifyPropertyChanged`, so you can use this property in a data bindings, for example to set the visibility of a control as a fallback. +To detect errors in initializing the OpenGL environment, `GLCanvasElement` exposes an `IsGLInitializedProperty` dependency property that shows whether or nor the loading of the element and its OpenGL setup were successful. This property is only valid when the element is loaded, i.e. its `IsLoaded` property is true. When the element is not loaded, the value of `IsGLInitialized` will be null. `GLCanvasElement` implements `INotifyPropertyChanged`, so you can use this property in a data bindings, for example to set the visibility of a control as a fallback. Attempting to change this property is illegal. ## How to use Silk.NET diff --git a/src/AddIns/Uno.WinUI.Graphics3DGL/GLCanvasElement.cs b/src/AddIns/Uno.WinUI.Graphics3DGL/GLCanvasElement.cs index d260dc766f3c..6cef9512f466 100644 --- a/src/AddIns/Uno.WinUI.Graphics3DGL/GLCanvasElement.cs +++ b/src/AddIns/Uno.WinUI.Graphics3DGL/GLCanvasElement.cs @@ -39,7 +39,7 @@ namespace Uno.WinUI.Graphics3DGL; /// This is only available on WinUI and on skia-based targets running with hardware acceleration. /// This is currently only available on the WPF and X11 targets (and WinUI). /// -public abstract partial class GLCanvasElement : Grid, INativeContext, INotifyPropertyChanged +public abstract partial class GLCanvasElement : Grid, INativeContext { private const int BytesPerPixel = 4; private static readonly Dictionary _xamlRootToWrapper = new(); @@ -48,7 +48,7 @@ public abstract partial class GLCanvasElement : Grid, INativeContext, INotifyPro private readonly Func? _getWindowFunc; - private bool? _isGlInitialized; + private bool _changingGlInitialized; // valid if and only if GLCanvasElement was loaded at least once and OpenGL is available on the running platform private INativeOpenGLWrapper? _nativeOpenGlWrapper; @@ -224,25 +224,41 @@ private void OnClosed(object _, object __) public void Invalidate() => NativeDispatcher.Main.Enqueue(Render, NativeDispatcherPriority.Idle); #endif + public static DependencyProperty IsGLInitializedProperty { get; } = + DependencyProperty.Register( + nameof(IsGLInitialized), + typeof(bool?), + typeof(GLCanvasElement), + new PropertyMetadata(null, (PropertyChangedCallback)((dO, _) => + { + var @this = (GLCanvasElement)dO; + if (!@this._changingGlInitialized) + { + throw new InvalidOperationException($"{nameof(GLCanvasElement)}.{nameof(IsGLInitializedProperty)} is read-only."); + } + + // We should have arrived here from set_IsGLInitialized, so we could put this line at the end of the + // setter. Instead, we set it to false here to prevent users from calling SetValue.IsGLInitializedProperty + // _inside_ a call to GLCanvasElement.set_IsGLInitialized. This way, if a user intercepts this + // change (e.g. with SubscribeToPropertyChanged) and attempts to make a nested SetValue call, we still + // explode in their face. + @this._changingGlInitialized = false; + }))); + /// /// Indicates whether this element was loaded successfully or not, including the OpenGL context creation and setup. /// This property is only valid when the element is loaded. When the element is not loaded in the visual tree, the value will be null. /// public bool? IsGLInitialized { - get => _isGlInitialized; + get => (bool?)GetValue(IsGLInitializedProperty); private set { - _isGlInitialized = value; - RaisePropertyChanged(); + _changingGlInitialized = true; + SetValue(IsGLInitializedProperty, value); } } - public event PropertyChangedEventHandler? PropertyChanged; - - protected void RaisePropertyChanged([CallerMemberName] string? propertyName = null) - => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - private void OnLoaded(object sender, RoutedEventArgs routedEventArgs) { _nativeOpenGlWrapper = GetOrCreateNativeOpenGlWrapper(XamlRoot!, _getWindowFunc);