Changed Event
Summary
The Changed event runs logic immediately after a property value has been successfully set and validated. Use it to react to a value change and update other properties, synchronize related data, or trigger side effects.
When does it fire?
Changed fires at the end of the property set pipeline, after validation succeeds:
Set property → ReadOnly → AutoCorrect → Validate → Dirty dependents → Changed
Changed also fires on properties that were dirtied by the change. For example, if setting Quantity dirties TotalPrice, and TotalPrice has a Compute subscriber, then after TotalPrice recomputes, its Changed subscriber fires too.
Syntax
Property-level syntax:
productUom.SellableOption.OnChanged()
.Do((uom, args) => { /* logic */ });
Entity-level syntax:
productUom.OnChanged(u => u.SellableOption)
.Do((uom, args) => { /* logic */ });
Fluent API
| Step | Method | Description |
|---|---|---|
| Required | .Do(action) |
The action to execute when the value changes |
Do overloads
| Signature | Description |
|---|---|
.Do(entity => { }) |
React with access to the entity |
.Do((entity, args) => { }) |
React with access to the entity and event args (services, data access) |
Scenarios
1. Enforcing a single default selling unit
When a UOM is marked as the default selling unit, all other UOMs on the same product should be cleared of that designation.
[Logic]
public class ProductUomBL(ProductUom.Logic productUom)
{
[RegisterLogic]
public void EnforceSingleDefaultSellingUnit()
{
productUom.OnChanged(u => u.SellableOption)
.Do((uom, args) =>
{
var product = uom.Product;
if (product == null) return;
if (uom.SellableOption == SellableOption.DefaultSellingUnit)
{
var otherDefaults = product.Uoms
.Where(u => u != uom && u.SellableOption == SellableOption.DefaultSellingUnit);
foreach (var other in otherDefaults)
other.SellableOption = SellableOption.Sellable;
}
});
}
}
2. Updating accessory quantities when the parent line quantity changes
When a sales order detail's quantity changes, all associated accessory lines should have their quantities recalculated.
[Logic]
public class SalesOrderDetailBL(SalesOrderDetail.Logic salesOrderDetail)
{
[RegisterLogic]
public void UpdateAccessoryQuantities()
{
salesOrderDetail.OnChanged(d => d.Quantity)
.Do((detail, args) =>
{
if (!detail.Accessories.Any() || detail.Product is null) return;
foreach (var accessoryDetail in detail.Accessories)
{
if (accessoryDetail.Product == null) continue;
var accessory = detail.Product.Accessories
.FirstOrDefault(pa => pa.AccessoryProduct == accessoryDetail.Product);
if (accessory == null) continue;
accessoryDetail.Quantity = Math.Round(
detail.Quantity * accessory.Quantity,
accessory.RoundingType);
}
});
}
}
3. Splitting a full name into first and last name
When a virtual FullName property is set directly, split it back into its component parts. This pairs with a Compute subscriber that builds the full name from first/last.
[Logic]
public class ContactBL(Contact.Logic contact)
{
[RegisterLogic]
public void SplitFullName()
{
contact.OnChanged(c => c.FullName)
.Do(c =>
{
var parts = c.FullName?.Split(' ', 2);
if (parts?.Length == 2)
{
c.FirstName = parts[0];
c.LastName = parts[1];
}
});
}
}
Note: A Compute/Changed subscriber pair on the same property will not cause an infinite loop. The Changed subscriber does not fire when the value is set by Compute.
4. Recalculating unit price when total price is directly edited
When a user manually changes the total price on a line, back-calculate the unit price from the new total divided by the quantity.
[Logic]
public class SalesOrderDetailBL(SalesOrderDetail.Logic salesOrderDetail)
{
[RegisterLogic]
public void RecalcUnitPriceFromTotal()
{
salesOrderDetail.OnChanged(d => d.TotalPrice)
.Do(d =>
{
if (d.Quantity == 0 && d.TotalPrice != 0)
d.Quantity = 1;
if (d.Quantity != 0)
d.UnitPrice = d.TotalPrice / d.Quantity;
});
}
}
5. Synchronizing customer data to the order header
When the sell-to customer changes on a sales order, copy the customer's billing address to the order.
[Logic]
public class SalesOrderBL(SalesOrder.Logic salesOrder)
{
[RegisterLogic]
public void SyncCustomerAddress()
{
salesOrder.OnChanged(o => o.SellToCustomer)
.Do((order, args) =>
{
if (order.SellToCustomer?.PrimaryContact != null)
order.BillToContact = order.SellToCustomer.PrimaryContact;
});
}
}
Notes
- Changed fires synchronously within the same property-set operation.
- Setting other properties inside a Changed handler will trigger their own set pipelines (AutoCorrect → Validate → Changed), so be mindful of cascading effects.
- Use Changed for imperative side effects. Use Compute for derived values that can be lazily recalculated.