© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
S. HoeflingGetting Started with the Uno Platform and WinUI 3https://doi.org/10.1007/978-1-4842-8248-9_18

18. Offline Data Access

Skye Hoefling1  
(1)
Rochester, NY, USA
 

When building a mobile application , there is no guarantee that you will always have a stable Internet connection. It is best to build your application with the expectation that the connection will be severed, and it should seamlessly transition between offline mode and online mode. Since more and more devices are becoming more portable such as hybrid laptops that act as tablets, our applications need to handle this on other platforms than just mobile.

The mark of a good application is one where the user does not know when it is in online mode or offline mode. The data they are interested in viewing is available to them.

To handle offline data, you will need to add a local data store such as a database stored on device that can cache any data that you retrieve while in online mode. This means records, files, etc. need to be saved on device so they can be accessed when the network status changes.

In this chapter we will be updating our UnoDrive application to add support for offline mode. We will be adding a local data store to cache our data and present it to the user regardless of network connection.

Offline Data Access Strategies

When building an offline-first application, the data access techniques vary from an application that is guaranteed to always have a stable connection or no formal offline mode. In a typical application that guarantees a connection, you will be querying the API directly and storing that data in memory as it is presented to the user. Many requests in modern applications have a very fast response time, so this technique is valid when we have a guaranteed connection.

Our current application implementation follows the standard data retrieval pattern that renders it directly to the page. This is common of web, desktop, and some mobile applications because offline data is not required:
  1. 1.

    Retrieve data from the API.

     
  2. 2.

    Display on page.

     
Consider a page that retrieves a user’s name and wants to display it on the screen like our dashboard. An application that guarantees a stable connection will have code like Listing 18-1. In this code snippet, we call our remote API to retrieve our data and store it into a local property, which will render it to the screen.
Void LoadData()
{
  var myData = GetDataFromApi();
  Name = myData.Name;
}
Listing 18-1

Sample code for retrieving the user’s name with stable connection

In contrast, an offline-first application adds a few extra steps when retrieving the same data. The data should be stored in a local data store, and the presentation code will always be retrieving data from that data store, never the in-memory data directly from the API. This means you should read from the local data store first and then try and refresh the data from the API. The order of operations will look like this:
  1. 1.

    Read data from the local data store.

     
  2. 2.

    Display on page.

     
  3. 3.

    Retrieve data from the API.

     
  4. 4.

    Write data to the local data store.

     
  5. 5.

    Update the display on page.

     
Updating our preceding small sample code to handle offline data can be seen in Listing 18-2.
void LoadData()
{
  var localData = GetLocalData();
  if (localData != null)
  {
    Name = localData.Name;
  }
  var myData = GetDataFromApi();
  WriteLocalData(myData);
  Name = myData.Name;
}
Listing 18-2

Sample code for retrieving the user’s name with the offline data store

When working with offline data, you have the potential to update the user interface two times with our current strategy. The final solution means you will have a user interface that renders very quickly.

Tip

When implementing an offline data access strategy, once you render the cached data, there should still be an indicator such as a spinner to communicate to the user that data is still loading. It will create a jarring user experience if the page flashes after loading the live data.

Caching Strategies

There are two main caching strategies that are used when building offline-capable applications:
  • Cache everything at startup.

  • Cache at time of read.

Both strategies have their own advantages and disadvantages, and it is best to choose the right one for your application.

Caching everything at startup is typically used on mission-critical enterprise line-of-business applications. These applications need everything to be available when the application loads. This means during the application startup sequence, there needs to be a stable connection to download all the data on device and synchronize any data that has not been sent to the central API. A downside of this technique is it can take a long time for all the data to be synchronized in both download and upload tasks. In the context of a line-of-business application, this is typically an acceptable compromise. The user will know they need to wait before they can use the application in an offline mode.

Caching at time of read means the application will have a fast startup time and the user will not have any delays to getting into their application and performing their tasks. As the user performs tasks in the application, that data will be cached, and when the user needs to load it again, the data will be available in both offline mode and online mode. The major downside to caching at time of read is if the user needs to access data while in offline mode, they must view it at least once during an online session. This technique is more suitable for applications that are not running business-critical software and is okay for stale data. It is also useful for applications that do more writing than reading, such as an application that requires the user to insert new records into a database.

In our UnoDrive application , we will be implementing a caching strategy that caches at the time of read. This strategy makes the most sense as downloading all the possible OneDrive data for a user can be a very long task and the user may not need it all.

Local Data Store and LiteDB

A local data store is a database or file that is stored in the application that is used to cache data to be retrieved at runtime. When choosing the correct local data store, there are many options available in the .NET community, and it is important to pick the one that makes the most sense for you and your team. You can use a SQL or NoSQL database or even a flat file that stores the data in JSON, XML, or your favorite markup language.

There are many data store and caching tools available in the .NET ecosystem. In our application we are using LiteDB. You should decide what is best for you and your team. Akavache is a popular alternative to LiteDB as it offers a powerful caching data store that is popular with the reactiveui community:
Our UnoDrive application is going to use LiteDB as it is a lightweight NoSQL database that is fast and works across all the platforms in Uno Platform. You can learn more about LiteDB from their website or GitHub page:
Tip

The most important thing to look for when selecting your local data store is the platform support. You may find something that is great in some of your platforms, but not all of them. Ensure it works across all the platforms that your application will be targeting.

LiteDB makes the most sense for UnoDrive as it is more than capable of storing our OneDrive records and returning them quickly. All images and assets we will want to download will be stored as a file and not in the database.

Add the LiteDB NuGet Package

Add the LiteDB NuGet package to all of the target platform head projects:
  • UnoDrive.Mobile

  • UnoDrive.Skia.Gtk

  • UnoDrive.Skia.Wpf

  • UnoDrive.Wasm

  • UnoDrive.Windows

Add the PackageReference code to each csproj file as seen in Listing 18-3.
<PackageReference Include="LiteDB" Version="5.0.11" />
Listing 18-3

Add PackageReference for LiteDB in all target platform head projects

Create the DataStore

The local data store LiteDB is unlike a centralized database that guarantees to be created and available. We will need to ensure our LiteDB is created and saved on device prior to using it. This means we need to define where on the device the file is saved and reference that file when instantiating an instance of the LiteDatabase object.

In our UnoDrive.Shared project , create two new files in the Data folder named IDataStore and DataStore. See Figure 18-1 for a screenshot of the Visual Studio Solution Explorer.

A screenshot depicts the solution explorer window, which includes the solution unoDrive. The dot solution items, Platforms, and unoDrive Shared are subcategories. In the Data folder, DataStore dot cs and IDatastore dot c s are selected.

