XamDataGrid では IsUndoEnabled プロパティを True に設定することによって Undo(元に戻す)/ Redo(やり直し) 操作を有効にすることができます。
履歴に保存する最大操作数は UndoLimit プロパティから指定することができます。

<igWPF:XamDataGrid .....
	IsUndoEnabled="True"
	UndoLimit="10">
.....
</igWPF:XamDataGrid>

上記設定により、XamDataGrid のセルの値の変更や新規行追加といった編集操作に加え、列の並べ替えやグループ化、行の展開などのユーザー操作を「元に戻す/やり直す」ことが可能になります。

これらのユーザー操作に加えて行の削除操作を「元に戻す/やり直す」には、 UndeleteRecordsStrategy クラスを実装します。UndeleteRecordsStrategy クラスでは CanUndelete および Undelete メソッドをオーバーライドする必要があります。特に Undelete メソッドでは、渡されたRecordInfoインスタンスから実際のデータ項目にマップしたディクショナリを返す点に注意してください。
以下は、XamDataGrid で使用するデータが ObsevableCollection の場合の UndeleteRecordsStrategy クラスの実装例です。

public abstract class UnboundCellRecordUndeleteStrategy : UndeleteRecordsStrategy
{
	private List<Field> _unboundFields;
	private Dictionary<object, IList<object>> _unboundValues;
	private Dictionary<object, object> _newToOldMapping;

	protected UnboundCellRecordUndeleteStrategy(IList<Record> records)
	{
		if (records == null)
			throw new ArgumentNullException("records");

		if (records.Count > 0)
		{
			List<Field> unboundFields = new List<Field>();
			FieldLayout fl = records[0].FieldLayout;

			foreach (Field field in fl.Fields)
			{
				if (field.BindingType == BindingType.UseNameBinding || field.IsPlaceholderForTreeView)
					continue;

				if (field.AlternateBinding != null)
				{
					BindingMode? bindingMode = null;

					if (field.AlternateBinding is Binding)
						bindingMode = ((Binding)field.AlternateBinding).Mode;
					else if (field.AlternateBinding is MultiBinding)
						bindingMode = ((MultiBinding)field.AlternateBinding).Mode;

					if (bindingMode != null)
					{
						switch (bindingMode)
						{
							case BindingMode.OneWay:
							case BindingMode.Default:
							case BindingMode.OneTime:
								continue;
						}
					}
				}

				unboundFields.Add(field);
			}

			int unboundFieldCount = unboundFields.Count;

			if (unboundFieldCount > 0)
			{
				_unboundFields = unboundFields;
				_unboundValues = new Dictionary<object, IList<object>>();

				foreach (Record record in records)
				{
					DataRecord dataRecord = record as DataRecord;

					if (null == dataRecord)
						continue;

					object dataItem = dataRecord.DataItem;

					if (dataItem == null)
						continue;

					object[] unboundValues = new object[unboundFieldCount];

					for (int i = 0; i < unboundFieldCount; i++)
						unboundValues[i] = dataRecord.Cells[unboundFields[i]].Value;

					_unboundValues[dataItem] = unboundValues;
				}
			}
		}
	}

	public override IDictionary<RecordInfo, object> Undelete(IList<RecordInfo> oldRecords)
	{
		IDictionary<RecordInfo, object> oldToNewMapping = this.UndeleteOverride(oldRecords);

		if (null != oldToNewMapping && oldToNewMapping.Count > 0)
		{
			_newToOldMapping = new Dictionary<object, object>();

			foreach (KeyValuePair<RecordInfo, object> pair in oldToNewMapping)
				_newToOldMapping[pair.Value] = pair.Key.DataItem;
		}

		return oldToNewMapping;
	}

	public override void ProcessUndeletedRecords(IList<DataRecord> recordsCreated)
	{
		if (_newToOldMapping == null || _newToOldMapping.Count == 0)
			return;

		// we're only using this to reset the unbound cell values
		if (_unboundFields == null || _unboundFields.Count == 0)
			return;

		foreach (DataRecord newRecord in recordsCreated)
		{
			object oldDataItem;

			if (!_newToOldMapping.TryGetValue(newRecord.DataItem, out oldDataItem))
				continue;

			IList<object> unboundValues;

			if (!_unboundValues.TryGetValue(oldDataItem, out unboundValues))
				continue;

			for (int i = 0; i < unboundValues.Count; i++)
			{
				Field fld = _unboundFields[i];

				Debug.Assert(fld.Owner == newRecord.FieldLayout, "Record is associated with a different field layout!");

				// if somehow the field was removed then skip it
				if (fld.Owner == newRecord.FieldLayout && fld.Index >= 0)
					newRecord.Cells[fld].Value = unboundValues[i];
			}
		}
	}

