wpfui icon indicating copy to clipboard operation
wpfui copied to clipboard

FluentWindow doesn't open in a published Remote Desktop Services app

Open jonasnordlund opened this issue 8 months ago • 5 comments

Describe the bug

This might be a tricky one, but we've run into an issue with a published Remote Desktop Services app. In this case, a FluentWindow only shows up as an "undrawn" window preview when hovering on its icon in the task bar, and otherwise doesn't show up. If we instead change the window type to the regular Window, everything works normally again, including the contained and styled WPF-UI elements.

Everything also works within a full RDP session; this is about published apps specifically.

It's as if Remote Desktop Services isn't fully compatible with something that WPF-UI does, such understanding the window bounds when it's this custom frame or something?

To Reproduce

  1. Write an application with this XAML code:
<ui:FluentWindow x:Class="WPFUITest.TestWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPFUITest"
        xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
        mc:Ignorable="d" Title="Test Window" Height="200" Width="500">
    <TextBlock Margin="4" HorizontalAlignment="Center">This is a FluentWindow.</TextBlock>
</ui:FluentWindow>
  1. Publish the compiled application with Remote Desktop Services.
  2. Launch it from the RD Web Access interface.

Expected behavior

That the FluentWindow opens with a custom drawn frame, or potentially a fallback solution and a reuglar Window if this kind of session is detected. However, this is problematic because the title bar may then be doubled if the FluentWindow is using a <ui:TitleBar>.

Screenshots

No response

OS version

Windows Server 2019 Standard, build 10.0.17763

.NET version

.NET 9.0

WPF-UI NuGet version

4.0.2

Additional context

No response

jonasnordlund avatar May 27 '25 13:05 jonasnordlund

Alright, some additional troubleshooting and findings done on this one.

None of WindowBackdropType, ExtendsContentIntoTitleBar, WindowCornerPreference, AllowsTransparency, WindowStyle, ResizeMode with more "friendly"/unthemed settings seem to help.

Various tricks made in a Loaded event handler to force focus, force invalidation, force activation do not seem to help.

I went digging in the source code and discovered that it's probably something with OnExtendsContentIntoTitleBarChanged and maybe either UnsafeNativeMethods.RemoveWindowTitlebarContents or how the custom chrome is set.

I tried overriding the method like this to avoid invoking the code altogether:

        protected override void OnExtendsContentIntoTitleBarChanged(bool oldValue, bool newValue)
        {
            //base.OnExtendsContentIntoTitleBarChanged(oldValue, newValue);
        }

This brought back the window borders and made things work again. So I'll try to go from here and experiment with mechanisms to detect the kind of session (Published RDS App or not?) and e.g. collapsing the <ui:TitleBar> etc. if detected and simply using the default chrome.

jonasnordlund avatar May 27 '25 14:05 jonasnordlund

Hi, what if you try something like this? In a remote environment I encountered the same problem, it turned out to be caused by setting GlassFrameThickness = new Thickness(-1), if I recall correctly.

[DllImport("dwmapi.dll")]
private static extern int DwmIsCompositionEnabled(out bool enabled);

private static bool IsCompositionEnabled()
{
    if (DwmIsCompositionEnabled(out bool enabled) == 0)
        return enabled;
    return false;
}


protected override void OnExtendsContentIntoTitleBarChanged(bool oldValue, bool newValue)
{
    if (newValue)
    {
        if (IsCompositionEnabled())
        {
            base.OnExtendsContentIntoTitleBarChanged(oldValue, newValue);
        }
        else
        {
            var chrome = new System.Windows.Shell.WindowChrome {
                CaptionHeight = 30,
                GlassFrameThickness = new Thickness(0),
                ResizeBorderThickness = new Thickness(6)
            };
            System.Windows.Shell.WindowChrome.SetWindowChrome(this, chrome);
        }
    }
    else
    {
        base.OnExtendsContentIntoTitleBarChanged(oldValue, newValue);
    }
}

Alessio2405 avatar May 28 '25 07:05 Alessio2405

it turned out to be caused by setting GlassFrameThickness = new Thickness(-1),

Yep, that can cause strange issues.

(edit)

But, I just wrote a simple console app