Figure 18-1

Visual Studio Solution Explorer – DataStore and IDataStore added to the project

Right now, we are not going to define any methods in the IDataStore as we are just configuring our database file. The DataStore will be our implementation of writing and reading data to and from the LiteDB database file. We are going to define our database file location and handle the differences between WPF and the rest of the platforms. We learned earlier in this book that WPF has a different application local folder, so we will add platform-specific code to handle the location. See code in Listing 18-4 with an initial DataStore implementation of the constructor and the database file location.
public class DataStore : IDataStore
{
  readonly string databaseFile;
  public DataStore()
  {
#if HAS_UNO_SKIA_WPF
    var applicationFolder = Path.Combine(
      ApplicationData.Current.TemporaryFolder.Path,
      "UnoDrive");
    databaseFile = Path.Combine(
      applicationFolder, "UnoDriveData.db");
#else
    databaseFile = Path.Combine(
      ApplicationData.Current.LocalFolder.Path,
      "UnoDriveData.db");
#endif
  }
}
Listing 18-4

DataStore – define databaseFile and the constructor

Note

LiteDB will automatically create the database file if it does not exist. If the database file does exist, it will use the file.

Next, we will need to register our DataStore with the Dependency Injection container so it can be used in various classes throughout our application. In the App.xaml.cs file, update the ConfigureServices method to register our DataStore as a transient service. See Listing 18-5 for new registration code and Listing 18-6 for complete ConfigureServices code.
services.AddTransient<IDataStore, DataStore>();
Listing 18-5

Register IDataStore with the Dependency Injection container

protected override void ConfigureServices(
  IServiceCollection services)
{
  services.AddLoggingForUnoDrive();
  services.AddAuthentication();
  services.AddTransient<INavigationService, NavigationService>();
  services.AddTransient<
    INetworkConnectivityService, NetworkConnectivityService>();
  services.AddTransient<IGraphFileService, GraphFileService>();
  services.AddTransient<IDataStore, DataStore>();
}
Listing 18-6

Complete App.xaml.cs ConfigureServices method

Authentication and Token Caching

The UnoDrive authentication is using the Microsoft Authentication Library (MSAL) to connect to Azure Active Directory. The .NET library MSAL.NET provides the concept of token caching, which means when you log in, it can cache your token and automatically log in when you launch the application. This also allows you to log in using the MSAL.NET APIs when in offline mode.

iOS and Android have built-in token caching, and this works out of the box with no additional code. As for the rest of the platforms, we will need to implement our own token caching mechanism to fully support offline mode.

MSAL.NET provides an ITokenCache interface, which has extension points to configure custom serialization and deserialization of the token cache. The extension points allow you to configure a callback, which will be called for writes and reads.

We are going to implement our own custom token caching strategy that uses an encrypted LiteDB as the data store. LiteDB provides a simple way to encrypt the database by supplying any password in the connection string. To start, create a new file in the UnoDrive.Shared project under the Authentication folder named TokenCacheStorage.cs . See Figure 18-2 for a screenshot of the Visual Studio Solution Explorer.

A screenshot depicts the solution explorer window, which includes the solution unoDrive. The dot solution items, Platforms, and unoDrive Shared are subcategories. In the Authentication folder, Token CacheStorage dot c s is selected.

Figure 18-2

Visual Studio Solution Explorer – TokenCacheStorage.cs

The TokenCacheStorage class is going to be a static class, so there are not going to be any instance variables to store in the connection string. Create a private static method that returns the connection string. We will follow a similar pattern as we did in the DataStore class except we will specify a file name and password. To encrypt a LiteDB file, you only need to specify a password, and it happens automatically. See code in Listing 18-7 for the initial creation of TokenCacheStorage and method to retrieve the connection string.
static class TokenCacheStorage
{
  static string GetConnectionString()
  {
#if HAS_UNO_SKIA_WPF
    var applicationFolder = Path.Combine(
      ApplicationData.Current.TemporaryFolder.Path,
      "UnoDrive");
    var databaseFile = Path.Combine(
      applicationFolder,
      "UnoDrive_MSAL_TokenCache.db");
#else
    var databaseFile = Path.Combine(
      ApplicationData.Current.LocalFolder.Path,
      "UnoDrive_MSAL_TokenCache.db");
#endif
    return $"Filename={databaseFile};Password=UnoIsGreat!";
  }
}
Listing 18-7

TokenCacheStorage – GetConnectionString method implementation

Note

We supplied a hard-coded password, which accomplishes the goal of encryption. In an application that requires more security, this password should be install dependent by using a key unique to the device or installation.

Create the entry point for the TokenCacheStorage with a static method named EnableSerialization . This method will take a parameter of ITokenCache, which we get from the built MSAL.NET object. The ITokenCache has two methods SetBeforeAccess and SetAfterAccess, which allow us to hook into the token data and store it. Create a method stub for both SetBeforeAccess and SetAfterAccess . See the code snippet in Listing 18-8.
static class TokenCacheStorage
{
  // omitted GetConnectionString() method
  public static void EnableSerialization(ITokenCache tokenCache)
  {
    tokenCache.SetBeforeAccess(BeforeAccessNotification);
    tokenCache.SetAfterAccess(AfterAccessNotification);
  }
  static void BeforeAccessNotification(
    TokenCacheNotificationArgs args)
  {
    // TODO – add implementation
  }
  static void AfterAccessNotification(
    TokenCacheNotification args)
  {
    // TODO – add implementation
  }
}
Listing 18-8

TokenCacheStorage – EnableSerialization and method stubs

At this point when the token is acquired, the BeforeAccessNotification and AfterAccessNotification methods will be invoked, but they do not have any implementation yet.

The BeforeAccessNotifcation method is used just prior to the authentication action. We will add code that uses the LiteDB and checks for an existing cached token. If a token is found, it will allow the authentication to perform a silent login.

The AfterAccessNotification method is used just after a successful authentication action. A token is provided in the method arguments, and then we will use the LiteDB to store the token for a future login attempt.

We need to define a data model to store our cached token data. We are going to dump the entire string into a single column on the data model. Create a private nested class named TokenRecord, which will make it only accessible from inside the scope of TokenCacheStorage. See the code snippet in Listing 18-9.
static class TokenCacheStorage
{
  // omitted code
  class TokenRecord
  {
    public string Data { get; set; }
  }
}
Listing 18-9

TokenCacheStorage – add the TokenRecord data model

