© 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_13

13. Microsoft Graph, Web APIs, and MyFilesPage

Skye Hoefling1  
(1)
Rochester, NY, USA
 

The Microsoft Graph is the API we are going to use to communicate with Microsoft OneDrive to retrieve files and folders in our UnoDrive application . It is a RESTful API, which you can integrate with using any language, that provides read and write access to many Microsoft services, including OneDrive.

In this chapter we will be building on the authentication concepts that we learned in Chapter 11. Upon successfully logging into the application, you will have a stored access token that can be used for any Microsoft Graph API request. We will be creating the building blocks of our main user interface by implementing the Microsoft Graph integration . By the end of this chapter, we will have a full implementation that displays some items on the screen retrieved from OneDrive. This will be a basic implementation, and we will be expanding on these concepts in future chapters.

Microsoft Graph

The Microsoft Graph (the Graph) is the best way to integrate with various Microsoft services including Office 365, OneDrive, and many more. It provides a central RESTful API that applications of any language and platform can integrate with. Prior to using the Graph, you will need to obtain a valid access token, which is usually done using the Microsoft Authentication Library also, known as MSAL. When authenticating you will request scopes to gain access to various endpoints on the Graph. The authentication concepts have already been covered in Chapter 11.

Our UnoDrive application is an introduction to the Microsoft Graph and what you can do with it. You can learn more about it and what you can do by going to the official Microsoft documentation: https://docs.microsoft.com/graph/overview . If you want to explore the Graph, you can use the Graph Explorer tool, which allows you to make API calls right from the sandbox tool: https://developer.microsoft.com/graph/graph-explorer .

Web APIs and Uno Platform

In Uno Platform integrating with a simple Web API is very straightforward. At the basic level, you will need to create an HttpClient and specify your address and any headers.

Consider a basic request that tries to read data at https://localhost/api/. See the standard integration in Listing 13-1.
string url = "https://localhost/api/";
HttpClient client = new HttpClient();
HttpResponseMessage response = await client.GetAsync(url);
Listing 13-1

Standard HttpClient implementation

Graph Service Implementation

As we build our Microsoft Graph and user interface in this chapter, we are going to work from the bottom up. This means we will start by implementing the necessary service code and finish with the user interface code.

Add NuGet Packages

The Microsoft Graph SDK is a NuGet package that we can include in all our project heads and is the first step in implementing any integration. Add the code from Listing 13-2 to all of your project heads: UnoDrive.Windows, UnoDrive.Wasm, UnoDrive.Skia.Wpf, UnoDrive.Skia.Gtk, and UnoDrive.Mobile.
<PackageReference
  Include="Microsoft.Graph"
  Version="4.32.0" />
Listing 13-2

Microsoft.Graph NuGet package reference

The WebAssembly (WASM) target needs updates to the LinkerConfig.xml; otherwise, certain APIs may be removed at compile time. Add the code from Listing 13-3 to your LinkerConfig.xml.
<assembly fullname="Microsoft.Graph" />
<assembly fullname="Microsoft.Graph.Core" />
Listing 13-3

WebAssembly (WASM) LinkerConfig.xml updates for the Microsoft.Graph NuGet package

Note

The linker is a compile-time tool that runs on various platforms to remove unused APIs, which keeps the generated assembly size small. By adding the Microsoft Graph assemblies to the LinkerConfig.xml, it prevents those APIs from being removed. This is specifically important for WASM as the first load of the application requires the assembly to be downloaded. The smaller it is, the faster the application will load.

Data Models

The data models , also known as entities , store the resulting data used in the Microsoft Graph. We will have our own data structure that we will map the results of the APIs to. These data models will be the return type on our implementation of our service integration. This helps create a clean separation between the Microsoft Graph SDK and our implementation. In our case this is valuable as we don’t need all the data being returned from the SDK. Creating the data model allows us to have only properties that our application needs.

We need to capture two types of data models: an item type and item details . In the UnoDrive.Shared project, create a new root directory named Data. In that new folder, you will create two new classes named OneDriveItem.cs and OneDriveItemType.cs. See Figure 13-1 for a screenshot of the Visual Studio solution Explorer.

A screenshot represents the two solutions out of nine solutions in the visual studio for UnoDrive, namely one drive item dot cs and one drive item type dot cs, respectively.

Figure 13-1

Data models in the Visual Studio Solution Explorer

The OneDriveItemType is a simple data model that is an enum that only stores if the item is a file or a folder. An enum is a useful structure in .NET that allows you to define a set of constants that can be strongly type-checked.

Note

.NET enums or enumerations are very useful when you have an object that needs type-checking. By encapsulating the enum values, other parts of your application can quickly check the enum values to determine various business rules and code paths.

Implement the OneDriveItemType by using the code in Listing 13-4.
public enum OneDriveItemType
{
  File = 0,
  Folder = 1
}
Listing 13-4

OneDriveItemType implementation

The OneDriveItem will store details about a specific item, which can be either a folder or a file. The OneDriveItem will encapsulate the OneDriveItemType we just defined along with other properties we will need later in the service implementation. See the full code for the OneDriveItem in Listing 13-5.
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 ImageSource ThumbnailSource { get; set; }
}
Listing 13-5

OneDriveItem implementation

.NET 6 Mobile Data Models

In .NET 6 there are special target frameworks for the various mobile targets such as net6.0-android, net6.0-ios, net6.0-maccatalyst, net6.0-macos, and others. The backward compatibility layer for the .NET Standard with these target frameworks is very thin. If you are using a third-party package that isn’t specifically compiled for your target framework, specifically the mobile targets of .NET 6, you may get runtime errors. These errors can be hidden in very obscure error messages.

This section focuses on issues that may still happen in the Microsoft.Graph SDK or other similar libraries you are using. It is best to get a compiled binary for your specific target framework to avoid these problems. For example, if you are using net6.0-android , ensure your binary has been compiled for net6.0-android specifically.

