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));
シリアライズ
デフォルトでは、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 StudioのJSONビューワで確認)
デシリアライズ
デフォルトでは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を定義してください。
ネストした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で実装
今回はカウントアップを行うボタンを複数作成するアプリケーションを例に実装していきます。
ボタンはリピートボタンを利用しているため、マウスダウンの状態で、カウントがどんどん進むことを想定しています。
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>
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を作っていきます。
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に貼り付けた際にデザイナーでエラーが発生しないよう、ビルドアクションを埋め込みリソースにしておきます。
<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
作成するアプリケーション
複数の、クリック回数を表示するボタンからなるアプリケーションを作成します。
追加ボタンにより、クリックできるボタンを動的に増やすことができます。
本記事では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用のプロジェクトテンプレートを作成してみたいと思います。
テンプレートの準備
まずは普通にコンソールアプリケーションプロジェクトを作成します。(名前は何でもいいです。)
WPFに必要な参照を追加します。
MainWindow.xamlファイルを追加(xmlファイルなどを追加して拡張子変更)し、ビルドアクションをResourceにします。
<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
テンプレート作成
テンプレートが準備できましたら、「プロジェクト」→「テンプレートのエクスポート」を選択します。
プロジェクトテンプレートを選択し、次へを押します。(プロジェクトが複数ある場合はドロップダウンで選択してください。)
テンプレート名などを設定し、完了を押します。
プロジェクト作成
プロジェクト作成ダイアログを開くと、作成したテンプレートが追加されていることが確認できます。
テンプレートの削除
下記3か所のフォルダから削除すれば、ダイアログに出てこなくなります。(Visual Studio側から削除する方法があれば更新します。)
ユーザー/Documents/Visual Studio 2017/MyExported Templates
ユーザー/Documents/Visual Studio 2017/Templates/ProjectTemplates
ユーザー/AppData/Roaming/Microsoft/VisualStudio/バージョン/ProjectTemplatesCache