	protected abstract IDictionary<RecordInfo, object> UndeleteOverride(IList<RecordInfo> oldRecords);
}

public class ListUndeleteStrategy : UnboundCellRecordUndeleteStrategy
{
	public ListUndeleteStrategy(IList<Record> records)
		: base(records)
	{
		// since this class is set up to reuse the same old items, we can only
		// support a subset of list types. also since we want to ensure the 
		// grid knows when the records are undeleted we're limiting this to 
		// known classes that send collection change notifications as well.
		foreach (Record record in records)
		{
			IEnumerable dataSource = record.RecordManager.SourceItems;

			if (dataSource is DataView || dataSource is DataViewManager)
				throw new InvalidOperationException("This class cannot be used with datatables/dataviews since it just reinserts the same item back into the collection.");

			if (dataSource is IBindingList)
				continue;

			if (dataSource is IList && dataSource is INotifyCollectionChanged)
				continue;

			throw new ArgumentException("The ListUndeleteStrategy is only supported with IBindingList and INotifyCollectionChanged sources");
		}
	}

	public override bool CanUndelete(IList<UndeleteRecordsStrategy.RecordInfo> oldRecords)
	{
		return true;
	}

	protected override IDictionary<RecordInfo, object> UndeleteOverride(IList<RecordInfo> oldRecords)
	{
		Dictionary<RecordInfo, object> oldToNewMapping = new Dictionary<RecordInfo, object>();

		RecordInfo[] records = oldRecords.ToArray();
		Comparison<RecordInfo> comparison = delegate (RecordInfo item1, RecordInfo item2)
		{
			return item1.DataItemIndex.CompareTo(item2.DataItemIndex);
		};

		// sort by the original index to ensure we get them in the original order
		Utilities.SortMergeGeneric(records, Utilities.CreateComparer(comparison));

		foreach (RecordInfo record in records)
		{
			RecordManager rm = record.RecordManager;
			IList list = rm.SourceItems as IList;

			if (list == null)
				continue;

			int newIndex = Math.Min(list.Count, record.DataItemIndex);
			object newDataItem = record.DataItem;

			list.Insert(newIndex, newDataItem);

			// since we're reusing the same object the old and new will be the same
			oldToNewMapping[record] = newDataItem;
		}

		return oldToNewMapping;
	}
}

以上のようにして実装した UndeleteRecordsStrategy の派生クラスである ListUndeleteStrategy のインスタンスを、XamDataGrid の RecordsDeleting イベントで以下のように引数の UndeleteStrategy プロパティに対して割り当てます。

private void xamDataGrid1_RecordsDeleting(object sender, Infragistics.Windows.DataPresenter.Events.RecordsDeletingEventArgs e)
{
	e.UndeleteStrategy = new ListUndeleteStrategy(e.Records);
}

以上によって行の削除を含めたユーザー操作の「元に戻す/やり直し」が可能になりました。

Undo/Redo 操作実行用に Button を二つ用意し、Command プロパティをバインドします。
(ここではコマンドを使用してプログラムから Undo/Redo を実行していますが、通常のキーボードショートカットである Ctrl + Z および Ctrl + Y によっても同じ動作が得られます。)

<StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Stretch">
	<Button
		Command="{x:Static igWPF:DataPresenterCommands.Undo}"
		CommandTarget="{Binding ElementName=xamDataGrid1}">
		<StackPanel>
			<Path 
				HorizontalAlignment="Center"
				Data="{StaticResource UndoIcon}"
				Fill="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType=ContentControl}}" />
			<TextBlock Text="元に戻す"/>
		</StackPanel>
	</Button>
	<Button 
		Command="{x:Static igWPF:DataPresenterCommands.Redo}"
		CommandTarget="{Binding ElementName=xamDataGrid1}">
		<StackPanel>
			<Path 
				HorizontalAlignment="Center"
				Data="{StaticResource RedoIcon}"
				Fill="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType=ContentControl}}" />
			<TextBlock Text="やり直し"/>
		</StackPanel>
	</Button>
</StackPanel>
Tagged:

製品について

Ultimate UI for WPF