ImmutableオブジェクトのJSONシリアライズ、デシリアライズ(C#)

今回はNewtonsoft.Jsonを利用して、Immutableオブジェクトをシリアライズ、デシリアライズしてみたいと思います。

.NetにはDataContractJsonSerializerが標準で用意されていますが、DataContract、DataMember属性を付加する必要があり、動作のカスタマイズが大変そうであったため、Newtonsoft.Jsonを採用しました。

対象オブジェクト

下記のクラスをシリアライズ、デシリアライズしてみたいと思います。

FullNameは導出項目のため、シリアライズ対象から外される必要があります。

public class Person
{
    public int Age { get; }
    public string FirstName { get; }
    public string LastName { get; }
    public Person Child { get; }
    public string FullName =>
        FirstName + " " + LastName;
    public Person(int age, string firstName, string lastName, Person child)
    {
        Age = age;
        FirstName = firstName;
        LastName = lastName;
        Child = child;
    }
}
var person = new Person(35, "John", "Smith",
                 new Person(10, "Richard", "Smith", null));


Newtonsoft.Json取得

NuGetでNewtonsoft.Jsonを取得します。
f:id:any-programming:20170606093411p:plain

シリアライズ

デフォルトでは、publicなgetterからの値をシリアライズします。

そのため、FullNameもシリアライズされてしまいます。

そこで導出項目は無視するようContractResolverを独自に定義します。

class SerializeResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var p = base.CreateProperty(member, memberSerialization);
        var field = member.DeclaringType.GetField($"<{member.Name}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic);

        // setterがなく、BackingFieldも持たないものは無視
        p.Ignored = field == null && p.Writable == false;
        return p;
    }
}

シリアライズは、このContractResolverを利用して下記のように記述できます。

var setting = new JsonSerializerSettings() { ContractResolver = new SerializeResolver() };
string json = JsonConvert.SerializeObject(person, setting);

問題なくシリアライズされていることが確認できます。(Visual StudioJSONビューワで確認)
f:id:any-programming:20170606123527p:plain

シリアライズ

デフォルトではsetterが存在しないプロパティはデシリアライズできません。

そこでプロパティが復元できるよう、独自のContractResolverを定義します。

class DeserializeResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var p = base.CreateProperty(member, memberSerialization);
        if (p.Writable == false)
        {
            var field = member.DeclaringType.GetField($"<{member.Name}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic);
            if (field != null)
            {
                // BackingFieldが存在する場合、ValueProviderをセット
                p.ValueProvider = new FieldValueProvider(field);
                p.Writable = true;
            }
        }
        return p;
    }
}

FieldValueProviderは下記のように定義できます。

class FieldValueProvider : IValueProvider
{
    FieldInfo _field;
    public FieldValueProvider(FieldInfo field) =>
        _field = field;
    public object GetValue(object target) =>
        _field.GetValue(target);
    public void SetValue(object target, object value) =>
        _field.SetValue(target, value);
}

シリアライズは、このContractResolverを利用して下記のように記述できます。

var setting = new JsonSerializerSettings() { ContractResolver = new DeserializeResolver() };
var person = JsonConvert.DeserializeObject<Person>(json, seting);


BackingFieldを使わない場合

BackingFieldはコンパイラが勝手に作成するFieldのため、名称が変わる可能性は0ではありません。

心配な場合は、private setを追加して独自のContractResolverを定義してください。

Fieldのシリアライズ、デシリアライズ

JsonProperty属性を付加すれば対象とみなされるようになります。

[JsonProperty]
int _field;