Next, we can implement both the BeforeAccessNotification and AfterAccessNotification methods. The BeforeAccessNotification method will be retrieving any data in the LiteDB file, and the AfterAccessNotification method will be writing the data to the LiteDB file. To ensure we don’t have additional rows in our TokenRecord, be sure to delete all records prior to writing. See the code snippet in Listings 18-10 and 18-11.
static void BeforeAccessNotification(
  TokenCacheNotificationArgs args)
{
  using (var db = new LiteDatabase(GetConnectionString()))
  {
    var tokens = db.GetCollection<TokenRecord>();
    var tokenRecord = tokens.Query().FirstOrDefault();
    var serializedCache = tokenRecord != null ?
      Convert.FromBase64String(tokenRecord.Data) : null;
    args.TokenCache.DeserializeMsalV3(serializedCache);
  }
}
Listing 18-10

TokenCacheStorage – BeforeAccessNotification method implementation

static void AfterAccessNotification(
  TokenCacheNotificationArgs args)
{
  var data = args.TokenCache.SerializeMsalV3();
  var serializedCache = Convert.ToBase64String(data);
  using (var db = new LiteDatabase(GetConnectionString()))
  {
    var tokens = db.GetCollection<TokenRecord>();
    tokens.DeleteAll();
    tokens.Insert(new TokenRecord
      { Data = serializedCache });
  }
}
Listing 18-11

TokenCacheStorage – AfterAccessNotification method implementation

That completes the TokenCacheStorage implementation – see Listing 18-12 for the complete code.
Static class TokenCacheStorage
{
  static string GetConnectionString()
  {
#if HAS_UNO_SKIA_WPF
    var applicationFolder = Path.Combine(
      ApplicationData.Current.TemporaryFolder.Path,
      "UnoDrive");
    var databaseFile = Path.Combine(
      applicationFolder,
      "UnoDrive_MSAL_TokenCache.db");
#else
    var databaseFile = Path.Combine(
      ApplicationData.Current.LocalFolder.Path,
      "UnoDrive_MSAL_TokenCache.db");
#endif
    return $"Filename={databaseFile};Password=UnoIsGreat!";
  }
  public static void EnableSerialization(ITokenCache tokenCache)
  {
    tokenCache.SetBeforeAccess(BeforeAccessNotification);
    tokenCache.SetAfterAccess(AfterAccessNotification);
  }
  static void BeforeAccessNotification(
    TokenCacheNotificationArgs args)
  {
    using (var db = new LiteDatabase(GetConnectionString()))
    {
      var tokens = db.GetCollection<TokenRecord>();
      var tokenRecord = tokens.Query().FirstOrDefault();
      var serializedCache = tokenRecord != null ?
        Convert.FromBase64String(tokenRecord.Data) : null;
      args.TokenCache.DeserializeMsalV3(serializedCache);
    }
  }
  static void AfterAccessNotification(
    TokenCacheNotificationArgs args)
  {
    var data = args.TokenCache.SerializeMsalV3();
    var serializedCache = Convert.ToBase64String(data);
    using (var db = new LiteDatabase(GetConnectionString()))
    {
      var tokens = db.GetCollection<TokenRecord>();
      tokens.DeleteAll();
      tokens.Insert(new TokenRecord
        { Data = serializedCache });
    }
  }
  class TokenRecord
  {
    public string Data { get; set; }
  }
}
Listing 18-12

TokenCacheStorage – complete implementation

To finish our changes to the authentication system, we need to invoke the EnableSerialization method right after building the IPublicClientApplication. In the UnoDrive.Shared project under the Authentication folder, open the AuthenticationConfiguration.cs file. Update the ConfigureAuthentication method to match the code in Listing 18-13. The changes we are making are storing the IPublicClientApplication in a local variable and then calling the EnableSerialization method with the UserTokenCache .
public void ConfigureAuthentication(IServiceCollection services)
{
  var builder = PublicClientApplicationBuilder
    .Create("9f500d92-8e2e-4b91-b43a-a9ddb73e1c30")
    .WithRedirectUri(GetRedirectUri())
    .WithUnoHelpers();
  var app = builder.Build();
#if !__ANDROID__ && !__IOS__
  TokenCacheStorage.EnableSerialization(app.UserTokenCache);
#endif
  services.AddSingleton(app);
  services.AddTransient<
    IAuthenticationService,
    AuthenticationService>();
}
Listing 18-13

AuthenticationConfiguration updates for UserTokenCache

This completes the authentication changes, and we now have our token caching strategy implemented across all the platforms. Now when you run the application and log in, it will cache the user. Following attempts to log in will use the cached data.

Simple Caching in the Dashboard

The Dashboard in UnoDrive is our application shell or container that provides all the menus and content for the user. In our left-pane menu, we pull information from the Microsoft Graph to display the user’s display name and email address . If the application is in offline mode, this data will not load. In this section we are going to apply the basics of data caching that we reviewed earlier in this chapter. As a quick refresher, our algorithm will be as follows:
  1. 1.

    Load cached data if it exists.

     
  2. 2.

    Pull data from the Microsoft Graph if network is available.

     
  3. 3.

    Write latest data to the data store.

     
  4. 4.

    Present the user interface.

     

To implement this algorithm, we will need to make changes to both the DataStore, which we started introducing in this chapter, and the Dashboard.

Add APIs to the DataStore

Before we can start adding methods to the DataStore, we need to create a new database model that represents the user information we would like to save. In the UnoDrive.Shared project under the Data folder, create a new file named UserInfo.cs. This class will contain the properties for the user’s display name and email address. See Figure 18-3 for a screenshot of the Visual Studio Solution Explorer.

A screenshot depicts the solution explorer window, which includes the solution unoDrive. The dot solution items, Platforms, and unoDrive Shared are subcategories. In the Data folder, UserInfo dot c s is selected.

Figure 18-3

Visual Studio Solution Explorer – UserInfo.cs

The UserInfo class will be used by LiteDB to write information to the local database. We will need to create three simple properties, an Id, Name, and Email. See the full code for this model in Listing 18-14.
public class UserInfo
{
  public string Id { get; set; }
  public string Name { get; set; }
  public string Email { get; set; }
}
Listing 18-14

UserInfo model definition for storing the user’s information

We can now start implementing the DataStore that uses the new UserInfo model. Open the IDataStore interface and define two new methods for storing and retrieving the user’s information. See Listing 18-15 for the interface definition.
public interface IDataStore
{
  void SaveUserInfo(UserInfo userInfo);
  UserInfo GetUserInfoById(string id);
}
Listing 18-15

IDataStore interface definition for SaveUserInfo and GetUserInfoById methods

