There are three basic kinds of events that GoDiagram generates: DiagramEvents, InputEvents, and ChangedEvents. This page talks about the latter, which are generated as Diagrams, GraphObjects, Models, or Model data objects are modified. See the page Events for the former two kinds of events.
ChangedEvents in GoDiagram are notifications of state changes, mostly object property changes. The ChangedEvent records the kind of change that occurred and enough information to be able to undo and redo them.
Changed events are produced by both Model and Diagram. They are multicast events, so you can subscribe or unsubscribe from Model.Changed and Diagram.Changed. For convenience you can also subscribe or unsubscribe to Model changes on a Diagram: Diagram.ModelChanged. ChangedEvents received by a Model change handler will have a non-null value for ChangedEvent.Model. Similarly, ChangedEvents received by a Diagram change handler will have non-null value for ChangedEvent.Diagram.
A Diagram always subscribes to its Model, so that it can automatically notice changes to the model and update its Parts accordingly. Furthermore the UndoManager, if enabled, automatically listens to changes to both the model and the diagram, so that it can record the change history and perform undo and redo.
Model ChangedEvents record state changes either to data in a model or to the Model itself. ChangedEvents for models are generated by calls to Model.Set and by Model property setters.
For property changes, that information includes the ChangedEvent.Object that was modified, the ChangedEvent.PropertyName, and the ChangedEvent.OldValue and ChangedEvent.NewValue values for that property. Property changes are identified by the ChangedEvent.Change property value being ChangeType.Property.
Some changes represent structural changes to the model, not just simple model data changes. "Structural" changes are the insertion, modification, or removal of relationships that the model is responsible for maintaining. In such cases the ChangedEvent.ModelChange property will be a value other than ModelChangeType.None. The following names for Property ChangedEvents correspond to structural model data changes:
The value of ChangedEvent.ModelChange will be the corresponding enum value. The value of ChangedEvent.PropertyName depends on the name of the actual data property that was modified. For example, for the model property change "LinkFromKey", the actual property name defaults to "From". But you might be using a different property name by having set GraphLinksModel.LinkFromKeyProperty to some other data property name.
Any property can be changed on a node data or link data object, by calling Model.Set. Such a call will result in the property name being recorded as the ChangedEvent.PropertyName. These cases are treated as normal property changes, not structural model changes, so ChangedEvent.ModelChange will be ModelChangeType.None. The value of ChangedEvent.Object will of course be the object that was modified.
Some changes may happen temporarily because some code, such as in a Tool, might want to use temporary objects for their own purposes. However your change listener might not be interested in such ChangedEvents. If that is the case, you may want to ignore the ChangedEvent if Model.SkipsUndoManager (or Diagram.SkipsUndoManager) is true.
Finally, there are property changes on the model itself. For a listing of such properties, see the documentation for Model, GraphLinksModel, and TreeModel. These cases are also treated as normal property changes, so ChangedEvent.ModelChange will be ModelChangeType.None. Both ChangedEvent.Model and ChangedEvent.Object will be the model itself.
Other kinds of changed events include ChangeType.Insert and ChangeType.Remove. In addition to all of the previously mentioned ChangedEvent properties used to record a property change, the ChangedEvent.OldParam and ChangedEvent.NewParam provide the "index" information needed to be able to properly undo and redo the change.
The following names for Insert and Remove ChangedEvents correspond to model changes to collections:
The final kind of model changed event is ChangeType.Transaction. These are not strictly object changes in the normal sense, but they do notify when a transaction starts or finishes, or when an undo or redo starts or finishes.
The following values of ChangedEvent.PropertyName describe the kind of transaction-related event that just occurred:
In each case the ChangedEvent.Object is the Transaction holding a sequence of ChangedEvents. The ChangedEvent.OldValue is the name of the transaction -- the string passed to UndoManager.StartTransaction or UndoManager.CommitTransaction. The various standard commands and tools that perform transactions document the transaction name(s) that they employ. But your code can employ as many transaction names as you like.
As a general rule, you should not make any changes to the model or any of its data in a handler for any Transaction ChangedEvent.
It is commonplace to want to update a server database when a transaction has finished. Use the ChangedEvent.IsTransactionFinished read-only property to detect that case. You'll want to implement a Changed listener as follows:
// notice whenever a transaction or undo/redo has occurred
diagram.ModelChanged += (s, e) => {
if (e.IsTransactionFinished) SaveModel(e.Model);
};
The value of Transaction.Changes will be a List of ChangedEvents, in the order that they were recorded.
Those ChangedEvents represent changes both to the Model and to the Diagram or its GraphObjects.
Model changes will have e.model != null
; diagram changes will have e.diagram != null
.
If you do not want to save the whole model at the end of each transaction, but only certain changes to the model, you can iterate over the list of changes to pick out the ones that you care about. For example, here is a listener that logs a message only when node data is added to or removed from the Model.NodeDataSource.
diagram.ModelChanged += (s, e) => {
// ignore unimportant Transaction events
if (!e.IsTransactionFinished) return;
// a Transaction
if (e.Object is not Transaction txn) return;
// iterate over all of the actual ChangedEvents of the Transaction
foreach (var te in txn.Changes) {
// ignore any kind of change other than adding/removing a node
if (te.ModelChange != ModelChangeType.NodeDataSource) return;
// record node insertions and removals
if (te.Change == ChangeType.Insert) {
Console.WriteLine(e.PropertyName + " added node with key: " + (te.NewValue as NodeData).Key);
} else if (e.Change == ChangeType.Remove) {
Console.WriteLine(e.PropertyName + " removed node with key: " + (te.OldValue as NodeData).Key);
}
}
};
The above listener will put out messages as the user adds nodes (including by copying) and deletes nodes. The ChangedEvent.PropertyName of the Transaction event (i.e. e in the code above) will be either "CommittedTransaction", "FinishedUndo", or "FinishedRedo". Note that a "FinishedUndo" of the removal of a node is really adding the node, just as the undo of the insertion of a node actually removes it.
Similarly, here is an example of noticing when links are connected, reconnected, or disconnected. This not only checks for insertions to and removals from GraphLinksModel.LinkDataSource, but also changes to the "From" and the "To" properties of the link data.
diagram.ModelChanged += (s, e) => {
// ignore unimportant Transaction events
if (!e.IsTransactionFinished) return;
// a Transaction
if (e.Object is not Transaction txn) return;
// iterate over all of the actual ChangedEvents of the Transaction
foreach (var te in txn.Changes) {
// record link insertions and removals
if (e.Change == ChangeType.Property) {
if (te.ModelChange == ModelChangeType.LinkFromKey) {
Console.WriteLine(e.PropertyName + " changed From key of link: " +
te.Object + " from: " + te.OldValue + " to: " + te.NewValue);
} else if (te.ModelChange == ModelChangeType.LinkToKey) {
Console.WriteLine(e.PropertyName + " changed To key of link: " +
te.Object + " from: " + te.OldValue + " to: " + te.NewValue);
}
} else if (e.Change == ChangeType.Insert && e.ModelChange == ModelChangeType.LinkDataSource) {
Console.WriteLine(e.PropertyName + " added link: " + te.NewValue);
} else if (e.Change == ChangeType.Remove && e.ModelChange == ModelChangeType.LinkDataSource) {
Console.WriteLine(e.PropertyName + " added link: " + te.OldValue);
}
}
};
Note: the above code only works for a GraphLinksModel, where the link data are separate objects.
Look at the Update Demo for a demonstration of how you can keep track of changes to a model when a transaction is committed or when an undo or redo is finished. The common pattern is to iterate over the ChangedEvents of the current Transaction in order to decide what to record in a database.
It is also possible to send incremental updates to a database using Model.ToIncrementalJson or Model.ToIncrementalData, which iterate over the changes in a transaction and group them into a JSON-formatted string or an object representing any updates.
diagram.ModelChanged += (s, e) => {
// ignore unimportant Transaction events
if (!e.IsTransactionFinished) return;
var json = (e.Model as MyModel).ToIncrementalJSON(e);
var data = (e.Model as MyModel).ToIncrementalData(e);
// ...
};
Caution: don't call JsonSerializer.Serialize
on the result of Model.ToIncrementalData,
because that will not properly handle any instances of classes that are referenced by the object's properties.
Instead call Model.ToIncrementalJson, which will produce a more compact textual serialization.
Diagram ChangedEvents record state changes to GraphObjects or RowColumnDefinitions in a diagram, or to a Layer in a diagram, or to the Diagram itself. For such events, ChangedEvent.Diagram will be non-null.
Most ChangedEvents for diagrams record property changes, such as when some code sets the TextBlock.Text property or the Part.Location property. There are never any ChangedEvents for diagrams that are ChangeType.Transaction.
Although ChangedEvents for diagrams are important for undo/redo in order to retain visual fidelity, one normally ignores them when saving models. Only ChangedEvents for models record state changes to model data. So for saving to a database, you will want to consider only those ChangedEvents for which ChangedEvent.Model is non-null.