XAMLHost 讓 WinForm / WPF 也能使用 Modem UI

//Build 2018 提出 XAML Islands 幫助 WPF/WinForm 應用程式使用 UWP 的 XAML controls,讓既有的應用程式可以在不同 Windows 10 設備有更好的體驗(例如:Windows InkFluent Design)。本篇介紹基本導入與使用時遇到的問題。

根據 UWP controls in desktop applicationsWindows Community Toolkit Documentation 的介紹,WPF 加入 UWP 控制項有幾個做法:

  • 利用 Wrapped controls

    Wrapped controls Windows Community Toolkit 提供,包裝幾個常用的類型:WebView, WebViewCompatible, InkCanvs / InkToolbar, MediaPlayerElmenetMapControl

    需要注意:不同的 Controls 支援的 OS 版本不同
    以 WebView 的使用方式爲例,如下:

    1. 安裝 Microsoft.Toolkit.Wpf.UI.Controls.WebView
    2. 為 WPF 加入 application manifest file (link),並設定下面的參數:
      <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
        <application>
          <!-- Windows 10 -->
          <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
        </application>
      </compatibility>
      
      <application xmlns="urn:schemas-microsoft-com:asm.v3">
         <windowsSettings>
           <!-- The combination of below two tags have the following effect :
           1) Per-Monitor for >= Windows 10 Anniversary Update
           2) System < Windows 10 Anniversary Update -->
           <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitor</dpiAwareness>
           <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
         </windowsSettings>
       </application>
    3. 最後在畫面中加入就可以使用了:
      <Window xmlns:local="clr-namespace:WpfWrappedControls"
              xmlns:toolkit="clr-namespace:Microsoft.Toolkit.Wpf.UI.Controls;assembly=Microsoft.Toolkit.Wpf.UI.Controls.WebView"
              mc:Ignorable="d"
              Title="MainWindow" Height="450" Width="800">
          <Grid>
              <toolkit:WebView Source="http://www.dotblogs.com.tw/pou" />
          </Grid>
      </Window>
  • 利用 WindowsXamlHost
    XAMLHost 能代理 Windows.UI.Xaml.UIElement 所有 controls,但至少需要 Windows 10 1809(17763) 以上。
    因此,在使用 WindowsXamlHost 需要先為 WPF 專案加入 C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.17763.0\Windows.winmd 參考。
    參考架構圖: 可得知 XAML Host API 與 WebView Win32 API 隨著 OS 已經推出,但是任然有些限制:Limitations
    往下介紹 WindowsXamlHost 的使用:
    1. 安裝 Microsoft.Toolkit.Wpf.UI.XamlHost 並設定 .NET Framework 4.6.2 以上;如果您的專案是 WinForms 可以參考 Get started 的步驟;
    2. 在 WPF 的 XAML 中加入 XamlHost 包裝的控制項目,並設定 InitialTypeName 為何種 Control 類型與 ChildChanged 事件來加入 XamlHost 實際要顯示的内容與對應注冊的事件:
      <Window x:Class="WpfAppHost.MainWindow"
              xmlns:xamlHost="clr-namespace:Microsoft.Toolkit.Wpf.UI.XamlHost;assembly=Microsoft.Toolkit.Wpf.UI.XamlHost">
        <Grid>
           <xamlHost:WindowsXamlHost InitialTypeName="Windows.UI.Xaml.Controls.Button" ChildChanged="WindowsXamlHost_ChildChanged"/>
        </Grid>
      </Window>
      private void WindowsXamlHost_ChildChanged(object sender, EventArgs e)
      {
          WindowsXamlHost windowsXamlHost = (WindowsXamlHost)sender;
          // 利用 Windows.UI.Xaml.Controls 做為轉換
          Windows.UI.Xaml.Controls.Button button = (Windows.UI.Xaml.Controls.Button)windowsXamlHost.Child;
          Windows.UI.Xaml.Controls.TextBlock txt = new Windows.UI.Xaml.Controls.TextBlock();
          txt.Text = "Click me";
          button.Content = txt;
      }
      ChildChanged 中建立 UI 需要的 Controls,但改用 Windows.UI.Xaml.UIElement 的内容來組合。
      這個跟 WPF 使用的 System.Windows.Controls 完全不同。
      另外,如果您在編寫時 Windows.UI.Xaml.UIElement 編譯失敗,請檢查是否匯入 C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.17763.0\Windows.winmd 參考了。
    3. 搭配自定義的 UWP Controls 到 WPF 專案中:
      除了上述介紹需要在 ChildChanged 事件中 code-behind 的逐一加入 UWP Controls 外,XamlHost 還提供直接匯入您在 UWP 建立好的 custom controls。
      1. 準備一個 UWP Class Library 的專案,裏面放置您要導入 WPF 的 UWP custom controls,例如:範例的專案名稱:MyUWPControls;
      2. 編輯 MyUWPControls 的專案檔案 (*.csproj):
        <!-- 要加在 Microsoft.Windows.UI.Xaml.CSharp.targets 之前 -->
        <PropertyGroup>
            <EnableTypeInfoReflection>false</EnableTypeInfoReflection>
            <EnableXBindDiagnostics>false</EnableXBindDiagnostics>
        </PropertyGroup>
        
        <Import Project="$(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets" />
        
        <!-- 要加在 Microsoft.Windows.UI.Xaml.CSharp.targets 之後 -->
        <PropertyGroup>
          <!-- WpfAppHost 是我的 WPF 專案名稱,它代表是 UWP 專案輸出的整合對象 -->
          <HostFrameworkProject>WpfAppHost</HostFrameworkProject>
        </PropertyGroup>
        <PropertyGroup>
          <!-- Copy source and build output files to hostapp folders -->
          <!-- Default Winforms/WPF projects do not use $Platform for build output folder -->
          <PostBuildEvent>
            xcopy "$(TargetDir)*.xbf"            "$(SolutionDir)$(HostFrameworkProject)\bin\$(Configuration)\$(ProjectName)\" /Y
            xcopy "$(ProjectDir)*.xaml"          "$(SolutionDir)$(HostFrameworkProject)\bin\$(Configuration)\$(ProjectName)\" /Y
            xcopy "$(ProjectDir)*.xaml.cs"       "$(SolutionDir)$(HostFrameworkProject)\$(ProjectName)\" /Y
            xcopy "$(ProjectDir)$(IntermediateOutputPath)*.g.*" "$(SolutionDir)$(HostFrameworkProject)\$(ProjectName)\" /Y
          </PostBuildEvent>
        </PropertyGroup>
        上面的參數是讓 UWP 專案在建置時,把 xbf, xaml, xaml.cs 複製到 host app 的目錄,也就是 WPF 專案裏。
        此時,您到 WPF 專案會發現多了一個跟 UWP 專案的目錄名稱,請把它加入 WPF 的專案裏面,如下圖: 可發現 UWP 專案中的 BlankPage1.xaml 被編程多個 *.g.cs*.xaml.cs,如果您開發 UWP 其實就會發現 XAML 編譯好的内容在建置本來就有這些,這些檔案記錄的是 XAML 配置的内容與參數。
        WPF 專案加入這些參考後,XamlHost 再使用時就會被匯入。 BlankPage1.xaml 是我加入的内容:
        <Page>
          <Grid>
           <TextBlock Text="{x:Bind WPFMessage}" FontSize="50"></TextBlock>
          </Grid>
        </Page>
        public sealed partial class BlankPage1 : Page
        {
            // 作爲 x:bind 的來源,如果您本身使用 ViewModel 要記得開放入口讓外部可以使用
            public string WPFMessage { get; set; }
        
            public BlankPage1()
            {
                this.InitializeComponent();
            }
        }
      3. 匯入之後在 WPF 專案要怎麽使用:
        <Window x:Class="WpfAppHost.MainWindow"
                xmlns:xamlHost="clr-namespace:Microsoft.Toolkit.Wpf.UI.XamlHost;assembly=Microsoft.Toolkit.Wpf.UI.XamlHost">
          <Grid>
            <!-- 利用 WindowsXamlHost 包裝 UWP 專案的内容 -->
            <xamlHost:WindowsXamlHost InitialTypeName="MyUWPControls.BlankPage1" ChildChanged="MyUWPPage_ChildChanged" />
          </Grid>
        </Window>
        InitialTypeName="MyUWPControls.BlankPage1" 放入的是 UWP 專案中的 namespace,再搭配 ChildChanged 事件來調整顯示的内容。
        private void MyUWPPage_ChildChanged(object sender, EventArgs e)
        {
            // 利用 GetUwpInternalObject() 把 UWP 的内容截取出來,並轉型成對應的控制項
            WindowsXamlHost windowsXamlHost = (WindowsXamlHost)sender;
            global::MyUWPControls.BlankPage1 myUWPPage = windowsXamlHost.GetUwpInternalObject() as global::MyUWPControls.BlankPage1;
        
            if (myUWPPage != null)
            {
                myUWPPage.WPFMessage = this.WPFMessage;
            }
        }
        改用這種方式會讓控制項目整合更加方便,雖然發現 XamlHost 相對容易很多,大部分的 Controls 都能相容,不過比較複雜的自定義控制項目或是比較新的控制項目就不一定會支援,建議參考 Limit 做比對。

補充

[範例程式]
31-WPFAndXamlHostSample

======
對於微軟在 Windows Apps 的發展,可發現它爲了讓既有的應用程式可以更好地運作在 Windows 10 的所有設備下不少苦心。
如果您的公司或是自己的專案是用 WPF 開發希望可以納入更多 UWP 的效果,真的可以考慮使用 XamlHost
更可以期待的是 .NET Core 3.0 更支援 Window Forms 應用程式的編程。

References