Next, we will implement the GetUserInfoById method as it is the easier of the two APIs. We already defined a local variable named databaseFile in the DataStore class. This will be used to instantiate an instance of the LiteDatabase so we can retrieve our records. To implement this, you will create the LiteDatabase, get our collection of UserInfo, and then try and read a record matching the input parameter. See the GetUserInfoById implementation in Listing 18-16.

Tip

When working with the LiteDatabase object, always ensure you are instantiating it with a using block or statement. This will dispose of the object at the end of the block and free up any file or memory locks on the data store. If you do not add the using block, you will need to manually invoke the Dispose() method. If you use the newer using statement syntax, that does not have any curly braces. It will automatically Dispose() at the end of the method.

public UserInfo GetUserInfoById(string userId)
{
  using (var db = new LiteDatabase(databaseFile))
  {
    var users = db.GetCollection<UserInfo>();
    return users.FindById(userId);
  }
}
Listing 18-16

DataStore GetUserInfoById method implementation

Next, we will implement the SaveUserInfo method, which has a few additional steps. Again, you will instantiate the LiteDatabase object using the local variable databaseFile. Then you will try and find an existing record that matches the ID from the supplied UserInfo parameter. If it exists, go and update the values, but if it does not exist, you will create a new record in the database. See the SaveUserInfo implementation in Listing 18-17.
public void SaveUserInfo(UserInfo userInfo)
{
  using (var db = new LiteDatabase(databaseFile))
  {
    var users = db.GetCollection<UserInfo>();
    var findUserInfo = users.FindById(userInfo.Id);
    if (findUserInfo != null)
    {
      findUserInfo.Name = userInfo.Name;
      findUserInfo.Email = userInfo.Email;
      users.Update(findUserInfo);
    }
    else
    {
      users.Insert(userInfo);
    }
  }
}
Listing 18-17

DataStore SaveUserInfo method implementation

That completes our changes to the DataStore for now, and we can start using it in the dashboard.

Add Offline Code to the Dashboard

To add data caching to the dashboard, we will be updating code in the DashboardViewModel . We have not started any work in this file yet for offline access, so we will need to inject the IDataStore and INetworkConnectivityService to give us access to the caching APIs as well as network status. Add new local variables to store references and inject them into the constructor. See updated constructor code for the DashboardViewModel in Listing 18-18.
public class DashboardViewModel :
  ObservableObject, IAuthenticationProvider, IInitialize
{
  IDataStore datastore;
  INetworkConnectivityService networkService;
  ILogger logger;
  public DashboardViewModel(
    IDataStore datastore,
    INetworkConnectivityService networkService,
    ILogger<DashboardViewModel> logger)
  {
    this.dataStore = datastore;
    this.networkService = networkService;
    this.logger = logger;
  }
}
Listing 18-18

DashboardViewModel updated constructor with IDataStore injection. Changes highlighted in bold

Next, we can update the LoadDataAsync method to perform caching. Following our algorithm from earlier, we need to try and read from the cache first and then load data and write it to the database. Now that we have access to both the INetworkConnectivityService and IDataStore , we can complete these tasks. See the updated implementation in Listing 18-19 with changes highlighted in bold.
public async Task LoadDataAsync()
{
  try
  {
    var objectId = ((App)App.Current)
      .AuthenticationResult.Account.HomeAccountId.ObjectId
    var userInfo = datastore.GetUserInfoById(objectId);
    if (userInfo != null)
    {
      Name = userInfo.Name;
      Email = userInfo.Email;
    }
    if (networkService.Connectivity !=
      NetworkConnectivityLevel.InternetAccess)
    {
      return;
    }
#if __WASM__
    var httpClient = new HttpClient(
      new Uno.UI.Wasm.WasmHttpHandler());
#else
    var httpClient = new HttpClient();
#endif
    var graphClient = new GraphServiceClient(httpClient);
    graphClient.AuthenticationProvider = this;
    var request = graphClient.Me
      .Request()
      .Select(user => new
      {
        Id = user.Id,
        DisplayName = user.DisplayName,
        UserPrincipalName = user.PrincipalName
      });
#if __ANDROID__ || __IOS__ || __MACOS__
    var response = await request.GetResponseAsync();
    var data = await response.Content.ReadAsStringAsync();
    var me = JsonConvert.DeserializeObject<User>(data);
#else
    var me = await request.GetAsync();
#endif
    if (me != null)
    {
      Name = me.DisplayName;
      Email = me.UserPrincipalName;
      userInfo = new UserInfo
      {
        Id = objectId,
        Name = Name,
        Email = Email
      };
      datastore.SaveUserInfo(userInfo);
    }
  }
  catch (Exception ex)
  {
    logger.LogError(ex, ex.Message);
  }
}
Listing 18-19

DashboardViewModel LoadDataAsync implementation updates for caching. Changes are in bold

That completes the data caching algorithm for the DashboardViewModel . There are no user interface changes needed as the change is just on how we store the data.

GraphFileService and MyFilesPage Caching

Adding data caching for the MyFilesPage is a little bit more involved than it was for the dashboard as we have more classes and interactions to handle. Conceptually, the implementation is the same. Since the MyFilesPage has loading indicators, we want to update the state of those as well as how we cache the data.

When the user starts loading the MyFilesPage, there are two spinning indicators that render: that in the status bar and that in the main content area. When we have cached data, we want to render the data as quickly as possible, so the main content indicator can go away. Since we will still be loading data, it is important to notify the user that it is processing, so we will keep the loading indicator in the status bar. Once we get our live data in online mode, all the indicators will disappear as it is implemented already.

To add data caching for the MyFilesPage, we have a few areas that need updating:
  • New APIs in the DataStore

  • Updates to the GraphFileService

  • Loading indicator changes in MyFilesPage and MyFilesViewModel

Add APIs to DataStore

The GraphFileService has two main APIs that we need to cache data from: GetRootFilesAsync and GetFilesAsync . These APIs convert into our data store as saving and reading the root files and saving and reading the files. Start by opening the IDataStore interface and adding new APIs to support this:
  • SaveRootId

  • GetRootId

  • GetCachedFiles

  • SaveCachedFiles

  • UpdateCachedFileById

See updated code for the IDataStore in Listing 18-20, which contains the interface definitions. New APIs are highlighted in bold.
public interface IDataStore
{
  void SaveUserInfo(UserInfo userInfo);
  UserInfo GetUserInfoById(string id);
  void SaveRootId(string rootId);
  string GetRootId();
  IEnumerable<OneDriveItem> GetCachedFiles(string pathId);
  void SaveCachedFiles(
    IEnumerable<OneDriveItem> children, string pathId);
  void UpdateCachedFileById(string itemId, string localFilePath);
}
Listing 18-20

