かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

WPF on .NET Core 3.0 で XAML Islands してみよう

しばらく目を離してると手順がガラッと変わっててびっくりしてる今日この頃。

docs.microsoft.com

WPF on .NET Core 3.0 で XAML Islands を試してみたいと思います。

プロジェクトの構成

前は WPF プロジェクトにライブラリ追加して…という感じだったのですが今は UWP プロジェクトを追加して…となってました。びっくり。 ということで一般的な XAML Islands を WPF で使う場合は、以下の 3 プロジェクト構成になることが多い感じです。

  • UWPApp (普通の UWP アプリプロジェクト)
  • UserControlLibrary (UWP のクラスライブラリ)
  • WPFApp (普通の WPF アプリプロジェクト)

UWPApp プロジェクトは XAML Islands を WPF でホストするための特殊な XamlApplication クラスをアプリケーションプロジェクトに持つ UWP アプリです。WPFApp から参照して使います。

UserControlLibrary は UWP のクラスライブラリプロジェクトで、UWP のユーザーコントロールを作って WPF アプリに追加したい場合は、ここに定義します。

WPFApp プロジェクトは、UWPApp と UserControlLibrary プロジェクトを参照していて Microsoft.Toolkit.Wpf.UI.Controls パッケージを参照に追加してます。

やってみよう

空のソリューションから始めてみます。

UWP プロジェクトの作成

まず、普通の UWP アプリケーションを作成します。選択する Windows のバージョンは 1903 です。XAML Islands の現在の対応バージョンです。ここら辺は来年出るという Windows UI Library 3.0 が出ると Creators Update 以降に対応になりますが今のところ 1903 です。

f:id:okazuki:20191010195109p:plain

MainPage.xaml/MainPage.xaml.cs は使わないので消します。

さて、ここから追加するライブラリーは基本的に何も言及していない場合は、最新のプレビューバージョンです。 UWP のプロジェクトに Microsoft.Toolkit.Win32.UI.XamlApplication の NuGet パッケージを追加します。

追加したら App.xaml を以下のように書き換えます。

<xaml:XamlApplication
    x:Class="UWPApp.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:xaml="using:Microsoft.Toolkit.Win32.UI.XamlHost"
    xmlns:local="using:UWPApp">

</xaml:XamlApplication>

そして App.xaml.cs を以下のようにします。シンプルですね。

using Microsoft.Toolkit.Win32.UI.XamlHost;

namespace UWPApp
{
    sealed partial class App : XamlApplication
    {
        public App()
        {
            Initialize();
        }
    }
}

WPF プロジェクトの作成

.NET Core の WPF プロジェクトを作って Microsoft.Toolkit.Wpf.UI.Controls の NuGet パッケージを追加します。 そして、UWP プロジェクトへの参照を WPF のプロジェクトに追加します。

f:id:okazuki:20191010195950p:plain

Configuration Manager (Visual Studio のツールバーの x86 とか書いてある部分のドロップダウンから開けたりします)で WPF のプロジェクトが Any CPU になっているものを x86 のときは x86、x64 のときは x64 になるように変更します。Debug と Release 両方で構成しておきましょう。

f:id:okazuki:20191010200310p:plain

コントロールを置いてみよう

InkCanvas などのラップ済みコントロールがある場合はそれをそのまま置いたりできます。無い場合は WindowsXamlHost クラスで InitialTypeName プロパティに型名を指定して、ChildChanged イベントでコントロールのプロパティを設定します。 例えば、TextBox を指定したい場合はこんな感じ。

<Window x:Class="WpfApp.MainWindow"
        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:WpfApp"
        xmlns:xaml="clr-namespace:Microsoft.Toolkit.Wpf.UI.XamlHost;assembly=Microsoft.Toolkit.Wpf.UI.XamlHost"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <xaml:WindowsXamlHost InitialTypeName="Windows.UI.Xaml.Controls.TextBox" ChildChanged="WindowsXamlHost_ChildChanged" />
    </Grid>
</Window>
using Microsoft.Toolkit.Wpf.UI.XamlHost;
using System;
using System.Windows;

