11
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【WPF】DataGridでコンスセルなUndoRedo

Last updated at Posted at 2025-08-30

この記事はWPFSheepCalendar第5日目の記事だす。

AI依存の無能Coderの皆さん、こんにちわ。私もそうです。
便利な道具を使うという誘惑には抗いがたい:sob:

”エンジニア”とか気軽に言ってるけど、もはや和製英語化してますね。
向こうじゃとても通じないだろ

さて、今回は私が個人的にまとめた Qiita内のC#erのための有用な記事リファレンス(2012年~) からブラッシュアップしていこうという記事シリーズの第1弾!となります パチパチ。

@matarillo 氏自身もかなり実力のあるCoderだと思われます。

2012年と随分古い記事ですが、Qiita内に他にこういう記事は見当たりません!あと誰もいいね付けてません!すごい記事なのに勿体ないですね。

というわけで、この記事を元にWPFのDataGridを使ってUndoRedoを実装します!

ConsCellを使わなくても、ImmutableStack<T>で同様の実装が可能だそうです。

https://learn.microsoft.com/en-us/dotnet/api/system.collections.immutable.immutablestack-1?view=net-8.0

ConsCellは 枝分かれUndo(Branch Undo) に適します。
※本稿では単純なUndoRedoのみです

🎷いいね/ストックいつもありがとうございます!🎉

  • 3ストック以上でセル編集単位のUndoRedoを実装します。-  → 載せたCell単位でのUndoRedo

Github

別に僕に何っっっのメリットもないので載せる意味はないと思うんですが、宣言通りに。
 $\color{lightblue}{\tiny \textsf{スターを付けてくださいお願い}}$

コンスセルとは?

Comparing Common LISP and Clojure: Cons Cells

※女性とひつじにも分かりやすいように書いています!

コンスセル(cons cell)は、プログラミング言語Lispやその派生言語で使われる基本的なデータ構造です。簡単に言うと、2つの要素をペアで持つ「箱」のようなものです。この箱は、2つの「スロット」(場所)があり、それぞれにデータや別のコンスセルを入れることができます。

補足
本実装では先頭の要素 : headそれ以外の要素 : tailを再帰処理により表現しています。
シンプルな構造だけど割とマジックだと思う

  • 左のポケット(car):1つ目のデータを入れる

  • 右のポケット(cdr):2つ目のデータを入れる

  • 女性の視点:コンスセルは、まるでアクセサリーのペアを入れるジュエリーボックスのようなもの。左のポケットに「ピアス」、右のポケットに「ネックレス」を入れる感じ。右のポケットに別のジュエリーボックスを入れることもできて、どんどんつなげられるの!

  • 羊の視点:羊さんが草を食べる順番を決めるリストだよ。左のポケットに「1番目の草」、右のポケットに「次の草のリスト」を入れる。こうやって、草を食べる順番をずーっとつなげて覚えておけるんだ!

どうやって使うの?

コンスセルは、データをペアで管理したり、リストを作ったりするのに超便利!

例えば:

  • 木構造(ツリー)を作る
  • データを順番に並べる(リスト)
  • プログラムで複雑な情報を整理する

この仕組みをUndoRedoに応用しようというわけです。

以下のCodeを実行すると以下が得られます。

実行結果

ChangeGridクラス

DataGridのCellに渡すHelperクラス

IList<Person> _itemsSource は DataGrid のデータ本体で、Undo/Redo のときに要素追加・削除やプロパティ更新を担う。

ChangeGrid.cs

using System.Windows.Controls;
using WPF_UndoDataGrid;



/// <summary>
/// DataGrid に対する「1回の変更」を表現するクラス。
/// 
/// 【責務】
/// - セルの変更・行の追加削除などを 1 単位の「変更」として保持する
/// - 変更前の値 (OldValue) と変更後の値 (NewValue) を記録する
/// - 対象セルを特定するための DataGridCellInfo を保持する
/// - ItemsSource (IList<Person>) への参照を持ち、行追加/削除の Undo に対応する
/// - Revert() で変更を取り消す (Undo)
/// - Apply() で変更を再実行する (Redo)
///
/// 【非責務】
/// - Undo/Redo の履歴管理(これは UndoManager が担当する)
/// - DataGrid の UI 更新やバインディング制御
/// - 複数変更のまとめ(トランザクション管理)
/// </summary>
public class ChangeGrid
{
    public DataGridCellInfo Cell { get; }
    public object? OldValue { get; }
    public object? NewValue { get; }

