CollectionChanged Event
Summary
The CollectionChanged event fires when items in an observable collection (child entities) are added, removed, replaced, or cleared. Use it to react to structural changes in parent-child relationships — for example, recalculating totals when a line item is removed from an order.
When does it fire?
CollectionChanged fires whenever the contents of an ObservableCollection change. This includes:
- Add: A new child entity is added to the collection
- Remove: A child entity is removed from the collection
- Replace: A child entity is replaced with another
- Clear (Reset): The entire collection is cleared
Note: Collections must use
CollectionLoadMode.LoadAll(observable collections) for this event to fire. Paged collections (CollectionLoadMode.Paged) do not support collection change events.
Syntax
salesOrder.Details.OnCollectionChanged()
.Do((parentEntity, args) => { /* logic */ });
The args parameter provides:
args.Action— the type of change (NotifyCollectionChangedAction):Add,Remove,Replace,Resetargs.NewItems— items added (for Add/Replace actions)args.OldItems— items removed (for Remove/Replace actions)
Scenarios
1. Recalculating availability when a detail line is removed
When a sales order detail is removed, recalculate availability for the remaining detail lines.
[Logic]
public class SalesOrderBL(SalesOrder.Logic salesOrder)
{
[RegisterLogic]
public void RecalcOnDetailRemoved()
{
salesOrder.Details.OnCollectionChanged().Do((order, args) =>
{
if (args.Action == NotifyCollectionChangedAction.Remove)
{
foreach (var detail in order.Details.ToList())
{
// Recalculate availability for remaining details
var subtotalState = (IPropertyState)order._State.Subtotal;
subtotalState.GetPropertyLogic().DependencyManager
.MarkDirty(order, subtotalState);
}
}
});
}
}
2. Setting line number value on newly added detail line
When a new detail line is added to the collection, initialize its line order.
[Logic]
public class SalesOrderBL(SalesOrder.Logic salesOrder)
{
[RegisterLogic]
public void InitializeNewDetails()
{
salesOrder.Details.OnCollectionChanged().Do((order, args) =>
{
if (args.Action == NotifyCollectionChangedAction.Add)
{
foreach (SalesOrderDetail detail in args.NewItems!)
{
if (detail.LineOrder == 0)
{
var maxLineOrder = order.Details
.Where(d => d != detail)
.Select(d => d.LineOrder)
.DefaultIfEmpty(0)
.Max();
detail.LineOrder = maxLineOrder + 1;
}
}
}
});
}
}
3. Validating maximum number of detail lines
Prevent adding more than a maximum number of lines to a sales order.
[Logic]
public class SalesOrderBL(SalesOrder.Logic salesOrder)
{
[RegisterLogic]
public void LimitDetailLines()
{
salesOrder.Details.OnCollectionChanged().Do((order, args) =>
{
if (args.Action == NotifyCollectionChangedAction.Add
&& order.Details.Count > 100)
{
var prompts = args.GetService<IUserPromptHandler>();
prompts.AddMessage("A sales order cannot have more than 100 detail lines",
PromptLevel.Error);
}
});
}
}
Observable vs. Paged Collections
Collection change events only work with observable collections (CollectionLoadMode.LoadAll).
| Feature | Observable (LoadAll) |
Paged |
|---|---|---|
| CollectionChanged events | Yes | No |
| Loaded into memory | All items at once | On demand |
| Use when | Small collections (< 100 items) | Large collections |
| Example | SalesOrder.Details |
Product.SalesOrderDetails |
Notes
- In your entity model, use
CollectionLoadMode.LoadAllon the[OppositeSideCollection]attribute for collections that need change events. - CollectionChanged fires synchronously within the same operation.
- For reacting to the deletion of an entity (as opposed to its removal from a collection), see Deleted event.
- When possible, use the Compute event to compute totals rather than the CollectionChanged event. The
DirtyWithRelationmethod on Compute events handles the most common collection-change scenario (recalculating parent aggregates) automatically.