namespace WpfApp
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void WindowsXamlHost_ChildChanged(object sender, EventArgs e)
        {
            var host = (WindowsXamlHost)sender;
            var textBox = host.Child as Windows.UI.Xaml.Controls.TextBox;
            if (textBox != null)
            {
                textBox.PlaceholderText = "xxxxxxx";
            }
        }
    }
}

これで実行すると、WPF には無い(何故無い)ウォータマークのついた TextBox が出来ます。

f:id:okazuki:20191010201459p:plain

こんなのは望んでない

俺は XAML で書きたいんだ!!InkCanvas とかは提供されてるということは、何か方法があるはずだ!ということでリポジトリで InkCanvas の実装を見ます。

github.com

ふむふむ、WindowsXamlHostBase クラスを継承して作るのね…ということでこんな感じで

using System;
using System.Windows;
using UWP = Windows.UI.Xaml.Controls;

namespace WpfApp
{
    public class UwpTextBox : Microsoft.Toolkit.Wpf.UI.XamlHost.WindowsXamlHostBase
    {
        public string PlaceholderText
        {
            get { return (string)GetValue(PlaceholderTextProperty); }
            set { SetValue(PlaceholderTextProperty, value); }
        }

        public static readonly DependencyProperty PlaceholderTextProperty =
            DependencyProperty.Register("PlaceholderText", typeof(string), typeof(UwpTextBox), new PropertyMetadata(null));

        public string Text
        {
            get { return (string)GetValue(TextProperty); }
            set { SetValue(TextProperty, value); }
        }

        public static readonly DependencyProperty TextProperty =
            DependencyProperty.Register("Text", typeof(string), typeof(UwpTextBox), new PropertyMetadata(null));

        public UwpTextBox() : base(typeof(UWP.TextBox).FullName)
        {
        }

        protected override void OnInitialized(EventArgs e)
        {
            // Bind dependency properties across controls
            // properties of FrameworkElement
            Bind(nameof(Style), StyleProperty, UWP.TextBox.StyleProperty);
            Bind(nameof(MaxHeight), MaxHeightProperty, UWP.TextBox.MaxHeightProperty);
            Bind(nameof(FlowDirection), FlowDirectionProperty, UWP.TextBox.FlowDirectionProperty);
            Bind(nameof(Margin), MarginProperty, UWP.TextBox.MarginProperty);
            Bind(nameof(HorizontalAlignment), HorizontalAlignmentProperty, UWP.TextBox.HorizontalAlignmentProperty);
            Bind(nameof(VerticalAlignment), VerticalAlignmentProperty, UWP.TextBox.VerticalAlignmentProperty);
            Bind(nameof(MinHeight), MinHeightProperty, UWP.TextBox.MinHeightProperty);
            Bind(nameof(Height), HeightProperty, UWP.TextBox.HeightProperty);
            Bind(nameof(MinWidth), MinWidthProperty, UWP.TextBox.MinWidthProperty);
            Bind(nameof(MaxWidth), MaxWidthProperty, UWP.TextBox.MaxWidthProperty);
            Bind(nameof(UseLayoutRounding), UseLayoutRoundingProperty, UWP.TextBox.UseLayoutRoundingProperty);
            Bind(nameof(Name), NameProperty, UWP.TextBox.NameProperty);
            Bind(nameof(Tag), TagProperty, UWP.TextBox.TagProperty);
            Bind(nameof(DataContext), DataContextProperty, UWP.TextBox.DataContextProperty);
            Bind(nameof(Width), WidthProperty, UWP.TextBox.WidthProperty);

            Bind(nameof(Text), TextProperty, UWP.TextBox.TextProperty);
            Bind(nameof(PlaceholderText), PlaceholderTextProperty, UWP.TextBox.PlaceholderTextProperty);

            base.OnInitialized(e);
        }
    }
}

そして、MainWindow.xaml で、これを使います。

<Window x:Class="WpfApp.MainWindow"
        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:WpfApp"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <local:UwpTextBox PlaceholderText="xxxxxxx" />
    </Grid>
</Window>

実行すると、同じ結果ですけど XAML が健康的になりました。

f:id:okazuki:20191010202917p:plain

ユーザーコントロールを使いたい

UWP のクラスライブラリープロジェクト(ターゲットは 1903)をつくって、そこにユーザーコントロールを追加します。

プロジェクトをアンロードして、</Project> の前の行に以下の定義を追加します。