    public IList<Person> _itemsSource { get; }

    public ChangeGrid(DataGridCellInfo cell, object? oldValue, object? newValue, IList<Person> itemsSource)
    {
        Cell = cell;
        OldValue = oldValue;
        NewValue = newValue;
        _itemsSource = itemsSource;
    }

    /// <summary>
    /// セルに新しい値を適用
    /// 発火:Redo
    /// </summary>
    public void Apply()
    {
        if (NewValue is null) throw new ArgumentNullException(nameof(NewValue));

        if (OldValue == null)
        {
            // Redo 行追加
            RestoreRow((Person)NewValue);
        }
        else
        {
            // Redo セル値復元
            SetCellValue(Cell, NewValue);
        }
    }

    /// <summary>
    /// セルを元の値に戻す
    /// 発火:Undo
    /// </summary>
    public void Revert()
    {
        if (OldValue == null)
        {
            if (NewValue == null)
                throw new Exception("NewValue is null");

            // Undo 行追加(新規追加を取り消す)
            RemoveRow((Person)NewValue);
        }
        else
        {
            // Undo セル値復元
            SetCellValue(Cell, OldValue);
        }
    }

    /// <summary>
    /// DataGrid の指定セルに値を設定する処理。
    /// </summary>
    public bool SetCellValue(DataGridCellInfo cellInfo, object? value)
    {
        if (cellInfo.Item == null || cellInfo.Column == null)
            return false;

        if (cellInfo.Column is DataGridBoundColumn boundColumn
            && boundColumn.Binding is System.Windows.Data.Binding binding)
        {
            var prop = cellInfo.Item.GetType().GetProperty(binding.Path.Path);
            if (prop == null) return false;

            // value が null ならセルを空にすることも許容
            prop.SetValue(cellInfo.Item, value);
            return true;
        }

        return false;
    }

    /// <summary>
    /// 行を追加(復元)
    /// </summary>
    public void RestoreRow(Person person)
    {
        _itemsSource.Add(person);
    }

    /// <summary>
    /// 行を削除(Undo 新規追加)
    /// </summary>
    public void RemoveRow(Person person)
    {
        _itemsSource.Remove(person);
    }
}

ConsCell Class

基本構造

  • リストを構成する最小単位のセル(ノード) を表現するクラス。
    head(要素)tail(次のセル)isTerminal(終端かどうか)を保持。

再帰的構造によりリスト全体を表現する。

private readonly T head;
private readonly ConsCell<T> tail;
private readonly bool isTerminal;

主なメソッド

  • Push public ConsCell<T> Push(T head)
    新しい要素を先頭に追加した 新インスタンス を返す。(元のリストは不変)

  • Concat
    2つのリストを連結。再帰的に処理。

  • Contains
    要素が含まれるかを逐次探索。

  • Reverse
    新しいリストを逆順に構築。

  • GetEnumerator
    foreach で列挙可能。

ICollection<T>実装のため、foreachやLinqが使えてテストも容易になります。
継承で余分なのもついてきますけどね。

ConceCell.cs
/// <summary>
/// 再帰的に定義された「連結リスト(Consリスト)」を表現するクラス。
/// 
/// 【責務】
/// - 先頭要素 (Head) と残りのリスト (Tail) を持つ単方向リストの基本構造
/// - 空リスト(終端セル)と要素を持つセルを区別する
/// - 要素の追加 (Push) や連結 (Concat) といった操作を提供する
///
/// 【特徴】
/// - イミュータブル設計(既存セルを変更せず、新しいセルを作成する)
/// - 空リスト判定 (IsEmpty) が可能
/// - Stack / UndoRedo の履歴構造などに利用しやすい
///
/// 【非責務】
/// - 標準コレクションの変更操作(Add, Clear, Remove は未対応)
/// </summary>
public class ConsCell<T> : ICollection<T>
{
    private readonly T head;
    private readonly ConsCell<T>? tail;
    private readonly bool isTerminal;

