這篇文章是WPFSheepCalendar第5天的文章。
依賴AI的無能程式員的大家,您好。我也是。
難以抵抗使用便利工具的誘惑😢
雖然隨便就叫自己「工程師」,但這樣的說法已經變成了日式英語。
在那邊可能根本無法理解。
那麼,這次是我個人總結的 針對C#程序員的有用文章參考列表(2012年~) 的系列文章第一彈!掌聲👏。
@matarillo 先生本人也是非常有實力的程式員。
雖然是2012年的相當舊文章,但在Qiita中找不到其他類似的文章!而且也沒有人按讚!明明是很棒的文章,真可惜。
因此,基於這篇文章,我們將會實作WPF的DataGrid的UndoRedo功能!
🎷非常感謝大家的讚和收藏!🎉
一年內沒有收到讚,這篇文章將會消失。
※為了讓女性和羊也能理解,因此我會這樣寫!
ConsCell(cons cell)是編程語言Lisp及其衍生語言中使用的基本數據結構。簡單來說,它就像一個擁有兩個元素的「箱子」。這個箱子有兩個「插槽」,可以各自存放數據或其他ConsCell。
ConsCell對於管理數據對或創建列表非常方便!
例如:
也就是想要將這個機制應用於UndoRedo。
執行以下代碼會得到以下結果。
傳遞給DataGrid的Cell的輔助類
IList<Person> _itemsSource
是DataGrid的數據本體,並在Undo/Redo時負責元素的添加、刪除或屬性的更新。
using System.Windows.Controls;
using WPF_UndoDataGrid;
/// <summary>
/// 表示對DataGrid的「一次更改」的類。
///
/// 【責任】
/// - 保存單元格的更改、行的添加或刪除等單位的「變更」
/// - 記錄修改前的值(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; set; }
public ChangeGrid(DataGridCellInfo cell, object? oldValue, object? newValue, IList<Person> itemsorece)
{
Cell = cell;
OldValue = oldValue;
NewValue = newValue;
_itemsSource = itemsorece;
}
/// <summary>
/// 將新值應用到單元格
/// 引發:Redo
/// </summary>
/// <exception cref="ArgumentNullException"></exception>
public void Apply()
{
if (NewValue is null) throw new ArgumentNullException(nameof(NewValue));
SetCellValue(Cell, (Person)NewValue);
}
// 將單元格恢復到原始值
public void Revert()
{
if (OldValue == null)
{
if (NewValue is null)
throw new Exception("NewValue is null");
_itemsSource.Remove((Person)NewValue); // Undo行添加
}
else
{
SetCellValue(Cell, OldValue);
}
}
/// <summary>
/// 設置DataGrid的指定單元格的值的處理。
///
/// 【處理流程】
/// 1. 從cellInfo獲取目標項目(Item)和列(Column)。
/// 2. 如果列是DataGridBoundColumn,則提取其綁定信息。
/// 3. 通過反射從Binding.Path中確定目標屬性。
/// 4. 本來應該將值設置到該屬性上。
///
/// 由於值被類型轉換為Person型,因此如果傳遞的值與單元格的類型不符(例如string或int),將會引發ArgumentException。
///
/// 【改善點】
/// - 本來應使用prop.SetValue(cellInfo.Item, value)將值賦給DataGrid綁定的屬性。
/// - _itemsSource.Add(...)應該獨立為「行添加」的專用處理。
/// </summary>
public void SetCellValue(DataGridCellInfo cellInfo, object? value)
{
if (cellInfo.Item == null || cellInfo.Column == null)
return;
if (cellInfo.Column is DataGridBoundColumn boundColumn)
{
var binding = boundColumn.Binding as System.Windows.Data.Binding;
if (binding == null) return;
var prop = cellInfo.Item.GetType().GetProperty(binding.Path.Path);
if (prop == null) return;
if (value is null)
throw new Exception("Value is null");
_itemsSource.Add((Person)value);
}
}
}
基本結構
head(元素)
、tail(下一個單元)
和isTerminal(是否為終端)
。通過遞歸結構表示整個列表。
private readonly T head;
private readonly ConsCell<T> tail;
private readonly bool isTerminal;
Push public ConsCell<T> Push(T head)
返回一個新實例,其中包含在開頭添加的新元素。(原始列表保持不變)
Concat
連接兩個列表,進行遞歸處理。
Contains
逐一搜索元素。
Reverse
構建新的反向列表。
GetEnumerator
支持foreach迭代。
由於實現了ICollection<T>
,因此可以使用foreach或Linq,使測試變得容易。
但會隨之帶來額外的繼承。
/// <summary>
/// 表示遞歸定義的「鏈表(ConsList)」的類。
///
/// 【責任】
/// - 擁有首個元素(Head)和餘下列表(Tail)的單向列表的基本結構
/// - 區分空列表(終端單元)和有元素的單元
/// - 提供元素添加(Push)和連接(Concat)等操作
///
/// 【特徵】
/// - 不可變設計(不修改現有單元,創建新單元)
/// - 能夠判斷是否為空列表(IsEmpty)
/// - 易於用於堆疊/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
}
實作UndoRedo的類。
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);
}
}
直接複製整個<Grid>
就可以了。
<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>
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using WPF_UndoDataGrid.classes;
namespace WPF_UndoDataGrid
{
/// <summary>
/// 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;
// 創建變更(行添加時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();
}
}
}
比較項目 | ConsCell(不可變) | Command+雙向列表 (History.cs ) |
---|---|---|
歷史的不變性 | 高(不會破壞Push中的節點) | 可變(更新CurrentState) |
分支歷史的表示 | 自然支持(可以通過結構表達分支) | 更容易破壞現有歷史(單一路徑) |
事務 | 自行實現(聚合設計不明確) | TransactionCommand 提供標準支持 |
Undo/Redo操作 | 基於堆棧操作。簡單但需要Redo用的堆棧 | 直接指針移動+即時執行 |
內存效率 | 通過結構共享相對輕量 | 可能因command對象過多而膨脹 |
實施成本 | 簡單(通用歷史結構) | 設計完善且功能多樣,但伴隨複雜化 |
基本流程幾乎相同
使用Mermaid繪製,請在PC版上查看。
在手機版上無法顯示。
<iframe id="qiita-embed-content__58420f26bb407a4324d05bf2de540d63"></iframe>
<iframe id="qiita-embed-content__850eee49db3763731978180a3bc8dda9"></iframe>
C#中實作Undo/Redo
命令模式的UndoRedo類在Git上可程式。
<iframe id="qiita-embed-content__03c018f2f88dc0b5bed1e262a1f3c90e"></iframe>
這有點複雜。
ConsCell結構可能不太常見。
針對C#程序員的有用文章參考列表(2012年~)
我想寫這篇文章是因為認為從舊文章中閱讀能加深理解,但考慮到AI時代,這樣的再評價趨勢可能會加速。
非常感謝您閱讀到這裡。如您覺得不錯請按好評,若覺得不妥請按差評👇或者瀏覽器後退,並請訂閱我們的頻道!