Click or drag to resize
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.

Basic case

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:

C#
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:

C#
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.

Advanced case

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):

C#
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):

C#
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 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:

C#
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:

C#
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.