    /// <summary>
    /// 空リスト(終端セル)を作成する
    /// </summary>
    public ConsCell()
    {
        this.isTerminal = true;
    }

    /// <summary>
    /// 値と次のセルを指定して新しい ConsCell を作成する
    /// </summary>
    public ConsCell(T value, ConsCell<T> tail)
    {
        this.head = value;
        this.tail = tail;
    }

    /// <summary>
    /// IEnumerable から ConsCell を構築する
    /// </summary>
    public ConsCell(IEnumerable<T> source)
        : this(EnsureNotNull(source).GetEnumerator())
    {
    }

    private static IEnumerable<T> EnsureNotNull(IEnumerable<T> source)
    {
        if (source == null)
            throw new ArgumentNullException(nameof(source));
        return source;
    }

    private ConsCell(IEnumerator<T> itor)
    {
        if (itor.MoveNext())
        {
            this.head = itor.Current;
            this.tail = new ConsCell<T>(itor);
        }
        else
        {
            this.isTerminal = true;
        }
    }

    /// <summary>
    /// 空リストかどうかを判定
    /// </summary>
    public bool IsEmpty => this.isTerminal;

    /// <summary>
    /// 先頭要素を取得(空リストなら例外)
    /// </summary>
    public T Head
    {
        get
        {
            ErrorIfEmpty();
            return this.head;
        }
    }

    /// <summary>
    /// 残りのリストを取得(空リストなら例外)
    /// </summary>
    public ConsCell<T> Tail
    {
        get
        {
            ErrorIfEmpty();
            return this.tail!;
        }
    }

    private void ErrorIfEmpty()
    {
        if (this.isTerminal)
            throw new InvalidOperationException("this is empty.");
    }

    /// <summary>
    /// 新しい要素を先頭に追加し、新しい ConsCell を返す
    /// </summary>
    public ConsCell<T> Push(T head) => new ConsCell<T>(head, this);

    /// <summary>
    /// このリストの末尾に別のリストを連結する
    /// </summary>
    public ConsCell<T> Concat(ConsCell<T> second)
    {
        if (this.isTerminal)
            return second;
        return this.tail!.Concat(second).Push(this.head);
    }

    /// <summary>
    /// 要素を含むかどうか
    /// </summary>
    public bool Contains(T item)
    {
        for (ConsCell<T> p = this; !p.isTerminal; p = p.tail!)
        {
            if (p.head == null && item == null) return true;
            if (p.head != null && p.head.Equals(item)) return true;
        }
        return false;
    }

    /// <summary>
    /// 要素数を返す
    /// </summary>
    public int Count
    {
        get
        {
            int c = 0;
            for (ConsCell<T> p = this; !p.isTerminal; p = p.tail!)
            {
                c++;
            }
            return c;
        }
    }

    /// <summary>
    /// 逆順の ConsCell を作成して返す
    /// </summary>
    public ConsCell<T> Reverse()
    {
        ConsCell<T> rev = new ConsCell<T>();
        for (ConsCell<T> p = this; !p.isTerminal; p = p.tail!)
        {
            rev = rev.Push(p.head);
        }
        return rev;
    }

