-
Notifications
You must be signed in to change notification settings - Fork 804
Description
Proposal: Replace Window.Visible with cached/proxy value
Instead of directly exposing the WinRT IWindow.Visible as a property, use a C# property instead that is updated on IWindow.Closed and IWindow.VisibilityChanged to protect against exceptions during teardown.
Summary
Currently, accessing Window.Visible directly maps to a WinRT/COM operation that throws a COMException if the Window has been closed. Since UI code may continue to be called until the state quiesces and operations that may query Window.Visible may already be taking place mid-flight, it would be safer for this property to provide cached/proxied window visibility state and for reading it to be a simple read operation that does not cross ABI boundaries and does not depend on the window handle still being valid.
Rationale
- As stated, operations may be mid-flight when the window is closed and generally one would not want to block the closing of the Window (in an event handler) for non-critical UI update code to complete. As such, it is "normal" for regular UI code to access properties of the window.
Window.Visibleis defined as a property and not a function; it is not the expectation that its execution will be crossing ABI boundaries and that it would be performing "meaningful" work;- Generally C# class properties are expected to be non-throwing in idiomatic, well-designed code
- Developers coming from other UI frameworks (including WPF and SWF, if I'm not mistaken) expect that as long as the reference to the window itself remains live (i.e. the
Windowhas not been cleaned up by the GC), access to its properties would be valid (though other operations might - of course - throw after the window has closed) - The
Windowfacade aroundIWindowshould make every reasonable (non-expensive and straightforward) effort to make it seem like you are interacting with a native C# object and accessing a property leading to aCOMExceptionbeing thrown flies in the face of that. - If a developer is accessing
Window.Visiblerepeatedly in their UI update code, it is not an unreasonable expectation that this is a quick and fast operation that does not incur much cost. Crossing ABI boundaries to query a value that rarely changes contradicts that, especially when there exists an easy fix for this.
Important Notes
This is what I have in my own project to protect against this:
public sealed partial class MainWindow : Window
{
private TypedEventHandler<object, WindowVisibilityChangedEventArgs> _visibilityHandler
= static (sender, args) => ((MainWindow)sender).IsVisible = args.Visible;
/// <summary>
/// Caches the window visibility and updates on the <see cref="Window.VisibilityChanged"/> event;
/// should be used in lieu of <see cref="Visible"/> to protect against COM exceptions when the
/// window has been destroyed.
/// </summary>
public bool IsVisible { get; private set; }
[Obsolete("Use " + nameof(IsVisible) + " instead to protect against COM exceptions")]
public new bool Visible => IsVisible;
public MainWindow()
{
this.InitializeComponent();
IsVisible = true;
VisibilityChanged += _visibilityHandler;
Closed += static (sender, _args) =>
{
var @this = (MainWindow)sender;
@this.VisibilityChanged -= @this._visibilityHandler;
@this.IsVisible = false;
};
// Remainder of initialization code
}
}Obviously this is not a sufficient solution in that it doesn't protect against Window.Current.Visible being called and must be manually copied to each Window class in a project.
Open Questions
- Is this technically a breaking change? I don't think so, as I can't see any previously working code no longer compiling after this change, nor compiling but no longer working as expected. There is a slight concern about the reactivity latency here (the time it takes for
Window.IsVisiblein the sample code above to reflect a change if the call races with the execution of theVisibiltyChangedcallback handler) but since theIWindow.Visibleproperty is non-synchronized and non-atomic (the actual state of the window's visibility may change immediately after the call determines the return value, resulting in stale information being returned), this does not seem materially different.