IDataStore GraphFileService data caching APIs. New APIs highlighted in bold

Now we can add the implementations in the DataStore class one at time in order with SaveRootId first. This will be like SaveUserInfo . Before we can add the implementation, we will need to add a new database model to store the rootId. In the UnoDrive.Shared project under the Data folder, create a new file named Setting. See Figure 18-4 for a screenshot of the Visual Studio Solution Explorer.

A screenshot depicts the solution explorer window, which includes the solution unoDrive. The dot solution items, Platforms, and unoDrive Shared are subcategories. In the Data folder, Setting dot c s is selected.

Figure 18-4

Visual Studio Solution Explorer – Setting

The Setting data model is a simple key/value pair model. This means the ID or key is the unique identifier and it stores some data with it. In our case we will use it to store the root ID. See Listing 18-21.
public class Setting
{
  public string Id { get; set; }
  public string Value { get; set; }
}
Listing 18-21

Setting data model definition

Now we can start implementing our DataStore APIs. The SaveRootId method is going to retrieve the Setting data model and try and find the hard-coded value " RootId " if it exists, and we will update it. Otherwise, we insert a new record. See Listing 18-22 for the method implementation.
public void SaveRootId(string rootId)
{
  using (var db = new LiteDatabase(databaseFile))
  {
    var settings = db.GetCollection<Setting>();
    var findRootIdSetting = settings.FindById("RootId");
    if (findRootIdSetting != null)
    {
      findRootIdSetting.Value = rootId;
      settings.Update(findRootIdSetting);
    }
    else
    {
      var newSetting = new Setting
      {
        Id = "RootId",
        Value = rootId
      };
      settings.Insert(newSetting);
    }
  }
}
Listing 18-22

DataStore SaveRootId method implementation

The GetRootId method will just be returning the stored Setting value. It will retrieve the hard-coded ID of "RootId". See Listing 18-23 for the GetRootId method implementation.
public string GetRootId()
{
  using (var db = new LiteDatabase(databaseFile))
  {
    var settings = db.GetCollection<Setting>();
    var rootId = settings.FindById("RootId");
    return rootId != null ? rootId.Value : string.Empty;
  }
}
Listing 18-23

DataStore GetRootId method implementation

Before implementing the APIs that manage the OneDriveItem , we need to update the data model. This data model uses an ImageSource property named ThumbnailSource , and it is not a compatible database type. To solve this problem, we can ignore the property by adding the [BsonIgnore] attribute over the property. See Listing 18-24 for the updated OneDriveItem implementation with the change highlighted in bold.
public class OneDriveItem
{
  public string Id { get; set; }
  public string Name { get; set; }
  public string Path { get; set; }
  public string PathId { get; set; }
  public DateTime Modified { get; set; }
  public string FileSize { get; set; }
  public OneDriveItemType Type { get; set; }
  public string ThumbnailPath { get; set; }
  [BsonIgnore]
  public ImageSource ThumbnailSource { get; set; }
}
Listing 18-24

OneDriveItem – ignore ThumbnailSource. Change highlighted in bold

Next, we are going to implement the GetCachedFiles method. This will return all the OneDriveItem files that are stored in the database. These can both be files and folders and are the core items displayed in the MyFilesPage . Our strategy for this method is to handle input errors such as a null or empty pathId parameter . Then we retrieve all items that match the input pathId parameter. The LiteDB engine provides LINQ queries that makes it easy to add filters to your request. See the GetCachedFiles method implementation in Listing 18-25.
public IEnumerable<OneDriveItem> GetCachedFIles(string pathId)
{
  if (string.IsNullOrEmpty(pathId))
  {
    return new OneDriveItem[0];
  }
  using (var db = new LiteDatabase(databaseFile))
  {
    var items = db.GetCollection<OneDriveItem>();
    return items
      .Query()
      .Where(item => item.PathId == pathId)
      .ToArray();
  }
}
Listing 18-25

DataStore GetCachedFiles method implementation

To implement SaveCachedFiles , we have a few additional steps from our other save methods. We will need to delete the stale items from the database in the current directory. In other words, all items at the current path are considered stale and need to be removed. Once that is done, we will delete each ThumbnailPath from local storage in the stale devices and then proceed to save new items. See the SaveCachedFiles method implementation in Listing 18-26.

Note

Our application assumes that the thumbnails will be written to the local storage after this method is called. That means it is safe for us to delete the thumbnail file as part of updating the cache.

public void SaveCachedFiles(
  IEnumerable<OneDriveItem> children, string pathId)
{
  using (var db = new LiteDatabase(databaseFile))
  {
    var items = db.GetCollection<OneDriveItem>();
    var staleItems = items
      .Query()
      .Where(i => i.PathId == pathId)
      .ToArray();
    if (staleItems != null && staleItems.Any())
    {
      items.DeleteMany(x => staleItems.Contains(x));
      foreach (var item in staleItems.Where(i =>
        !string.IsNullOrEmpty(i.ThumbnailPath)))
      {
        if (File.Exists(item.ThumbnailPath))
        {
          File.Delete(item.ThumbnailPath);
        }
      }
    }
    foreach (var item in children)
    {
      var findItem = items.FindById(item.Id);
      if (findItem != null)
      {
        items.Update()item);
      }
      else
      {
        items.Insert(item);
      }
    }
  }
}
Listing 18-26

DataStore SaveCachedFiles method implementation

The last method we are going to implement in the DataStore is UpdateCachedFileById . The purpose of this method is when the GraphFileService is downloading a new thumbnail file and saving it to local storage, it will invoke this API and update the ThumbnailPath property on the OneDriveItem stored in the database. The implementation strategy is to get the matching OneDriveItem by the ID and then update the value and update the record. See Listing 18-27 for the UpdateCachedFileById method implementation.
public void UpdateCachedFileById(
  string itemId, string localFilePath)
{
  using (var db = new LiteDatabase(databaseFile))
  {
    var items = db.GetCollection<OneDriveItem>();
    var findItems = items.FindById(itemId);
    if (findItem != null)
    {
      findItem.ThumbnailPath = localFilePath;
      items.Update(findItem);
    }
  }
}
Listing 18-27

DataStore UpdateCachedFileById method implementation

That completes all our changes to the IDataStore interface and DataStore. We will be using these APIs in the next section as we update the GraphFileService to support offline data caching.

Add Offline Code to GraphFileService

Now that we have completed our changes to the IDataStore interface and DataStore implementation , we can start integrating these changes into the GraphFileService. In this section we will be making code changes to update our APIs to support offline data access with our caching library. The APIs are a bit more complicated than what we did earlier in the DashboardViewModel , but the algorithm is still the same:
  1. 1.

    Load cached data.

     
  2. 2.

    Attempt to pull live data from the Microsoft Graph.

     
  3. 3.

    Store data in the database.

     
  4. 4.

    Return results.

     