    /// <summary>
    /// foreach に対応
    /// </summary>
    public IEnumerator<T> GetEnumerator()
    {
        for (ConsCell<T> p = this; !p.isTerminal; p = p.tail!)
            yield return p.head;
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => this.GetEnumerator();

    #region ICollection<T> 実装(読み取り専用)

    bool ICollection<T>.IsReadOnly => true;

    void ICollection<T>.CopyTo(T[] array, int arrayIndex)
    {
        for (ConsCell<T> p = this; !p.isTerminal; p = p.tail!)
        {
            if (array.Length <= arrayIndex)
                throw new ArgumentOutOfRangeException(nameof(arrayIndex));
            array[arrayIndex++] = p.head;
        }
    }

    void ICollection<T>.Add(T item) => throw new NotSupportedException();
    void ICollection<T>.Clear() => throw new NotSupportedException();
    bool ICollection<T>.Remove(T item) => throw new NotSupportedException();

    #endregion
}


UndoManager Class

UndoRedoを実装するクラス。

UndoManager.cs
    public class UndoManager
    {
        private ConsCell<ChangeGrid> undoStack = new ConsCell<ChangeGrid>();
        private ConsCell<ChangeGrid> redoStack = new ConsCell<ChangeGrid>();

 // 新しい操作を積む
 public void AddChange(ChangeGrid change)
 {
     undoStack = undoStack.Push(change);
     redoStack = new ConsCell<ChangeGrid>(); // 新規操作でRedoは消える
 }

 // Undo: 最新の変更を元に戻す
 public void Undo()
 {
     if (undoStack.IsEmpty)
         return;

     ChangeGrid change = undoStack.Head;
     change.Revert();
     undoStack = undoStack.Tail;
     redoStack = redoStack.Push(change);
 }


 // Redo: Undoした変更を再適用
 public void Redo()
 {
     if (redoStack.IsEmpty)
         return;

     ChangeGrid change = redoStack.Head;
     change.Apply();
     redoStack = redoStack.Tail;
     undoStack = undoStack.Push(change);
 }





呼び出し方法

  • 外観(XAML)

<Grid>以下を丸ごとコピペすればよいです。

image.png


<Window x:Class="WPF_UndoDataGrid.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPF_UndoDataGrid"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <DockPanel LastChildFill="True">
            <DataGrid
            DockPanel.Dock="Top"
            Height="300"
            x:Name="datagrid1"

                  AutoGenerateColumns="False"
                  CanUserAddRows="False"
                  ItemsSource="{Binding People}"
                  
                  CellEditEnding="OnCellEditEnding">
                <DataGrid.Columns>
                    <DataGridTextColumn Header="Name" Binding="{Binding Name, UpdateSourceTrigger=LostFocus}" Width="2*"/>
                    <DataGridTextColumn Header="Age"  Binding="{Binding Age,  UpdateSourceTrigger=LostFocus}" Width="*"/>
                    <DataGridTextColumn Header="City" Binding="{Binding City, UpdateSourceTrigger=LostFocus}" Width="2*"/>
                </DataGrid.Columns>


            </DataGrid>
            <StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
                <Button x:Name="AddButton"
                Content="Add"
                FontSize="30"
                Click="OnAddRowClick"
                Height="50" Width="200" />
                <StackPanel >
                <Button x:Name="UndoButton" Height="50" Width="200" 
            Content="Undo" FontSize="30"
            Click="UndoButton_Click"
DockPanel.Dock="Bottom"/>
                    <Button x:Name="RedoButton" Height="50" Width="200" 
            Content="Redo" FontSize="30"
            Click="RedoButton_Click"
DockPanel.Dock="Bottom"/>
                </StackPanel>
            </StackPanel>
        </DockPanel>
    </Grid>
</Window>


