How To implement calculated values |
This article describes how to keep a field's value updated from other values of the same item or other related items.
Consider a Record content-type, with the following content:
Name | Location | Duration (Text) | Seconds (Numeric) |
---|---|---|---|
Branko Petrović | Dubai, UAE | 11:54 | ? |
Stéphane Mifsud | La Crau, France | 11:35 | ? |
Tom Sietas | Athens, Greece | 10:12 | ? |
We want to fill the "Seconds" value from the "Duration" value.
First, open the object's custom class corresponding to your content-type ([Generated_Project]\Objects\Record.Custom.cs).
One possibility is to attach to the BeforeSave event:
protected override void OnInitialized() { base.OnInitialized(); this.BeforeSave += Record_BeforeSave; } void Record_BeforeSave(object sender, EventArgs e) { UpdateSecondsValue(); } private void UpdateSecondsValue() { //Split minutes and seconds String[] parts = this.__Duration.Split(':'); int minutes = parts.First().GetInt(); int seconds = parts.Length > 1 ? parts.Last().GetInt() : 0; //Update seconds this.__Seconds = minutes * 60 + seconds; }
You can also override the Save method:
public override void Save() { UpdateSecondsValue(); //Update value base.Save(); //Apply changes in database }
Not calling the base.Save() will prevent the database update.
Note: you could throw an exception (like ValidationException) before the base call if, for example, something went wrong with the duration format.
Consider a SpaceTraveler content-type, with the following content:
Nationality (choice targeting the Country content-type) | Name | Days in space | Flights |
---|---|---|---|
Russia | Gennady Padalka | 878 | 5 |
Russia | Yuri Malenchenko | 827 | 6 |
Russia | Sergei Krikalev | 803 | 6 |
United States | Scott Kelly | 520 | 4 |
United States | Michael Fincke | 381 | 3 |
Japan | Koichi Wakata | 347 | 4 |
We want to sum the number of flight days in each corresponding country.
To achieve that, we add the following code in the Country class ([Generated_Project]\Objects\Country.Custom.cs):
public override void Save() { SumFlightDays(); base.Save(); } internal void SumFlightDays() { List<SpaceTraveler> travelers = this.__SpaceTraveler_Nationality_Get(); //Gets travelers using the relation this.__TotalFlightDays = travelers.Sum(t => t.__Days); }
A problem remains: if a SpaceTraveler is changed, the sum of the country is not.
We can ensure the synchronization with a post-save code on the SpaceTraveler ([Generated_Project]\Objects\SpaceTraveler.Custom.cs):
public override void Save() { base.Save(); PFRunAsAdmin.Run(this.Site, false, (adminSite) => { Country country = this.__SpaceTraveler_Nationality_Get().FirstOrDefault(); country.Save(); //Internally calls SumFlightDays }); }
Note |
---|
The use of PFRunAsAdmin is recommended if you aren't sure about the permissions of the user saving the SpaceTraveler object. Without it, if a user saves the SpaceTraveler and does not have the permission to update the corresponding country, an exception will be thrown. The adminSite parameter given to the delegate is not used. It is due to the "false" sent to the Run method. It signifies that we want to temporally upgrade permissions on the current PFSite instance. If "true" was set, adminSite would have been a whole new instance of PFSite, requiring to load again the content-type and the current item. |
Now, we can observe that this code adds three new queries to the SpaceTraveler.Save (load country, load country travelers, save country).
The performance cost can increase if the number of SpaceTravelers grows.
A first possibility is to avoid loading all travelers and get the sum from an SQL aggregation:
internal void SumFlightDays() { PFQuery query = new PFQuery(); query.AddAggregation("FlightDaysCount", SpaceTraveler.FieldName_Days, PFQueryAggregationType.Sum); PFGroupedObjects aggregation = this.GetRelatedItemsByGroup( this.ParentApplication.Relation_SpaceTraveler_Nationality, query).First(); this.__TotalFlightDays = aggregation.Data.GetValueDecimal("FlightDaysCount"); }
There is another way, avoiding the query completely: apply the difference to the country.
This could be done by disabling the SumFlightDays method and apply this code in the SpaceTraveler class:
public override void Save() { decimal? daysDifference = null; //If the value has changed since its load from the database. if (this.Data.HasPropertyChanged(FieldName_Days)) { //Get the days value currently stored in the database (SQL is not queried). decimal? previousDays = (decimal?)this.Data.GetSavedValueObject(FieldName_Days); //The value could be empty if it has never been saved, like before creation. if (!previousDays.HasValue) previousDays = 0; daysDifference = this.__Days - previousDays; } base.Save(); if (daysDifference.HasValue) { PFRunAsAdmin.Run(this.Site, false, (adminSite) => { Country country = this.__SpaceTraveler_Nationality_Get().FirstOrDefault(); country.__TotalFlightDays += daysDifference.Value; country.Save(); }); } }
As you can see, there are a lot of possibilities with our API. Other interesting cases can be investigated like how to adapt each solution when a SpaceTraveler is deleted or when its nationality changes. We let you discover it.