var success = Windows.Win32.PInvoke.DwmIsCompositionEnabled(out var result);
Console.WriteLine($"Success: {success}, result: {result}");
Console.ReadLine();

…and result is 0x1 when running it as a published RD app (Windows Server 2019 server; macOS client). I've also embedded an app.manifest to declare Windows 8 through 10 compatibility; this doesn't seem to have an effect. So either that API lies (the docs suggest it might), or composition is considered enabled.

chucker avatar May 28 '25 08:05 chucker

I can't try on WS 2019, but with something like this solution, what do you think? First you check whether the current session ID equals console session ID. If so, you’re on the real console, otherwise you’re in a remote session so you can't rely on that (and method return false)


    [DllImport("dwmapi.dll", PreserveSig = true)]
    private static extern int DwmIsCompositionEnabled(out bool enabled);

    [DllImport("kernel32.dll")]
    private static extern uint WTSGetActiveConsoleSessionId();

    [DllImport("kernel32.dll")]
    private static extern bool ProcessIdToSessionId(
        uint dwProcessId,
        out uint pSessionId);
		
    private static bool IsRealDesktopCompositionEnabled()
    {
        uint consoleSess = WTSGetActiveConsoleSessionId();
        ProcessIdToSessionId((uint)Environment.ProcessId, out uint currentSess);

        bool onConsole = (currentSess == consoleSess);

        DwmIsCompositionEnabled(out bool enabled);

        return onConsole && enabled;
    }



bool isRealComposition = IsRealDesktopCompositionEnabled();

Alessio2405 avatar May 28 '25 12:05 Alessio2405

Somehow, the method via DwmIsCompositionEnabled didn't work for me, but maybe it's because we have enabled the RemoteFX feature on our RDP sessions (unsure if necessary for us though; I think I originally did this to troubleshoot this very issue). There are some hints from Microsoft that this setting might introduce RDP detection issues, making it suddenly appear as a local console.

Anyway, I crafted yet a variant here, that indeed works for me in detecting a RemoteApp scenario, even in my setup.

Here's the reworked event handler:

        protected override void OnExtendsContentIntoTitleBarChanged(bool oldValue, bool newValue)
        {
            var sessionKind = SessionKind.GetSessionKind();
            if (sessionKind == SessionKind.Kind.RemoteApp || sessionKind == SessionKind.Kind.Unknown)
            {
                // MessageBox.Show("RemoteApp or unknown session type; falling back!");
                var chrome = new WindowChrome
                {
                    CaptionHeight = 0,
                    CornerRadius = default,
                    GlassFrameThickness = new Thickness(0), // The single change from base.OnExtendsContentIntoTitleBarChanged()
                    ResizeBorderThickness = ResizeMode == ResizeMode.NoResize ? default : new Thickness(4),
                    UseAeroCaptionButtons = false
                };
                WindowChrome.SetWindowChrome(this, chrome);
                _ = UnsafeNativeMethods.RemoveWindowTitlebarContents(this);
            }
            else
            {
                // MessageBox.Show("Desktop or full RDP session detected; neither causes the rendering issue!");
                base.OnExtendsContentIntoTitleBarChanged(oldValue, newValue);
            }
        }

Finally, here's the supporting code for the SessionKind helper class as a gist. It works up the parent process chain until the root. The root is almost always explorer.exe in a "full desktop" RDP or normal, local desktop session, but should be rdpinit.exe in RemoteApp scenarios, The problem here is obviously that it's a method using heuristics and not an official way because Microsoft doesn't seem to like us to detect this an adapt behavior based on it. They might change the root process name in the future. But I'm happy with this method for now, and our apps.

Thanks for leading me the right way on this @Alessio2405!

I'll leave this issue open I suppose, because it's still a problem in WPF-UI and RemoteApp scenarios without any workarounds.

Update 2025-09: The parent process detection via Win32 in my helper class unfortunately requires local admin rights! There might be a better heuristic for this and RemoteApps. I currently use Win32 FindWindow() to find a window named "RemoteApp Marker Window". It seems to always be present, but note that this is merely a heuristic and might change in upcoming Windows Server / RDS editions.

jonasnordlund avatar May 28 '25 12:05 jonasnordlund