  • CodeBehind
    一番難儀した部分。
MainWindow.xaml.cs

using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using WPF_UndoDataGrid.classes;



namespace WPF_UndoDataGrid
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {

        // ---- Undo用の1レコード ----
         private readonly UndoManager _undoManager = new UndoManager();

        IList<Person> _itemsorce;

        public MainWindow()
        {
            InitializeComponent();



            _itemsorce = People;            

        }


        public ObservableCollection<Person> People { get; } = new()
        {
            new Person { Name = "Alice", Age = 28, City = "Tokyo" },
            new Person { Name = "Bob",   Age = 34, City = "Osaka" },
            new Person { Name = "Cathy", Age = 22, City = "Nagoya" },
        };



        private void OnAddRowClick(object sender, RoutedEventArgs e)
        {
            var newPerson = RandomPerson();

            People.Add(newPerson);

            datagrid1.ItemsSource = People;

            // Change を作る(行追加なので oldValue = null)
            var change = new ChangeGrid(
                new DataGridCellInfo(newPerson, datagrid1.Columns[0]),
                oldValue: null,
                newValue: newPerson,
               itemsorece: People // IList を渡す
            );


            _undoManager.AddChange(change);
        }

        Person RandomPerson()
        {
            var random = new Random();
            string[] prefectures =
   {
            "東京", "大阪", "福岡", "北海道", "京都",
            "愛知", "沖縄", "広島", "宮城", "長野"
        };
            // 名前候補(「ひつじ○○」)
            string[] nameSuffix = { "太郎", "花子", "次郎", "美咲", "健一", "真央", "翔", "未来", "一郎", "優子" };



            var people = new List<Person>();

            var person = new Person
            {
                Name = "ひつじ" + nameSuffix[random.Next(nameSuffix.Length)],
                Age = random.Next(20, 41), // 20~40の範囲
                City = prefectures[random.Next(prefectures.Length)]
            };


            return person;

        }


        private void UndoButton_Click(object sender, RoutedEventArgs e)
        {
            _undoManager.Undo();
        }

   
      

        private void RedoButton_Click(object sender, RoutedEventArgs e)
        {
            _undoManager.Redo();
        }
    }
}

コンスセル的UndoRedoの実装上の利点

  • 各操作は必ず新しい ConsCell ノードを生成するため、既存の履歴が壊れることはない。
  • 「Undo/Redo 途中で履歴が壊れる」「参照が上書きされる」といった典型的バグが発生しにくい。
  • デバッグやテストが容易になる。
  • ChangeGridのような差分データを扱うクラスがあればUndoRedoをそのまま実装可能、つまり応用が利く
  • ビジネスロジックや UI に特化した「専用 Undo クラス」を作らずに済み、コードの再利用性が高い。
  • 履歴管理だけでなく「版管理」「状態遷移の追跡」などにも応用可能。
  • 各操作で増えるのは「新しいノード + 差分オブジェクト」のみ。
  • リスト構造の共有により「過去の状態全コピー」が不要。
  • 特に GUI の入力履歴や業務アプリ程度の規模では、通常の List ベース Undo と比べてメモリ効率は大差ない。

コマンドpattern的UndoRedoの例

Mement Patten のUndoRedoクラスとの比較

比較項目 ConsCell(イミュータブル) Mement Pattrn
履歴の不変性 高い(Push 中のノードを壊さない) 可変(CurrentState を更新)
分岐履歴の表現 自然に対応(枝分かれを構造で表現可能) 既存履歴を破棄しがち(単一直線)
トランザクション 自作が必要(まとめる設計が明示的ではない) TransactionCommand により標準で対応
Undo/Redo 操作 スタック操作ベース。簡潔だが Redo 用スタックが必要 直接ポインタ移動+即時実行
メモリ効率 構造共有により全体では比較的軽量 Command オブジェクト多数で膨れる可能性あり
実装コスト シンプル(汎用履歴構造) 設計がしっかりして多機能だが複雑化も伴う

処理の流れを比較

Mermaidで書いているのでPC版で見てください。
スマホ版では表示されません

ConsCell型Mement

画像版(直接表示)
image.png

Mement Pattern型

  • 状態管理は Snapshot(状態のコピー)で行う

  • 双方向的に管理可能(Undo/Redo の行き来が簡単)

  • イメージ:[状態1] ⇄ [状態2] ⇄ [状態3] のようにリンクリスト的に前後移動

