Thursday, April 17, 2025

Detecting End-User Changes to Fields Before Saving/Abandoning Changes

There are of course many ways to approach how to detect if somebody changed a value in a field and set a flag to know if something needs to be "saved".  However, it can get complex if you are doing this for many MANY fields or also for a gallery of items where changes could be made to many of them at once.

This is an example of several ways to approach the problem so you can have some standardized code to make updates back to your datasource much easier and more efficient (and not throw errors).

TLDR Simple Setup

Assuming we have an original Collection of people and details, we want to create a Copy of that at the start of the process and then update the original (or copy) and ultimately compare differences in the fields to know if you need to update that record.

For the page where you're doing the edits, set the page's OnVisible to make a copy of the Collection:

ClearCollect(
    orig_cachedCollection,
    cachedCollection
);

Change all of your editable fields' OnChange (or OnSelect for say a CheckBox as shown) to fire code to update the Collection:

UpdateIf(
    cachedCollection,
    ID = ThisItem.ID,
    {Name: Self.Text}
)

All you'd need to change is the field you're updating (in blue) and for controls other than a Text Input field you'd use the appropriate commands to reference the value (e.g. Dropdown1.Selected.Value).

This gives you now two Collections:

  • orig_cachedCollection - original values without changes
  • cachedCollection - identical to original until any field is changed

As fields are changed and values updated, the cachedCollection is updated.  To see which fields have changed, a Filter() command is used to compare the two collections for all editable values:

Filter(
        cachedCollection As tmpItem,
        tmpItem.Name <> LookUp(
            orig_cachedCollection,
            ID = tmpItem.ID
        ).Name || tmpItem.Role <> LookUp(
            orig_cachedCollection,
            ID = tmpItem.ID
        ).Role || tmpItem.Email <> LookUp(
            orig_cachedCollection,
            ID = tmpItem.ID
        ).Email || tmpItem.Department <> LookUp(
            orig_cachedCollection,
            ID = tmpItem.ID
        ).Department || tmpItem.CC <> LookUp(
            orig_cachedCollection,
            ID = tmpItem.ID
        ).CC
    )

Note: The BLUE text above is essentially just comparing every single column of original to the current values.  So you'd change this to reflect whatever columns you have in your Collection.

You could use this combined w/ a CountRows() to see a real-time count of changed rows, or simply use it as part of a ForAll(Filter..., UpdateIf()) to update all rows that need to be updated on the original source as a final step:

ForAll(
    Filter(
        cachedCollection As tmpItem,
       //all of your field comparisons...
    ) As nextItem,
    UpdateIf(
        spListName,
        ID = nextItem.ID,
        {
            Name: nextItem.Name,
            Role: nextItem.Role,
            Department: nextItem.Department,
            Email: nextItem.Email,
            CC: nextItem.CC
        }
    )
);

Dropping this code in a Timer (autosave anyone?) and/or on a button that is selected for if/when you want to verify if there are changes to be saved is an excellent idea.

Why This Works Well

Simple Table updates and entries for a single item often don't require these kinds of checks.  However, sometimes we want to warn an end-user that they have unsaved changes and sometimes we want to improve performance by not directly updating the datasource until we're certain the change is intended.

The OnChange code is easy to copy/paste to all controls with minor changes:

UpdateIf(
    cachedCollection,
    ID = ThisItem.ID,
    {Name: Self.Text}
)

Only that blue text needs to be changed for each field and the red text for different kinds of fields (vs. a simple text input).  This makes the changes very simple and all fields can be updated quickly to include this functionality.

Drop Down:

UpdateIf(
    cachedCollection,
    ID = ThisItem.ID,
    {DepartmentSelf.Selected.Value}
)

Checkbox (with a Y or N written to the source):

UpdateIf(
    cachedCollection,
    ID = ThisItem.ID,
    {CC: If(Self.Selected, "Y", "N")}
)

As well, the Filter() code quickly lets us see any record that changed.  It doesn't dig into which specific field changed (which you definitely can do w/ a variation of this Filter()), but just tells you that this record changed and therefore "something" might need to happen (e.g. an end-user warning, a save pop-up, an auto-save, etc.).  This Filter() also means that we aren't attempting to push changes to records unnecessarily and therefore is less likely to either throw errors (if you were attempting to Patch() vs. UpdateIf()) or to overwrite changes done by others (3 different users changing records simultaneously and changes being written to all records by all users thus overwriting).

The code is also fairly easy to adapt to the different scenarios:

  • Enable Save button (by record or overall)
  • Popup to verify changes and list records
  • Include field names that changed
  • Create an even more elaborate save to verify that the original record in the datasource wasn't changed by anyone else before you attempt to save your changes

