Canvas apps and Offline tips and tricks

Hello everyone,

Let's talk about quite complex/surprising subjects around Canvas apps.
The main one will be around offline management in canvas apps, for a client we have a requirement to manage offline capabilities on our latest PowerApps Canvas application. But I will talk about other things which are good to know when you create your projects around that application.

We know that the offline capability exist on canvas apps : offline documentation, so what's the big deal you would say ?

Unfortunately we faced some unexpected issues and found out some interesting tips and tricks to have in mind when you will have to implement such a thing.

Here is the offline scenario we had to build with offline possibilites :

  • Usage of the entire app offline with possibility to load/save data from/to the CDS instance on the first and last screen.
  • Manage records with local collections
  • Link records from various collection between them
  • Load all data from an entity (not only the basic fields such as : text, int, date, ...) ie : Lookups

Let's give you some tips & tricks around the Canvas apps and also Offline capabilities

Tips and tricks :

For the following points, let's use this entities structure in the CDS:

1. Load related lookup values in local collection (offline)

Starting from the fact that you have a Car records with N Wheel records linked to a car.

When working on local collections, you have to be aware that a simple :
ClearCollect(WheelCollection, Wheels); won't load all related records (ie : car lookup) by default.

In order to make sure that you have loaded these data when you retrieve everything from the CDS, you have to make sure that at least one screen is using the attribute.
Using the attribute in a query won't be enough.

Quite dirty but efficient solution I used :
Create a dummy screen with label controls using the lookups i'm looking for such as First(WheelCollection).Car.Name. That line will make sure that when you retrieve the Wheel collection, the Car lookup will be loaded by default as well.

Last point here, make sure that the "core" entity and the related entities are connected to the same environment. If you have the entity Car on the CDS (current environment) and the Wheels on the CDS Dev instance, it won't work.

2. Multiple levels of expansion are not supported

It's possible that you face that Notification error while using the application on your mobile or tablet device.

This would simply be caused by a call on a screen control like :
First(BrandCollection).Car.Wheel.Name this will use 2 levels of linked entity which is not allowed when querying data from the WebApi (which is doing the canvas app in the background when querying data).

To spot it

You can simply follow the application scenario from your favorite browser with a develop tool opened. When you will end on the screen with the issue, you can see the network call with the exact webapi call which is falling and in the details you can see the entity and field causing the issue in it.

To fix it

In that case, you have to use the Lookup function to load the first row of data then retrieving the wanted attribute.

IE : Lookup(Cars, Car.Car = First(WheelCollection).Car, Brand).Name
This will return the brand name of a car which is filtered by a wheel property.

3. Creating new records on local Collection (offline)

When using local collections to manage your data (without internet in our case), you will use collections and add/update/remove record actions in it.

I will focus on the add action here.
Here is our process :

As you can see based on the picture above, when creating a record with specified fields, each fields not included on the creation CAN'T be updated later on. You have to be carefull to initialize the record with all data needed in the first place.

In this example, you would make sure that during the creation you specify the name AND the Car lookup even if it's with a blank value.

Solution :

4. Optimize the screens loading

When opening a screen from an other, you have several options.
Indeed, opening a screen with heavy logic or a lot of controls can take a while and this is not user friendly.

To optimize that you have an easy improvement : the screen LoadingSpinner property

  • LoadingSpinner.Controls : show a loader while all controls are loading properly
  • LoadingSpinner.Data : show a loader while all data are loaded properly

Here is a sample in video :
credit : Nicolas Kirrmann

5. Local collections (with lookups) to CDS (offline)

When exclusively working with local collections (including lookups) in your app you have to deal with an issue :
How will you link the new records without the generated GUID from your data source ?.

In order to link our records : Cars and Wheels, we need to have the Car GUID to link a wheel with a car obviously.

Good point is that when you use the Patch function, you can specify any property added to the object you are creating using the Defaults(entity) function.

Here is a sample to illustrate the above explanation

// Preparing our 2 collections
ClearCollect(CarCollection, Cars);  
ClearCollect(WheelCollection, Wheels);

/* Create a new Car record in the collection locally (only) 
   forcing the GUID of the record */
Patch(CarCollection, Defaults(Cars), {  
    carfup_carid : GUID(),
    carfup_name : "New Car"
});

/* Create a new Wheel record in the collection locally (only)
   forcing the GUID of the record and using the forced
   car GUID to link them together. */
Patch(WheelCollection, Defaults(Wheels), {  
    carfup_wheelid : GUID(),
    carfup_name : "New Wheel",
    carfup_Car: Last(CarCollection)
});

Result :

So far, we have records on our local collection with links between the Cars and the Wheels.

The next step would be to Patch the datasource with our local collections.

a. Upload the "core" records

By "core" records, I mean the records which are used as parent in your entity architecture (ie : brands or cars here).

For Entities with no lookups you can simply use the following line of code : Patch(Cars, CarCollection); the good thing with that is that it will generate a new record in the CDS with the forced GUID. That way you have the possibility to keep the local GUID which match the CDS GUID.

! Attention ! : this works as seen but according to Microsoft, they do not recommand to force the GUID and use it to push the value to your datasource. At your own risk !

Indeed the fact that you could force the GUID when it's supposed to be the datasource can break the GUID generation logic and the indexes on your datasource.

b. Upload the "related" records

By "related" records, I mean the records which contains lookups with your "core" records.

The above way to create records won't work when you have to specify custom lookups within your patch.
You will get the following as answer from the server :

The following error message tells you that the Patch function tries to update an existing record while you are trying to create one. Since the record doesn't exist in the target source yet, the messsage makes sense.
Also one really important thing you need to make sure before trying to push related records to your CDS, you HAVE TO load your core records in the datasource before even if you have those locally.
Of course when you will push the related records to CDS, it will try to link an existing core records in the CDS with your newly created related record.

In order to create your related records, you will have to avoid the forced GUID and then retrieve it from your datasource to have consistent data in your local app.

Here is our approach on that scenario :

Regarding the steps 1, 2 and 3, follow the instructions detailled above.

We will now focus the point 4 on the scenario, here is a piece of code :

// Get the wheels from the CDS
Collect(WheelCollection, Wheels);

// Add 2 new records to our local collection of wheels (with lookup to a Car)
Patch(WheelCollection, Defaults(Wheels),  
    {
        Wheel : GUID(),
        Name: "New local Wheel",
        Car: Last(CarCollection)
    }
);
Patch(WheelCollection, Defaults(Wheels),  
    {
        Wheel : GUID(),
        Name: "New local Wheel 2",
        Car: Last(CarCollection)
    }
);
// For each wheel in the collection, we push it to the CDS without forced GUID
// We removed the Wheel : GUID() line here.
ForAll(  
    WheelCollection,
    Patch(Wheels, Defaults(Wheels),
        {
            Name: WheelCollection[@Name],
            Car: WheelCollection[@Car]
        }
    )
);

// We refresh our local collection with the new values from the CDS
ClearCollect(WheelCollection, Wheels);  

To clear what we've just done with the code above :

  1. We get the values from our CDS instance for the wheels entity into a local collection
  2. We add 2 records on our local collection with forced GUID which could be use to link records or query it.
  3. For each record with forced GUID in our collection, we Patch it to our CDS instance without the GUID (as we want the CDS to generate it).
  4. We retrieve the values from CDS in our local collection so we have the latest data in our collection.

And tada, you're now able to manage local collections with local records and push them to your datasource without bad surprises.

I hope all these will help you create the best apps for your customer.

Happy Power Platform'in,
Clément

Thanks to Michel for his help on that topic !

Clement

Dynamics 365 CRM & Power Platform addict.