Prior to the release of .NET 6 for mobile targets, the Microsoft.Graph SDK didn’t fully support using it. When querying some of the APIs, you would see the error message in Listing 13-6.
System.TypeLoadException: Could not resolve type with
  token 01000023 from typeref (expected class
  'System.Threading.Tasks.ValueTask`1' in assembly 'mscorlib,
  Version=2.0.5.0, Culture=neutral,
  PublicKeyToken=7cec85d7bea7798e')
Listing 13-6

.NET 6 mobile exception with backward compatibility issues

This exception is unable to find a necessary object at runtime that the SDK depends on, which caused the runtime error.

The Microsoft Graph is a series of RESTful endpoints that return JSON strings of serialized data. To work around this problem, we can take the raw object and deserialize it manually instead of using the Microsoft.Graph SDK. We can write platform-specific code to handle this situation vs. using the objects directly from the SDK.

Caution

It is always best to use the third-party package the way it was intended to be used. If it doesn’t natively support your target framework, this is a valid workaround.

In the UnoDrive.Shared project under the Data directory, create five new files:
  • DriveItem.android.ios.macos.cs: Listing 13-8

  • DriveItemCollection.android.ios.macos.cs: Listing 13-9

  • Folder.android.ios.macos.cs: Listing 13-10

  • Thumbnail.android.ios.macos.cs: Listing 13-11

  • ThumbnailImage.android.ios.macos.cs: Listing 13-12

We only create data models for the mobile target frameworks because those will require manual invocations of the RESTful API. As we create the various data models that are needed, we will try and leverage the objects directly from the SDK when possible. It is not always possible to derserialize straight to the object.

Important

For each file created in this section, you will need to add the pre-processor directive for the platforms supported. Wrap the entire file in #if __ANDROID__ || __IOS__ || __MACOS__. This needs to be done for Listings 13-8 through 13-12.

The DriveItem object is our top-level OneDrive object that contains specific details about the item, which can be a file or folder. See code in Listing 13-7.
public class DriveItem
{
  [JsonPropertyName("id")]
  public string Id { get; set; }
  [JsonPropertyName("name")]
  public string Name { get; set; }
  [JsonPropertyName("parentReference")]
  public ItemReference ParentReference { get; set; }
  [JsonPropertyName("size")]
  public Int64? Size { get; set; }
  [JsonPropertyName("lastModifiedDateTime")]
  public DateTimeOffset? LastModifiedDateTime { get; set; }
  [JsonPropertyName("thumbnails")]
  public Thumbnail[] Thumbnails { get; set; }
  [JsonPropertyName("folder")]
  public Folder Folder { get; set; }
}
Listing 13-7

DriveItem implementation

The DriveItemCollection is used when the SDK is returning a collection of DriveItem objects. Since the DriveItem defines one specific item, the DriveItemCollection is useful to see all the items in the current path. See code in Listing 13-8.
public class DriveItemCollection
{
  [JsonPropertyName("value")]
  public DriveItem[] Value { get; set; }
}
Listing 13-8

DriveItemCollection implementation

The Folder object details how many children exist in that folder. See code in Listing 13-9.
public class Folder
{
  [JsonPropertyName("childCount")]
  public Int32? ChildCount { get; set; }
}
Listing 13-9

Folder implementation

The Thumbnail object contains various ThumbnailImage objects, which are used for various sizes. This is useful when you want to display a large vs. medium vs. small thumbnail image. See code in Listing 13-10.
public class Thumbnail
{
  [JsonPropertyName("id")]
  public string Id { get; set; }
  [JsonPropertyName("large")]
  public ThumbnailImage Large { get; set; }
  [JsonPropertyName("medium")]
  public ThumbnailImage Medium { get; set; }
  [JsonPropertyName("small")]
  public ThumbnailImage Small { get; set; }
}
Listing 13-10

Thumbnail implementation

The ThumbnailImage contains the details of the image and a URL to download the file. See code in Listing 13-11.
public class ThumbnailImage
{
  [JsonPropertyName("height")]
  public int Height { get; set; }
  [JsonPropertyName("width")]
  public int Width { get; set; }
  [JsonPropertyName("url")]
  public string Url { get; set; }
}
Listing 13-11

ThumbnailImage implementation

GraphFileService Interface

The Microsoft Graph implementation will be used in our Dependency Injection system so we can inject a simple interface to be used in the view models . There are two APIs that need to be implemented:
  • GetRootFilesAsync : Returns the items in the root OneDrive path

  • GetFilesAsync : Returns the items by a specified unique ID

In the UnoDrive.Shared project under the Services folder, create a new file named IGraphFileService.cs. This will be the interface definition for our graph implementation. See code in Listing 13-12 for the interface definition.
public interface IGraphFileService
{
  Task<IEnumerable<OneDriveItem>> GetRootFilesAsync();
  Task<IEnumerable<OneDriveItem>> GetFilesAsync();
}
Listing 13-12

IGraphFileService interface definition

Note

The IGraphFileService interface defines a return type object of OneDriveItem. This is the data model that we defined earlier in the chapter.

GraphFileService: Stubs

All the building blocks are in place for us to implement the GraphFileService. This class will contain the code that invokes Web APIs against the Microsoft Graph, which will allow us to integrate with OneDrive as we build out our UnoDrive application.

Start by creating the basic class and stubbing out the methods from the IGraphFileService interface . In the UnoDrive.Shared project under the Services folder, create a new file named GraphFileService.cs. Open this file and add the code from Listing 13-13.
public class GraphFileService : IGraphFileService
{
  public async
    Task<IEnumerable<OneDriveItem>> GetRootFilesAsync()
  {
    // TODO – add implementation
  }
  public async
    Task<IEnumerable<OneDriveItem>> GetFilesAsync(string id)
  {
    // TODO – add implementation
  }
}
Listing 13-13

GraphFileService stubbed implementation

GraphFileService: AuthenticationProvider

The Microsoft Graph SDK requires an implementation of the IAuthenticationProvider interface. This interface provides an API that includes the HttpRequestMessage . This gives our GraphFileService implementation the necessary entry point to set the access token on any request. To implement this, we will make the GraphFileService implement IAuthenticationProvider and update the HttpRequestMessage. See the code in Listing 13-14.
public class GraphFileService :
  IGraphFileService, IAuthenticationProvider
{
  // omitted code
  Task IAuthenticationProvider.AuthenticateRequestAsync(
    HttpRequestMessage request)
  {
    string token = ((App)App.Current)
      .AuthenticationResult
      ?.AccessToken;
    if (string.IsNullOrEmpty(token))
    {
      throw new Exception("No Access Token");
    }
    request.Headers.Authorization =
      new AuthenticationHeaderValue("Bearer", token);
    return Task.CompletedTask;
  }
}
Listing 13-14

GraphFileService IAuthenticationProvider implementation

Note

In Listing 13-14 we retrieve the access token from the current session. This method will throw an exception if the user has not been authenticated. We are going to make the assumption that this class is only ever used after the user has performed authentication.

To finish the IAuthenticationProvider implementation, we need to add the constructor and a class instance of GraphServiceClient . This object will be used in the API implementations to send requests to the Micorosft Graph. We will also need to inject an instance of the ILogger interface so we can log errors as we implement the APIs. See Listing 13-15 for the constructor code.
public class GraphFileService :
  IGraphFileService, IAuthenticationProvider
{
  GraphServiceClient graphClient;
  ILogger logger;
  public GraphFileService(ILogger<GraphFileService> logger)
  {
    this.logger = logger;
    var httpClient = new HttpClient();
    graphClient = new GraphServiceClient(httpClient);
    graphClient.AuthenticationProvider = this;
  }
  // omitted code
}
Listing 13-15

GraphFileService constructor implementation

To connect our GraphFileService implementation to the Microsoft.Graph SDK, we need to set the graphClient.AuthenticationProvider to be our current instance. This will then invoke our implementation of IAuthetnicationProvider as needed by the GraphServiceClient object.

Gr aphFileService: GetFilesAsync

Let’s work on the implementation of GetFilesAsync(string id). The goal of this API is given a unique ID value, the method will return the items at that corresponding OneDrive location. Our implementation is broken into three parts:
  1. 1.

    Request OneDrive data.

     
  2. 2.

    Map data to an UnoDrive object.

     
  3. 3.

    Retrieve thumbnails and write to local storage.

     
Using the Microsoft.Graph SDK, we can request all children given a particular unique identifier. Start building your request as seen in Listing 13-16.
public async Task<IEnumerable<OneDriveItem>>
  GetFilesAsync(string id)
{
  var request = graphClient
    .Me
    .Drive
    .Items[id]
    .Children
    .Request()
    .Expand("thumbnails");
}
Listing 13-16

GraphFileService GetFilesAsync request builder

The code in Listing 13-16 goes through several APIs to get our request object. You start by using the Me API to access the current user. Then the Drive API tells the builder that you want OneDrive data . Items[id] denotes the specific drive path to look in. Children returns all the items in that specific drive path. Then Request() generates the request object that can be invoked. Finally, the Expand("thumbnails") ensures that thumbnail data is returned as it is optional.

Next, we can invoke the request and convert the results to an array. See Listing 13-17 for the invocation added to our code.
public async Task<IEnumerable<OneDriveItem>>
  GetFilesAsync(string id)
{
  var request = graphClient
    .Me
    .Drive
    .Items[id]
    .Children
    .Request()
    .Expand("thumbnails");
  var oneDriveItems = (await request.GetAsync()).ToArray();
}
Listing 13-17

GraphFileService GetFilesAsync request invocation

We now have all the OneDrive items for the specified unique ID that was provided as a parameter to GetFilesAsync . Let’s take this data and map it from the SDK model to our UnoDrive model, which will be usable by our application. See updated code in Listing 13-18.
public async Task<IEnumerable<OneDriveItem>>
  GetFilesAsync(string id)
{
  var request = graphClient
    .Me
    .Drive
    .Items[id]
    .Children
    .Request()
    .Expand("thumbnails");
  var oneDriveItems = (await request.GetAsync()).ToArray();
  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);
}
Listing 13-18

GraphFileService GetFilesAsync data mapping to the UnoDrive object

