microsoft / microsoft-ui-xaml

Windows UI Library: the latest Windows 10 native controls and Fluent styles for your applications
MIT License
6.29k stars 675 forks source link

Possible to subclass Microsoft.UI.Xaml.Window and reference subclass of subclass in XAML? #9116

Open StevoSM opened 10 months ago

StevoSM commented 10 months ago

In our applications, we have some functionality we would like to add to all of our Windows. We thought to subclass Microsoft.UI.Xaml.Window and then subclass from there for all the various Windows. We seem to get very close, but we are getting a cryptic error when referencing these final Windows in XAML.

In this example, we have MainWindow -> BaseWindow -> Microsoft.UI.Xaml.Window. It's based off the C++ WinUI sample app.

MainWindow.idl

namespace Test {
    [default_interface]
    unsealed runtimeclass BaseWindow : Microsoft.UI.Xaml.Window {
        BaseWindow();
    }

    [default_interface]
    runtimeclass MainWindow : BaseWindow {
        MainWindow();
    }
}

MainWindow.xaml.h

#pragma once

#include "BaseWindow.g.h"
#include "MainWindow.g.h"

namespace winrt::Test::implementation {
    struct BaseWindow : BaseWindowT<BaseWindow> {
        BaseWindow() = default;
    };
}
namespace winrt::Test::factory_implementation {
    struct BaseWindow : BaseWindowT<BaseWindow, implementation::BaseWindow>{};
}

namespace winrt::Test::implementation {
    struct MainWindow : MainWindowT<MainWindow, Test::implementation::BaseWindow> {
        MainWindow() {}
        void myButton_Click(IInspectable const& sender, Microsoft::UI::Xaml::RoutedEventArgs const& args);
    };
}

namespace winrt::Test::factory_implementation {
    struct MainWindow : MainWindowT<MainWindow, implementation::MainWindow>{};
}

MainWindow.xaml.cpp

#include "pch.h"
#include "MainWindow.xaml.h"

#include "BaseWindow.g.cpp"
#include "MainWindow.g.cpp"

using namespace winrt;
using namespace Microsoft::UI::Xaml;

namespace winrt::Test::implementation {
    void MainWindow::myButton_Click(IInspectable const&, RoutedEventArgs const&) {
        myButton().Content(box_value(L"Clicked"));
    }
}

MainWindow.xaml

<?xml version="1.0" encoding="utf-8"?>
<Window
    x:Class="Test.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Test"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
        <Button x:Name="myButton" Click="myButton_Click">Click Me</Button>
    </StackPanel>

</Window>

And the error message is C2338 static_assert failed: 'Class must derive from implements<> or ClassT<> where the first template parameter is the derived class name, e.g. struct D : implements<D, ...>'

How can we adjust the above code to get this to work? Is this even possible?

DarranRowe commented 10 months ago

IIRC, there was a problem that forced the base window runtime class into a separate WinRT component. If it was defined in the same executable, the Xaml compiler would generate bad code when targeting C++.

As a little example.

MiniBaseWindow.zip

The Width and Height properties do nothing, but they mostly exist to show that you can make them available in Xaml. If you were to hook them up to use AppWindow.Resize, then it should work. You could also grab the HWND and use one of the Windows API functions to resize the window too.

As a reminder, the component where BaseWindow is implemented is a WinRT component. So if you try to use it without referencing it through Visual Studio's project system, then you may get a class not registered error. If you do, try using Registration Free WinRT. But this shouldn't be needed normally.

StevoSM commented 10 months ago

@DarranRowe Thanks so much for the reply and the sample. If I understand correctly, this should be easily doable in the same Project, but due to a XAML compiler bug, it's not possible currently. However, putting the subclass in a different Project does work currently like your sample shows. Is that accurate?

DarranRowe commented 10 months ago

Yes. To fully see what goes wrong, you have to look in the Xaml generated MainWindow.xaml.g.h.

The sample that I uploaded starts off with:

//------------------------------------------------------------------------------
//     This code was generated by a tool.
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
//------------------------------------------------------------------------------
#pragma once

#if __has_include(<winrt/Microsoft.UI.Xaml.Controls.h>)
#include <winrt/Microsoft.UI.Xaml.Controls.h>
#endif
#if __has_include(<winrt/Microsoft.UI.Xaml.h>)
#include <winrt/Microsoft.UI.Xaml.h>
#endif

namespace winrt::MiniBaseWindow::implementation
{
    using IInspectable = ::winrt::Windows::Foundation::IInspectable;

    template <typename D, typename ... I>
    struct MainWindowT : public ::winrt::MiniBaseWindow::implementation::MainWindow_base<D,
        ::winrt::Microsoft::UI::Xaml::Markup::IComponentConnector,
        I...>
    {
//...

However, if you keep this base window runtime class in the same project, it starts off with:

//------------------------------------------------------------------------------
//     This code was generated by a tool.
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
//------------------------------------------------------------------------------
#pragma once

#if __has_include(<winrt/Microsoft.UI.Xaml.Controls.h>)
#include <winrt/Microsoft.UI.Xaml.Controls.h>
#endif
#if __has_include(<winrt/Microsoft.UI.Xaml.h>)
#include <winrt/Microsoft.UI.Xaml.h>
#endif

namespace winrt::MiniBaseWindow2::implementation
{
    using IInspectable = ::winrt::Windows::Foundation::IInspectable;

    template <typename D, typename ... I>
    struct MainWindowT : public ::winrt::MiniBaseWindow2::implementation::MainWindow_base<D,
        ::winrt::BaseWindowComponent::implementation::BaseWindow,
        I...>
    {
//...

What's more, if the second one was to compile, it would fail to do much due to the IComponentConnector interface being used to hook up important things.

StevoSM commented 10 months ago

Thanks for the additional details. I did have a look through the code in your project as well as the generated code to see the differences. I even tried to manually edit the generated code in my project to match, but there were always additional errors that were equally difficult to understand or figure what to do.

Hopefully someone from MSFT will mark this as a bug and we can hope to get it fixed in an upcoming release.