ネストしたImmutableオブジェクトの更新(C#)

C#(7.0)でImmutableクラスを作成する場合、下記のような記述になります。

public class Person
{
    public int Age { get; }
    public string Name { get; }
    public Person Child { get; }
    public Person(int age, string name, Person child)
    {
        Age = age;
        Name = name;
        Child = child;
    }
}

下記のようなデータがあったとして、Richardの年齢を更新したデータを作成するのはとても面倒です。

var person = new Person(35, "John",
                new Person(10, "Richard", null));

そこでリフレクションを利用して、簡単に更新データを作成できるようにしてみたいと思います。

コード内で利用されているCreate、Scan、Zipオペレータは下記記事を参照してください。
LINQ独自オペレータ メモ - 何でもプログラミング

With関数定義

ExpressionTreeを利用してオブジェクトを更新します。

一つのオブジェクトの更新は、MemberwiseCloneとBackingFieldの更新で実現しています。

自動生成される<Name>k__BackingFieldは予告なく名称が変わる可能性がありますので、不安な場合はprivate set;を追加して、PropertyInfo.SetValueを利用するようにしてください。

public static TObj With<TObj, TValue>(this TObj obj, Expression<Func<TObj, TValue>> expression, TValue value)
{
    // MemberExpressionをたどる
    var members = EnumerableEx.Create(expression.Body as MemberExpression, x => x.Expression as MemberExpression)
        .TakeWhile(x => x != null)
        .ToArray();

    // 各メンバの現在値取得
    var objs = members.Reverse()
        .Scan((object)obj, (x, m) => ((PropertyInfo)m.Member).GetValue(x))
        .ToArray();

    // 値を下層から順次更新
    return (TObj)objs.Reverse().Skip(1)
        .Zip(members.Select(x => x.Member.Name))
        .Aggregate((object)value, (state, x) => With(x.Item1, x.Item2, state));
}
static object With(object obj, string propertyName, object value)
{
    var type = obj.GetType();
    var clone = type.GetMethod("MemberwiseClone", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(obj, null);
    type.GetField($"<{propertyName}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(clone, value);
    return clone;
}


利用

冒頭のデータに対し下記の様に記述することにより、Richardの年齢が11に更新された新たなデータが作成されます。

var newPerson = person.With(x => x.Child.Age, 11);






WPF --- Bindingで配列をObservableCollectionに変換

現状、ItemsControlのItemsSourceに通常の配列をバインドした際、配列を新しくする度にコントロールが全て再作成されます。

通常の配列の代わりにObservableCollectionをバインドすることにより、変更箇所のみViewが更新されるようになります。(要素の追加でViewが作成、要素の削除でViewも削除、要素の更新でViewの再利用が行われます。)

これによりパフォーマンスの向上だけでなく、マウスなどのフォーカスも正常に動作するようになります。

今回は配列をObservableCollectionに変換する拡張マークアップを作成してみたいと思います。

コード内で利用しているReplaceAt、ForEachオペレータは下記記事を参照してください。
LINQ独自オペレータ メモ - 何でもプログラミング

通常のBindingで実装

今回はカウントアップを行うボタンを複数作成するアプリケーションを例に実装していきます。

ボタンはリピートボタンを利用しているため、マウスダウンの状態で、カウントがどんどん進むことを想定しています。

f:id:any-programming:20170606001007p:plain

Xaml

RepeatButtonのCommandで、ItemsControlのDataContextのIncrementコマンドに自身のインデックスを渡すよう実装してあります。

<ItemsControl ItemsSource="{Binding Counts}" AlternationCount="100">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <RepeatButton Content="{Binding}"
                          Command="{Binding DataContext.Increment, RelativeSource={RelativeSource AncestorType=ItemsControl}}" 
                          CommandParameter="{Binding Path=(ItemsControl.AlternationIndex), RelativeSource={RelativeSource TemplatedParent}}" />
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>


C#

Commandクラスは、単にExecuteをラムダで登録できるようにしたクラスです。

DataContext = new ViewModel();

class ViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public int[] Counts { get; private set; } = new int[3];
    public Command<int> Increment { get; }
    public ViewModel()
    {
        Increment = new Command<int>(i =>
        {
            Counts = Counts.ReplaceAt(i, Counts[i] + 1).ToArray();
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Counts)));
        });
    }
}


実行して、ボタンの上でマウスダウンの状態を保っても、カウントアップが継続しないことが確認できます。