The goal of the object mapping code is to simplify the data structure so we can use it in our presentation layer. We do not need all the items in the Microsoft.Graph SDK object, and it makes it easier to access when we work in a flatter structure.

With our data mapped correctly to the UnoDrive object, we can now read the thumbnail data, which is another web request. Create a new private method named StoreThumbnailsAsync(DriveItem[] oneDriveItems, IDictionary<string, OneDriveItem> childrenTable). This method will contain the logic for iterating through all the items at the current path, downloading and storing the thumbnail data so it can be used in our UnoDrive object.

The Microsoft.Graph SDK stores three types of thumbnails for each item: small, medium, and large. We are only going to be using the medium thumbnail as this works well across our various form factors. Our logic for the StoreThumbnailsAsync method is as follows:
  1. 1.

    Check for thumbnails.

     
  2. 2.

    Retrieve thumbnail data.

     
  3. 3.

    Save thumbnail bytes to disk.

     
  4. 4.

    Update ImageSource on the UnoDrive object.

     
See Listing 13-19 for complete StoreThumbnailsAsync() code.
async Task StoreThumbnailsAsync(
  DriveItem[] oneDriveItems,
  IDictionary<string, OneDriveItem> childrenTable)
{
  for (int index = 0; index < oneDriveItems.Length; index++)
  {
    var currentItem = oneDriveItems[index];
    var thumbnails = currentItem.Thumbnails?.FirstOrDefault();
    if (thumbnails == null ||
      !childrenTable.ContainsKey(currentItem.Id))
    {
      continue;
    }
    var url = thumbnails.Medium.Url;
    var httpClient = new HttpClient();
    var thumbnailResponse = await httpClient.GetAsync(url);
    if (!thumbnailResponse.IsSuccessStatusCode)
    {
      continue;
    }
    var imagesFolder = Path.Combine(
      Windows.Storage.ApplicationData.Current.LocalFolder.Path,
      "thumbnails");
    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();
      await System.IO.File.WriteAllBytesAsync(
        localFilePath, bytes);
      var image = new BitmapImage(new Uri(localFilePath));
      childrenTable[currentItem.Id].ThumbnailSource = image;
    }
    catch(Exception ex)
    {
      logger.LogError(ex, ex.Message);
    }
  }
}
Listing 13-19

GraphFileService implementation for StoreThumbnailsAsync

The StoreThumbnailsAsync code is completed, and we can finish up our current implementation of GetFilesAsync . Since C# is passing the objects by reference to StoreThumbnailsAsync, the childrenTable does not need to be returned.

Note

When an object is passed by reference to another method, that means you retain the same object in memory. Any updates to that object within the scope of the method are persisted to the calling member. In other words, the changes that happen in StoreThumbnailsAsync are available to that object in the GetFilesAsync method.

We can take the current object and return it as we need to. It is currently in a dictionary form, and we need to convert it to a standard IEnumerable . Update your GetFilesAsync code to invoke the new method StoreThumbnailsAsync and return the final childrenTable . See the code snippet in Listing 13-20 and complete code in Listing 13-21.
await StoreThumbnailsAsync(oneDriveItems, childrenTable);
return childrenTable.Select(x => x.Value);
Listing 13-20

GraphFileService GetFilesAsync StoreThumbnailsAsync and return data

public async Task<IEnumerable<OneDriveItem>>
  GetFilesAsync(string id)
{
  var request = graphClient
    .Me
    .Drive
    .Items[id]
    .Children
    .Request()
    .Expand("thumbnails");
  var oneDriveItems = (await request.GetAsync()).ToArray();
  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);
    await StoreThumbnailsAsync(oneDriveItems, childrenTable);
    return childrenTable.Select(x => x.Value);
}
Listing 13-21

GraphFileService GetFilesAsync current implementation

. NET 6 Mobile Considerations

Earlier in this chapter, we reviewed .NET 6 considerations as some libraries do not fully support all .NET 6 target frameworks like the mobile targets we are using in our UnoDrive application . We will need to add platform-specific code for these and manually deserialize the JSON string.

In the GetFilesAsync API, update the request builder and invocation code to the snippet in Listing 13-22.
var request = graphClient
  .Me
  .Drive
  .Items[id]
  .Children
  .Request()
  .Expand("thumbnails");
#if __ANDROID__ || __IOS__ || __MACOS__
  var response = await request.GetResponseAsync();
  var data = await response.Content.ReadAsStringAsync();
  var collection = JsonSerializer.Deserialize
    <UnoDrive.Models.DriveItemCollection>(data);
  var oneDriveItems = collection.Value;
#else
  var oneDriveItems = (await request.GetAsync()).ToArray();
#endif
Listing 13-22

GraphFileService GetFilesAsync .NET 6 request builder snippet

Using the specific .NET 6 models we created creates a breaking change in the StoreThumbnailsAsync method. In the .NET 6 mobile targets, it expects UnoDrive.Models.DriveItem, and in the other platforms, it can use the Microsoft.Graph SDK. To resolve this problem, we will add a pre-processor directive to the method signature. See the updated code snippet in Listing 13-23.
#if __ANDROID__ || __IOS__ || __MACOS__
  async Task StoreThumbnailsAsync(
    UnoDrive.Models.DriveItem[] oneDriveItems,
    IDictionary<string, OneDriveItem> childrenTable)
#else
  async Task StoreThumbnailsAsync(
    DriveItem[] oneDriveItems,
    IDictionary<string, OneDriveItem> childrenTable)
#endif
Listing 13-23

GraphFileService StoreThumbnailsAsync .NET 6 method signature

WPF Considerations

In the private method StoreThumbnailsAsync , there are things to consider for the WPF platform . We are using the Windows.Storage API to determine the correct file path to place the cached thumbnail into. This path varies depending on the platform, and we will need to make sure it is being stored correctly. All the platforms do a good job at this except for WPF; it places the file in a generic temporary directory that isn’t application specific. For this platform we will need to add WPF-specific code to handle the path.

Update the imageFolder declaration code in the StoreThumbnailsAsync method – see the code snippet in Listing 13-24.
#if HAS_UNO_SKIA_WPF
  var applicationFolder = Path.Combine(
    Windows.Storage.ApplicationData.Current.TemporaryFolder.Path,
    "UnoDrive");
  var imageFolder = Path.Combine(
    applicationFolder, "thumbnails");
#else
  var imageFolder = Path.Combine(
    Windows.Storage.ApplicationData.Current.LocalFolder.Path,
    "thumbnails");
#endif
Listing 13-24

GraphFileService StoreThumbnailsAsync WPF-specific imageFolder code snippet

All the other platforms support File.WriteAllBytesAsync except WPF. In this case we need to add WPF-specific code for writing the thumbnail to disk to use the synchronous API. See the code snippet in Listing 13-25.
#if HAS_UNO_SKIA_WPF
  System.IO.File.WriteAllBytes(localFilePath, bytes);
#else
  await System.IO.File.WriteAllBytesAsync(localFilePath, bytes);
#endif
Listing 13-25

GraphFileService StoreThumbnailsAsync WPF-specific WriteAllBytes code snippet

C ross-Platform ImageSource Considerations

The last step in the StoreThumbnailsAsync method is to read the bytes of the cached thumbnail file and set it to the ImageSource on our OneDriveItem. The standard way to do this is by using a BitmapImage and Uri as seen in Listing 13-26.
var image = new BitmapImage(new Uri(localFilePath));
Listing 13-26

Instantiate BitmapImage from file

This technique is only working correctly on Windows and the mobile targets. It doesn’t work correctly on WebAssembly (WASM), WPF, or GTK. Update our BitmapImage code to handle the various platforms as seen in the code snippet in Listing 13-27.
#if __UNO_DRIVE_WINDOWS__ || __ANDROID__ || __IOS__ || __MACOS__
  var image = new BitmapImage(new Uri(localFilePath));
#else
  var image = new BitmapImage();
  image.SetSource(new MemoryStream(bytes));
#endif
Listing 13-27

GraphFileService StoreThumbnailsAsync cross-platform code for BitmapImage from file

