diff --git a/CommunityToolkit.Graph.Uwp/Helpers/RoamingSettings/BaseRoamingSettingsDataStore.cs b/CommunityToolkit.Graph.Uwp/Helpers/RoamingSettings/BaseRoamingSettingsDataStore.cs deleted file mode 100644 index 844b7ee..0000000 --- a/CommunityToolkit.Graph.Uwp/Helpers/RoamingSettings/BaseRoamingSettingsDataStore.cs +++ /dev/null @@ -1,303 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using Microsoft.Toolkit.Uwp.Helpers; -using Windows.Storage; - -namespace CommunityToolkit.Graph.Uwp.Helpers.RoamingSettings -{ - /// - /// A base class for easily building roaming settings helper implementations. - /// - public abstract class BaseRoamingSettingsDataStore : IRoamingSettingsDataStore - { - /// - public EventHandler SyncCompleted { get; set; } - - /// - public EventHandler SyncFailed { get; set; } - - /// - public bool AutoSync { get; } - - /// - public string Id { get; } - - /// - public string UserId { get; } - - /// - public IDictionary Cache { get; private set; } = new Dictionary(); - - /// - /// Gets an object serializer for converting objects in the data store. - /// - protected IObjectSerializer Serializer { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The id of the target Graph user. - /// A unique id for the data store. - /// An IObjectSerializer used for serializing objects. - /// Determines if the data store should sync for every interaction. - public BaseRoamingSettingsDataStore(string userId, string dataStoreId, IObjectSerializer objectSerializer, bool autoSync = true) - { - AutoSync = autoSync; - Id = dataStoreId; - UserId = userId; - Serializer = objectSerializer; - } - - /// - /// Create a new instance of the data storage container. - /// - /// A task. - public abstract Task Create(); - - /// - /// Delete the instance of the data storage container. - /// - /// A task. - public abstract Task Delete(); - - /// - /// Determines whether a setting already exists. - /// - /// Key of the setting (that contains object). - /// True if a value exists. - public bool KeyExists(string key) - { - return Cache?.ContainsKey(key) ?? false; - } - - /// - /// Determines whether a setting already exists in composite. - /// - /// Key of the composite (that contains settings). - /// Key of the setting (that contains object). - /// True if a value exists. - public bool KeyExists(string compositeKey, string key) - { - if (KeyExists(compositeKey)) - { - ApplicationDataCompositeValue composite = (ApplicationDataCompositeValue)Cache[compositeKey]; - if (composite != null) - { - return composite.ContainsKey(key); - } - } - - return false; - } - - /// - /// Retrieves a single item by its key. - /// - /// Key of the object. - /// Default value of the object. - /// Type of object retrieved. - /// The T object. - public T Read(string key, T @default = default) - { - if (Cache != null && Cache.TryGetValue(key, out object value)) - { - return DeserializeValue(value); - } - - return @default; - } - - /// - /// Retrieves a single item by its key in composite. - /// - /// Key of the composite (that contains settings). - /// Key of the object. - /// Default value of the object. - /// Type of object retrieved. - /// The T object. - public T Read(string compositeKey, string key, T @default = default) - { - if (Cache != null) - { - ApplicationDataCompositeValue composite = (ApplicationDataCompositeValue)Cache[compositeKey]; - if (composite != null) - { - object value = composite[key]; - if (value != null) - { - return DeserializeValue(value); - } - } - } - - return @default; - } - - /// - /// Saves a single item by its key. - /// - /// Key of the value saved. - /// Object to save. - /// Type of object saved. - public void Save(string key, T value) - { - // Update the cache - Cache[key] = SerializeValue(value); - - if (AutoSync) - { - // Update the remote - Task.Run(() => Sync()); - } - } - - /// - /// Saves a group of items by its key in a composite. This method should be considered - /// for objects that do not exceed 8k bytes during the lifetime of the application - /// (refers to Microsoft.Toolkit.Uwp.Helpers.IObjectStorageHelper.SaveFileAsync``1(System.String,``0) - /// for complex/large objects) and for groups of settings which need to be treated - /// in an atomic way. - /// - /// Key of the composite (that contains settings). - /// Objects to save. - /// Type of object saved. - public void Save(string compositeKey, IDictionary values) - { - var type = typeof(T); - var typeInfo = type.GetTypeInfo(); - - if (KeyExists(compositeKey)) - { - ApplicationDataCompositeValue composite = (ApplicationDataCompositeValue)Cache[compositeKey]; - - foreach (KeyValuePair setting in values.ToList()) - { - string key = setting.Key; - object value = SerializeValue(setting.Value); - if (composite.ContainsKey(setting.Key)) - { - composite[key] = value; - } - else - { - composite.Add(key, value); - } - } - - // Update the cache - Cache[compositeKey] = composite; - - if (AutoSync) - { - // Update the remote - Task.Run(() => Sync()); - } - } - else - { - ApplicationDataCompositeValue composite = new ApplicationDataCompositeValue(); - foreach (KeyValuePair setting in values.ToList()) - { - string key = setting.Key; - object value = SerializeValue(setting.Value); - composite.Add(key, value); - } - - // Update the cache - Cache[compositeKey] = composite; - - if (AutoSync) - { - // Update the remote - Task.Run(() => Sync()); - } - } - } - - /// - /// Determines whether a file already exists. - /// - /// Key of the file (that contains object). - /// True if a value exists. - public abstract Task FileExistsAsync(string filePath); - - /// - /// Retrieves an object from a file. - /// - /// Path to the file that contains the object. - /// Default value of the object. - /// Type of object retrieved. - /// Waiting task until completion with the object in the file. - public abstract Task ReadFileAsync(string filePath, T @default = default); - - /// - /// Saves an object inside a file. - /// - /// Path to the file that will contain the object. - /// Object to save. - /// Type of object saved. - /// Waiting task until completion. - public abstract Task SaveFileAsync(string filePath, T value); - - /// - public abstract Task Sync(); - - /// - /// Delete the internal cache. - /// - protected void DeleteCache() - { - Cache.Clear(); - } - - /// - /// Use the serializer to deserialize a value appropriately for the type. - /// - /// The type of object expected. - /// The value to deserialize. - /// An object of type T. - protected T DeserializeValue(object value) - { - try - { - return Serializer.Deserialize((string)value); - } - catch - { - // Primitive types can't be deserialized. - return (T)Convert.ChangeType(value, typeof(T)); - } - } - - /// - /// Use the serializer to serialize a value appropriately for the type. - /// - /// The type of object being serialized. - /// The object to serialize. - /// The serialized object. - protected object SerializeValue(T value) - { - var type = typeof(T); - var typeInfo = type.GetTypeInfo(); - - // Skip serialization for primitives. - if (typeInfo.IsPrimitive || type == typeof(string)) - { - // Update the cache - return value; - } - else - { - // Update the cache - return Serializer.Serialize(value); - } - } - } -} diff --git a/CommunityToolkit.Graph.Uwp/Helpers/RoamingSettings/IRoamingSettingsDataStore.cs b/CommunityToolkit.Graph.Uwp/Helpers/RoamingSettings/IRoamingSettingsDataStore.cs deleted file mode 100644 index 00186ed..0000000 --- a/CommunityToolkit.Graph.Uwp/Helpers/RoamingSettings/IRoamingSettingsDataStore.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.Toolkit.Uwp.Helpers; - -namespace CommunityToolkit.Graph.Uwp.Helpers.RoamingSettings -{ - /// - /// Defines the contract for creating storage containers used for roaming data. - /// - public interface IRoamingSettingsDataStore : IObjectStorageHelper - { - /// - /// Gets a value indicating whether the values should immediately sync or not. - /// - bool AutoSync { get; } - - /// - /// Gets access to the key/value pairs cache directly. - /// - IDictionary Cache { get; } - - /// - /// Gets the id of the data store. - /// - string Id { get; } - - /// - /// Gets the id of the target user. - /// - string UserId { get; } - - /// - /// Gets or sets an event handler for when a remote data sync completes successfully. - /// - EventHandler SyncCompleted { get; set; } - - /// - /// Gets or sets an event handler for when a remote data sync fails. - /// - EventHandler SyncFailed { get; set; } - - /// - /// Create a new storage container. - /// - /// A Task. - Task Create(); - - /// - /// Delete the existing storage container. - /// - /// A Task. - Task Delete(); - - /// - /// Syncronize the internal cache with the remote storage endpoint. - /// - /// A Task. - Task Sync(); - } -} \ No newline at end of file diff --git a/CommunityToolkit.Graph.Uwp/Helpers/RoamingSettings/OneDriveDataSource.cs b/CommunityToolkit.Graph.Uwp/Helpers/RoamingSettings/OneDriveDataSource.cs deleted file mode 100644 index 9b95c99..0000000 --- a/CommunityToolkit.Graph.Uwp/Helpers/RoamingSettings/OneDriveDataSource.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.IO; -using System.Text; -using System.Threading.Tasks; -using CommunityToolkit.Authentication; -using CommunityToolkit.Graph.Extensions; -using Microsoft.Graph; -using Microsoft.Toolkit.Uwp.Helpers; - -namespace CommunityToolkit.Graph.Uwp.Helpers.RoamingSettings -{ - /// - /// Helpers for interacting with files in the special OneDrive AppRoot folder. - /// - internal static class OneDriveDataSource - { - private static GraphServiceClient Graph => ProviderManager.Instance.GlobalProvider?.GetClient(); - - // Create a new file. - // This fails, because OneDrive doesn't like empty files. Use Update instead. - // public static async Task Create(string fileWithExt) - // { - // var driveItem = new DriveItem() - // { - // Name = fileWithExt, - // }; - // await Graph.Users[userId].Drive.Special.AppRoot.ItemWithPath(fileWithExt).Request().CreateAsync(driveItem); - // } - - /// - /// Updates or create a new file on the remote with the provided content. - /// - /// The type of object to save. - /// A representing the asynchronous operation. - public static async Task Update(string userId, string fileWithExt, T fileContents, IObjectSerializer serializer) - { - var json = serializer.Serialize(fileContents) as string; - using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); - - return await Graph.Users[userId].Drive.Special.AppRoot.ItemWithPath(fileWithExt).Content.Request().PutAsync(stream); - } - - /// - /// Get a file from the remote. - /// - /// The type of object to return. - /// A representing the asynchronous operation. - public static async Task Retrieve(string userId, string fileWithExt, IObjectSerializer serializer) - { - Stream stream = await Graph.Users[userId].Drive.Special.AppRoot.ItemWithPath(fileWithExt).Content.Request().GetAsync(); - - string streamContents = new StreamReader(stream).ReadToEnd(); - - return serializer.Deserialize(streamContents); - } - - /// - /// Delete the file from the remote. - /// - /// A representing the asynchronous operation. - public static async Task Delete(string userId, string fileWithExt) - { - await Graph.Users[userId].Drive.Special.AppRoot.ItemWithPath(fileWithExt).Request().DeleteAsync(); - } - } -} diff --git a/CommunityToolkit.Graph.Uwp/Helpers/RoamingSettings/OneDriveDataStore.cs b/CommunityToolkit.Graph.Uwp/Helpers/RoamingSettings/OneDriveDataStore.cs deleted file mode 100644 index b1f95bd..0000000 --- a/CommunityToolkit.Graph.Uwp/Helpers/RoamingSettings/OneDriveDataStore.cs +++ /dev/null @@ -1,154 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Toolkit.Uwp.Helpers; -using Windows.Storage; - -namespace CommunityToolkit.Graph.Uwp.Helpers.RoamingSettings -{ - /// - /// A DataStore for managing roaming settings in OneDrive. - /// - public class OneDriveDataStore : BaseRoamingSettingsDataStore - { - /// - /// Retrieve an object stored in a OneDrive file. - /// - /// The type of object to retrieve. - /// The id of the target Graph user. - /// The name of the file. - /// An object serializer for handling deserialization. - /// The deserialized file contents. - public static async Task Get(string userId, string fileName, IObjectSerializer serializer) - { - return await OneDriveDataSource.Retrieve(userId, fileName, serializer); - } - - /// - /// Update the contents of a OneDrive file. - /// - /// The type of object being stored. - /// The id of the target Graph user. - /// The name of the file. - /// The object to store. - /// An object serializer for handling serialization. - /// A task. - public static async Task Set(string userId, string fileName, T fileContents, IObjectSerializer serializer) - { - await OneDriveDataSource.Update(userId, fileName, fileContents, serializer); - } - - /// - /// Delete a file from OneDrive by name. - /// - /// The id of the target Graph user. - /// The name of the file. - /// A task. - public static async Task Delete(string userId, string fileName) - { - await OneDriveDataSource.Delete(userId, fileName); - } - - /// - /// Initializes a new instance of the class. - /// - public OneDriveDataStore(string userId, string syncDataFileName, IObjectSerializer objectSerializer, bool autoSync = true) - : base(userId, syncDataFileName, objectSerializer, autoSync) - { - } - - /// - public override Task Create() - { - return Task.CompletedTask; - } - - /// - public override async Task Delete() - { - // Clear the cache - Cache.Clear(); - - // Delete the remote. - await Delete(UserId, Id); - } - - /// - public override async Task FileExistsAsync(string filePath) - { - var roamingSettings = await Get(UserId, Id, Serializer); - return roamingSettings != null; - } - - /// - public override async Task ReadFileAsync(string filePath, T @default = default) - { - return await Get(UserId, filePath, Serializer) ?? @default; - } - - /// - public override async Task SaveFileAsync(string filePath, T value) - { - await Set(UserId, filePath, value, Serializer); - - // Can't convert DriveItem to StorageFile, so we return null instead. - return null; - } - - /// - public override async Task Sync() - { - try - { - // Get the remote - string fileName = Id; - IDictionary remoteData = null; - try - { - remoteData = await Get>(UserId, fileName, Serializer); - } - catch - { - // If get fails, we know the remote store does not exist. - } - - bool needsUpdate = false; - if (remoteData != null) - { - // Update local cache with additions from remote - foreach (string key in remoteData.Keys.ToList()) - { - // Only insert new values. Existing keys should be overwritten on the remote. - if (!Cache.ContainsKey(key)) - { - Cache.Add(key, remoteData[key]); - needsUpdate = true; - } - } - } - else if (Cache.Count > 0) - { - // The remote does not yet exist, and we have data to save. - needsUpdate = true; - } - - if (needsUpdate) - { - // Send updates for local values, overwriting the remote. - await Set(UserId, fileName, Cache, Serializer); - } - - SyncCompleted?.Invoke(this, new EventArgs()); - } - catch - { - SyncFailed?.Invoke(this, new EventArgs()); - } - } - } -} diff --git a/CommunityToolkit.Graph.Uwp/Helpers/RoamingSettings/RoamingSettingsHelper.cs b/CommunityToolkit.Graph.Uwp/Helpers/RoamingSettings/RoamingSettingsHelper.cs deleted file mode 100644 index badd0c5..0000000 --- a/CommunityToolkit.Graph.Uwp/Helpers/RoamingSettings/RoamingSettingsHelper.cs +++ /dev/null @@ -1,188 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using CommunityToolkit.Authentication; -using CommunityToolkit.Graph.Extensions; -using Microsoft.Toolkit.Uwp.Helpers; -using Windows.Storage; - -namespace CommunityToolkit.Graph.Uwp.Helpers.RoamingSettings -{ - /// - /// An enumeration of the available data storage methods for roaming data. - /// - public enum RoamingDataStore - { - /// - /// Store data using open extensions on the Graph User. - /// - UserExtensions, - - /// - /// Store data in a Graph User's OneDrive. - /// - OneDrive, - } - - /// - /// A helper class for syncing data to roaming data store. - /// - public class RoamingSettingsHelper : IRoamingSettingsDataStore - { - /// - /// Gets the internal data store instance. - /// - public IRoamingSettingsDataStore DataStore { get; private set; } - - /// - public EventHandler SyncCompleted { get; set; } - - /// - public EventHandler SyncFailed { get; set; } - - /// - public bool AutoSync => DataStore.AutoSync; - - /// - public IDictionary Cache => DataStore.Cache; - - /// - public string Id => DataStore.Id; - - /// - public string UserId => DataStore.UserId; - - /// - /// Creates a new RoamingSettingsHelper instance for the currently signed in user. - /// - /// Which specific data store is being used. - /// Whether the values should immediately sync or not. - /// Whether the values should immediately sync on change or wait until Sync is called explicitly. - /// An object serializer for serialization of objects in the data store. - /// A new instance of the RoamingSettingsHelper configured for the current user. - public static async Task CreateForCurrentUser(RoamingDataStore dataStore = RoamingDataStore.UserExtensions, bool syncOnInit = true, bool autoSync = true, IObjectSerializer serializer = null) - { - var provider = ProviderManager.Instance.GlobalProvider; - if (provider == null || provider.State != ProviderState.SignedIn) - { - throw new InvalidOperationException("The GlobalProvider must be set and signed in to create a new RoamingSettingsHelper for the current user."); - } - - var me = await provider.GetClient().Me.Request().GetAsync(); - return new RoamingSettingsHelper(me.Id, dataStore, syncOnInit, autoSync, serializer); - } - - /// - /// Initializes a new instance of the class. - /// - /// The id of the target Graph User. - /// Which specific data store is being used. - /// Whether the values should immediately sync or not. - /// Whether the values should immediately sync on change or wait until Sync is called explicitly. - /// An object serializer for serialization of objects in the data store. - public RoamingSettingsHelper(string userId, RoamingDataStore dataStore = RoamingDataStore.UserExtensions, bool syncOnInit = true, bool autoSync = true, IObjectSerializer serializer = null) - { - // TODO: Infuse unique identifier from Graph registration into the storage name. - string dataStoreName = "communityToolkit.roamingSettings"; - - if (serializer == null) - { - serializer = new SystemSerializer(); - } - - switch (dataStore) - { - case RoamingDataStore.UserExtensions: - DataStore = new UserExtensionDataStore(userId, dataStoreName, serializer, autoSync); - break; - - case RoamingDataStore.OneDrive: - DataStore = new OneDriveDataStore(userId, dataStoreName, serializer, autoSync); - break; - - default: - throw new ArgumentOutOfRangeException(nameof(dataStore)); - } - - DataStore.SyncCompleted += (s, e) => SyncCompleted?.Invoke(this, e); - DataStore.SyncFailed += (s, e) => SyncFailed?.Invoke(this, e); - - if (syncOnInit) - { - _ = Sync(); - } - } - - /// - /// An indexer for easily accessing key values. - /// - /// The key for the desired value. - /// The value found for the provided key. - public object this[string key] - { - get => DataStore.Read(key); - set => DataStore.Save(key, value); - } - - /// - public Task FileExistsAsync(string filePath) => DataStore.FileExistsAsync(filePath); - - /// - public bool KeyExists(string key) => DataStore.KeyExists(key); - - /// - public bool KeyExists(string compositeKey, string key) => DataStore.KeyExists(compositeKey, key); - - /// - public T Read(string key, T @default = default) => DataStore.Read(key, @default); - - /// - public T Read(string compositeKey, string key, T @default = default) => DataStore.Read(compositeKey, key, @default); - - /// - public Task ReadFileAsync(string filePath, T @default = default) => DataStore.ReadFileAsync(filePath, @default); - - /// - public void Save(string key, T value) => DataStore.Save(key, value); - - /// - public void Save(string compositeKey, IDictionary values) => DataStore.Save(compositeKey, values); - - /// - public Task SaveFileAsync(string filePath, T value) => DataStore.SaveFileAsync(filePath, value); - - /// - /// Create a new storage container. - /// - /// A Task. - public Task Create() => DataStore.Create(); - - /// - /// Delete the existing storage container. - /// - /// A Task. - public Task Delete() => DataStore.Delete(); - - /// - /// Syncronize the internal cache with the remote storage endpoint. - /// - /// A Task. - public async Task Sync() - { - try - { - await DataStore.Sync(); - } - catch - { - // Sync may fail if the storage container does not yet exist. - await DataStore.Create(); - await DataStore.Sync(); - } - } - } -} \ No newline at end of file diff --git a/CommunityToolkit.Graph.Uwp/Helpers/RoamingSettings/UserExtensionDataStore.cs b/CommunityToolkit.Graph.Uwp/Helpers/RoamingSettings/UserExtensionDataStore.cs deleted file mode 100644 index d450d81..0000000 --- a/CommunityToolkit.Graph.Uwp/Helpers/RoamingSettings/UserExtensionDataStore.cs +++ /dev/null @@ -1,201 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Graph; -using Microsoft.Toolkit.Uwp.Helpers; -using Windows.Storage; - -namespace CommunityToolkit.Graph.Uwp.Helpers.RoamingSettings -{ - /// - /// An IObjectStorageHelper implementation using open extensions on the Graph User for storing key/value pairs. - /// - public class UserExtensionDataStore : BaseRoamingSettingsDataStore - { - /// - /// Retrieve the value from Graph User extensions and cast the response to the provided type. - /// - /// The type to cast the return result to. - /// The id of the user. - /// The id of the user extension. - /// The key for the desired value. - /// The value from the data store. - public static async Task Get(string userId, string extensionId, string key) - { - return (T)await Get(userId, extensionId, key); - } - - /// - /// Retrieve the value from Graph User extensions by extensionId, userId, and key. - /// - /// The id of the user. - /// The id of the user extension. - /// The key for the desired value. - /// The value from the data store. - public static async Task Get(string userId, string extensionId, string key) - { - var userExtension = await GetExtensionForUser(userId, extensionId); - return userExtension.AdditionalData[key]; - } - - /// - /// Set a value by key in a Graph User's extension. - /// - /// The id of the user. - /// The id of the user extension. - /// The key for the target value. - /// The value to set. - /// A task upon completion. - public static async Task Set(string userId, string extensionId, string key, object value) - { - await UserExtensionsDataSource.SetValue(userId, extensionId, key, value); - } - - /// - /// Creates a new roaming settings extension on a Graph User. - /// - /// The id of the user. - /// The id of the user extension. - /// The newly created user extension. - public static async Task Create(string userId, string extensionId) - { - var userExtension = await UserExtensionsDataSource.CreateExtension(userId, extensionId); - return userExtension; - } - - /// - /// Deletes an extension by id on a Graph User. - /// - /// The id of the user. - /// The id of the user extension. - /// A task upon completion. - public static async Task Delete(string userId, string extensionId) - { - await UserExtensionsDataSource.DeleteExtension(userId, extensionId); - } - - /// - /// Retrieves a user extension. - /// - /// The id of the user. - /// The id of the user extension. - /// The target extension. - public static async Task GetExtensionForUser(string userId, string extensionId) - { - var userExtension = await UserExtensionsDataSource.GetExtension(userId, extensionId); - return userExtension; - } - - private static readonly IList ReservedKeys = new List { "responseHeaders", "statusCode", "@odata.context" }; - - /// - /// Initializes a new instance of the class. - /// - public UserExtensionDataStore(string userId, string extensionId, IObjectSerializer objectSerializer, bool autoSync = true) - : base(userId, extensionId, objectSerializer, autoSync) - { - } - - /// - /// Creates a new roaming settings extension on the Graph User. - /// - /// The newly created Extension object. - public override async Task Create() - { - await Create(UserId, Id); - } - - /// - /// Deletes the roamingSettings extension from the Graph User. - /// - /// A void task. - public override async Task Delete() - { - // Delete the cache - Cache.Clear(); - - // Delete the remote. - await Delete(UserId, Id); - } - - /// - /// Update the remote extension to match the local cache and retrieve any new keys. Any existing remote values are replaced. - /// - /// The freshly synced user extension. - public override async Task Sync() - { - try - { - IDictionary remoteData = null; - - try - { - // Get the remote - Extension extension = await GetExtensionForUser(UserId, Id); - remoteData = extension.AdditionalData; - } - catch - { - } - - if (Cache != null) - { - // Send updates for all local values, overwriting the remote. - foreach (string key in Cache.Keys.ToList()) - { - if (ReservedKeys.Contains(key)) - { - continue; - } - - if (remoteData == null || !remoteData.ContainsKey(key) || !EqualityComparer.Default.Equals(remoteData[key], Cache[key])) - { - Save(key, Cache[key]); - } - } - } - - if (remoteData != null) - { - // Update local cache with additions from remote - foreach (string key in remoteData.Keys.ToList()) - { - if (!Cache.ContainsKey(key)) - { - Cache.Add(key, remoteData[key]); - } - } - } - - SyncCompleted?.Invoke(this, new EventArgs()); - } - catch - { - SyncFailed?.Invoke(this, new EventArgs()); - } - } - - /// - public override Task FileExistsAsync(string filePath) - { - throw new NotImplementedException(); - } - - /// - public override Task ReadFileAsync(string filePath, T @default = default) - { - throw new NotImplementedException(); - } - - /// - public override Task SaveFileAsync(string filePath, T value) - { - throw new NotImplementedException(); - } - } -} \ No newline at end of file diff --git a/CommunityToolkit.Graph/CommunityToolkit.Graph.csproj b/CommunityToolkit.Graph/CommunityToolkit.Graph.csproj index 0f03508..3af7782 100644 --- a/CommunityToolkit.Graph/CommunityToolkit.Graph.csproj +++ b/CommunityToolkit.Graph/CommunityToolkit.Graph.csproj @@ -10,10 +10,12 @@ - ProviderExtensions: Extension on IProvider for accessing a pre-configured GraphServiceClient instance. Windows Community Toolkit Graph Provider Extensions + 9.0 + diff --git a/CommunityToolkit.Graph/Helpers/RoamingSettings/IRemoteSettingsStorageHelper.cs b/CommunityToolkit.Graph/Helpers/RoamingSettings/IRemoteSettingsStorageHelper.cs new file mode 100644 index 0000000..66601d0 --- /dev/null +++ b/CommunityToolkit.Graph/Helpers/RoamingSettings/IRemoteSettingsStorageHelper.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Toolkit.Helpers; + +namespace CommunityToolkit.Graph.Helpers.RoamingSettings +{ + /// + /// Describes a remote settings storage location with basic sync support. + /// + /// The type of keys to use for accessing values. + public interface IRemoteSettingsStorageHelper : ISettingsStorageHelper + { + /// + /// Gets or sets an event that fires whenever a sync request has completed. + /// + EventHandler SyncCompleted { get; set; } + + /// + /// Gets or sets a value an event that fires whenever a remote sync request has failed. + /// + EventHandler SyncFailed { get; set; } + + /// + /// Update the remote extension to match the local cache and retrieve any new keys. Any existing remote values are replaced. + /// + /// The freshly synced user extension. + Task Sync(); + } +} diff --git a/CommunityToolkit.Graph/Helpers/RoamingSettings/OneDriveDataSource.cs b/CommunityToolkit.Graph/Helpers/RoamingSettings/OneDriveDataSource.cs new file mode 100644 index 0000000..4f4c686 --- /dev/null +++ b/CommunityToolkit.Graph/Helpers/RoamingSettings/OneDriveDataSource.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using CommunityToolkit.Authentication; +using CommunityToolkit.Graph.Extensions; +using Microsoft.Graph; +using Microsoft.Toolkit.Helpers; + +namespace CommunityToolkit.Graph.Helpers.RoamingSettings +{ + /// + /// Helpers for interacting with files in the special OneDrive AppRoot folder. + /// + internal static class OneDriveDataSource + { + private static GraphServiceClient Graph => ProviderManager.Instance.GlobalProvider?.GetClient(); + + /// + /// Updates or create a new file on the remote with the provided content. + /// + /// The type of object to save. + /// A representing the asynchronous operation. + public static async Task SetFileAsync(string userId, string itemPath, T fileContents, IObjectSerializer serializer) + { + var json = serializer.Serialize(fileContents) as string; + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); + + return await Graph.Users[userId].Drive.Special.AppRoot.ItemWithPath(itemPath).Content.Request().PutAsync(stream); + } + + /// + /// Get a file from the remote. + /// + /// The type of object to return. + /// A representing the asynchronous operation. + public static async Task GetFileAsync(string userId, string itemPath, IObjectSerializer serializer) + { + Stream stream = await Graph.Users[userId].Drive.Special.AppRoot.ItemWithPath(itemPath).Content.Request().GetAsync(); + + string streamContents = new StreamReader(stream).ReadToEnd(); + + return serializer.Deserialize(streamContents); + } + + /// + /// Delete the file from the remote. + /// + /// A representing the asynchronous operation. + public static async Task DeleteItemAsync(string userId, string itemPath) + { + await Graph.Users[userId].Drive.Special.AppRoot.ItemWithPath(itemPath).Request().DeleteAsync(); + } + + public static async Task CreateFolderAsync(string userId, string folderName, string path = null) + { + var folderDriveItem = new DriveItem() + { + Name = folderName, + Folder = new Folder(), + }; + + if (path != null) + { + await Graph.Users[userId].Drive.Special.AppRoot.ItemWithPath(path).Children.Request().AddAsync(folderDriveItem); + } + else + { + await Graph.Users[userId].Drive.Special.AppRoot.Children.Request().AddAsync(folderDriveItem); + } + } + + public static async Task> ReadFolderAsync(string userId, string folderPath) + { + IDriveItemChildrenCollectionPage folderContents = await Graph.Users[userId].Drive.Special.AppRoot.ItemWithPath(folderPath).Children.Request().GetAsync(); + + var results = new List<(DirectoryItemType, string)>(); + foreach (var item in folderContents) + { + var itemType = (item.Folder != null) + ? DirectoryItemType.Folder + : item.Size != null + ? DirectoryItemType.File + : DirectoryItemType.None; + + var itemName = item.Name; + + results.Add((itemType, itemName)); + } + + return results; + } + } +} diff --git a/CommunityToolkit.Graph/Helpers/RoamingSettings/OneDriveStorageHelper.cs b/CommunityToolkit.Graph/Helpers/RoamingSettings/OneDriveStorageHelper.cs new file mode 100644 index 0000000..90e4259 --- /dev/null +++ b/CommunityToolkit.Graph/Helpers/RoamingSettings/OneDriveStorageHelper.cs @@ -0,0 +1,102 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using CommunityToolkit.Authentication; +using CommunityToolkit.Graph.Extensions; +using Microsoft.Toolkit.Helpers; + +namespace CommunityToolkit.Graph.Helpers.RoamingSettings +{ + /// + /// A base class for easily building roaming settings helper implementations. + /// + public class OneDriveStorageHelper : IFileStorageHelper + { + /// + /// Gets the id of the Graph user. + /// + public string UserId { get; } + + /// + /// Gets an object serializer for converting objects in the data store. + /// + public IObjectSerializer Serializer { get; } + + /// + /// Creates a new instance using the userId retrieved from a Graph "Me" request. + /// + /// A serializer used for converting stored objects. + /// A new instance of the configured for the current Graph user. + public static async Task CreateForCurrentUserAsync(IObjectSerializer objectSerializer = null) + { + var provider = ProviderManager.Instance.GlobalProvider; + if (provider == null || provider.State != ProviderState.SignedIn) + { + throw new InvalidOperationException($"The {nameof(ProviderManager.GlobalProvider)} must be set and signed in to create a new {nameof(OneDriveStorageHelper)} for the current user."); + } + + var me = await provider.GetClient().Me.Request().GetAsync(); + var userId = me.Id; + + return new OneDriveStorageHelper(userId, objectSerializer); + } + + /// + /// Initializes a new instance of the class. + /// + /// The id of the target Graph user. + /// A serializer used for converting stored objects. + public OneDriveStorageHelper(string userId, IObjectSerializer objectSerializer = null) + { + UserId = userId ?? throw new ArgumentNullException(nameof(userId)); + Serializer = objectSerializer ?? new SystemSerializer(); + } + + /// + public async Task ReadFileAsync(string filePath, T @default = default) + { + return await OneDriveDataSource.GetFileAsync(UserId, filePath, Serializer) ?? @default; + } + + /// + public Task> ReadFolderAsync(string folderPath) + { + return OneDriveDataSource.ReadFolderAsync(UserId, folderPath); + } + + /// + public async Task CreateFileAsync(string filePath, T value) + { + await OneDriveDataSource.SetFileAsync(UserId, filePath, value, Serializer); + } + + /// + public Task CreateFolderAsync(string folderName) + { + return OneDriveDataSource.CreateFolderAsync(UserId, folderName); + } + + /// + /// Ensure a folder exists at the path specified. + /// + /// The name of the new folder. + /// The path to create the new folder in. + /// A task. + public Task CreateFolderAsync(string folderName, string folderPath) + { + return OneDriveDataSource.CreateFolderAsync(UserId, folderName, folderPath); + } + + /// + public Task DeleteItemAsync(string itemPath) + { + return OneDriveDataSource.DeleteItemAsync(UserId, itemPath); + } + } +} diff --git a/CommunityToolkit.Graph.Uwp/Helpers/RoamingSettings/UserExtensionsDataSource.cs b/CommunityToolkit.Graph/Helpers/RoamingSettings/UserExtensionDataSource.cs similarity index 97% rename from CommunityToolkit.Graph.Uwp/Helpers/RoamingSettings/UserExtensionsDataSource.cs rename to CommunityToolkit.Graph/Helpers/RoamingSettings/UserExtensionDataSource.cs index 61218c2..2a035b6 100644 --- a/CommunityToolkit.Graph.Uwp/Helpers/RoamingSettings/UserExtensionsDataSource.cs +++ b/CommunityToolkit.Graph/Helpers/RoamingSettings/UserExtensionDataSource.cs @@ -10,12 +10,12 @@ using CommunityToolkit.Graph.Extensions; using Microsoft.Graph; -namespace CommunityToolkit.Graph.Uwp.Helpers.RoamingSettings +namespace CommunityToolkit.Graph.Helpers.RoamingSettings { /// /// Manages Graph interaction with open extensions on the user. /// - internal static class UserExtensionsDataSource + internal static class UserExtensionDataSource { private static GraphServiceClient Graph => ProviderManager.Instance.GlobalProvider?.GetClient(); @@ -91,7 +91,7 @@ public static async Task CreateExtension(string userId, string extens "\"extensionName\": \"" + extensionId + "\"," + "}"; - HttpRequestMessage hrm = new HttpRequestMessage(HttpMethod.Post, requestUrl); + HttpRequestMessage hrm = new (HttpMethod.Post, requestUrl); hrm.Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); await Graph.AuthenticationProvider.AuthenticateRequestAsync(hrm); HttpResponseMessage response = await Graph.HttpProvider.SendAsync(hrm); diff --git a/CommunityToolkit.Graph/Helpers/RoamingSettings/UserExtensionStorageHelper.cs b/CommunityToolkit.Graph/Helpers/RoamingSettings/UserExtensionStorageHelper.cs new file mode 100644 index 0000000..ee150ae --- /dev/null +++ b/CommunityToolkit.Graph/Helpers/RoamingSettings/UserExtensionStorageHelper.cs @@ -0,0 +1,264 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Authentication; +using CommunityToolkit.Graph.Extensions; +using Microsoft.Graph; +using Microsoft.Toolkit.Extensions; +using Microsoft.Toolkit.Helpers; + +namespace CommunityToolkit.Graph.Helpers.RoamingSettings +{ + /// + /// An IObjectStorageHelper implementation using open extensions on the Graph User for storing key/value pairs. + /// + public class UserExtensionStorageHelper : IRemoteSettingsStorageHelper + { + private static readonly IList ReservedKeys = new List { "responseHeaders", "statusCode", "@odata.context" }; + private static readonly SemaphoreSlim SyncLock = new (1); + + /// + /// Gets or sets an event that fires whenever a sync request has completed. + /// + public EventHandler SyncCompleted { get; set; } + + /// + /// gets or sets an event that fires whenever a remote sync request has failed. + /// + public EventHandler SyncFailed { get; set; } + + /// + /// Gets the id for the target extension on a Graph user. + /// + public string ExtensionId { get; } + + /// + /// Gets the id of the target Graph user. + /// + public string UserId { get; } + + /// + /// Gets an object serializer for converting objects in the data store. + /// + public IObjectSerializer Serializer { get; } + + /// + /// Gets a cache of the stored values, converted using the provided serializer. + /// + public IReadOnlyDictionary Cache => _cache; + + private readonly Dictionary _cache; + private bool _cleared; + + /// + /// Creates a new instance using the userId retrieved from a Graph "Me" request. + /// + /// The id for the target extension on a Graph user. + /// A serializer used for converting stored objects. + /// A new instance of the configured for the current Graph user. + public static async Task CreateForCurrentUserAsync(string extensionId, IObjectSerializer objectSerializer = null) + { + if (extensionId == null) + { + throw new ArgumentNullException(nameof(extensionId)); + } + + var provider = ProviderManager.Instance.GlobalProvider; + if (provider == null || provider.State != ProviderState.SignedIn) + { + throw new InvalidOperationException($"The {nameof(ProviderManager.GlobalProvider)} must be set and signed in to create a new {nameof(UserExtensionStorageHelper)} for the current user."); + } + + var me = await provider.GetClient().Me.Request().GetAsync(); + var userId = me.Id; + + return new UserExtensionStorageHelper(extensionId, userId, objectSerializer); + } + + /// + /// An indexer for easily accessing key values. + /// + /// The key for the desired value. + /// The value found for the provided key. + public object this[string key] + { + get => ISettingsStorageHelperExtensions.Read(this, key); + set => Save(key, value); + } + + /// + /// Initializes a new instance of the class. + /// + /// The id for the target extension on a Graph user. + /// The id of the target Graph user. + /// A serializer used for converting stored objects. + public UserExtensionStorageHelper(string extensionId, string userId, IObjectSerializer objectSerializer = null) + { + ExtensionId = extensionId ?? throw new ArgumentNullException(nameof(extensionId)); + UserId = userId ?? throw new ArgumentNullException(nameof(userId)); + Serializer = objectSerializer ?? new SystemSerializer(); + + _cache = new Dictionary(); + _cleared = false; + } + + /// + public void Save(string key, T value) + { + _cache[key] = SerializeValue(value); + } + + /// + public bool TryRead(string key, out TValue value) + { + if (_cache.TryGetValue(key, out object cachedValue)) + { + value = DeserializeValue(cachedValue); + return true; + } + else + { + value = default; + return false; + } + } + + /// + public bool TryDelete(string key) + { + return _cache.Remove(key); + } + + /// + public void Clear() + { + _cache.Clear(); + _cleared = true; + } + + /// + /// Synchronize the cache with the remote: + /// - If the cache has been cleared, the remote will be deleted and recreated. + /// - Any cached keys will be saved to the remote, overwriting existing values. + /// - Any new keys from the remote will be stored in the cache. + /// + /// The freshly synced user extension. + public virtual async Task Sync() + { + await SyncLock.WaitAsync(); + + try + { + IDictionary remoteData = null; + + // Check if the extension should be cleared. + if (_cleared) + { + // Delete and re-create the remote extension. + await UserExtensionDataSource.DeleteExtension(UserId, ExtensionId); + Extension extension = await UserExtensionDataSource.CreateExtension(UserId, ExtensionId); + remoteData = extension.AdditionalData; + + _cleared = false; + } + else + { + // Get the remote extension. + Extension extension = await UserExtensionDataSource.GetExtension(UserId, ExtensionId); + remoteData = extension.AdditionalData; + } + + // Send updates for all local values, overwriting the remote. + foreach (string key in _cache.Keys.ToList()) + { + if (ReservedKeys.Contains(key)) + { + continue; + } + + if (!remoteData.ContainsKey(key) || !EqualityComparer.Default.Equals(remoteData[key], Cache[key])) + { + Save(key, _cache[key]); + } + } + + if (remoteData != null) + { + // Update local cache with additions from remote + foreach (string key in remoteData.Keys.ToList()) + { + if (ReservedKeys.Contains(key)) + { + continue; + } + + if (!_cache.ContainsKey(key)) + { + _cache.Add(key, remoteData[key]); + } + } + } + + SyncCompleted?.Invoke(this, new EventArgs()); + } + catch + { + SyncFailed?.Invoke(this, new EventArgs()); + } + finally + { + SyncLock.Release(); + } + } + + /// + /// Use the serializer to deserialize a value appropriately for the type. + /// + /// The type of object expected. + /// The value to deserialize. + /// An object of type T. + protected T DeserializeValue(object value) + { + try + { + return Serializer.Deserialize((string)value); + } + catch + { + // Primitive types can't be deserialized. + return (T)Convert.ChangeType(value, typeof(T)); + } + } + + /// + /// Use the serializer to serialize a value appropriately for the type. + /// + /// The type of object being serialized. + /// The object to serialize. + /// The serialized object. + protected object SerializeValue(T value) + { + var type = typeof(T); + var typeInfo = type.GetTypeInfo(); + + // Skip serialization for primitives. + if (typeInfo.IsPrimitive || type == typeof(string)) + { + // Update the cache + return value; + } + else + { + // Update the cache + return Serializer.Serialize(value); + } + } + } +} \ No newline at end of file diff --git a/SampleTest/MainPage.xaml b/SampleTest/MainPage.xaml index edd557b..a3a4ed7 100644 --- a/SampleTest/MainPage.xaml +++ b/SampleTest/MainPage.xaml @@ -52,9 +52,6 @@ - - - PersonViewSample.xaml - - RoamingSettingsView.xaml - - @@ -161,10 +157,6 @@ Designer MSBuild:Compile - - MSBuild:Compile - Designer - diff --git a/SampleTest/Samples/RoamingSettings/RoamingSettingsView.xaml b/SampleTest/Samples/RoamingSettings/RoamingSettingsView.xaml deleted file mode 100644 index 398282c..0000000 --- a/SampleTest/Samples/RoamingSettings/RoamingSettingsView.xaml +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - -