Functional Reactive Programming + MVVM --- C# WPF
下記記事にて、Functional Reactive ProgrammingライブラリであるSodiumとWPFを連携させました(F#)
SodiumでFunctional Reactive Programming (F#) --- WPF連携 - 何でもプログラミング
今回は、C#にてSodiumとWPFを連携させてみたいと思います。
アプリケーションコード
いつも通り、カウンタを実装してみたいと思います。
Streamを引数とするコンストラクタを持ち、DiscreteCellプロパティを公開するクラスを定義します。
引数のStreamがICommandに変換され、DiscreteCellの変更がINotifyPropertyChangedとして通知されるDataContextを利用します。(実装は後述します。)
(Sodiumは最近、CellとDiscreteCellを区別するようになりました。基本はDiscreteCellを使うことになると思います。)
<Window x:Class="WpfApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="100" Width="200"> <Grid Height="23" Margin="10"> <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> </Window>
class Counter { public DiscreteCell<int> Count { get; } public Counter(Stream<Unit> increment, Stream<Unit> decrement) { Count = Transaction.Run(() => { DiscreteCellLoop<int> count = new DiscreteCellLoop<int>(); var stream = increment.MapTo(1).OrElse(decrement.MapTo(-1)).Snapshot(count, (dx, x) => x + dx); count.Loop(stream.Hold(0)); return count; }); } }
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); DataContext = new DataContext<Counter>(); } }
DataContext
今回もDynamicObjectを利用して実装します。
まずコンストラクタの引数(複数のStream)を作成し、コンストラクタを呼び出してインスタンスを作成します。
作成したStreamのSinkはCommandとして公開されます。
続いてインスタンスのDiscreteCellプロパティをListenしてPropertyChangedに関連付けます。
public class DataContext<T> : DynamicObject, INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; object _instance; Dictionary<string, ICommand> _commands = new Dictionary<string, ICommand>(StringComparer.OrdinalIgnoreCase); Dictionary<string, object> _propertyValues = new Dictionary<string, object>(); List<IListener> _listeners = new List<IListener>(); public DataContext() { // コンストラクタの引数(Streams)を作成し、インスタンス作成 var constructor = typeof(T).GetConstructors().Single(); var args = constructor.GetParameters().Select(parameter => { var type = parameter.ParameterType.GetGenericArguments()[0]; return GetType().GetMethod(nameof(CreateStream), BindingFlags.Instance | BindingFlags.NonPublic) .MakeGenericMethod(type).Invoke(this, new object[] { parameter.Name }); }).ToArray(); _instance = constructor.Invoke(args); // インスタンスのDiscreteCellプロパティとPropertyChangedを関連付け foreach (var property in typeof(T).GetProperties()) { var cell = property.GetValue(_instance); var type = property.PropertyType.GetGenericArguments()[0]; GetType().GetMethod(nameof(Listen), BindingFlags.Instance | BindingFlags.NonPublic) .MakeGenericMethod(type).Invoke(this, new object[] { property.Name, cell }); } } // DiscreteCellをListenし、PropertyChangedを呼び出す void Listen<U>(string name, DiscreteCell<U> cell) => _listeners.Add(cell.Calm().Listen(x => { _propertyValues[name] = x; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); })); // Streamを発火するCommandを作成・保存し、Streamを返す Stream<U> CreateStream<U>(string name) { var sink = new StreamSink<U>(); _commands[name] = new Command<U>(sink.Send); return sink; } public override bool TryGetMember(GetMemberBinder binder, out object result) { result = _propertyInfos.ContainsKey(binder.Name) ? _propertyInfos[binder.Name].GetValue(_model) : _commands[binder.Name]; return true; } }
Elm Architectureを利用したMVVMでのUserControl --- C# WPF
下記記事にて、C#でElm Architectureを利用してWPFアプリケーションを作成しました。
Elm Architectureを利用したMVVM --- C# WPF - 何でもプログラミング
今回はElm ArchitectureでUserControlを実装してみたいと思います。
F#版での実装は下記記事を参照してください。
F#でWPF --- Elm Architectureで実装されたUserControl - 何でもプログラミング
アプリケーションコード
Counterコントロールを配置しただけのViewと、値を保存・変更するだけのModel・Updaterになります。
引き続き、このCounterコントロールを作成していきます。
<Window x:Class="WpfApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfApp" Title="MainWindow" Height="100" Width="200"> <Grid> <local:Counter Value="{Binding Value}" ValueChanged="{Binding SetValue}" Margin="10" Height="23" /> </Grid> </Window>
class Model { public int Value { get; } public Model(int value) => Value = value; }
class Updater { public Model SetValue(Model model, int value) => new Model(value); }
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); DataContext = new DataContext(new Model(0), new Updater()); } }
Counterコントロール
Increment及びDecrementが押された場合にMain側に通知し(ValueChanged)、Main側の値(Value)が変わった場合にカウンタの値を更新するように実装してあります。
C#で利用しているDependencyPropertyManagerは下記記事を参照してください。
DependencyProperty定義の記述量削減 - 何でもプログラミング
また、RegisterWithUpdateName、DataContextクラスは後程実装いたします。
<UserControl x:Class="WpfApp.Counter" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 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 Height="23"> <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> </UserControl>
public partial class Counter : UserControl { static DependencyPropertyManager<Counter> _dp = new DependencyPropertyManager<Counter>(); // 値が変わった場合にUpdaterのSetCountを呼ぶように登録 public static readonly DependencyProperty ValueProperty = _dp.RegisterWithUpdateName<int>(nameof(Updater.SetCount)); public int Value { get => (int)_dp.Get(this); set => _dp.Set(this, value); } public static readonly DependencyProperty ValueChangedProperty = _dp.Register<ICommand>(); public ICommand ValueChanged { get => (ICommand)_dp.Get(this); set => _dp.Set(this, value); } class Model { public int Count { get; } public Model(int count) => Count = count; } class Updater { Counter _uc; public Updater(Counter uc) => _uc = uc; public PostCommand Increment(Model model) => new PostCommand(_uc.ValueChanged, model.Count + 1); public PostCommand Decrement(Model model) => new PostCommand(_uc.ValueChanged, model.Count - 1); public Model SetCount(Model model, int count) => new Model(count); } public Counter() { InitializeComponent(); // UserControlのDataContextはMain側で利用されるので、Content(今回はGrid)のDataContextにセット ((FrameworkElement)Content).DataContext = new DataContext(new Model(0), new Updater(this)); } }
RegisterWithUpdateName
値変更のコールバック時に、UserControlのContentのDataContextのExecute(後述します。)を呼び出します。
public DependencyProperty RegisterWithUpdateName<TValue>(string updateName, TValue defaultValue = default(TValue), [CallerMemberName]string name = "") { name = name.Substring(0, name.Length - "Property".Length); var metadata = new PropertyMetadata(defaultValue, (obj, args) => (((obj as UserControl).Content as FrameworkElement).DataContext as DataContext).Execute(updateName, args.NewValue)); return DependencyProperty.Register(name, typeof(TValue), typeof(TClass), metadata); }
DataContext
元々のDataContextではModelを返す関数しか対応していませんでしたが、Model更新後にCommandを実行できるよう、PostCommandクラスを返す関数にも対応させます。
またRegisterWithUpdateNameからCommandを呼び出せるよう、Execute関数も実装します。
public class PostCommand { public Action Execute { get; } public object Model { get; } public PostCommand(ICommand command, object parameter, object model = null) { Model = model; Execute = () => command?.Execute(parameter); } }
public class DataContext : DynamicObject, INotifyPropertyChanged { object _model; Dictionary<string, PropertyInfo> _propertyInfos; Dictionary<string, ICommand> _commands; public event PropertyChangedEventHandler PropertyChanged; public DataContext(object initialModel, object updater) { _model = initialModel; _propertyInfos = initialModel.GetType().GetProperties().ToDictionary(x => x.Name); _commands = updater.GetType().GetMethods().ToDictionary( method => method.Name, method => (ICommand)new Command<object>(parameter => { object prevModel = _model; object[] parameters = parameter == null ? new[] { prevModel } : new[] { prevModel, parameter }; object result = method.Invoke(updater, parameters); if (result.GetType() == _model.GetType()) _model = result; // PostCommandのModelをセット(nullの場合は変更なし) else if (result is PostCommand) _model = ((PostCommand)result).Model ?? _model; if (prevModel != _model) foreach (var property in _propertyInfos.Values) if (property.GetValue(prevModel) != property.GetValue(_model)) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property.Name)); // PostCommandの場合、Command実行 (result as PostCommand)?.Execute(); })); } public override bool TryGetMember(GetMemberBinder binder, out object result) { result = _propertyInfos.ContainsKey(binder.Name) ? _propertyInfos[binder.Name].GetValue(_model) : _commands[binder.Name]; return true; } // 外部からコマンドを呼び出す用 public void Execute(string name, object parameter) => _commands[name].Execute(parameter); }
Elm Architectureを利用したMVVM --- C# WPF
下記記事にてF#でElm ArchitectureをWPFに導入してみました。
F#でWPF --- Elm Architectureを利用したMVVM - 何でもプログラミング
今回はC#で近いものを実装してみたいと思います。
F#の方ではサポートしたModel→ViewModelの変換は省略していますので、必要な場合は追加で実装してください。
アプリケーションコード
今回もカウンタアプリケーションを実装してみたいと思います。
後述しますDataContextクラスを利用すると、下記のようなアプリケーションコードになります。
F#の時と異なり、Modelの更新は判別共用体を利用するのではなく、メソッドを定義することでCommandとして呼び出されるようにしています。
Xaml
<Window x:Class="WpfApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="100" Width="200"> <Grid Height="23" Margin="10"> <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> </Window>
class Model { public int Count { get; } public Model(int count) => Count = count; } class Updater { public Model Increment(Model model) => new Model(model.Count + 1); public Model Decrement(Model model) => new Model(model.Count - 1); } public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); DataContext = new DataContext(new Model(0), new Updater()); } }
DataContextクラス
今回もDynamicObjectを利用して実装します。
Modelのプロパティと、Updaterで定義されたメソッドを呼び出すCommandを公開しています。
Commandクラスは単にExecuteをラムダで登録できるようにしたクラスです。
public class DataContext : DynamicObject, INotifyPropertyChanged { object _model; Dictionary<string, PropertyInfo> _propertyInfos; Dictionary<string, ICommand> _commands; public event PropertyChangedEventHandler PropertyChanged; public DataContext(object initialModel, object updater) { _model = initialModel; _propertyInfos = initialModel.GetType().GetProperties().ToDictionary(x => x.Name); _commands = updater.GetType().GetMethods().ToDictionary( method => method.Name, method => (ICommand)new Command<object>(parameter => { object prevModel = _model; object[] parameters = parameter == null ? new[] { prevModel } : new[] { prevModel, parameter }; _model = method.Invoke(updater, parameters); // 変更箇所通知 if (prevModel != _model) foreach (var property in _propertyInfos.Values) if (property.GetValue(prevModel) != property.GetValue(_model)) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property.Name)); })); } public override bool TryGetMember(GetMemberBinder binder, out object result) { result = _propertyInfos.ContainsKey(binder.Name) ? _propertyInfos[binder.Name].GetValue(_model) : _commands[binder.Name]; return true; } }
C# Immutableオブジェクト 導出項目のキャッシュ機能
例として下記のようなVector2クラスを考えます。
Lengthプロパティは、呼び出される毎に計算を行っています。
処理が軽い場合は特に問題ありませんが、導出に時間がかかるプロパティは結果をキャッシュしてあるほうが好ましいです。
今回はキャッシュを行う機能を実装してみたいと思います。
public class Vector2 { public int X { get; } public int Y { get; } public double Length => Math.Sqrt(X * X + Y * Y); public Vector2(int x, int y) { X = x; Y = y; } }
Cache構造体
簡単のため、入力はValueTupleを利用します。(ValueTupleを利用しない場合は、TArgsを増やした複数のCache構造体を定義してください。)
入力に変化がない場合は、前回の値を返すように実装しています。
構造体である理由は、Immutableクラスが変更・複製されたときに、キャッシュを共有してしまわないようにするためです。
struct Cache<TArgs, TResult> { bool _initialized; TArgs _args; TResult _result; Func<TArgs, TResult> _f; public Cache(Func<TArgs, TResult> f) { _initialized = false; _args = default(TArgs); _result = default(TResult); _f = f; } public TResult Get(TArgs args) { if (_initialized && args.Equals(_args)) return _result; _initialized = true; _args = args; _result = _f(args); return _result; } }
Vector2書換え
新たにキャッシュのフィールドを追加し、LengthプロパティではGetを呼び出しています。
public class Vector2 { public int X { get; } public int Y { get; } Cache<(int, int), double> _length = new Cache<(int x, int y), double>(args => Math.Sqrt(args.x * args.x + args.y * args.y)); public double Length => _length.Get((X, Y)); public Vector2(int x, int y) { X = x; Y = y; } }
動作確認
同じオブジェクトに対し、二回目以降のLength呼出ではキャッシュが返るようになります。
Withで複製した場合でも問題なく動作していることも確認できます。(Withに関しましては、下記記事を参照してください。)
ネストしたImmutableオブジェクトの更新(C#) - 何でもプログラミング
Vector2 v = new Vector2(1, 1); v.Length; v.Length; // キャッシュが返る Vector2 v2 = v.With(x => x.Y, 2); v.Length; // キャッシュが返る v2.Length; v2.Length; // キャッシュが返る
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">
これで、ボタン上でマウスダウンを保持するとカウントが進むようになります。