GraphFileService: GetRootFilesAsync

The GetRootFilesAsync API is intended to get the files at the root path of the user’s OneDrive. It does not include any parameters and depends on GetFilesAsync . This means we need to implement an algorithm to retrieve the unique ID for the root path. The Microsoft.Graph SDK provides a special Root API on the request builder just for this scenario.

Add basic implementation with the request builder to retrieve the correct ID. See Listing 13-28 for code.
public async Task<IEnumerable<OneDriveItem>> GetRootFilesAsync()
{
  var request = graphClient
    .Me
    .Drive
    .Root
    .Request();
  var rootNode = await request.GetAsync();
  return await GetFilesAsync(rootNode.Id);
}
Listing 13-28

GraphFileService GetRootFilesAsync basic implementation

Making web requests is always susceptible to exceptions being thrown. Let’s wrap this in a try-catch block and add some error handling. See updated code in Listing 13-29.
public async Task<IEnumerable<OneDriveItem>> GetRootFilesAsync()
{
  var rootPathId = string.Empty;
  try
  {
    var request = graphClient
      .Me
      .Drive
      .Root
      .Request();
    var rootNode = await request.GetAsync();
    if (rootNode == null ||
      string.IsNullOrEmpty(rootNode.Id))
    {
      throw new KeyNotFoundException(
        "Unable to find OneDrive Root Folder");
    }
    rootPathId = rootNode.Id;
  }
  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(rootNode.Id);
}
Listing 13-29

GraphFileService GetRootFilesAsync current implementation

.NET 6 Mobile Considerations

Earlier in this chapter, we reviewed .NET 6 considerations as some libraries do not fully support all .NET 6 target frameworks like the mobile targets we are using in our UnoDrive application. We will need to add platform-specific code for these platforms and manually deserialize the JSON string.

In the GetRootFilesAsync API, update the request builder and invocation code to the snippet in Listing 13-30.
var request = graphClient
  .Me
  .Drive
  .Root
  .Request();
#if __ANDROID__ || __IOS__ || __MACOS__
  var response = await request.GetResponseAsync();
  var data = await response.Content.ReadAsStringAsync();
  var rootNode = JsonSerializer.Deserialize<DriveItem>(data);
#else
  var rootNode = await request.GetAsync();
#endif
Listing 13-30

GraphFileService GetRootFilesAsync .NET 6 request builder snippet

Dependency Injection Setup

The Microsoft Graph service implementation is complete, and we now have an IGraphFileService interface and GraphFileService implementation. The next step is to update our Dependency Injection container and register the new classes, which will allow us to inject it into the view models.

In the UnoDrive.Shared project, open the file App.xaml.cs and add the code in Listing 13-31 to the ConfigureServices method. See complete ConfigureServices code in Listing 13-32.
services.AddTransient<IGraphFileService, GraphFileService>();
Listing 13-31

Register IGraphFileService 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>();
}
Listing 13-32

App.xaml.cs complete ConfigureServices code

ViewModel Implementation

Moving up the stack from the OneDrive integration, we will be using the IGraphFileService in the MyFilesViewModel. Our goal is to display all the items and the current path and manage loading state. Right now, we are focusing just on loading the items at the root directory, and we will be handling navigation and offline data in future chapters.

Constructor

Currently, MyFilesViewModel should be a stubbed-out empty class with nothing in it. This class needs the ILogger and IGraphFileService. Let’s create local variables and inject them into the constructor. See code in Listing 13-33.
public class MyFilesViewModel
{
  IGraphFileService graphFileService;
  ILogger logger;
  public MyFilesViewModel(
    IGraphFileService graphFileService,
    ILogger<MyFilesViewModel> logger)
  {
    this.graphFileService = graphFileService;
    this.logger = logger;
  }
}
Listing 13-33

MyFilesViewModel constructor injection

The items in the current OneDrive location need to be stored in a collection that can be used in the user interface . We are going to use a basic List<T> to store our OneDriveItems . To properly notify the user interface, a view model must implement INotifyPropertyChanged, and since we are using the CommunityToolkit.Mvvm NuGet package , we have an implementation named ObservableObject that already implements the interface for us. The ObservableObject class provides a useful helper method named SetProperty(), which will update the local variable with the value. Update the MyFilesViewModel to inherit from ObservableObject . See the code snippet in Listing 13-34.
public class MyFilesViewModel : ObservableObject
{
  // omitted code
}
Listing 13-34

Class declaration for MyFilesViewModel inheriting from ObservableObject

Note

When data binding collections, you can also use an ObservableCollection, which automatically notifies the View or page when changes happen to the collection. We are not using an ObservableCollection as our entire collection changes as the page is loaded and there is no need for dynamic data loading. When designing your application, pick the data structure that makes the most sense for your business rules.

Now we can add a new property and private variable for storing the current items in the OneDrive path. Create a local variable of type List<OneDriveItem>, which will be the source of items in the current path. Then create a public property that will return that variable. The setter on this property will be used to set and notify the user interface. When we get to state management, we will need to come back to this setter and notify the user interface of other properties. See the code snippet in Listing 13-35 for the new FilesAndFolders property.
List<OneDriveItem> filesAndFolders;
public List<OneDriveItem> FilesAndFolders
{
  get => filesAndFolders;
  set
  {
    SetProperty(ref filesAndFolders, value);
  }
}
Listing 13-35

MyFilesViewModel FilesAndFolders property

Now that our FilesAndFolders property is created, we can instantiate it in the constructor. Add the code snippet in Listing 13-36 to the constructor.
FilesAndFolders = new List<OneDriveItem>();
Listing 13-36

MyFilesViewModel – instantiate FilesAndFolders list

The constructor code is complete. You can see the full constructor code in Listing 13-37.
public MyFilesViewModel(
  IGraphFileService graphFileService,
  ILogger<MyFilesViewModel> logger)
{
  this.graphFileService = graphFileService;
  this.logger = logger;
  FilesAndFolders = new List<OneDriveItem>();
}
Listing 13-37

MyFilesViewModel complete constructor code

Properties and Data Binding

The public properties are used as a bridge to the user interface via data binding. These are the values that are used in the various controls on the user interface. When we implemented the constructor, we added our main property FilesAndFolders that stores the items in the current OneDrive path. In this section we are going to add the remaining properties and make the necessary data binding updates.

We need to create four additional properties:
  • IsPageEmpty : True if there are no items on the current page and the page is not loading data.

  • CurrentFolderPath : A string that represents the current OneDrive path.

  • NoDataMessage : A simple message that is displayed if there is no data on the current page.

  • IsStatusBarLoading : If true, a ProgressRing or message is displayed telling the user that data is loading.

Let’s start by stubbing out all our properties, and then we can work through them one at a time. Add the code snippet from Listing 13-38.
public bool IsPageEmpty { get; set; }
public string CurrentFolderPath { get; set; }
public string NoDataMessage { get; set; }
public bool IsStatusBarLoading { get; set; }
Listing 13-38

MyFilesViewModel property stubs

As implemented in Listing 13-38, the user interface will never be notified on changes to any of these. Some of them will not need to explicitly notify the user interface as that is managed elsewhere. IsStatusBarLoading and NoDataMessage are easy to implement as they follow the standard convention for data binding and ObservableObject . For both of them, create a private variable and use the SetProperty() method. See the updated code snippet in Listing 13-39.
string noDataMessage;
public string NoDataMessage
{
  get => noDataMessage;
  set => SetProperty(ref noDataMessage, value);
}
bool isStatusBarLoading;
public bool IsStatusBarLoading
{
  get => isStatusBarLoading;
  set => SetProperty(ref isStatusBarLoading, value);
}
Listing 13-39

MyFilesViewModel IstStatusBarLoading and NoDataMessage property implementations

Tip

After implementing a property to use the data binding convention as seen in Listing 13-39, you must always interact with the property and not the local variable. If you do not follow that rule, your user interface will not be notified of changes. Example: Update the loading status via IsStatusBarLoading instead of isStatusBarLoading.

