Click or drag to resize

Undo and Redo

[This is preliminary documentation and is subject to change.]

Undo Basics

When undo is enabled by calling BcfDataSetEnableMemoryUndo or EnableFileUndo or EnableFileUndoSession new transactions will serve as undo/redo step after BcfTransactionCommit. See:

Undo/Redo works based on CalculationWorks.BusinessModelBcfTransaction rollback-info. BCF allows to exclude columns from undo/redo. This can be done by setting IncludeInUndo = false.

When a transaction commits it removes such cell-updates from its items before it is passed to UndoRepository.
  • What happens with empty transactions?

    A transaction without items will be dropped after commit. Undo- and redo-stacks remain unchanged.

    When updating undo-excluded cells only, the transaction will be empty if columns have set ShouldPersist = false; otherwise transaction will contain an update-CurrentDataStateId-item.

  • What happens with computed columns having IncludeInUndo = false?

    Those cells will be recomputed when their row is restored (undo delete | redo add) or when a cell-value it depends on, is changed by undo/redo.

There are three overloads to enable undo/redo.

Memory Based Undo

Using BcfDataSet.EnableMemoryUndo() initializes memory based undo. Memory undo causes BcfUndoRepository to keep references on transaction-objects until the stacks are cleared.

Memory undo is easy to use and very fast.

Caution note Caution
Having many large transactions in repository over a long time may cause garbage collection issue: application blocks while GC collects. For example having 100.000 transactions on undo-stack and each of them contains 10.000 items, GC has to check at least (depends on item-type) 1.000.000.000 object-references when it collects 2nd generation objects.
To reduce memory consumption you can limit the number of undo items by setting MaxItems. When exceeding the limit the oldest transaction will be removed from undo-stack before the new one is pushed.

File Based Undo

Using BcfDataSet.EnableFileUno(Stream, Stream) initializes file based undo. File based undo requires some additional code for serialization. To cause BCF-Editor to generate the code check "Support Undo Persistence" on "Project-Tab / DataSetSetup".

Use file based undo if you cannot limit MaxItems or when having columns containing large strings or byte-arrays.

File Based Undo Session

Using BcfDataSet.EnableFileUndoSession(Stream, Stream, byte[]) initializes a file based undo session. File based undo session requires the same additional code like File Based Undo.

In opposition to File Based Undo a session is recoverable from the files. The dataset-schema the files were created must match the schema the files will be restored. Use the 3rd parameter to place a custom header inside a file to perform a compatibility-check (e.g. assembly version). Before restoring a session using RestoreUndoRepository check the compatibility using GetCustomHeaderAsync.

Caution note Caution

Do not use "File Based Undo Session"-feature as "Save As" - file-format. File migration after dataset-schema update is not supported.

Differences between Memory and File Undo

Special settings may cause different values when comparing results using memory and file undo. This is because lifetime of BcfRow depends on undo. Using memory-undo a deleted row will still be referenced. When undo delete the row-object will be restored as row. Using file-undo deleting a row will store cell-values in file and the row will be discarded. When undo delete, a new row-object is created and cell-values will be restored from file. So far so good. Having a not computed column with IncludeInUndo = false, after undo delete, using memory-undo the cell-value will be unchanged; using file-undo cell-value is columns default value. The same issue appears when adding fields to a row.

Note Note
BCF-Editor shows a warning when "Support Undo Persistence" is enabled and a column is not computed and excluded from undo.
Note Note
Use always same undo/redo approach in product-code and unit-test. When using a file based approach, consider using memory-streams in unit-tests.

File undo does not manage object-references. For example: having a string-array on main-row and on child-row a computed column simply references the parent-string-array. Every time a new string-array is set on parent row, all child-rows string-array cells are updated and for each cell the old and new string-arrays are serialized into file. When undo, parent and child string-arrays will be deserialized to distinct locations in memory. So all string-array cells have the same content but different references. Excluding the computed column from undo will result in reduced file-size, better performance and reference equality of string-arrays.

Trouble Shooting
IncludeInUndo is read-only in BCF-Editor. How to exclude a column?

Check "Allow Partial Undo" on "Project-Tab / DataSetSetup".

Which columns should be excluded from undo?

All computed columns, except calculation is expensive.

Since faults are not serializable all rules are excluded from undo when using file-undo. Memory undo without excluded columns causes dataset to accept values from undo-items without checking dependencies of computed columns and rules. All values will be restored and calculation will be omitted.

I have columns with non-serializable type. How to use file-undo?

If the columns are computed, consider exclude from undo; otherwise define a surrogate-converter and configure it in BCF-Editor "Menu - View - TypeSettings".

I have a column (not computed) of a disposable type and use DiscardAction to dispose values. Using memory-undo will dispose the values not until Clear. How can I switch to file-undo? Will DiscardAction work with file-undo?

DiscardAction works using file-undo. As like as for non-serializable types: create a not disposable and serializable surrogate-type and add surrogate-conversion.

I used List<int> as column type. After adding a column of int[] I get compiler warnings and or exceptions with "invalid DataContract".

For the reason: read the messages and refer to .net documentation. It is an known limitation using DataContracts. To work around use same list-type anywhere or use a surrogate-converter for one type.

I use an interface as column type. When setting a cell-value I get an exception with "DataContract".

Same issue appears with base-type columns and instance-types. You have to register all possible instance-types. BCF-Editor generates a partial class "{dataset-name}UndoItems" at the end of generated DataSetSetup-file. Create another part of the class and add the instance-types using the KnownType-Attribute (like other types in generated code).

Using Surrogates
Example: Using a serialization surrogate.
C#
// When having a column of IDictionary<int, string> not excluded from undo we have to configure a serialization surrogate to use file-based undo.
// The surrogate type (the serializable type) is: KeyValuePair<TKey, TValue>[]
// Place the surrogate converter code below info the project you referenced in BCF-Editor/Project tab/DataSetSetup/C# Project File
// Add an entry in BCF-Editor/Edit/TypeSettings with Type=IDictionary<int, string> and SurrogateConverter=new DictionaryInterfaceSurrogate<int, string>()
// Hint: Do not forget reanalyze (F5) after adding the code. Adjust namespaces.
public class DictionaryInterfaceSurrogate<TKey, TValue> : BcfSurrogateConverter<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>[]> 
{
    public override KeyValuePair<TKey, TValue>[] ConvertToSurrogate(IDictionary<TKey, TValue> value) { return value?.ToArray(); }
    public override IDictionary<TKey, TValue> ConvertToValue(KeyValuePair<TKey, TValue>[] dataContractObject) { return dataContractObject?.ToDictionary(e => e.Key, e => e.Value); }
}