<PropertyGroup>
  <EnableTypeInfoReflection>false</EnableTypeInfoReflection>
  <EnableXBindDiagnostics>false</EnableXBindDiagnostics>
</PropertyGroup>

プロジェクトをロードして WPF と UWP のプロジェクトの両方に参照を追加します。

そして、ユーザーコントロールを追加して XAML を以下のようにします。

<UserControl
    x:Class="MyUserControls.MyUserControl1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:MyUserControls"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400">

    <StackPanel>
        <TextBox />
        <Button Content="Greet" />
    </StackPanel>
</UserControl>

WindowsXamlHost でさくっと WPF 上に置きます。

<Window x:Class="WpfApp.MainWindow"
        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:WpfApp"
        xmlns:xamlhost="clr-namespace:Microsoft.Toolkit.Wpf.UI.XamlHost;assembly=Microsoft.Toolkit.Wpf.UI.XamlHost"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <xamlhost:WindowsXamlHost InitialTypeName="MyUserControls.MyUserControl1" />
    </Grid>
</Window>

おk

f:id:okazuki:20191010204820p:plain

これも XAML の平和のためにラッパ=を作ろうとしたらアプリが落ちました。残念。

ViewModel を共有したい

これは試行錯誤中ですが、動くのは出来ました。

WPF 側のプロジェクトに Prism.Core (stable 版の最新) を入れます。 そして、適当に ViewModel をこしらえます。

using Prism.Commands;
using Prism.Mvvm;

namespace WpfApp
{
    public class MainWindowViewModel : BindableBase
    {
        private string _input;
        public string Input
        {
            get { return _input; }
            set { SetProperty(ref _input, value); }
        }

        private string _output;
        public string Output
        {
            get { return _output; }
            set { SetProperty(ref _output, value); }
        }

        private DelegateCommand _convertCommand;
        public DelegateCommand ConvertCommand =>
            _convertCommand ?? (_convertCommand = new DelegateCommand(ExecuteConvertCommand, CanExecuteConvertCommand)
                .ObservesProperty(() => Input));

        private void ExecuteConvertCommand()
        {
            Output = Input.ToUpper();
        }

        private bool CanExecuteConvertCommand()
        {
            return !string.IsNullOrWhiteSpace(Input);
        }
    }
}

そして Window の DataContext に設定します。ついでに Output プロパティを WPF の TextBlock にバインドしておきます。

<Window x:Class="WpfApp.MainWindow"
        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:WpfApp"
        xmlns:xamlhost="clr-namespace:Microsoft.Toolkit.Wpf.UI.XamlHost;assembly=Microsoft.Toolkit.Wpf.UI.XamlHost"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>
    <StackPanel>
        <xamlhost:WindowsXamlHost InitialTypeName="MyUserControls.MyUserControl1" ChildChanged="WindowsXamlHost_ChildChanged" />
        <TextBlock Text="{Binding Output}" />
    </StackPanel>
</Window>

WindowsXamlHost の ChildChanged イベントで DataContext を伝搬させます。

using Microsoft.Toolkit.Wpf.UI.XamlHost;
using MyUserControls;
using System;
using System.Windows;

namespace WpfApp
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void WindowsXamlHost_ChildChanged(object sender, EventArgs e)
        {
            var host = (WindowsXamlHost)sender;
            var control = host.Child as MyUserControl1;
            if (control != null)
            {
                control.DataContext = DataContext;
            }
        }
    }
}

そして UWP 側のユーザーコントロールで Binding します。(x:Bind じゃないのが残念な点)

<UserControl
    x:Class="MyUserControls.MyUserControl1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:MyUserControls"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400">

    <StackPanel>
        <TextBox Text="{Binding Input, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
        <Button Content="Greet" 
                Command="{Binding ConvertCommand}"/>
    </StackPanel>
</UserControl>

OK 動いた。

f:id:okazuki:20191010210227p:plain

デプロイどうするの?

WPF のプロジェクトを publish するとエラー。

f:id:okazuki:20191010210931p:plain

Visual Studio の Configuration を Release にすると Visual Studio がフリーズ。(待てば戻ってくるのかな…?)

f:id:okazuki:20191010211024p:plain

とりあえず時間切れなのでデプロイなどについては後日調べてみることにします。