Ultimately, creating a copy of the original, then working w/ a Collection to ultimately write this out to your datasource allows you to pivot to an array of different scenarios without changing your core approach.

Has Anything Changed?

This is of course very easy.  Using our original Filter() again with a CountRows() gives us a Boolean we can use to indicate "something" changed:

CountRows(Filter(
        cachedCollection As tmpItem,
        //all of your field comparisons...
    ))>0

This gives us a TRUE if there were any changes and FALSE if there were none.  As I mentioned briefly above, you might need to use this code in several places, so having this on a Button that you use a Select() on for all the places in your screen you need to run this is a good idea.

Enhancing to Stop Overwriting Other Users

Starting with the Filter Code again from above:

Filter(
        cachedCollection As tmpItem,
        tmpItem.Name <> LookUp(
            orig_cachedCollection,
            ID = tmpItem.ID
        ).Name || tmpItem.Role <> LookUp(
            orig_cachedCollection,
            ID = tmpItem.ID
        ).Role || tmpItem.Email <> LookUp(
            orig_cachedCollection,
            ID = tmpItem.ID
        ).Email || tmpItem.Department <> LookUp(
            orig_cachedCollection,
            ID = tmpItem.ID
        ).Department || tmpItem.CC <> LookUp(
            orig_cachedCollection,
            ID = tmpItem.ID
        ).CC
    )

NOTE: Again, the blue section should reflect your own fields and anything that *might* have changed.

We now have a list of all the items we plan to write.  We can then go get the Datasource values to verify nothing changed while this user was in the app.  If we are using SharePoint, then the easiest way to do this is to get the items and then compare the 'Version Hitory' field.  If they are identical, then you're good, if they are not, then "somebody" has updated that.

Imagine grabbing everything from our SharePoint list to compare to all of the items we cached previously:

ClearCollect(
    currentSPValues,
    spListName
)

Then we can use a more comprehensive Filter() to leave us with all of the items that were Updated and anything that somebody else changed:

Filter(
Filter(
        currentSPValues As nextItem,
        //all of your field comparisons...
        ).ID)
    ) As currentValues,
    'Version History'<>LookUp(cachedCollection,ID=currentValues.ID).'Version History'
)

What this is doing is taking our original Filter, but using the results to Filter the list of items just pulled from SharePoint (before we try and save), and Filters again then comparing the 'Version History' field.  If we wanted to then even display information about these we can show the results of this final Filter of:

  • Who made the change ('Modified By'
  • When the change was made (Modified)
  • What fields changed (by comparing to our field values to theirs)
We can now warn our end-user of individual fields that the other user changed and the end-user ALSO changed to verify if there is any disagreement before saving, or we can actually recognize that some other user changed different fields from our end-user and simply ignore it all and go ahead and save things.  This sets us up for all scenarios very nicely.

Why Don't You Just write Directly To The Source?!

Well, that really just depends.  Sometimes that's fine.  Other times it is not.  However, the main problem with doing direct-to-source changes is that it slows overall performance of applications at moments where the end-user is interacting with it (e.g. editing a field).  As well, this can cause refreshes to happen for your Gallery for each edit you make.

While this might be OK for some scenarios, when people are editing multiple fields for multiple items at once, it makes it very performance intensive.  As well, the ability to "cancel" or "undo" changes is more difficult by attempting to roll back to prior values.

There certainly are scenarios where simple applications should just write straight to the source.  However, for multi-item-editing style scenarios, this pattern fits closer.  Especially if there are multiple people using the app at the same time.  You don't want people making rapid changes to the same fields at the same time without some kind of safety net.

NOTE: Consider fields where we have triggers/notifications tied to their OnChange value in Power Automate and we don't want to start emailing people or starting other processes just because someone clicked the wrong button.

Even if we connect straight to the source, we still might wind up in scenarios where we attempt to overwrite changes done by others because our app would need to constantly be polling to see if any changes were made by someone else or we'd need to verify that what we think we're changing is still the same as it was when we started.  So many (if not most) times it makes much more sense to just commit to caching and therefore improving overall performance at the same time using nearly the same solution.

Final Thoughts

This is mainly just a cut/paste job for you to drop into your own app to quickly add this functionality w/ just minor changes to the various field, datasource, and collection names.  

Creative UX can lead you to a scenario where this kind of code will become helpful so that you can quickly enable the functionality without getting too far into the weeds.  


No comments:

Post a Comment

Because some d-bag is throwing 'bot posts at my blog I've turned on full Moderation. If your comment doesn't show up immediately then that's why.

DIAF Visualpath team