  • デメリット:
    → スナップショットコピーのコストが高い(大きな状態ではメモリ消費が増える)
    → 状態単位での巻き戻しなので、細粒度の操作差分管理には向かない

関連記事

C#でUndo/Redoを実装した
commandパターンのUndoRedo ClassがGitにあります。

あとがき

ちょっと複雑です。
ConsCell構造はあまり見慣れないのではないかと

Qiita内のC#erのための有用な記事リファレンス(2012年~)
を書こうと思ったのは古い記事から読めば理解度が深まるのではないかと考えたためですが、AI時代という事もありこのような再評価の流れが加速するのではないかと考える次第です。

ここまで長々と読んでいただきありがとうございました。良いと思ったらGoodボタン、悪いと思ったらBadボタン:point_down:ないしブラウザバック、ちゃんねる登録をお願いします!

Cell単位でのUndoRedo

Gitにも反映させておきます。

難しいと思った人ー:raised_hand:
実は意外と簡単に実装出来ます。

(AIで)ChangeCellクラスを書いて、Cell単位用のUndoManager Classを自分で書いただけです(ChangeGrid → ChangeCell)にしただけ

ChngeCell.cs
namespace WPF_UndoDataGrid.classes
{
    using System;
    using System.Windows.Controls;
    using System.Windows.Data;

    namespace WPF_UndoDataGrid
    {
        /// <summary>
        /// DataGrid に対する「セルの変更」を表現するクラス。
        /// 
        /// 【責務】
        /// - セルの編集(値の変更)を 1 単位の「変更」として保持する
        /// - 変更前の値 (OldValue) と変更後の値 (NewValue) を記録する
        /// - 対象セルを特定するための DataGridCellInfo を保持する
        /// - Revert() で変更を取り消す (Undo)
        /// - Apply() で変更を再実行する (Redo)       
        /// </summary>
        public class ChangeCell
        {
            /// <summary>
            /// どのセルが変更対象かを表す情報
            /// </summary>
            public DataGridCellInfo Cell { get; }

            public object? OldValue { get; }
            public object? NewValue { get; }

            public ChangeCell(DataGridCellInfo cell, object? oldValue, object? newValue)
            {
                Cell = cell;
                OldValue = oldValue;
                NewValue = newValue;

                // ChangeGrid ではここで ItemsSource (IList<Person>) も保持していた。
                //  ChangeCell はセル編集専用なので保持しない。
            }

            /// <summary>
            /// セルに新しい値を適用 (Redo)
            /// </summary>
            public void Apply()
            {
                if (NewValue is null)
                    throw new ArgumentNullException(nameof(NewValue));

                // ChangeGrid: NewValue  Person にキャストし
                //_itemsSource に追加する実装が残っていた。
                // ChangeCell: セルのプロパティに直接代入する
                SetCellValue(Cell, NewValue);
            }

            /// <summary>
            /// セルを元の値に戻す (Undo)
            /// </summary>
            public void Revert()
            {
                // ChangeGrid: OldValue == null の場合、行追加のUndoとして _itemsSource.Remove を行っていた。
                // ChangeCell: 単純に OldValue をセルへ戻すのみ。行削除は扱わない。
+                SetCellValue(Cell, OldValue);
            }

            /// <summary>
            /// DataGrid の指定セルに値を設定する処理。
            /// </summary>
            private void SetCellValue(DataGridCellInfo cellInfo, object? value)
            {
                if (cellInfo.Item == null || cellInfo.Column == null)
                    return;

                if (cellInfo.Column is DataGridBoundColumn boundColumn)
                {
                    if (boundColumn.Binding is Binding binding)
                    {
                        // cellInfo.Item の実際の型から、Binding で指定されたプロパティをリフレクションで取得する
                        var prop = cellInfo.Item.GetType().GetProperty(binding.Path.Path);
                        if (prop != null)
                        {
                            // _itemsSource.Add が呼ばれていた。
                            // ChangeCell: 本来の形で prop.SetValue によりプロパティへ代入している。
+                            prop.SetValue(cellInfo.Item, value);
                        }
                    }
                }
            }
        }
    }
}



これはChangeGridと内容が殆ど変わらないのですが、Cell単位の複雑な処理を入れる事を見越してこのように別クラスを用意することがあります。

ChangeGrid classで完結させるなら
割と冗長になるのでお勧めしません。

以下の2点を書き換えればOK
1.DataGridCellInfo が有効かどうかで判定

Revert Method

public void Revert()
{
    // セル情報が有効かどうかで分岐

    bool isSucess = Cell.Column != null && OldValue != null ? SetCellValue(Cell, OldValue) :SetCellValue(Cell, null);
    
    if(isSucess)
    {
      
        // --- 行単位の変更を戻す ---
        if (OldValue == null)
        {
            if (NewValue == null)
                throw new Exception("NewValue is null");

            // Undo 行追加(新しく追加された行を削除)
            _itemsSource.Remove((Person)NewValue);
        }
        else
        {
            // Undo 行削除(削除された行を復元)
            _itemsSource.Add((Person)OldValue);
        }
    }
}

Applay Method

