SourceChord

C#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。

CretatorsUpdateから、UWPでもTypeConverter的なものが使えるようになりました

WPFとUWPでは、同じXAMLという仕組みを用いていますが、細かい部分を見ると「アレがない」「コレがない」といった、細かい違いがあります。

そんな違いの一つとして、「UWPにはTypeConverterがない」という違いがありました。

ですが、Windows 10 Creators Updateからは、UWPでも簡単にTypeConverter的なものを作れるようになりました。
(あくまでも「的な」ものなので、TypeConverterとはちょっと実装方法が異なります。)

コレ、結構便利な変更点だと思うんだけど、全然話題になってないのでまとめてみたいと思います。

サンプルコードは以下の場所に置いておきました。 github.com

TypeConvertertって?

まずはザックリとTypeConverterとは何か、という説明です。

例えば、以下のようなXAMLコードで、

        <Button Margin="5,10,5,10"
                Content="Button" />

文字列で書かれた5,10,5,10という値がThickness型のMarginプロパティに設定できるのは、TypeConverterにより文字列⇒Thickness型への変換が行われているためです。

コントロールなどを作っていると、独自の型をプロパティに持つことがあると思います。
そんな時に、TypeConverterを作っておくと、独自型のプロパティも文字列で手短に定義できるようになるので、とても便利です。

WPFでのTypeConverter実装方法

WPFでは、以下のようにTypeConverter派生クラスを作成し、その派生クラスをTypeConverter属性として目的のクラスに付与しておくと、こうやってXAML上から文字列で独自型のプロパティを設定できるようになります。

f:id:minami_SC:20170510004528p:plain
(※Person型のPersonプロパティにXAMLから文字列で設定している例)

どうでもいいですけど、英語の名前でJohn Smithっていうと、日本語で名前のサンプルとしてよく出てくる山田太郎とか田中太郎って感じのイメージになるらしいですね。

    [TypeConverter(typeof(PersonTypeConverter))]
    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

    public class PersonTypeConverter : TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            return sourceType == typeof(string);
        }

        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
        {
            var text = (string)value;
            var list = text.Split(',')
                           .Select(o => o.Trim())
                           .ToList();


            if (list.Count() != 2)
            {
                throw new ArgumentException($"'{value}' Invalid value. Person must contains 2 items.");
            }

            return new Person() { FirstName = list[0], LastName = list[1] };
        }
    }

UWPにはTypeConverterがない!!

このとても便利なTypeConverterという仕組みですが、UWPにはTypeConverterが用意されていません。

そのため、独自の型をプロパティに持つコントロールとかを作ると、入り組んだマークアップが必要になるケースがたびたびありました。

Windows10 Creators Updateでの対応

そんなUWPですが、Creators Updateからは、TypeConverterに似た感じで「XAML上に定義した文字列⇒独自型のオブジェクト」に変換する仕組みが提供されました。

TypeConverterクラスがUWPで使えるようになったわけではなく、別の手段が用意されています。

UWPでの対応方法

詳細は以下の記事に詳しくまとまっています。
http://timheuer.com/blog/archive/2017/02/15/implement-type-converter-uwp-winrt-windows-10-xaml.aspx

かいつまんで説明すると、以下のような感じです。

XAML上で文字列から変換できるようにしたいクラスに対して、以下の2点の実装を行います。

  • 文字列から対象の型へと変換するstatic関数を作成する
  • 対象のクラスに、上記static関数の関数名を引数にしてCreateFromString属性を付加する

これだけで、WPFでTypeConverterを使ってやってた事ができるようになります。

サンプル

ここから、実際にユーザーコントロールを作り、UWPのTypeConverter的機能を使ってXAMLから複雑なプロパティを設定してみたいと思います。

ここでは、FirstName/LastNameプロパティを持つPersonというクラスを作ります。
そして、このPerson型のプロパティを持ち、それを整形して画面上に表示するPersonViewerというコントロールを作ってみます。

こんな感じのユーザーコントロールを作ってみます。
f:id:minami_SC:20170510004556p:plain

PersonクラスとPersonViewerの作成

まずは、こんな感じのPersonクラスを作ります。

Person.cs

    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

続いて、このPersonクラスを画面表示するためのPersonViewerというコントロールを作ります。
xamlとコードビハインドはそれぞれ以下のような感じです。

PersonViewer.xaml

<UserControl
    x:Class="TypeConverterUwp.PersonViewer"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:TypeConverterUwp"
    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">
    <Grid x:Name="root" Margin="5">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <TextBlock Margin="5"
                   HorizontalAlignment="Left"
                   Text="FirstName: " />
        <TextBlock Grid.Column="1"
                   Margin="5"
                   HorizontalAlignment="Left"
                   Text="{Binding Person.FirstName}" />
        <TextBlock Grid.Row="1"
                   Margin="5"
                   HorizontalAlignment="Left"
                   Text="LastName: " />
        <TextBlock Grid.Row="1"
                   Grid.Column="1"
                   Margin="5"
                   HorizontalAlignment="Left"
                   Text="{Binding Person.LastName}" />
    </Grid>
</UserControl>

PersonViewer.xaml.cs

    public sealed partial class PersonViewer : UserControl
    {
        public PersonViewer()
        {
            this.InitializeComponent();
            root.DataContext = this;
        }

        public Person Person
        {
            get { return (Person)GetValue(PersonProperty); }
            set { SetValue(PersonProperty, value); }
        }
        // Using a DependencyProperty as the backing store for Person.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty PersonProperty =
            DependencyProperty.Register("Person", typeof(Person), typeof(PersonViewer), new PropertyMetadata(null));
    }

MainPage.xaml

で、このコントロールを使う際には、以下のようにPersonプロパティを、ちょっと入り組んだXAMLで記述する必要があります。

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <local:PersonViewer>
            <local:PersonViewer.Person>
                <local:Person FirstName="John" LastName="Smith"/>
            </local:PersonViewer.Person>
        </local:PersonViewer>
    </Grid>

ちょっと面倒ですね。。。

XAML上から文字列で定義できるように修正

Creators Updateから使えるようになった、CreateFromString属性を使った方法でPersonクラスを以下のように修正します。

Person.cs

    [Windows.Foundation.Metadata.CreateFromString(MethodName = "TypeConverterUwp.Person.ConvertToPerson")]
    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }

        public static Person ConvertToPerson(string value)
        {
            var text = (string)value;
            var list = text.Split(',')
                           .Select(o => o.Trim())
                           .ToList();

            if (list.Count() != 2)
            {
                throw new ArgumentException($"'{value}' Invalid value. Person must contains 2 items.");
            }

            return new Person() { FirstName = list[0], LastName = list[1] };
        }
    }

これで、以下のようにXAML上から文字列で定義できるようになります。

f:id:minami_SC:20170510004644p:plain

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <local:PersonViewer Person="John, Smith" />
    </Grid>

対象プラットフォーム

このCreateFromStringという属性自体は、Aniverssary Update(14393)で追加された属性なので、ターゲットプラットフォームを14393にしていてもビルドできます。
(XAML側のコードを書かなければ・・・)

しかし、Aniverssary Update環境で、このTypeConverter的なマークアップをしたXAMLコードを読み込むと、実行時に例外が発生します。

CreateFromString属性でTypeConverter的な用途に使う場合には、プロジェクトのターゲットの最小バージョンを、以下のように「Windows 10 Creators Update(10.0; ビルド 15063)」として置いた方がよいかと思います。

f:id:minami_SC:20170510004632p:plain