WPFでElm Architecture --- Xaml利用しない版 --- 可変個のコントロール --- データによりコントロールの種類変更

下記記事にて可変個のコントロールを表示できるよう実装しました。
WPFでElm Architecture --- Xaml利用しない版 --- 可変個のコントロール - 何でもプログラミング

ただし、すべてのコントロールが同じで個数のみが可変であったため、今回は入力データによってコントロールが分岐するような実装をしてみたいと思います。

アプリケーションコード

ボタンを押すと対応した図形が追加され、図形上で左クリックでサイズが大きくなり、図形上で右クリックをすると削除されるようなアプリケーションを作成してみます。

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

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利用しない版 --- 可変個のコントロール - 何でもプログラミング