  public void Apply()
{
    // セル情報が有効かどうかで分岐
    if (Cell.Column != null)
    {
        // --- セル単位の変更を適用 ---
        if (NewValue != null)
        {
            SetCellValue(Cell, NewValue);  // セルを新しい値に更新
        }
        else
        {
            // NewValue が null → セルを空にする
            SetCellValue(Cell, null);
        }
    }
    else
    {
        // --- 行単位の変更を適用 ---
        if (OldValue == null)
        {
            // 行追加のやり直し (Redo)
            if (NewValue == null)
                throw new Exception("NewValue is null");

            _itemsSource.Add((Person)NewValue);
        }
        else
        {
            // 行削除のやり直し (Redo)
            _itemsSource.Remove((Person)OldValue);
        }
    }
}

CellUndoManager

型名を代えただけです。ジェネリック型にするという手もありますが、判定が必要になるので手抜き

CellUndoManager.cs
using WPF_UndoDataGrid.classes.WPF_UndoDataGrid;

namespace WPF_UndoDataGrid.classes
{
    public class CellUndoManager
    {
        private ConsCell<ChangeCell> cellUndoStack = new ConsCell<ChangeCell>();

        private ConsCell<ChangeCell> cellRedoStack = new ConsCell<ChangeCell>();




        public void AddCellChange(ChangeCell change)
        {
            cellUndoStack = cellUndoStack.Push(change);
            cellRedoStack = new ConsCell<ChangeCell>(); // 新規操作でRedoは消える
        }



        public void CellUndo()
        {
            if (cellUndoStack.IsEmpty)
                return;

            ChangeCell change = cellUndoStack.Head;
            change.Revert();  // 変更を取り消す
            cellUndoStack = cellUndoStack.Tail;  //最新を取り除き、次の変更が先頭になる
            cellRedoStack = cellRedoStack.Push(change); // Redoスタックへ移す
        }


        // Redo: Undoした変更を再適用
        public void CellRedo()
        {
            if (cellRedoStack.IsEmpty)
                return;

            var change = cellRedoStack.Head;
            change.Apply();
            cellRedoStack = cellRedoStack.Tail;
            cellUndoStack = cellUndoStack.Push(change);
        }

    }
}


呼び出し

Datagrid1_CellEditEndingイベントを利用して書き込みますが、
やや複雑な処理です。

Undo,Redo呼び出しは前回とほぼ一緒で、CellUndoManagerから呼び出すだけ。

Datagrid1_CellEditEnding
  private void Datagrid1_CellEditEnding(object? sender, DataGridCellEditEndingEventArgs e)
  {
      if (e.EditAction != DataGridEditAction.Commit)
          return;

      // 編集前後の値を取り出す
      var binding = (e.Column as DataGridBoundColumn)?.Binding as System.Windows.Data.Binding;
      if (binding == null) return;

+      PropertyInfo? prop = e.Row.Item.GetType()
+                  .GetProperty(binding.Path.Path);
      if (prop == null) return;

      var oldValue = prop.GetValue(e.Row.Item);

      // EditingElement から新しい値を取得
      string? newText = null;
      if (e.EditingElement is TextBox tb)
          newText = tb.Text;

      object? newValue = newText;
      if (prop.PropertyType != typeof(string) && newText != null)
      {
          // 型変換
+          newValue = Convert.ChangeType(newText, prop.PropertyType);
      }

      // Undo/Redo Change を作成
      var change = new ChangeCell(
          new DataGridCellInfo(e.Row.Item, e.Column),
          oldValue,
          newValue
      );

      _CellundoManager.AddCellChange(change);
  }


如何でしたか?(構文)

このように書くのは如何なものかと思いますが、何分如何様に書くか思案んすると何様に書くしかございませんと愚行する次第でして。

戯言は置いといて
ConsCellを使うと同様の単純なClassを追加するだけで処理を追加できるのがお分かりいただけたかと思います。
呼び出しの差異がやや面倒でしょうか。

さてストックして頂いた甲斐はありましたか?

おまけ ジェネリッククラス化する

そもそも書きなれていないので慣れておく。
流石に初心者的か。。。