これはCountsが更新されてボタンが再作成され、マウスのフォーカスが外されてしまっているからです。

ToObservableCollectionExtension

ObservableCollectionを返すValueConverteを作成し、Bindingにセットして返しています。

Convertの中でObservableCollectionを更新しています。(更新アルゴリズムは適宜変更してください。)

ValueConverterクラスは、単にConvertをラムダで登録できるようにしたクラスです。

public class ToObservableCollectionExtension : MarkupExtension
{
    string _path;
    public ToObservableCollectionExtension(string path) => 
        _path = path;
    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        var target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
        var element = target.TargetObject as DependencyObject;
        // DependencyObjectでない場合(Templateのパース時)はthisを返す。
        if (element == null)
            return this;

        var collection = new ObservableCollection<object>();
        var converter = new ValueConverter<IEnumerable, ObservableCollection<object>>(src =>
        {
            src.Cast<object>().ForEach((i, x) =>
            {
                // 要素が不足しているので追加
                if (collection.Count <= i)
                    collection.Add(x);

                // 内容が異なれば更新
                else if (x != collection[i])
                    collection[i] = x;
            });
            // 余分な要素を削除
            for (int i = src.Cast<object>().Count(); i < collection.Count; ++i)
                collection.RemoveAt(collection.Count - 1);

            return collection;
        });

        return new Binding(_path) { Converter = converter }.ProvideValue(serviceProvider);
    }
}


ToObservableCollectionの利用

ItemsSourceのBindingの部分を、ToObservableCollectionに置き換えます。

<ItemsControl ItemsSource="{local:ToObservableCollection Counts}" AlternationCount="100">

これで、ボタン上でマウスダウンを保持するとカウントが進むようになります。





F#でWPF --- Elm Architectureで実装されたUserControl

あるアプリケーションを作成した後に、それをコントロール化して更に大きなアプリケーションを作成したい時があります。

今回は、下記記事で作成したカウンタアプリケーションをコントロール化してみたいと思います。
F#でWPF --- Elm Architectureを利用したMVVM - 何でもプログラミング

アプリケーションコード

Counterコントロールは、表示する値の"Value"と、値が変更されたときのコマンド"ValueChanged"からなります。

IncrementなのかDecrementなのかは、ユーザーは気にする必要はありません。

F#側は送られてきた値を保持して、Valueに通知しているだけです。

以降は、これを実現するCounterを作っていきます。

f:id:any-programming:20170531163613p:plain
Xaml

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:local="clr-namespace:WPFApplication;assembly=WPFApplication"
        Title="MainWindow" Height="100" Width="200">
    <Grid>
        <local:Counter Value="{Binding Value}" ValueChanged="{Binding SetValue}" Margin="10" Height="23" />
    </Grid>
</Window>

F#

type Model = { Value : int }

let initialModel = { Value = 0 }

type Msg = SetValue of int

let update model msg =
    match msg with
    | SetValue x -> 
        { Value = x }        