Note

Our algorithm is still the same, but we do not mention presenting the user interface as the GraphFileService returns data and is not part of the presentation layer of the application.

Before we start updating the GraphFileService, we need to update the IGraphFileService contract. When a user opens a page and it loads cached data, they can click a folder on that page to start opening the next page. This means we will be handling race conditions as well as offline data. To help us solve this problem, we need to include a CancellationToken to every API in the IGraphFileService interface. This will allow the presentation layer to cancel the request from user interaction and start a new request.

Consider the user is loading the main page and there is a lot of data to pull and they already have cached folders rendering on the screen. If they click one of the folders and the data payload is smaller, it could render the second page first. Then when the first page load Task completes, it would overwrite the current state. With the CancellationToken we can stop the first request where it is and move on, which will effectively stop our race conditions.

In addition to the CancellationToken, the APIs will need to include a callback that can be triggered in the presentation layer. The callback can be thought of as a function pointer that allows your service code to invoke a method or segment of code in the presentation layer. It will be used to update the user interface at various steps along the way in the GraphFileService.

Open the IGraphFileService interface and update all the methods to include a callback function and CancellationToken parameter. See the updated interface in Listing 18-28.
public interface IGraphFileService
{
  Task<IEnumerable<OneDriveItem>> GetRootFilesAsync(
    Action<IEnumerable<OneDriveItem>, bool> cacheCallback = null,
    CancellationToken cancellationToken = default);
  Task<IEnumerable<OneDriveItem>> GetFilesAsync(
    string id,
    Action<IEnumerable<OneDriveItem>, bool> cacheCallback = null,
    CancellationToken cancellationToken = default);
}
Listing 18-28

IGraphFileService interface methods updated to accept CancellationToken. Changes are highlighted in bold

Next, you will need to update the method signatures in your GraphFileService implementation. You can see the final method signatures as we go through the implementation details of each.

Let’s update the constructor code with our injectable services. We need to add INetworkConnectivityService for determining network status and IDataStore for access to the caching APIs. See Listing 18-29 for the updated constructor definition; the new code is highlighted in bold.
public class GraphFileService :
  IGraphFileService, IAuthenticationProvider
{
  GraphServiceClient graphClient;
  IDataStore dataStore;
  INetworkConnectivityService networkConnectivity;
  ILogger logger;
  public GraphFileService(
    IDataStore dataStore,
    INetworkConnectivityService networkConnectivity,
    ILogget<GraphFileService> logger)
  {
    this.dataStore = dataStore;
    this.networkConnectivity = networkConnectivity;
    this.logger = logger;
#if __WASM__
    var httpClient = new HttpClient(
      new Uno.UI.Wasm.WasmHttpHandler());
#else
    var httpClient = new HttpClient();
#endif
    graphClient = new GraphServiceClient(httpClient);
    graphClient.AuthenticationProvider = this;
  }
  // omitted code
}
Listing 18-29

GraphFileService constructor updates for INetworkConnectivityService and IDataStore. Changes highlighted in bold

We are going to update our implementation of GetFilesAsync first as that is the primary method in the GraphFileService . The GetRootFilesAsync depends on it, so it makes sense to complete GetFilesAsync first.

To update the GetFilesAsync implementation, we have a few things that need completing:
  • Update the method signature to match the interface.

  • Invoke the caching callback method if we can retrieve cached data.

  • Skip network requests to the Microsoft Graph if there is no network availability.

  • Store data to the local data store prior to returning.

You can see the complete updated implementation of GetFilesAsync in Listing 18-30; the changes will be highlighted in bold.
public async Task<IEnumerable<OneDriveItem>> GetFilesAsync(
  string id,
  Action<IEnumerable<OneDriveItem>, bool> cachedCallback = null,
  CancellationToken cancellationToken = default)
{
  if (cachedCallback != null)
  {
    var cachedChildren = dataStore
      .GetCachedFiles(id)
      .OrderByDescending(item => item.Type)
      .ThenBy(item => item.Name);
    cachedCallback(cachedChildren, true);
  }
  logger.LogInformation(
    $”Network Connectivity: {networkConnectivity.Connectivity}”);
  if (networkConnectivity.Connectivity !=
    NetworkConnectivityLevel.InternetAccess)
  {
    return default;
  }
  cancellationToken.ThrowIfCancellationRequested();
  var request = graphClient.Me.Drive.Items[id].Children
    .Request()
    .Expand("thumbnails");
#if __ANDROID__ || __IOS__ || __MACOS__
  var response =
    await request.GetResponseAsync(cancellationToken);
  var data = await response.Content.ReadAsStringAsync();
  var collection = JsonConvert.DeserializeObject<
    UnoDrive.Models.DriveItemCollection>(data);
  var oneDriveItems = collection.Value;
#else
  var oneDriveItems = (await request.GetAsync(cancellationToken))
    .ToArray();
#endif
  var childrenTable = oneDriveItems
    .Select(driveItem => new OneDriveItem
    {
      Id = driveItem.Id,
      Name = driveItem.Name,
      Path = driveItem.ParentReference.Path,
      PathId = driveItem.ParentReference.Id,
      FileSize = $"{driveItem.Size}",
      Modified = driveItem.LastModifiedDateTime.HasValue ?
        driveItem.LastModifiedDateTime.Value.LocalDateTime :
        DateTime.Now,
      Type = driveItem.Folder != null ?
        OneDriveItemType.Folder :
        OneDriveItemType.File
    })
    .OrderByDescending(item => item.Type)
    .ThenBy(item => item.Name)
    .ToDictionary(item => item.Id);
  cancellationToken.ThrowIfCancellationRequested();
  var children = childrenTable
    .Select(item => item.Value)
    .ToArray();
  if (cachedCallback != null)
  {
    cachedCallback(children, false);
  }
  dataStore.SaveCachedFiles(children, id);
  await StoreThumbnailsAsync(oneDriveItems, childrenTable
    cachedCallback, cancellationToken);
  return childrenTable.Select(x => x.Value);
}
Listing 18-30

GraphFileService GetFilesAsync updates for data caching and task cancellation. Changes highlighted in bold