To determine if the page is empty, we need to certify that the FilesAndFolders list is empty and the page is not loading. There is no need for us to create a local variable as this property will be notified whenever the FilesAndFolders list or IsStatusBarLoading is updated. We will be updating the notification code for these properties in Listings 13-42 and 13-43. See the updated code snippet for IsPageEmpty in Listing 13-40.
public bool IsPageEmpty =>
  !IsStatusBarLoading && !FilesAndFolders.Any();
Listing 13-40

MyFilesViewModel IsPageEmpty property implementation

The last property we are going to implement is the CurrentFolderPath property. This is a string that represents our current path in OneDrive, and we can obtain this from any item in the FilesAndFolders property. We will attempt to use the first item in the list and then use the Path property . If there are no items in the list, it will return an empty string. See updated property code in Listing 13-41.
public bool IsPageEmpty =>
  !IsStatusBarLoading && !FilesAndFolders.Any();
public string CurrentFolderPath =>
  FilesAndFolders.FirstOrDefault()?.Path;
string noDataMessage;
public string NoDataMessage
{
  get => noDataMessage;
  set => SetProperty(ref noDataMessage, value);
}
bool isStatusBarLoading;
public bool IsStatusBarLoading
{
  get => isStatusBarLoading;
  set => SetProperty(ref isStatusBarLoading, value);
}
Listing 13-41

MyFilesViewModel CurrentFolderPath property implementation

The four properties have been implemented, but both IsPageEmpty and CurrentFolderPath are not notifying the user interface on changes. As it is currently implemented, this value will be loaded once in the user interface and never change. Both properties are dependent on changes to the FilesAndFolders property. We can notify the user interface by using the method OnPropertyChanged() and providing the property. This will notify the user interface of changes. See the updated FilesAndFolders implementation in Listing 13-42.
List<OneDriveItem> filesAndFolders;
public List<OneDriveItem> FilesAndFolders
{
  get => filesAndFolders;
  set
  {
    SetProperty(ref filesAndFolders, value);
    OnPropertyChanged(nameof(CurrentFolderPath));
    OnPropertyChanged(nameof(IsPageEmpty));
  }
}
Listing 13-42

MyFilesViewModel updated implementation for FilesAndFolders to notify the user interface

Tip

When using OnPropertyChanged you can specify the property name either using a string such as "CurrentFolderPath" or using nameof(CurrentFolderPath). Using the nameof() syntax protects your code against property name changes in the future.

The property IsPageEmpty depends on both FilesAndFolders and IsStatusBarLoading , which means we need to manually trigger the OnPropertyChanged from the setter of IsStatusBarLoading, just like we did in FilesAndFolders. See the updated IsStatusBarLoading code snippet in Listing 13-43.
bool isStatusBarLoading;
public bool IsStatusBarLoading
{
  get => isStatusBarLoading;
  set
  {
    SetProperty(ref isStatusBarLoading, value);
    OnPropertyChanged(nameof(IsPageEmpty));
  }
}
Listing 13-43

MyFilesViewModel IsStatusBarLoading property updates

This completes our property and data binding code. The completed property code can be seen in Listing 13-44.
List<OneDriveItem> filesAndFolders;
public List<OneDriveItem> FilesAndFolders
{
  get => filesAndFolders;
  set
  {
    SetProperty(ref filesAndFolders, value);
    OnPropertyChanged(nameof(CurrentFolderPath));
    OnPropertyChanged(nameof(IsPageEmpty));
  }
}
public bool IsPageEmpty =>
  !IsStatusBarLoading && !FilesAndFolders.Any();
public string CurrentFolderPath =>
  FilesAndFolders.FirstOrDefault()?.Path;
string noDataMessage;
public string NoDataMessage
{
  get => noDataMessage;
  set => SetProperty(ref noDataMessage, value);
}
bool isStatusBarLoading;
public bool IsStatusBarLoading
{
  get => isStatusBarLoading;
  set
  {
    SetProperty(ref isStatusBarLoading, value);
    OnPropertyChanged(nameof(IsPageEmpty));
  }
}
Listing 13-44

MyFilesViewModel complete property and data binding code

Data Loading

With all the properties and data binding complete, we can implement our algorithm to load the data from the IGraphFileService implementation. Create two method stubs, which will be used in our data loading algorithm:
  • LoadDataAsync : Entry point for data loading

  • UpdateFiles : Updates the FilesAndFolders list and handles errors if empty or null

See stubbed code in Listing 13-45.
async Task LoadDataAsync(string pathId = null)
{
  // TODO – add implementation
}
void UpdateFiles(IEnumerable<OneDriveItem> files)
{
  // TODO – add implementation
}
Listing 13-45

MyFilesViewModel data loading stubbed methods

Implementing the second method UpdateFiles will be an easy starting point for us. The goal of this method is to update the FilesAndFolders property or display an error message if null or empty. See the completed implementation in Listing 13-46.
void UpdateFiles(IEnumerable<OneDriveItem> files)
{
  if (files == null)
  {
    NoDataMessage = "Unable to retrieve data from API " +
      "check network connection";
    logger.LogInformation("No data retrieved from API, " +
      "ensure you have a stable internet connection");
    return;
  }
  else if (!files.Any())
  {
    NoDataMessage = "No files or folders";
  }
  FilesAndFolders = files.ToList();
}
Listing 13-46

MyFilesViewModel completed UpdateFiles implementation

Now we can start implementing the LoadDataAsync API, which will be using the IGraphFileService to get our data. We have four things we need to do for our basic algorithm:
  1. 1.

    Notify the user interface that data is loading.

     
  2. 2.

    Load the data.

     
  3. 3.

    Update FilesAndFolders.

     
  4. 4.

    Notify the user interface that data is done loading.

     