Counter(Xaml

Xamlは下記のように定義しました。

UserControlのContentに代入するため、外枠はGridで定義してあります。

また、コントロールとしてMainWindow.xamlに貼り付けた際にデザイナーでエラーが発生しないよう、ビルドアクションを埋め込みリソースにしておきます。
f:id:any-programming:20170531165433p:plain

f:id:any-programming:20170531164948p:plain

<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="23" d:DesignWidth="200">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="23"/>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="23"/>
    </Grid.ColumnDefinitions>
    <Button Command="{Binding Decrement}" Content="◀" Grid.Column="0" />
    <TextBox Text="{Binding Count}" Grid.Column="1" IsReadOnly="True" TextAlignment="Center" VerticalContentAlignment="Center" />
    <Button Command="{Binding Increment}" Content="▶" Grid.Column="2" />
</Grid>


Counter(F#)

最終的なCounterクラスは下記のようになります。

以降、順を追って説明していきます。

type CounterModel = { Count : int }

let initialCounterModel = { Count = 0 }

type CounterMsg =
    | Increment
    | Decrement
    | SetCount of int

type Counter() as this = 
    inherit UserControl()
    static let value = DependencyProperty<Counter, int>()
    static let valueChanged = DependencyProperty<Counter, ICommand>()

    let update model msg =
        match msg with
        | Increment ->
            model, Post.Command(valueChanged.Get(this), model.Count + 1)
        | Decrement ->
            model, Post.Command(valueChanged.Get(this), model.Count - 1)
        | SetCount x ->
            { Count = x }, Post.None

    do this.Start("Counter.xaml", DataContext(initialCounterModel, update, id), (value, SetCount))

    static member val ValueProperty = value.Register()
    member this.Value with get() = value.Get(this) and set(x) = value.Set(this, x)

    static member val ValueChangedProperty = valueChanged.Register()
    member this.ValueChanged with get() = valueChanged.Get(this) and set(x) = valueChanged.Set(this, x)


まずはDependencyPropertyを作成します。

コード内のDependencyProperty<_, _>クラスは下記記事を参照してください。
DependencyProperty定義の記述量削減(F#) - 何でもプログラミング

static let value = DependencyProperty<Counter, int>()
static let valueChanged = DependencyProperty<Counter, ICommand>()

static member val ValueProperty = value.Register()
member this.Value with get() = value.Get(this) and set(x) = value.Set(this, x)

static member val ValueChangedProperty = valueChanged.Register()
member this.ValueChanged with get() = valueChanged.Get(this) and set(x) = valueChanged.Set(this, x)


続いてロジック部分を実装します。

アプリケーションの時と異なり、UIからのコマンド内でアプリケーション側のコマンドを呼びだしています。(Increment、Decrement)

またアプリケーション側からValue変更通知があった時にCountを更新しています。(SetCount)(Value変更通知とSetCountの接続は後ほど行います。)

アプリケーション側のコマンドを呼び出すために、DataContextクラスをPostを受け取るように変更しました。

type CounterModel = { Count : int }

let initialCounterModel = { Count = 0 }

type CounterMsg =
    | Increment
    | Decrement
    | SetCount of int

let update model msg =
    match msg with
    | Increment ->
        model, Post.Command(valueChanged.Get(this), model.Count + 1)
    | Decrement ->
        model, Post.Command(valueChanged.Get(this), model.Count - 1)
    | SetCount x ->
        { Count = x }, Post.None
type Post =
    | None
    | Command of ICommand * obj

type DataContext<'msg, 'm, 'vm>(initialModel    : 'm, 
                                updateModel     : 'm -> 'msg -> 'm * Post, 
                                createViewModel : 'm -> 'vm) as this =
    inherit DynamicObject()
    let propertyChanged   = Event<_, _>()
    let mutable model     = initialModel
    let mutable viewModel = createViewModel model

    let propertyDictionary = 
        typeof<'vm>.GetProperties()
        |> Array.map (fun x -> x.Name, x)
        |> dict

    let commandDictionary =
        let messageDictionary =
            FSharpType.GetUnionCases(typeof<'msg>)
            |> Array.map (fun x -> x.Name, x)
            |> dict
    
        let executeMessage name value =
            let args = match value with
                       | null                                   -> [||]
                       | x when FSharpType.IsTuple(x.GetType()) -> FSharpValue.GetTupleFields(x)
                       | x                                      -> [| x |]            
            let msg = FSharpValue.MakeUnion(messageDictionary.Item(name), args) :?> 'msg
            let updated = updateModel model msg
            model <- fst updated
            let prevViewModel = viewModel
            viewModel <- createViewModel model
            typeof<'vm>.GetProperties()
            |> Array.iter (fun x -> if x.GetValue(viewModel) <> x.GetValue(prevViewModel) then 
                                        propertyChanged.Trigger(this, PropertyChangedEventArgs(x.Name)))

            // View更新後の処理(Commandの呼び出し)
            match snd updated with
            | None                        -> ()
            | Command(command, parameter) -> if command <> null then command.Execute(parameter)

        let createCommand (msg:UnionCaseInfo) =
            { new ICommand with
                member this.CanExecute _ = true
                [<CLIEvent>]
                member this.CanExecuteChanged = Event<_, _>().Publish 
                member this.Execute parameter = executeMessage msg.Name parameter }

        FSharpType.GetUnionCases(typeof<'msg>)
        |> Array.map (fun x -> x.Name, createCommand x)
        |> dict

    // Post処理がない時用のコンストラクタ
    new(initialModel, updateModel : 'm -> 'msg -> 'm, createViewModel) = 
        DataContext(initialModel, (fun x y -> updateModel x y, Post.None), createViewModel)

    interface INotifyPropertyChanged with
        [<CLIEvent>]
        member this.PropertyChanged = propertyChanged.Publish

    override this.TryGetMember(binder : GetMemberBinder, [<Out>] result : obj byref) = 
        if propertyDictionary.ContainsKey binder.Name then
            result <- propertyDictionary.Item(binder.Name).GetValue(viewModel)
        else
            result <- commandDictionary.Item(binder.Name)
        true


最後に、Xamlを読み込んでDataContextをセットし、それをUserControlのContentにセットして、さらにDependencyPropertyとMsgの接続を行う関数を定義します。

対象のMsgを呼び出すところは、もう少しスマートに記述できるかもしれません。

type UserControl with
    member this.Start(xamlPath : string, dataContext : DataContext<'msg, 'm, 'vm>, [<ParamArray>] maps : (DependencyProperty<'owner, 'value> * ('value -> 'msg))[]) = 
        let grid = loadEmbeddedXaml<Grid> xamlPath    
        grid.DataContext <- dataContext
        this.Content <- grid
        maps |> Array.iter (fun (property, msg) -> 
            property.Changed(this :?> 'owner).Add(fun x ->
                dataContext.TryGetMember(SimpleGetMemberBinder(caseName (msg x)))
                |> snd :?> ICommand
                |> (fun c -> c.Execute(x))))
let loadEmbeddedXaml<'a> fileName = 
    Assembly.GetExecutingAssembly().GetManifestResourceStream(fileName)
    |> XamlReader.Load
    :?> 'a

let caseName (x : obj) =
    FSharpValue.GetUnionFields(x, x.GetType()) |> fst |> (fun x -> x.Name)

type SimpleGetMemberBinder(name) =
    inherit GetMemberBinder(name, false)
    override this.FallbackGetMember(target, errorSuggestion) = raise (NotImplementedException())






DependencyProperty定義の記述量削減(F#)

下記記事にてC#でDependencyPropertyの記述量を削減してみました。
DependencyProperty定義の記述量削減 - 何でもプログラミング

今回はF#でDependencyPropertyの記述量を削減してみたいと思います。

内容はほぼAttachedPropertyの時と同じですので、下記記事も参考にしてみてください。
AttachedProperty定義の記述量削減(F#) - 何でもプログラミング

DependencyProperty管理クラス

Register、Get、Set、Changedを管理するクラスになります。

メモリリークしないよう、Changedで利用するEventは、ConditionalWeakTableに配置してあります。

member valで定義されたものは末尾に@が自動で付与されるので、"Property@"となっています。

type DependencyProperty<'owner, 'value when 'owner :> DependencyObject and 'owner : not struct>() =
    let mutable property : DependencyProperty = null
    let table = ConditionalWeakTable<'owner, Event<'value>>()
    member this.Register(?defaultValue : 'value, [<CallerMemberName>]?propertyName : string) =
        assert (property = null)
        let name = propertyName.Value.Substring(0, propertyName.Value.Length - "Property@".Length)
        let metadata = PropertyMetadata(defaultValue |> Option.defaultValue(Unchecked.defaultof<'value>), 
                                        PropertyChangedCallback(fun obj args -> match table.TryGetValue(obj :?> 'owner) with
                                                                                | true, ev -> ev.Trigger(args.NewValue :?> 'value)
                                                                                | false, _ -> () ))
        property <- DependencyProperty.Register(name, typeof<'value>, typeof<'owner>, metadata)
        property
    member this.Get(obj : 'owner) = obj.GetValue(property) :?> 'value
    member this.Set(obj : 'owner, value : 'value) = obj.SetValue(property, value)
    member this.Changed(obj : 'owner) = table.GetOrCreateValue(obj).Publish


利用例

上記クラスを利用すると、下記のようなコードになります。

毎回ownerクラスを指定したくない場合は、一段クラスか関数をかましてください。

type MyControl() as this =
    inherit UserControl()
    static let value = DependencyProperty<MyControl, int>()
    do  value.Changed(this).Add( (* 処理 *) )

    static member val ValueProperty = value.Register()
    member this.Value with get() = value.Get(this) and set(x) = value.Set(this, x)






F#でWPF --- ItemsControlのItemのCommand

作成するアプリケーション

複数の、クリック回数を表示するボタンからなるアプリケーションを作成します。

追加ボタンにより、クリックできるボタンを動的に増やすことができます。
f:id:any-programming:20170525140454p:plain

本記事ではElm Architectureを利用しますので、下記記事を参照してください。
F#でWPF --- Elm Architectureを利用したMVVM - 何でもプログラミング

ItemsControl第一歩

ItemsControlの実装は、何となく下記のようなものになるかと思います。

ただしこの記述だと、CountUpコマンドがCountsの各要素に存在する必要があります。

Elm Architectureを利用した場合、Commandは親に集約させる必要があるため、親要素へBindingする必要があります。

<ItemsControl ItemsSource="{Binding Counts}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Button Content="{Binding}" Command="{Bindin CountUp}" />
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>


F#側のコード

CountUpで指定インデックスの要素をインクリメントし、AddCountでCountsの要素を増やしています。

type Model = { Counts : int list }

let initialModel = { Counts = [] }

type Msg = 
    | CountUp of int
    | AddCount

let update model msg =
    match msg with
    | CountUp index -> 
        { Counts = model.Counts |> List.mapi (fun i x -> if i = index then x + 1 else x) }        
    | AddCount ->
        { Counts = 0 :: model.Counts }


AlternationCountとRelativeSourceを駆使して実装

RelativeSourceを利用することにより、親要素のCommandにBindingすることが可能になります。

また、何番目の要素からのCommandかを伝えるため、AlternationCountを利用しています。(最大値は現状適当な値を入れています。)

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        Title="MainWindow" Height="200" Width="200">
    <Grid>
        <ItemsControl ItemsSource="{Binding Counts}" AlternationCount="100" Margin="0,0,0,35">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <StackPanel />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Button Content="{Binding}" 
                            Command="{Binding DataContext.CountUp, RelativeSource={RelativeSource AncestorType=ItemsControl}}" 
                            CommandParameter="{Binding Path=(ItemsControl.AlternationIndex), RelativeSource={RelativeSource TemplatedParent}}" />
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
        <Button Command="{Binding AddCount}" Content="追加" HorizontalAlignment="Right" Margin="0,0,10,10" VerticalAlignment="Bottom" Width="75"/>
    </Grid>
</Window>


独自のMarkup作成

上記でも所望の動作をしますが、毎回この記述をするのも面倒ですし、CommandParameterが定義されていない(独自プロパティ)可能性もあります。

そこで独自のマークアップを定義してみます。

type ItemCommandExtension(path : string) =
    inherit MarkupExtension()
    override this.ProvideValue(serviceProvider : IServiceProvider) = 
        let target = serviceProvider :?> IProvideValueTarget
        match target.TargetObject with
        | :? FrameworkElement as control -> 
            let property = target.TargetProperty :?> DependencyProperty
            assert (typeof<ICommand>.IsAssignableFrom(property.PropertyType))

            // 親要素をたどる
            let ancestors = 
                control :> DependencyObject
                |> List.unfold (Option.ofObj >> Option.map(fun x -> x, VisualTreeHelper.GetParent(x)))

            let itemsControl = ancestors |> Seq.ofType<ItemsControl> |> Seq.head

            // コンテナがContentPresenterでない場合は変更してください。
            let container = ancestors |> Seq.ofType<ContentPresenter> |> Seq.head 

            // アイテムのインデックスを取得
            let index = itemsControl.ItemContainerGenerator.IndexFromContainer(container)
 
            // parameterにindexを付与したCommandに変換
            let converter = Wpf.createConverter (fun (command : ICommand) -> 
                Wpf.createCommand (function                        
                   | null -> command.Execute(index)
                   | x    -> command.Execute(index, x)))

            // BindingのProvideValueを利用
            Binding(path, Source = itemsControl.DataContext, Converter = converter).ProvideValue(serviceProvider)

        // DataTemplateの場合、TargetObjectがShareDPで呼ばれることがあり、その場合はthisを返す約束となっています。
        | _ -> this :> obj 
module Seq =
    let ofType<'a> source = System.Linq.Enumerable.OfType<'a>(source)

module Wpf =
    let createCommand<'a> f = 
        { new ICommand with 
            member this.CanExecute _ = true
            [<CLIEvent>]
            member this.CanExecuteChanged = Event<_, _>().Publish 
            member this.Execute parameter = parameter :?> 'a |> f }

    let createConverter<'a, 'b> (f : 'a -> 'b) = 
        { new IValueConverter with
            override this.Convert(value, targetType, parameter, culture) = value :?> 'a |> f :> obj                
            override this.ConvertBack(value, targetType, parameter, culture) = raise (NotImplementedException()) }       

ItemsControlを下記のように書き換えると、所望の動作をします。

<ItemsControl ItemsSource="{Binding Counts}" Margin="0,0,0,35">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Button Content="{Binding}" Command="{local:ItemCommand CountUp}" />
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>






Visual Studio 2017でプロジェクトテンプレート作成

F#でWPFプログラミングをする際、プロジェクトテンプレートにコンソールアプリケーションしか存在しないため、毎回参照設定などを追加する必要があります。

今回はWPF用のプロジェクトテンプレートを作成してみたいと思います。

テンプレートの準備

まずは普通にコンソールアプリケーションプロジェクトを作成します。(名前は何でもいいです。)
f:id:any-programming:20170524182809p:plain

WPFに必要な参照を追加します。
f:id:any-programming:20170524183029p:plain

MainWindow.xamlファイルを追加(xmlファイルなどを追加して拡張子変更)し、ビルドアクションをResourceにします。
f:id:any-programming:20170524183500p:plain

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>

    </Grid>
</Window>


Program.fsを下記のように書き換えます。

open System
open System.Windows

[<STAThread>]
[<EntryPoint>]
let main argv = 
    let window = Application.LoadComponent(Uri("MainWindow.xaml", UriKind.Relative)) :?> Window

    Application().Run(window) |> ignore
    0


テンプレート作成

テンプレートが準備できましたら、「プロジェクト」→「テンプレートのエクスポート」を選択します。
f:id:any-programming:20170524183829p:plain

プロジェクトテンプレートを選択し、次へを押します。(プロジェクトが複数ある場合はドロップダウンで選択してください。)
f:id:any-programming:20170524183924p:plain

テンプレート名などを設定し、完了を押します。
f:id:any-programming:20170524184504p:plain

プロジェクト作成

プロジェクト作成ダイアログを開くと、作成したテンプレートが追加されていることが確認できます。
f:id:any-programming:20170524185905p:plain

テンプレートの削除

下記3か所のフォルダから削除すれば、ダイアログに出てこなくなります。(Visual Studio側から削除する方法があれば更新します。)

ユーザー/Documents/Visual Studio 2017/MyExported Templates
ユーザー/Documents/Visual Studio 2017/Templates/ProjectTemplates
ユーザー/AppData/Roaming/Microsoft/VisualStudio/バージョン/ProjectTemplatesCache