Calculate a Property Value Based on Values from a Detail Collection in EF Core
- 6 minutes to read
This topic describes how to implement a business class, so that one of its properties is calculated based on a property (or properties) of the objects contained in the child object collection.
Tip
A complete sample project is available in the DevExpress Code Examples database at https://supportcenter.devexpress.com/ticket/details/e305/how-to-calculate-a-master-property-based-on-values-from-a-details-collection.
Initial Class Implementation
A Product
class has a collection of Order
objects. The Product
and Order
classes are associated by the One-to-Many relationship, which means that a Product
object may be associated with several Order
objects. The collection of Order
objects is aggregated. Order
objects are created, belonging to one of the Product
objects. When the master object is removed, all objects in its aggregated collection are removed as well.
The following code snippet illustrates the Product
class implementation:
using DevExpress.Persistent.Base;
using DevExpress.Persistent.BaseImpl.EF;
// ...
[DefaultClassOptions]
public class Product : BaseObject {
public virtual string Name { get; set; }
public virtual IList<Order> Orders { get; set; } = new ObservableCollection<Order>();
}
The following code snippet illustrates the Order
class implementation:
using DevExpress.Persistent.Base;
using DevExpress.Persistent.BaseImpl.EF;
// ...
[DefaultClassOptions]
public class Order : BaseObject {
public virtual string Description { get; set; }
public virtual decimal Total { get; set; }
public virtual Product Product { get; set; }
}
In the code above, the Order
class contains the Total
property and the Product
class has the MaximumOrder
and OrdersTotal
properties. These Product
‘s properties are calculated based on Total
properties of the aggregated Orders
. The OrderCount
property is also added to the Product
class. This property exposes the number of aggregated Orders
.
Implement Non-Persistent Calculated Properties
Use one of the following techniques to implement “lazy” calculated properties that are calculated on demand:
Calculate Property Values in Code
Omit the property setter to implement a non-persistent property. The following code snippet demonstrates the implementation of three calculated properties: OrdersCount
, OrdersTotal
, and MaximumOrder
.
Note
The calculated property implementation technique demonstrated in this section allows property values to be calculated based on complex business logic that cannot be expressed with the Criteria Operator syntax. In cases when the Criteria Operator syntax is sufficient to calculate the property values, use the technique described in the Use Criteria Operator Syntax to Calculate Property Values section.
[DefaultClassOptions]
public class Product : BaseObject {
// ...
private int? fOrdersCount = null;
public int? OrdersCount {
get {
return fOrdersCount;
}
}
private decimal? fOrdersTotal = null;
public decimal? OrdersTotal {
get {
return fOrdersTotal;
}
}
private decimal? fMaximumOrder = null;
public decimal? MaximumOrder {
get {
return fMaximumOrder;
}
}
}
Implement an UpdateCalculatedProperties
method that recalculates the OrdersCount
, OrdersTotal
, and MaximumOrder
calculated properties’ values. The method’s implementation must contain business logic used to calculate the values of these properties. Call UpdateCalculatedProperties
from the OnCreated() and OnLoaded() method implementations.
[DefaultClassOptions]
public class Product : BaseObject {
// ...
public void UpdateCalculatedProperties() {
decimal tempMaximum = 0m;
decimal tempTotal = 0m;
foreach (Order detail in Orders) {
if (detail.Total > tempMaximum) {
tempMaximum = detail.Total;
}
tempTotal += detail.Total;
}
fMaximumOrder = tempMaximum;
fOrdersTotal = tempTotal;
fOrdersCount = Orders.Count;
}
public override void OnCreated() {
base.OnCreated();
UpdateCalculatedProperties();
}
public override void OnLoaded() {
base.OnLoaded();
UpdateCalculatedProperties();
}
}
Use Criteria Operator Syntax to Calculate Property Values
This technique is optimal when property value calculation logic can be expressed with Criteria Operator syntax. With this technique the calculated property value can be used for data shaping operations (filtering, sorting, grouping, and so on) in server-based data access modes.
To use Criteria Syntax, decorate properties with the PersistentAliasAttribute with a Criteria Syntax expression as a parameter. To calculate the property value based on the specified expression, call the BaseObject.EvaluateAlias method from the property getter.
[DefaultClassOptions]
public class Product : BaseObject, INotifyPropertyChanged {
// ...
[PersistentAlias("[Orders].Count()")]
public int OrdersCount {
get {
return EvaluateAlias<int>();
}
}
[PersistentAlias("[Orders].Sum(Total)")]
public decimal OrdersTotal {
get {
return EvaluateAlias<decimal>();
}
}
[PersistentAlias("[Orders].Max(Total)")]
public decimal MaximumOrder {
get {
return EvaluateAlias<decimal>()
}
}
}
Tip
To immediately update a calculated property’s value displayed in the UI as a user changes property editor values, decorate properties used in the expression with the ImmediatePostDataAttribute.
Add an UpdateCalculatedProperties
method and call it from the OnCreated() and OnLoaded() method implementations. On the next step, you will use this method to update the UI when the Orders
collection changes.
[DefaultClassOptions]
public class Product : BaseObject {
// ...
public void UpdateCalculatedProperties() {
}
public override void OnCreated() {
base.OnCreated();
UpdateCalculatedProperties();
}
public override void OnLoaded() {
base.OnLoaded();
UpdateCalculatedProperties();
}
}
Update UI When Calculated Property Values Change
To automatically recalculate the calculated property values when items are added to or removed from the Orders
collection as well as when Product
objects are loaded, handle the Orders
collection’s CollectionChanged
event. In the event handler, call the UpdateCalculatedProperties
method implemented in the previous step.
[DefaultClassOptions]
public class Product : BaseObject {
// ...
public override void OnCreated() {
base.OnCreated();
UpdateCalculatedProperties();
((ObservableCollection<Order>)this.Orders).CollectionChanged += Product_CollectionChanged;
}
public override void OnLoaded() {
base.OnLoaded();
UpdateCalculatedProperties();
((ObservableCollection<Order>)this.Orders).CollectionChanged += Product_CollectionChanged;
}
private void Product_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) {
UpdateCalculatedProperties();
}
}
For the user interface to reflect the changes in the calculated property values, explicitly generate a notification through the INotifyPropertyChanged interface. To do this, implement the INotifyPropertyChanged
interface and modify the Product
class as shown below:
[DefaultClassOptions]
public class Product : BaseObject, INotifyPropertyChanged {
// ...
private string name;
public virtual string Name {
get { return name; }
set {
if (name != value) {
name = value;
RaisePropertyChanged(nameof(Name));
}
}
}
// ...
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string name) {
if(PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
public void UpdateCalculatedProperties() {
// ...
RaisePropertyChanged(nameof(OrdersCount));
RaisePropertyChanged(nameof(OrdersTotal));
RaisePropertyChanged(nameof(MaximumOrder));
}
}
To automatically recalculate calculated property values when items in the Orders
collection are modified, handle the Order
class’s PropertyChanged
event. In the event handler, call the Product.UpdateCalculatedProperties
method.
[DefaultClassOptions]
public class Order : BaseObject {
// ...
private void Order_PropertyChanged(object sender, PropertyChangedEventArgs e) {
Product?.UpdateCalculatedProperties();
}
public override void OnLoaded() {
base.OnLoaded();
((INotifyPropertyChanged)this).PropertyChanged += Order_PropertyChanged;
}
public override void OnCreated() {
base.OnCreated();
((INotifyPropertyChanged)this).PropertyChanged += Order_PropertyChanged;
}
}