See implementation in Listing 13-47.
async Task LoadDataAsync(string pathId = null)
{
  IsStatusBarLoading = true;
  IEnumerable<OneDriveItem> data;
  if (string.IsNullOrEmpty(pathId)
  {
    data = await graphFileService.GetRootFilesAsync();
  }
  else
  {
    data = await graphFileService.GetFilesAsync(pathId);
  }
  UpdateFiles(data);
  IsStatusBarLoading = false;
}
Listing 13-47

MyFilesViewModel LoadDataAsync basic implementation

This implementation works but does not consider exceptions that may be thrown. If an exception is thrown, the IsStatusBarLoading property is never reset, so the user will see the loading indicator forever, which means the user will need to restart the application. To solve this problem, let’s wrap the code in a try-catch block and handle a basic exception. See updated code in Listing 13-48.
async Task LoadDataAsync(string pathId = null)
{
  IsStatusBarLoading = true;
  try
  {
    IEnumerable<OneDriveItem> data;
    if (string.IsNullOrEmpty(pathId)
    {
      data = await graphFileService.GetRootFilesAsync();
    }
    else
    {
      data = await graphFileService.GetFilesAsync(pathId);
    }
    UpdateFiles(data);
  }
  catch (Exception ex)
  {
    logger.LogError(ex, ex.Message);
  }
  finally
  {
    IsStatusBarLoading = false;
  }
}
Listing 13-48

MyFilesViewModel LoadDataAsync complete implementation

Note

By updating the IsStatusBarLoading property in the finally block of the try-catch, it ensures that it will always be invoked even if there is an exception. This will keep the user interface responsive for the user.

ViewModel Entry Point and IInitialize

MyFilesViewModel is just about implemented. We have the constructor, properties , and data loading code all completed. When the object is instantiated, there is no entry point code that invokes LoadDataAsync(). We need to add an IInitialize interface and add it to the MyFilesViewModel. This interface will provide a simple API that we can certify is invoked at instantiation time.

In the UnoDrive.Shared project, create a new file under the Mvvm folder named IInitialize.cs. See the screenshot in Figure 13-2.

A screenshot represents the three solutions out of nine solutions in the visual studio for Uno Drive. C initialize cs is selected from M v v m, UnoDrive Shared.

Figure 13-2

Visual Studio Solution Explorer – IInitialize.cs

Open the newly created IInitialize.cs and define our interface with an async method named InitializeAsync() . See code in Listing 13-49 for the interface definition.
public interface IInitialize
{
  Task InitializeAsync();
}
Listing 13-49

IInitialize interface definition

Now that we have our interface defined, we can update the MyFilesViewModel to inherit the interface and provide an implementation. The code in Listing 13-50 has all the code omitted except the new interface and an empty implementation . See code in Listing 13-50 for the stubbed implementation .
public class MyFilesViewModel : ObservableObject, IInitialize
{
  public async Task InitializeAsync()
  {
    // TODO – add implementation
  }
}
Listing 13-50

MyFilesViewModel stubbed implementation of IInitialize

When the InitializeAsync() method is invoked, we want to load the data by invoking the LoadDataAsync method. See the implementation in Listing 13-51.
public async Task InitializeAsync()
{
  await LoadDataAsync();
}
Listing 13-51

MyFilesViewModel InitializeAsync implementation

We have added our new interface IInitialize and an implementation to our MyFilesViewModel , but we have not connected this to the instantiation of the view model. To complete our implementation, we need to update the MyFilesPage.xaml.cs to invoke the interface method.

Every page has an instance of DataContext , and in our application that is the view model. We are using an automatic wireup strategy for the view model, which means we can guarantee the DataContext is set and ready to use after the constructor completes. We need to complete two tasks in the MyFilesPage.xaml.cs :
  • Create a local property named ViewModel that is of type MyFilesViewModel.

  • Override the OnNavigatedTo() method and invoke the InitializeAsync() method.

Add implementation for the ViewModel property as seen in Listing 13-52.
public MyFilesViewModel ViewModel =>
  (MyFilesViewModel)DataContext;
Listing 13-52

MyFilesPage ViewModel property implementation

Now that we have our property defined, we can override the OnNavigatedTo() method , which is invoked right after navigation. In this method we will check if the ViewModel property is of type IInitialize and if it is invoke the InitializeAsync() method. See the code snippet in Listing 13-53.
protected override async void OnNavigatedTo(
  NavigationEventArgs e)
{
  base.OnNavigatedTo(e);
  if (ViewModel is IInitialize initializeViewModel)
  {
    await initializeViewModel.InitializeAsync();
  }
}
Listing 13-53

MyFilesPage OnNavigatedTo implementation

Now when the page loads, the view model will be initialized, and our data loading code will be invoked.

Complete View Model Code

We implemented our MyFilesViewModel in chunks to make it easier to understand the various moving parts. You can see the completed view model code in Listing 13-54.
public class MyFilesViewModel : ObservableObject, IInitialize
{
  IGraphFileService graphFileService;
  ILogger logger;
  public MyFilesViewModel(
    IGraphFileService graphFileService,
    ILogger<MyFilesViewModel> logger)
  {
    this.graphFileService = graphFileService;
    this.logger = logger;
    FilesAndFolders = new List<OneDriveItem>();
  }
  List<OneDriveItem> filesAndFolders;
  public List<OneDriveItem> FilesAndFolders
  {
    get => filesAndFolders;
    set
    {
      SetProperty(ref filesAndFolders, value);
      OnPropertyChanged(nameof(CurrentFolderPath));
      OnPropertyChanged(nameof(IsPageEmpty));
    }
  }
  public bool IsPageEmpty =>
    !IsStatusBarLoading && !FilesAndFolders.Any();
  public string CurrentFolderPath =>
    FilesAndFolders.FirstOrDefault()?.Path;
  string noDataMessage;
  public string NoDataMessage
  {
    get => noDataMessage;
    set => SetProperty(ref noDataMessage, value);
  }
  bool isStatusBarLoading;
  public bool IsStatusBarLoading
  {
    get => isStatusBarLoading;
    set => SetProperty(ref isStatusBarLoading, value);
  }
  async Task LoadDataAsync(string pathId = null)
  {
    IsStatusBarLoading = true;
    try
    {
      IEnumerable<OneDriveItem> data;
      if (string.IsNullOrEmpty(pathId)
      {
        data = await graphFileService.GetRootFilesAsync();
      }
      else
      {
        data = await graphFileService.GetFilesAsync(pathId);
      }
      UpdateFiles(data);
    }
    catch (Exception ex)
    {
      logger.LogError(ex, ex.Message);
    }
    finally
    {
      IsStatusBarLoading = false;
    }
  }
  void UpdateFiles(IEnumerable<OneDriveItem> files)
  {
    if (files == null)
    {
      NoDataMessage = "Unable to retrieve data from API " +
        "check network connection";
      logger.LogInformation("No data retrieved from API, " +
        "ensure you have a stable internet connection");
      return;
    }
    else if (!files.Any())
    {
      NoDataMessage = "No files or folders";
    }
    FilesAndFolders = files.ToList();
  }
  public async Task InitializeAsync()
  {
    await LoadDataAsync();
  }
}
Listing 13-54

Complete MyFilesViewModel implementation

User Interface Implementation

We have now implemented the Microsoft Graph integration and the MyFilesViewModel. The last thing for us to implement in this chapter is the user interface so we can see our data on the screen.

Our plan is to add an address bar and a content area where the address bar will show the current OneDrive path and the content area will be a folders and files explorer window. We will be adding navigation to the explorer in future chapters. In this chapter we are just going to focus on the basic user interface.

Note

All XAML code snippets in this section omit the xmlns and other declarations at the top of the file. Some of the snippets will explicitly mention when you need to edit them.

Create a Grid with two rows. Place a second Grid in the first row to manage the address bar and a ScrollViewer in the second row to manage the files and folders explorer. See the code snippet in Listing 13-55.
<Grid>
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto" />
    <RowDefinition Height="*" />
  </Grid.RowDefinitions>
  <Grid Grid.Row="0" Margin="0, 0, 0, 20">
  </Grid>
  <ScrollViewer Grid.Row="1">
  </ScrollViewer>
</Grid>
Listing 13-55

MyFilesPage basic grid structure

Address Bar

In the first nested Grid, we are going to build our address bar . We will build upon this in future chapters. For now it will display a read-only TextBox that renders the current OneDrive path.

First, define the ColumnDefinitions to include only one column. See the code snippet in Listing 13-56.
<ColumnDefinitions>
  <ColumnDefinition />
</ColumnDefinitions>
Listing 13-56

MyFilesPage address bar grid column definition

With the grid configured, we can place our TextBox control inside. We want our TextBox to be read-only and not usable by the user. This means there are several properties that we need to swap. In the MyFilesViewModel we defined a bindable property named CurrentFolderPath . This property needs set to the Text property using the data binding markup extension. See the code snippet in Listing 13-57 for the TextBox XAML.
<TextBox
  Grid.Column="0"
  Margin="10, 0, 0, 0"
  Padding="10, 6, 36, 5"
  IsReadOnly="True"
  IsFocusEngaged="False"
  IsEnabled="False"
  Foreground="Black"
  Background="#F2F2F2"
  Text="{Binding CurrentFolderPath}" />
Listing 13-57

MyFilesPage address bar TextBox

In our design we want to have a small ProgressRing spinning on the right-hand side of the TextBox while data is loading. Since we are using a Grid, we can easily stack items on top of each other by placing the ProgressRing in the same column. When stacking controls using this technique, the last item is always on top as items are added to the view stack from top to bottom. Add your ProgressRing and right-align it as seen in Listing 13-58.
<ProgressRing
  Grid.Column="0"
  Width="20"
  Height="20"
  HorizontalAlignment="Right"
  Margin="0, 0, 10, 0" />
Listing 13-58

MyFilesPage address bar ProgressRing

In Chapter 12 we implemented converters to help us display controls on the screen by data binding bool values. To manage the Visibility of the ProgressRing, we are going to put those converters to use. See updated ProgressRing code in Listing 13-59.
<ProgressRing
  Grid.Column="0"
  Width="20"
  Height="20"
  HorizontalAlignment="Right"
  Margin="0, 0, 10, 0"
  IsActive="{Binding IsStatusBarLoading}"
  Visibility="{Binding IsStatusBarLoading,
    Converter={StaticResource BoolToVisibilityConverter}}" />
Listing 13-59

MyFilesPage address bar ProgressRing with Visibility

With the additions of IsActive and Visibility, the ProgressRing will only render and spin when IsStatusBarLoading is set to true in the MyFilesViewModel .

We now need to consider cross-platform scenarios as not every platform will render the ProgressRing correctly or at all. We will need to adjust the sizing for Android slightly. All platforms that use Skia will also need updates as the ProgressRing does not currently work with WinUI Uno Platform.

Add a custom style for the ProgressRing so we can simplify the XAML slightly. In the UnoDrive.Shared project under StylesControls, create a new file named ProgressRing.xaml. See the screenshot in Figure 13-3.

A screenshot represents the three solutions out of nine in the visual studio for UnoDrive. Progress Ring x a m l is selected from controls of styles, UnoDrive Shared.

Figure 13-3

Visual Studio Solution Explorer for ProgressRing.xaml

To include our new ProgressRing.xaml file into our global styles , we need to ensure that the ResourceDictionary is added in the _Controls.xaml. See the code snippet in Listing 13-60 for updated styles in _Controls.xaml.
<ResourceDictionary>
  <ResourceDictionary Source="Button.xaml" />
  <ResourceDictionary Source="TextBlock.xaml" />
  <ResourceDictionary Source="ProgressRing.xaml" />
</ResourceDictionary>
Listing 13-60

Updated _Controls.xaml to include ProgressRing.xaml

Now we can add our styles to the ProgressRing.xaml file. At the top of the file, we need to ensure we add our xmlns for Android so we can add platform-specific XAML. Then we need to add the Android xmlns to the ignorable. See xmlns definitions in Listing 13-61.
xmlns:android="http://uno.ui/android"
mc:Ignorable="android"
Listing 13-61

ProgressRing.xaml Android-specific xmlns

Once you have updated the root ResourceDictionary node with the correct xmlns, we can start adding our address bar–specific ProgressRing style. All platforms will use a Width and Height of 20, and Android will use a value of 15. See the code snippet in Listing 13-62.
<Style x:Name="AddressBarProgressRing" TargetType="ProgressRing">
  <Setter Property="Width" Value="20" />
  <Setter Property="Height" Value="20" />
  <android:Setter Property="Width" Value="15" />
  <android:Setter Property="Height" Value="15" />
</Style>
Listing 13-62

ProgressRing.xaml AddressBarProgressRing style

To use this new style, we need to explicitly reference it by name AddressBarProgressRing . Back in the MyFilesPage.xaml , update the ProgressRing we defined in our address bar to use this style, and then remove the Height and Width properties. See the updated code snippet in Listing 13-63.
<ProgressRing
  Grid.Column="0"
  Style="{StaticResource AddressBarProgressRing}"
  HorizontalAlignment="Right"
  Margin="0, 0, 10, 0"
  IsActive="{Binding IsStatusBarLoading}"
  Visibility="{Binding IsStatusBarLoading,
    Converter={StaticResource BoolToVisibilityConverter}}" />
Listing 13-63

MyFilesPage address bar completed ProgressRing

Next, we need to handle Skia platforms as none of them on WinUI are rendering the ProgressRing correctly. Instead, we will render a TextBlock that displays the message Loading . . . to communicate to the user that data is loading. At the top of the MyFilesPage.xaml , you will need to add the skia xmlns and not_skia xmlns and add only the skia xmlns to the ignorable. See xmlns definitions in Listing 13-64.
xmlns:not_skia="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:skia="http://uno.ui/skia"
mc:Ignorable="d skia"
Listing 13-64

MyFilesPage skia and not_skia xmlns definitions

Note

If you add not_skia to the ignorable from Listing 13-64, then it will not include the not_skia items correctly on Windows targets. It is important to only include skia to the ignorable in this case.

To start we can update our existing ProgressRing to use the not_skia platform – see updated code in Listing 13-65.
<not_skia:ProgressRing
  Grid.Column="0"
  Style="{StaticResource AddressBarProgressRing}"
  HorizontalAlignment="Right"
  Margin="0, 0, 10, 0"
  IsActive="{Binding IsStatusBarLoading}"
  Visibility="{Binding IsStatusBarLoading,
    Converter={StaticResource BoolToVisibilityConverter}}" />
