WPFでElm Architecture --- Xaml利用しない版 --- 可変個のコントロール --- データによりコントロールの種類変更
下記記事にて可変個のコントロールを表示できるよう実装しました。
WPFでElm Architecture --- Xaml利用しない版 --- 可変個のコントロール - 何でもプログラミング
ただし、すべてのコントロールが同じで個数のみが可変であったため、今回は入力データによってコントロールが分岐するような実装をしてみたいと思います。
アプリケーションコード
ボタンを押すと対応した図形が追加され、図形上で左クリックでサイズが大きくなり、図形上で右クリックをすると削除されるようなアプリケーションを作成してみます。
itemsとselectorとfactoryが渡せるwith_Childrenを用いてコントロールの切り替えを実現しています。(with_Childrenの実装は後述)
コード内で利用されているLINQオペレータは下記記事を参照してください。
LINQ独自オペレータ メモ - 何でもプログラミング
図形モデル定義
interface IShape { double Size { get; } } class Circle : IShape { public double Size { get; } public Circle(double size) { Size = size; } } class Square : IShape { public double Size { get; } public Square(double size) { Size = size; } } class Triangle : IShape { public double Size { get; } public Triangle(double size) { Size = size; } }
アプリケーションモデル定義
class Model { public IShape[] Shapes { get; } public Model(IShape[] shapes) { Shapes = shapes; } }
モデル更新メッセージ定義
abstract class Message { } class AddCircle : Message { } class AddSquare : Message { } class AddTriangle : Message { } class IncrementSize : Message { public int Index { get; } public IncrementSize(int index) { Index = index; } } class RemoveShape : Message { public int Index { get; } public RemoveShape(int index) { Index = index; } }
モデル更新関数
public static Model Update(Model model, Message message) { switch (message) { case AddCircle msg: return new Model(model.Shapes.InsertLast(new Circle(30)).ToArray()); case AddSquare msg: return new Model(model.Shapes.InsertLast(new Square(30)).ToArray()); case AddTriangle msg: return new Model(model.Shapes.InsertLast(new Triangle(30)).ToArray()); case IncrementSize msg: { IShape increment(IShape src) => src is Circle ? new Circle(src.Size + 10) as IShape : src is Square ? new Square(src.Size + 10) as IShape : new Triangle(src.Size + 10) as IShape; return new Model(model.Shapes.UpdateAt(msg.Index, increment).ToArray()); } case RemoveShape msg: return new Model(model.Shapes.RemoveAt(msg.Index).ToArray()); default: throw new Exception("invalid message"); } }
View作成関数
enum Shape { Circle, Square, Triangle } public static FrameworkElement Create(ICell<Model> model, Action<Message> send) { return new StackPanel() .with_Margin(10, 10, 10, 10) .with_Children( new Button() .with_Content("Circle追加") .with_Margin(5, 5, 5, 5) .with_Click(() => send(new AddCircle())), new Button() .with_Content("Square追加") .with_Margin(5, 5, 5, 5) .with_Click(() => send(new AddSquare())), new Button() .with_Content("Triangle追加") .with_Margin(5, 5, 5, 5) .with_Click(() => send(new AddTriangle())), new StackPanel() .with_Children( model.Map(x => x.Shapes), item => item is Circle ? Shape.Circle : item is Square ? Shape.Square : Shape.Triangle, (id, item, index) => { var element = id == Shape.Circle ? new Ellipse().with_Fill(Brushes.Red) as FrameworkElement: id == Shape.Square ? new Rectangle().with_Fill(Brushes.Green) as FrameworkElement: new Path().with_Data(Geometry.Parse("M0,100 H100 L50,0 Z")).with_Stretch(Stretch.Fill).with_Fill(Brushes.Blue) as FrameworkElement; return element .with_Margin(5, 5, 5, 5) .with_Width(item.Map(x => x.Size)) .with_Height(item.Map(x => x.Size)) .with_MouseLeftButtonDown(x => send(new IncrementSize(index))) .with_MouseRightButtonDown(x => send(new RemoveShape(index))); } ) ); }
with_Children
固定コントロールの場合のwith_Childrenと比べて、入力データからコントロールの識別子を返すselector関数が追加され、factoryの引数にも識別子が渡されるようになっています。
新しいデータのコントロール識別子に変更がなければそのままデータを送出し、識別子が異なる場合はコントロールを作り直しています。
もっと賢い差分アルゴリズムを利用すると、コントロールの生成が最小限に抑えられると思います。
public static TPanel with_Children<TPanel, TItem, TViewID>( this TPanel element, ICell<TItem[]> items, Func<TItem, TViewID> selector, Func<TViewID, ICell<TItem>, int, FrameworkElement> factory) where TPanel : Panel { List<CellSink<TItem>> sinks = new List<CellSink<TItem>>(); List<TViewID> viewIDs = new List<TViewID>(); element.Children.Clear(); items.Listen(xs => { for (int i = 0; i < xs.Length; ++i) { // コントロールが存在 if (i < sinks.Count) { TViewID viewID = selector(xs[i]); // ViewID変更なし if (Equals(viewID, viewIDs[i])) { sinks[i].Send(xs[i]); } // ViewIDが変わった else { sinks[i] = new CellSink<TItem>(xs[i]); viewIDs[i] = viewID; element.Children.RemoveAt(i); element.Children.Insert(i, factory(viewID, sinks[i], i)); } } // コントロールが足りない else { sinks.Add(new CellSink<TItem>(xs[i])); viewIDs.Add(selector(xs[i])); element.Children.Add(factory(viewIDs.Last(), sinks.Last(), i)); } } // コントロールが過剰 for (int i = 0; i < sinks.Count - xs.Length; ++i) { sinks.RemoveAt(sinks.Count - 1); viewIDs.RemoveAt(viewIDs.Count - 1); element.Children.RemoveAt(element.Children.Count - 1); } }); return element; }
factoryの外側のスコープのCellを利用する場合
そのままCellを利用するとリークを起こしてしまうので、別途追加のCellを受け取るwith_Childrenが必要となります。
下記記事にても同様の実装がありますので、こちらを参照してください。
WPFでElm Architecture --- Xaml利用しない版 --- 可変個のコントロール - 何でもプログラミング