In the GetFilesAsync API , we passed the cachedCallback and cancellationToken to the private method StoreThumbnailsAsync. This is done because there are network requests to the Microsoft Graph that we may want to cancel. This method will be used only for writing to the data cache and invoking the callback if we detect changes. There is no need for an early load like in GetFilesAsync. See updated StoreThumbnailsAsync code in Listing 18-31; the changes are highlighted in bold.
#if __ANDROID__ || __IOS__ || __MACOS__
async Task StoreThumbnailsAsync(
  UnoDrive.Models[] oneDriveItems,
  IDictionary<string, OneDriveItem> childrenTable,
  Action<IEnumerable<OneDriveItem>, bool> cachedCallback = null,
  CancellationToken cancellationToken = default)
#else
  DriveItem[] oneDriveItems,
  IDictionary<string, OneDriveItem> childrenTable,
  Action<IEnumerable<OneDriveItem>, bool> cachedCallback = null,
  CancellationToken cancellationToken = default)
#endif
{
  for (int index = 0; index < oneDriveItems.Length; index++)
  {
    var currentItem = oneDriveItem[index];
    var thumbnails = currentItem.Thumbnails?.FirstOrDefault();
    if (thumbnails == null ||
      !childrenTable.ContainsKey(currentItem.Id))
    {
      Continue;
    }
#if __WASM__
    var httpClient = new HttpClient(
      new Uno.UI.Wasm.WasmHttpHandler());
#else
    var httpClient = new HttpClient();
#endif
    var thumbnailResponse = await httpClient.GetAsync(
      url, cancellationToken);
    if (!thumbnailResponse.IsSuccessStatusCode)
    {
      Continue;
    }
#if HAS_UNO_SKIA_WPF
    var applicationFolder = Path.Combine(
      ApplicationData.Current.TemporaryFolder.Path,
      "UnoDrive");
    var imagesFolder = Path.Combine(
      applicationFolder, "thumbnails");
#else
    var imagesFolder = Path.Combine(
      ApplicationData.Current.LocalFolder.Path,
      "thumbnails");
#endif
    var name = $"{currentItem.Id}.jpeg";
    var localFilePath = Path.Combine(imagesFolder, name);
    try
    {
      if (!System.IO.Directory.Exists(imagesFolder))
      {
        System.IO.Directory.CreateDirectory(imagesFolder);
      }
      if (System.IO.File.Exists(localFilePath))
      {
        System.IO.File.Delete(localFilePath);
      }
      var bytes = await thumbnailResponse.Content
        .ReadAsByteArrayAsync();
#if HAS_UNO_SKIA_WPF
      System.IO.File.WriteAllBytes(localFilePath, bytes);
#else
      await System.IO.File.WriteAllBytesAsync(
        localFilePath, bytes, cancellationToken);
#endif
#if __UNO_DRIVE_WINDOWS__ || __ANDROID__ || __IOS__
      var image = new BitmapImage(new Uri(localFilePath));
#else
      var image = new BitmapImage();
      image.SetSource(new MemoryStream(bytes));
#endif
      childrenTable[currentItem.Id].ThumbnailSource = image;
      if (cachedCallback != null)
      {
        var children = childrenTable
          .Select(item => item.Value)
          .ToArray();
        cachedCallback(children, value);
      }
      dataStore.UpdateCachedFileById(
        currentItem.Id, localFilePath);
      cancellationToken.ThrowIfCancellationRequested();
    }
    catch (TaskCanceledException ex)
    {
      logger.LogWarning(ex, ex.Message);
      throw;
    }
    catch (Exception ex)
    {
      logger.LogError(ex, ex.Message);
    }
  }
}
Listing 18-31

GraphFileService StoreThumbnailsAsync updates for data caching and task cancellation. Changes highlighted in bold

That completes most of the work for updating the GraphFileService , getting the presentation layer to update and storing the cached data in our LiteDB data store. The last method we need to update is GetRootFilesAsync . We need to complete the following tasks in this method:
  • Update the method signature to include callback and CancellationToken .

  • Read the root ID from the data store.

  • Check network connectivity.

  • Write the root ID to the data store.

See updated code for GetRootFilesAsync in Listing 18-32; the updated code will be highlighted in bold.
public async TaskIEnumerable<OneDriveItem> GetRootFilesAsync(
  Action<IEnumerable<OneDriveItem>, bool> cachedCallback = null,
  CancellationToken cancellationToken = default)
{
  var rootPathId = dataStore.GetRootId();
  if (networkConnectivity.Connectivity ==
    NetworkConnectivityLevel.InternetAccess)
  {
    try
    {
#if __ANDROID__ || __IOS__ || __MACOS__
      var response = await request
        .GetResponseAsync(cancellationToken);
      var data = await response.Content.ReadAsStringAsync();
      var rootNode = JsonConvert
        .DeserializeObject<DriveItem>(data);
#else
      var rootNode = await request.GetAsync(cancellationToken);
#endif
      if (rootNode == null || string.IsNullOrEmpty(rootNode.Id))
      {
        throw new KeyNotFoundException("Unable to find " +
          "OneDrive Root Folder");
      }
      rootPathId = rootNode.Id;
      dataStore.SaveRootId(rootPathId);
    }
    catch (TaskCanceledException ex)
    {
      logger.LogWarning(ex, ex.Message);
      throw;
    }
    catch (KeyNotFoundException ex)
    {
      logger.LogWarning("Unable to retrieve data from Graph " +
        "API, it may not exist or there could be a connection " +
        "issue");
      logger.LogWarning(ex, ex.Message);
      throw;
    }
    catch (Exception ex)
    {
      logger.LogWarning("Unable to retrieve root OneDrive 
        " + folder");
      logger.LogWarning(ex, ex.Message);
    }
  }
  return await GetFilesAsync(
    rootPathId, cachedCallback, cancellationToken);
}
Listing 18-32

GraphFileService GetRootFilesAsync updates for data caching and task cancellation. Changes highlighted in bold

That completes all the code changes we need to make for the GraphFileService. We can start working on the user interface changes. We made a change to the IGraphFileService interface to include the CancellationToken and a callback, which we will be using in the user interface.

Update Loading Indicators

The service and database layers are now complete, and we can start editing the presentation layer . We want to update the MyFilesViewModel to eagerly load the content from the cached data store and render it on the page. Then the application will make the web request to the Microsoft Graph to get the true live data and update the data store.

This means we need to change the current behavior of the loading indicators to handle cached data. If there is cached data, we will not display the loading indicator in the main content area, only that in the status bar . But if there is no cached data for that page, we will render both loading indicators. We will start with the MyFilesViewModel and finish with changes in the MyFilesPage.

To handle the two different loading indicators, we need to add a new property to the view model to track when we want it to display. Add a new property to the Properties section of your MyFilesViewModel as seen in the code snippet of Listing 18-33.
public bool IsMainContentLoading =>
  IsStatusBarLoading && !FilesAndFolders.Any();