Listing 13-65

MyFilesPage address bar – added not_skia target to ProgressRing

Now, we can add our Skia-specific TextBlock. It will follow similar rendering rules to the ProgressRing as it will be aligned on the right side of the TextBox. See the code snippet in Listing 13-66.
<skia:TextBlock
  Grid.Column="0"
  Text="Loading . . ."
  FontSize="12"
  Margin="0, 0, 10, 0"
  Foreground="Black"
  HorizontalAlignment="Right"
  VerticalAlignment="Center"
  Visibility="{Binding IsStatusBarLoading,
    Converter={StaticResource BoolToVisibilityConverter}}" />
Listing 13-66

MyFilesPage address bar Skia-specific loading TextBlock

Files and Folders Explorer

The ScrollViewer is the second control we added earlier, and that will contain our files and folders explorer. Our ScrollViewer will use a StackPanel to contain the various user interface controls that make up our explorer. See the code snippet in Listing 13-67.
<ScrollViewer Grid.Row="1">
  <StackPanel>
  </StackPanel>
</ScrollViewer>
Listing 13-67

MyFilesPage explorer StackPanel definition

We are going to use a GridView to display our various items as it provides a nice way to display a collection of data in a grid instead of a list. Before we add it to the MyFilesPage.xaml, we need to add a global style. In the UnoDrive.Shared project under StylesControls, create a new file named GridView.xaml . See Figure 13-4 for a screenshot of the Visual Studio Solution Explorer.

A screenshot represents the 3 out of 9 projects UnoDrive. GridView x a m l is selected from controls of styles, it is under UnoDrive Shared.

Figure 13-4

Visual Studio Solution Explorer GridView.xaml

Just like the ProgressRing.xaml file, we will need to ensure it is included in our ResourceDictionary . Update the _Controls.xaml file to include our new GridView.xaml – see code in Listing 13-68.
<ResourceDictionary>
  <ResourceDictionary Source="Button.xaml" />
  <ResourceDictionary Source="TextBlock.xaml" />
  <ResourceDictionary Source="GridView.xaml" />
  <ResourceDictionary Source="ProgressRing.xaml" />
</ResourceDictionary>
Listing 13-68

Add GridView.xaml to _Controls.xaml

In the GridView.xaml we can add our default style for the GridView control. This style is going to be a global style , which means it will be used on any GridView used in the entire application. There will be no need to add an explicit Style property in the MyFilesPage.xaml file. See the style XAML code in Listing 13-69.
<Style TargetType="GridView">
  <Setter Property="CanDragItems" Value="False" />
  <Setter Property="AllowDrop" Value="False" />
  <Setter Property="CanReorderItems" Value="False" />
  <Setter Property="SelectionMode" Value="Single" />
  <Setter Property="FlowDirection" Value="LeftToRight" />
  <Setter Property="HorizontalContentAlignment" Value="Center" />
  <Setter Property="IsItemClickEnabled" Value="True" />
</Style>
Listing 13-69

GridView.xaml global style implementation

Now that the global style is implemented, we can add our GridView to the MyFilesPage in our ScrollViewer. In the MyFilesViewModel we defined a public property named FilesAndFolders that is a list; this will be data bound to the ItemsSource property. The ItemsSource property on the GridView represents all the items to display. If there are no items in FilesAndFolders, we should hide the GridView so we can display other controls. You will need to bind IsPageEmpty to Visibility. See the code snippet in Listing 13-70.
<GridView
  ItemsSource="{Binding FilesAndFolders}"
  Visibility="{Binding IsPageEmpty, Converter=
    {StaticResource BoolNegationToVisibilityConverter}}" />
Listing 13-70

MyFilesPage explorer GridView

