しばらく目を離してると手順がガラッと変わっててびっくりしてる今日この頃。
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 です。
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 のプロジェクトに追加します。
Configuration Manager (Visual Studio のツールバーの x86 とか書いてある部分のドロップダウンから開けたりします)で WPF のプロジェクトが Any CPU になっているものを x86 のときは x86、x64 のときは x64 になるように変更します。Debug と Release 両方で構成しておきましょう。
コントロールを置いてみよう
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 が出来ます。
こんなのは望んでない
俺は XAML で書きたいんだ!!InkCanvas
とかは提供されてるということは、何か方法があるはずだ!ということでリポジトリで InkCanvas
の実装を見ます。
ふむふむ、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 が健康的になりました。
ユーザーコントロールを使いたい
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
これも 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 動いた。
デプロイどうするの?
WPF のプロジェクトを publish するとエラー。
Visual Studio の Configuration を Release にすると Visual Studio がフリーズ。(待てば戻ってくるのかな…?)
とりあえず時間切れなのでデプロイなどについては後日調べてみることにします。