  • 型名が違うだけで同じ処理を書くとき
  • 分岐書く必要がないとき

一応、Gitにも反映させたけどStar就けないと非公開です。

フィールド


     // ---- Undo用の1レコード ----
    private readonly UndoManager<ChangeGrid> _undoManager = new UndoManager<ChangeGrid>();

    private readonly UndoManager<ChangeCell> _CellundoManager = new UndoManager<ChangeCell>();

UndoManager.cs

全部<T>にするだけじゃダメで、型制約:where Tが必要
更にIChangeAction interface が必要。
where Tの制約がないとメソッドが呼べない

→ 「T は必ず IChangeAction を実装していないとダメだよ」とコンパイラに伝えている

whereが何なのか知らなかった件

namespace WPF_UndoDataGrid.classes
{
    /// <summary>
    /// Undo/Redo を管理するジェネリッククラス
    /// T  IChangeAction を実装した型である必要がある
    /// </summary>
+    public class UndoManager<T> where T : IChangeAction
    {
        // Undo用のスタック
        private ConsCell<T> undoStack = new ConsCell<T>();

        // Redo用のスタック
        private ConsCell<T> redoStack = new ConsCell<T>();

        /// <summary>
        /// 新しい操作を追加
        /// </summary>
        public void AddChange(T change)
        {
            undoStack = undoStack.Push(change);
            redoStack = new ConsCell<T>(); // 新規操作でRedoはクリア
        }

        /// <summary>
        /// Undo: 最新の変更を元に戻す
        /// </summary>
        public void Undo()
        {
            if (undoStack.IsEmpty)
                return;

            T change = undoStack.Head;
            change.Revert();
            undoStack = undoStack.Tail;
            redoStack = redoStack.Push(change);
        }

        /// <summary>
        /// Redo: Undoした変更を再適用
        /// </summary>
        public void Redo()
        {
            if (redoStack.IsEmpty)
                return;

            T change = redoStack.Head;
            change.Apply();
            redoStack = redoStack.Tail;
            undoStack = undoStack.Push(change);
        }
    }

    /// <summary>
    /// Undo/Redo 対象の共通インターフェース
    /// </summary>
+    public interface IChangeAction
+    {
+        void Apply();
+        void Revert();
+    }
}


変換先のChangeGrid、ChangeCellにもIChangeActionの継承が必要。暗黙の型変換をしてくれないので。

必要なのはたったこれだけ。

public class ChangeGrid : IChangeAction
{
]

  public class ChangeCell : IChangeAction
{
}

既存のCodeに変更をほぼ加えずにジェネリッ
ク化出来ました。パチパチ👏

ジェネリッククラス内で分岐処理を書く場合

ジェネリッククラス内で分岐処理を書きたい場合(というか書きたくない場合)、Interfeaceでカスタムロジックを書いて、直接呼び出すという手がある

public class ChangeGrid : IChangeAction
{
    public void Apply() { /* ... */ }
    public void Revert() { /* ... */ }

    public void CustomProcess()
    {
        Console.WriteLine("Grid専用処理");
    }
}
public class UndoManager<T> where T : IChangeAction
{
    public void DoSomething(T change)
    {
        // 型の判断はしない。呼ぶだけ!
        change.CustomProcess();
    }
}

以上です。SeeYouSheep.

11
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?