If the page is empty, we need to communicate to the user that there is no data by rendering a message No data found in a TextBlock. See the code snippet in Listing 13-71.
<TextBlock
  Text="No data found"
  Visibility="{Binding IsPageEmpty,
    Converter={StaticResource BoolToVisibilityConverter}}" />
Listing 13-71

MyFilesPage explorer TextBlock for no data found

The last step for our files and folders explorer is to render the ProgressRing in the center of the content area if data is loading. Just like the address bar earlier, we will need to use our not_skia vs. skia xmlns to display a ProgressRing or TextBlock. First, add the ProgressRing using the not_skia xmlns as seen in Listing 13-72.
<not_skia:ProgressRing
  Width="300"
  Height="300"
  IsActive="{Binding IsStatusBarLoading}"
  Visibility="{Binding IsStatusBarLoading,
    Converter={StaticResource BoolToVisibilityConverter}}" />
Listing 13-72

MyFilesPage explorer ProgressRing for not_skia

Now, we need to add the skia-specific XAML to display the TextBlock. See the code in Listing 13-73.
<skia:TextBlock
  Text="Loading . . ."
  FontSize="40"
  Foreground="Black"
  HorizontalAlignment="Center"
  VerticalAlignment="Center"
  Visibility="{Binding IsStatusBarLoading,
    Converter={StaticResource BoolToVisibilityConverter}}" />
Listing 13-73

MyFilesPage explorer TextBlock for skia

Note

In the address bar styles for the ProgressRing, we added a style to handle the Width and Height for Android vs. the other platforms. We are not doing that here as the center of the page has more screen space than the small TextBox from earlier.

MyFilesPage.xaml Complete Code

This completes our implementation of MyFilesPage.xaml for this chapter. We will be adding to it in future chapters. See the completed code in Listing 13-74.
<Grid>
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto" />
    <RowDefinition Height="*" />
  </Grid.RowDefinitions>
  <Grid Grid.Row="0" Margin="0, 0, 0, 20">
    <Grid.ColumnDefinitions>
      <ColumnDefinition />
    </Grid.ColumnDefinitions>
    <TextBox
      Grid.Column="0"
      Margin="10, 0, 0, 0"
      Padding="10, 6, 36, 5"
      IsReadOnly="True"
      IsFocusEngaged="False"
      IsEnabled="False"
      Foreground="Black"
      Background="#F2F2F2"
      Text="{Binding CurrentFolderPath}" />
    <not_skia:ProgressRing
      Grid.Column="0"
      Style="{StaticResource AddressBarProgressRing}"
      HorizontalAlignment="Right"
      Margin="0, 0, 10, 0"
      IsActive="{Binding IsStatusBarLoading}"
      Visibility="{Binding IsStatusBarLoading, Converter=
        {StaticResource BoolToVisibilityConverter}}" />
    <skia:TextBlock
      Grid.Column="0"
      Text="Loading . . ."
      FontSize="12"
      Margin="0, 0, 10, 0"
      Foreground="Black"
      HorizontalAlignment="Right"
      VerticalAlignment="Center"
      Visibility="{Binding IsStatusBarLoading, Converter=
        {StaticResource BoolToVisibilityConverter}}" />
  </Grid>
  <ScrollViewer Grid.Row="1">
    <StackPanel>
      <GridView
        ItemsSource="{Binding FilesAndFolders}"
        Visibility="{Binding IsPageEmtpy, Converter=
          {StaticResource BoolNegationToVisibilityConverter}}" />
      <TextBlock
        Text="No data found"
        Visibility="{Binding IsPageEmpty, Converter=
          {StaticResource BoolToVisibilityConverter}}" />
      <not_skia:ProgressRing
        Width="300"
        Height="300"
        IsActive="{Binding IsStatusBarLoading}"
        Visibility="{Binding IsStatusBarLoading, Converter=
          {StaticResource BoolToVisibilityConverter}}" />
      <skia:TextBlock
        Text="Loading . . ."
        FontSize="40"
        Foreground="Black"
        HorizontalAlignment="Center"
        VerticalAlignment="Center"
        Visibility="{Binding IsStatusBarLoading, Converter=
          {StaticResource BoolToVisibilityConverter}}" />
    </StackPanel>
  </ScrollViewer>
</Grid>
Listing 13-74

MyFilesPage.xaml completed code

Testing the Code

We now have a full-stack implementation of using the Microsoft Graph to pull data from OneDrive and render something on the screen. The address bar will work as we expect, but the files and folders explorer is just going to display object names on the screen and not the files and folders. That is intentional as we will implement images in Chapter 14. Now go and run the app on your various platforms. See screenshots of the running application in Figure 13-5 and Figure 13-6 for Windows, Figure 13-7 and Figure 13-8 for WASM, Figure 13-9 and Figure 13-10 for WPF, Figure 13-11 and Figure 13-12 for Linux, Figure 13-13 for Android, Figure 13-14 for iOS, and Figure 13-15 and Figure 13-16 for macOS.

Windows

A screenshot of the windows application depicts a name, email, and options like my file, recent, shared, recycle bin, and sign out. My file is selected and has a new and upload button. It exhibits arc.

Figure 13-5

Windows application with ProgressRing spinning

A screenshot of the windows application exhibits name, email, an option like my file, recent, shared recycle bin, and sign out. My file option is selected and has two buttons, new and upload.

Figure 13-6

Windows application with root OneDrive path loaded

WebAssembly (WASM)

A screenshot of the windows application exhibits name, email, an option like my file, recent, shared recycle bin, and sign out. My file option is selected and has two buttons, new and upload. It exhibits a loading circle.

Figure 13-7

WebAssembly (WASM) application with ProgressRing spinning

A screenshot depicts name, email, an option like my file, recent, shared recycle bin, and sign out. My file option is selected and has two buttons, new and upload. It exhibits UnoDrive data and items.

Figure 13-8

WebAssembly (WASM) application with root OneDrive path loaded

WPF

A screenshot depicts name, email, an option like my file, recent, shared recycle bin, and sign out. My file option is selected and has two buttons, new and upload. It exhibits the loading process.

Figure 13-9

WPF application with Loading… TextBlock

A screenshot depicts name, email, an option like my file, recent, shared recycle bin, and sign out. My file option is selected and has two buttons, new and upload. The page reads UnoDrive data and items.

Figure 13-10

WPF application with root OneDrive path loaded

GTK

An untitled window depicts name, email, an option like my file, recent, shared recycle bin, and sign out. My file option is selected and has two buttons, new and upload. It exhibits loading.

Figure 13-11

GTK application with Loading… TextBlock

A screenshot depicts name, email, an option like my file, recent, shared recycle bin, and sign out. My file option is selected and has two buttons, new and upload. It exhibits UnoDrive data and items.

Figure 13-12

GTK application with root OneDrive path loaded

Android

A double screenshot of mobile. 1, My files depict two buttons, new and upload, with a loading circle. 2, My files page depicts UnoDrive data UnoDrive item.

Figure 13-13

Android application with ProgressRing spinning on the left and root OneDrive path loaded on the right

iOS

A double screenshot of mobile. 1, My files depict two buttons, new and upload, with a loading circle. 2, My files page reads UnoDr UnoDr UnoDr.

Figure 13-14

iOS application with ProgressRing spinning on the left and root OneDrive path loaded on the right

macOS

A screenshot depicts name, email, an option like my file, recent, shared recycle bin, and sign out. My file option is selected and has two buttons, new and upload. It exhibits a loading circle.

Figure 13-15

macOS application with ProgressRing spinning

A screenshot depicts name, email, an option like my file, recent, shared recycle bin, and sign out. My file option is selected and has two buttons, new and upload. The page reads UnoDriveData One Drive item.

Figure 13-16

macOS a pplication with root OneDrive path loaded

Conclusion

In this chapter we covered a lot of material with retrieving data from OneDrive and displaying it on the main landing page of the application. We are going to be building on the code in future chapters. If you had any trouble following along or your application isn’t working right, go and download the sample code before moving on to the next chapter: https://github.com/SkyeHoefling/UnoDrive/tree/main/Chapter%2013 .

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

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