Listing 18-33

MyFilesViewModel IsMainContentLoading property implementation

The goal of the IsMainContentLoading property is to only render if the status bar loading indicator is visible and there are no files rendered on the page. We can use the FilesAndFolders property to determine what is on the page.

Since the IsMainContentLoading property does not explicitly invoke OnPropertyChanged() , it currently will not tell the user interface that it has changed. We need to update the dependent properties to manually trigger this. You need to add a new line to the setter of FilesAndFolders and IsStatusBarLoading properties to trigger this. See the code snippet in Listing 18-34; the code additions are highlighted in bold.
List<OneDriveItem> filesAndFolders;
public List<OneDriveItem> FilesAndFolders
{
  get => filesAndFolders;
  set
  {
    SetProperty(ref filesAndFolders, value);
    OnPropertyChanged(nameof(CurrentFolderPath));
    OnPropertyChanged(nameof(IsPageEmpty));
    OnPropertyChanged(nameof(IsMainContentLoading));
  }
}
public bool IsMainContentLoading =>
  IsStatusBarLoading && !FilesAndFolders.Any();
bool isStatusBarLoading;
public bool IsStatusBarLoading
{
  get => isStatusBarLoading;
  set
  {
    SetProperty(ref isStatusBarLoading, value);
    OnPropertyChanged(nameof(IsPageEmpty));
    OnPropertyChanged(nameof(IsMainContentLoading));
  }
}
Listing 18-34

MyFilesViewModel IsMainContentLoading OnPropertyChanged events. Changes are highlighted in bold

In the GraphFileService we changed the method signature to accept a CancellationToken , which allows us to cancel the Task at any point in time after it has started. This is useful when there is a long-running request and the user does not want to wait. They can click the back button, and it will need to cancel the Task and start a new Task. The addition of the CancellationToken requires some changes into how we process the LoadDataAsync method in the MyFilesViewModel.

To add CancellationToken support, you will need to add instance variables for the MyFilesViewModel as the LoadDataAsync can be invoked from several code paths. Having the CancellationToken as an instance variable allows the method to determine if there is a running task and cancel it as the last one always overrides any existing task. See the updated code for LoadDataAsync in Listing 18-35; changes will be highlighted in bold.
CancellationTokenSource cancellationTokenSource;
TaskCompletedSource<bool> currentLoadDataTask;
async Task LoadDataAsync(
  string pathId = null,
  Action presentationCallback = null)
{
  if (cancellationTokenSource != null &&
    !cancellationTokenSource.IsCancellationRequested)
  {
    cancellationTokenSource.Cancel();
    await currentLoadDataTask?.Task;
  }
  currentLoadDataTask = new TaskCompletionSource<bool>(
    TaskCreationOptions.RunContinuationsAsynchronously);
  cancellationTokenSource = new CancellationTokenSource();
  var cancellationToken = cancellationTokenSource.Token;
  try
  {
    IsStatusBarLoading = true;
    IEnumerable<OneDriveData> data;
    Action<IEnumerable<OneDriveItem>, bool> updateFilesCallback =
      (items, isCached) => UpdateFiles(items, null, isCached);
    if (string.IsNullOrEmpty(pathId))
    {
      data = await graphFileService.GetRootFilesAsync(
        updateFilesCallback, cancellationToken);
    }
    else
    {
      data = await graphFileService.GetFiles(
        pathId, updateFilesCallback, cancellationToken);
    }
  }
  catch (Exception ex)
  {
    logger.LogError(ex, ex.Message);
  }
  finally
  {
    cancellationTokenSource = default;
    cancellationToken = default;
    Forward.NotifyCanExecuteChanged();
    Back.NotifyCanExecuteChanged();
    IsStatusBarLoading = false;
    currentLoadDataTask.SetResult(true);
  }
}
Listing 18-35

MyFilesViewModel LoadDataAsync updates to use new API from IGraphFileService and CancellationToken. Changes highlighted in bold

Now, we can modify the UpdateFiles method, which has an adjusted method signature to invoke callbacks if the data is cached. This is a method that will be invoked from the MyFilesViewModel and the GraphFileService. The change to this method is relatively small but has impact. You will adjust the method signature and invoke the callback if the data is cached. See the updated code snippet in Listing 18-36 with the changes highlighted in bold.
void UpdateFiles(
  IEnumerable<OneDriveItem> files,
  Action presentationCallback,
  bool isCached = false)
{
  if (files == null)
  {
    NoDataMessage = "Unable to retrieve data from API, 
      " + "check network connection";
    logger.LogInformation("No data retrieved from API, 
      " + "ensure have a stable internet connection");
    return;
  }
  else if (!files.Any())
  {
    NoDataMessage = "No files or folders";
  }
  FilesAndFolders = files.ToList();
  if (isCached)
  {
    presentationCallback?.Invoke();
  }
}
Listing 18-36

MyFilesViewModel UpdateFiles to invoke callback. Changes highlighted in bold

The MyFilesViewModel changes are complete, and now we can update the loading indicator in the MyFilesPage.xaml file. Currently all the indicators are using the property IsStatusBarLoading , and we want to update the not_skia and skia indicator in the main content area to use IsMainContentLoading . Open the MyFilesPage.xaml and find the not_skia:ProgressRing and skia:TextBlock in the Grid.Row="1" section. Update the controls to match the code snippet in Listing 18-37.
<not_skia:ProgressRing
  Width="300"
  Height="300"
  IsActive=”{Binding IsMainContentLoading}”
  Visibility=”{Binding IsMainContentLoading, Converter=
    {StaticResource BoolToVisibilityConverter}}” />
<skia:TextBlock
  Text="Loading . . ."
  FontSize="40"
  Foreground="Black"
  HorizontalAlignment="Center"
  VerticalAlignment="Center"
  Visibility=”{Binding IsMainContentLoading, Converter=
    {StaticResource BoolToVisibilityConverter}}” />
Listing 18-37

MyFilesPage.xaml updates – loading indicator for main content area to use IsMainContentLoading. Changes highlighted in bold

That completes the updates for the loading indicators for the user interface. You can now launch the application and test it out!

Conclusion

In this chapter we implemented several new concepts to handle offline data access and better page state management. This chapter is really an introduction where we used LiteDB as our local data store to cache data. We then updated the presentation layer to eagerly render any cached data while we wait for the Microsoft Graph to respond with real data.

If you had any trouble following along with the code in this chapter, you can download the completed code from GitHub: https://github.com/SkyeHoefling/UnoDrive/tree/main/Chapter%2018 .

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.102.225