Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions dotnet/Devolutions.MsRdpEx.Avalonia/RdpClientView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public class RdpClientView : NativeControlHost, IDisposable
private RdpDisplayMode displayMode = RdpDisplayMode.FitToWindow;
private int zoomLevel = 100;
private bool disposed;
private IPlatformHandle? detachedNativeControl;

public RdpClientView()
{
Expand Down Expand Up @@ -519,6 +520,16 @@ public void Dispose()
// leaves a fullscreen shell behind.
ExitFullScreen();
TeardownSession();

// NativeControlHost no longer tracks a control after its deferred
// destruction callback. If the view is still detached, release the
// retained HWND explicitly now that the owner has permanently closed
// the session.
if (detachedNativeControl is { } control)
{
detachedNativeControl = null;
base.DestroyNativeControlCore(control);
}
}

protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
Expand Down Expand Up @@ -583,6 +594,15 @@ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e

protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
{
// Avalonia reparents the returned HWND into the new native host. Reuse
// the existing control so a docking or tab detach does not replace the
// connected ActiveX session with a fresh, unconfigured surface.
if (detachedNativeControl is { } control)
{
detachedNativeControl = null;
return control;
}

nint window = CreateWindowExW(
0,
StaticWindowClass,
Expand Down Expand Up @@ -623,6 +643,16 @@ protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle paren

protected override void DestroyNativeControlCore(IPlatformHandle control)
{
// NativeControlHost destroys controls that remain outside the visual
// tree past its short reparenting grace period. A docking tab can stay
// detached much longer, so retain its HWND until either it is attached
// again or the owning view is explicitly disposed.
if (!disposed)
{
detachedNativeControl = control;
return;
}

// Release the OLE host while the window still exists; the base
// implementation destroys the window itself afterwards.
TeardownSession();
Expand Down
61 changes: 61 additions & 0 deletions dotnet/MsRdpEx_Test/AvaloniaHosting.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using Devolutions.MsRdpEx.Avalonia;

using Avalonia.Controls.Platform;
using Avalonia.Platform;

namespace MsRdpEx.Tests
{
public class AvaloniaHostingTests
Expand Down Expand Up @@ -111,5 +114,63 @@ public void OleScopeDefersOwnerUninitializeWhileSessionsRemain()
RdpActiveXSession.EnterOleScope();
Assert.True(RdpActiveXSession.ExitOleScope(true));
}

[Fact]
public void RdpClientViewReusesDetachedNativeControl()
{
using TestableRdpClientView view = new();
TestNativeControlHandle control = new();

view.DetachNativeControl(control);

Assert.False(control.IsDestroyed);
Assert.Same(control, view.AttachNativeControl(new TestPlatformHandle()));

view.Dispose();
view.DetachNativeControl(control);
Assert.True(control.IsDestroyed);
}

[Fact]
public void RdpClientViewDisposeDestroysDetachedNativeControl()
{
TestableRdpClientView view = new();
TestNativeControlHandle control = new();

view.DetachNativeControl(control);
view.Dispose();

Assert.True(control.IsDestroyed);
}

private sealed class TestableRdpClientView : RdpClientView
{
public IPlatformHandle AttachNativeControl(IPlatformHandle parent)
{
return base.CreateNativeControlCore(parent);
}

public void DetachNativeControl(IPlatformHandle control)
{
base.DestroyNativeControlCore(control);
}
}

private class TestPlatformHandle : IPlatformHandle
{
public nint Handle => 1;

public string HandleDescriptor => "TEST";
}

private sealed class TestNativeControlHandle : TestPlatformHandle, INativeControlHostDestroyableControlHandle
{
public bool IsDestroyed { get; private set; }

public void Destroy()
{
IsDestroyed = true;
}
}
}
}