Updated remote object storage helpers with new WCT interfaces (#131)
* Renamed RoamingSettings to ObjectStorage, updated to use new storage interfaces from WCT, and migrated to *.Graph package * Applied updates from WCT ObjectStorage PR feed * Updated dependencies and RoamingSettings namespace * Updated and added tests
This commit is contained in:
Родитель
aa57a6f045
Коммит
6fdcc7dbe2
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A base class for easily building roaming settings helper implementations.
|
||||
/// </summary>
|
||||
public abstract class BaseRoamingSettingsDataStore : IRoamingSettingsDataStore
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public EventHandler SyncCompleted { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public EventHandler SyncFailed { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool AutoSync { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Id { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UserId { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDictionary<string, object> Cache { get; private set; } = new Dictionary<string, object>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets an object serializer for converting objects in the data store.
|
||||
/// </summary>
|
||||
protected IObjectSerializer Serializer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BaseRoamingSettingsDataStore"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userId">The id of the target Graph user.</param>
|
||||
/// <param name="dataStoreId">A unique id for the data store.</param>
|
||||
/// <param name="objectSerializer">An IObjectSerializer used for serializing objects.</param>
|
||||
/// <param name="autoSync">Determines if the data store should sync for every interaction.</param>
|
||||
public BaseRoamingSettingsDataStore(string userId, string dataStoreId, IObjectSerializer objectSerializer, bool autoSync = true)
|
||||
{
|
||||
AutoSync = autoSync;
|
||||
Id = dataStoreId;
|
||||
UserId = userId;
|
||||
Serializer = objectSerializer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance of the data storage container.
|
||||
/// </summary>
|
||||
/// <returns>A task.</returns>
|
||||
public abstract Task Create();
|
||||
|
||||
/// <summary>
|
||||
/// Delete the instance of the data storage container.
|
||||
/// </summary>
|
||||
/// <returns>A task.</returns>
|
||||
public abstract Task Delete();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a setting already exists.
|
||||
/// </summary>
|
||||
/// <param name="key">Key of the setting (that contains object).</param>
|
||||
/// <returns>True if a value exists.</returns>
|
||||
public bool KeyExists(string key)
|
||||
{
|
||||
return Cache?.ContainsKey(key) ?? false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a setting already exists in composite.
|
||||
/// </summary>
|
||||
/// <param name="compositeKey">Key of the composite (that contains settings).</param>
|
||||
/// <param name="key"> Key of the setting (that contains object).</param>
|
||||
/// <returns>True if a value exists.</returns>
|
||||
public bool KeyExists(string compositeKey, string key)
|
||||
{
|
||||
if (KeyExists(compositeKey))
|
||||
{
|
||||
ApplicationDataCompositeValue composite = (ApplicationDataCompositeValue)Cache[compositeKey];
|
||||
if (composite != null)
|
||||
{
|
||||
return composite.ContainsKey(key);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a single item by its key.
|
||||
/// </summary>
|
||||
/// <param name="key">Key of the object.</param>
|
||||
/// <param name="default">Default value of the object.</param>
|
||||
/// <typeparam name="T">Type of object retrieved.</typeparam>
|
||||
/// <returns>The T object.</returns>
|
||||
public T Read<T>(string key, T @default = default)
|
||||
{
|
||||
if (Cache != null && Cache.TryGetValue(key, out object value))
|
||||
{
|
||||
return DeserializeValue<T>(value);
|
||||
}
|
||||
|
||||
return @default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a single item by its key in composite.
|
||||
/// </summary>
|
||||
/// <param name="compositeKey"> Key of the composite (that contains settings).</param>
|
||||
/// <param name="key">Key of the object.</param>
|
||||
/// <param name="default">Default value of the object.</param>
|
||||
/// <typeparam name="T">Type of object retrieved.</typeparam>
|
||||
/// <returns>The T object.</returns>
|
||||
public T Read<T>(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<T>(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return @default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a single item by its key.
|
||||
/// </summary>
|
||||
/// <param name="key">Key of the value saved.</param>
|
||||
/// <param name="value">Object to save.</param>
|
||||
/// <typeparam name="T">Type of object saved.</typeparam>
|
||||
public void Save<T>(string key, T value)
|
||||
{
|
||||
// Update the cache
|
||||
Cache[key] = SerializeValue(value);
|
||||
|
||||
if (AutoSync)
|
||||
{
|
||||
// Update the remote
|
||||
Task.Run(() => Sync());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="compositeKey">Key of the composite (that contains settings).</param>
|
||||
/// <param name="values">Objects to save.</param>
|
||||
/// <typeparam name="T">Type of object saved.</typeparam>
|
||||
public void Save<T>(string compositeKey, IDictionary<string, T> values)
|
||||
{
|
||||
var type = typeof(T);
|
||||
var typeInfo = type.GetTypeInfo();
|
||||
|
||||
if (KeyExists(compositeKey))
|
||||
{
|
||||
ApplicationDataCompositeValue composite = (ApplicationDataCompositeValue)Cache[compositeKey];
|
||||
|
||||
foreach (KeyValuePair<string, T> 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<string, T> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a file already exists.
|
||||
/// </summary>
|
||||
/// <param name="filePath">Key of the file (that contains object).</param>
|
||||
/// <returns>True if a value exists.</returns>
|
||||
public abstract Task<bool> FileExistsAsync(string filePath);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an object from a file.
|
||||
/// </summary>
|
||||
/// <param name="filePath">Path to the file that contains the object.</param>
|
||||
/// <param name="default">Default value of the object.</param>
|
||||
/// <typeparam name="T">Type of object retrieved.</typeparam>
|
||||
/// <returns>Waiting task until completion with the object in the file.</returns>
|
||||
public abstract Task<T> ReadFileAsync<T>(string filePath, T @default = default);
|
||||
|
||||
/// <summary>
|
||||
/// Saves an object inside a file.
|
||||
/// </summary>
|
||||
/// <param name="filePath">Path to the file that will contain the object.</param>
|
||||
/// <param name="value">Object to save.</param>
|
||||
/// <typeparam name="T">Type of object saved.</typeparam>
|
||||
/// <returns>Waiting task until completion.</returns>
|
||||
public abstract Task<StorageFile> SaveFileAsync<T>(string filePath, T value);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract Task Sync();
|
||||
|
||||
/// <summary>
|
||||
/// Delete the internal cache.
|
||||
/// </summary>
|
||||
protected void DeleteCache()
|
||||
{
|
||||
Cache.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use the serializer to deserialize a value appropriately for the type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of object expected.</typeparam>
|
||||
/// <param name="value">The value to deserialize.</param>
|
||||
/// <returns>An object of type T.</returns>
|
||||
protected T DeserializeValue<T>(object value)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Serializer.Deserialize<T>((string)value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Primitive types can't be deserialized.
|
||||
return (T)Convert.ChangeType(value, typeof(T));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use the serializer to serialize a value appropriately for the type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of object being serialized.</typeparam>
|
||||
/// <param name="value">The object to serialize.</param>
|
||||
/// <returns>The serialized object.</returns>
|
||||
protected object SerializeValue<T>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the contract for creating storage containers used for roaming data.
|
||||
/// </summary>
|
||||
public interface IRoamingSettingsDataStore : IObjectStorageHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the values should immediately sync or not.
|
||||
/// </summary>
|
||||
bool AutoSync { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets access to the key/value pairs cache directly.
|
||||
/// </summary>
|
||||
IDictionary<string, object> Cache { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the id of the data store.
|
||||
/// </summary>
|
||||
string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the id of the target user.
|
||||
/// </summary>
|
||||
string UserId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an event handler for when a remote data sync completes successfully.
|
||||
/// </summary>
|
||||
EventHandler SyncCompleted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an event handler for when a remote data sync fails.
|
||||
/// </summary>
|
||||
EventHandler SyncFailed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new storage container.
|
||||
/// </summary>
|
||||
/// <returns>A Task.</returns>
|
||||
Task Create();
|
||||
|
||||
/// <summary>
|
||||
/// Delete the existing storage container.
|
||||
/// </summary>
|
||||
/// <returns>A Task.</returns>
|
||||
Task Delete();
|
||||
|
||||
/// <summary>
|
||||
/// Syncronize the internal cache with the remote storage endpoint.
|
||||
/// </summary>
|
||||
/// <returns>A Task.</returns>
|
||||
Task Sync();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Helpers for interacting with files in the special OneDrive AppRoot folder.
|
||||
/// </summary>
|
||||
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);
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// Updates or create a new file on the remote with the provided content.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of object to save.</typeparam>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
public static async Task<DriveItem> Update<T>(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<DriveItem>(stream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a file from the remote.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of object to return.</typeparam>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
public static async Task<T> Retrieve<T>(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<T>(streamContents);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete the file from the remote.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
public static async Task Delete(string userId, string fileWithExt)
|
||||
{
|
||||
await Graph.Users[userId].Drive.Special.AppRoot.ItemWithPath(fileWithExt).Request().DeleteAsync();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A DataStore for managing roaming settings in OneDrive.
|
||||
/// </summary>
|
||||
public class OneDriveDataStore : BaseRoamingSettingsDataStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieve an object stored in a OneDrive file.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of object to retrieve.</typeparam>
|
||||
/// <param name="userId">The id of the target Graph user.</param>
|
||||
/// <param name="fileName">The name of the file.</param>
|
||||
/// <param name="serializer">An object serializer for handling deserialization.</param>
|
||||
/// <returns>The deserialized file contents.</returns>
|
||||
public static async Task<T> Get<T>(string userId, string fileName, IObjectSerializer serializer)
|
||||
{
|
||||
return await OneDriveDataSource.Retrieve<T>(userId, fileName, serializer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the contents of a OneDrive file.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of object being stored.</typeparam>
|
||||
/// <param name="userId">The id of the target Graph user.</param>
|
||||
/// <param name="fileName">The name of the file.</param>
|
||||
/// <param name="fileContents">The object to store.</param>
|
||||
/// <param name="serializer">An object serializer for handling serialization.</param>
|
||||
/// <returns>A task.</returns>
|
||||
public static async Task Set<T>(string userId, string fileName, T fileContents, IObjectSerializer serializer)
|
||||
{
|
||||
await OneDriveDataSource.Update(userId, fileName, fileContents, serializer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a file from OneDrive by name.
|
||||
/// </summary>
|
||||
/// <param name="userId">The id of the target Graph user.</param>
|
||||
/// <param name="fileName">The name of the file.</param>
|
||||
/// <returns>A task.</returns>
|
||||
public static async Task Delete(string userId, string fileName)
|
||||
{
|
||||
await OneDriveDataSource.Delete(userId, fileName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OneDriveDataStore"/> class.
|
||||
/// </summary>
|
||||
public OneDriveDataStore(string userId, string syncDataFileName, IObjectSerializer objectSerializer, bool autoSync = true)
|
||||
: base(userId, syncDataFileName, objectSerializer, autoSync)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task Create()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task Delete()
|
||||
{
|
||||
// Clear the cache
|
||||
Cache.Clear();
|
||||
|
||||
// Delete the remote.
|
||||
await Delete(UserId, Id);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<bool> FileExistsAsync(string filePath)
|
||||
{
|
||||
var roamingSettings = await Get<object>(UserId, Id, Serializer);
|
||||
return roamingSettings != null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<T> ReadFileAsync<T>(string filePath, T @default = default)
|
||||
{
|
||||
return await Get<T>(UserId, filePath, Serializer) ?? @default;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<StorageFile> SaveFileAsync<T>(string filePath, T value)
|
||||
{
|
||||
await Set(UserId, filePath, value, Serializer);
|
||||
|
||||
// Can't convert DriveItem to StorageFile, so we return null instead.
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task Sync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get the remote
|
||||
string fileName = Id;
|
||||
IDictionary<string, object> remoteData = null;
|
||||
try
|
||||
{
|
||||
remoteData = await Get<IDictionary<string, object>>(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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// An enumeration of the available data storage methods for roaming data.
|
||||
/// </summary>
|
||||
public enum RoamingDataStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Store data using open extensions on the Graph User.
|
||||
/// </summary>
|
||||
UserExtensions,
|
||||
|
||||
/// <summary>
|
||||
/// Store data in a Graph User's OneDrive.
|
||||
/// </summary>
|
||||
OneDrive,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A helper class for syncing data to roaming data store.
|
||||
/// </summary>
|
||||
public class RoamingSettingsHelper : IRoamingSettingsDataStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the internal data store instance.
|
||||
/// </summary>
|
||||
public IRoamingSettingsDataStore DataStore { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public EventHandler SyncCompleted { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public EventHandler SyncFailed { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool AutoSync => DataStore.AutoSync;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDictionary<string, object> Cache => DataStore.Cache;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Id => DataStore.Id;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UserId => DataStore.UserId;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new RoamingSettingsHelper instance for the currently signed in user.
|
||||
/// </summary>
|
||||
/// <param name="dataStore">Which specific data store is being used.</param>
|
||||
/// <param name="syncOnInit">Whether the values should immediately sync or not.</param>
|
||||
/// <param name="autoSync">Whether the values should immediately sync on change or wait until Sync is called explicitly.</param>
|
||||
/// <param name="serializer">An object serializer for serialization of objects in the data store.</param>
|
||||
/// <returns>A new instance of the RoamingSettingsHelper configured for the current user.</returns>
|
||||
public static async Task<RoamingSettingsHelper> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RoamingSettingsHelper"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userId">The id of the target Graph User.</param>
|
||||
/// <param name="dataStore">Which specific data store is being used.</param>
|
||||
/// <param name="syncOnInit">Whether the values should immediately sync or not.</param>
|
||||
/// <param name="autoSync">Whether the values should immediately sync on change or wait until Sync is called explicitly.</param>
|
||||
/// <param name="serializer">An object serializer for serialization of objects in the data store.</param>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An indexer for easily accessing key values.
|
||||
/// </summary>
|
||||
/// <param name="key">The key for the desired value.</param>
|
||||
/// <returns>The value found for the provided key.</returns>
|
||||
public object this[string key]
|
||||
{
|
||||
get => DataStore.Read<object>(key);
|
||||
set => DataStore.Save(key, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> FileExistsAsync(string filePath) => DataStore.FileExistsAsync(filePath);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool KeyExists(string key) => DataStore.KeyExists(key);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool KeyExists(string compositeKey, string key) => DataStore.KeyExists(compositeKey, key);
|
||||
|
||||
/// <inheritdoc />
|
||||
public T Read<T>(string key, T @default = default) => DataStore.Read<T>(key, @default);
|
||||
|
||||
/// <inheritdoc />
|
||||
public T Read<T>(string compositeKey, string key, T @default = default) => DataStore.Read(compositeKey, key, @default);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<T> ReadFileAsync<T>(string filePath, T @default = default) => DataStore.ReadFileAsync(filePath, @default);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Save<T>(string key, T value) => DataStore.Save<T>(key, value);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Save<T>(string compositeKey, IDictionary<string, T> values) => DataStore.Save<T>(compositeKey, values);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<StorageFile> SaveFileAsync<T>(string filePath, T value) => DataStore.SaveFileAsync<T>(filePath, value);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new storage container.
|
||||
/// </summary>
|
||||
/// <returns>A Task.</returns>
|
||||
public Task Create() => DataStore.Create();
|
||||
|
||||
/// <summary>
|
||||
/// Delete the existing storage container.
|
||||
/// </summary>
|
||||
/// <returns>A Task.</returns>
|
||||
public Task Delete() => DataStore.Delete();
|
||||
|
||||
/// <summary>
|
||||
/// Syncronize the internal cache with the remote storage endpoint.
|
||||
/// </summary>
|
||||
/// <returns>A Task.</returns>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// An IObjectStorageHelper implementation using open extensions on the Graph User for storing key/value pairs.
|
||||
/// </summary>
|
||||
public class UserExtensionDataStore : BaseRoamingSettingsDataStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieve the value from Graph User extensions and cast the response to the provided type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to cast the return result to.</typeparam>
|
||||
/// <param name="userId">The id of the user.</param>
|
||||
/// <param name="extensionId">The id of the user extension.</param>
|
||||
/// <param name="key">The key for the desired value.</param>
|
||||
/// <returns>The value from the data store.</returns>
|
||||
public static async Task<T> Get<T>(string userId, string extensionId, string key)
|
||||
{
|
||||
return (T)await Get(userId, extensionId, key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve the value from Graph User extensions by extensionId, userId, and key.
|
||||
/// </summary>
|
||||
/// <param name="userId">The id of the user.</param>
|
||||
/// <param name="extensionId">The id of the user extension.</param>
|
||||
/// <param name="key">The key for the desired value.</param>
|
||||
/// <returns>The value from the data store.</returns>
|
||||
public static async Task<object> Get(string userId, string extensionId, string key)
|
||||
{
|
||||
var userExtension = await GetExtensionForUser(userId, extensionId);
|
||||
return userExtension.AdditionalData[key];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a value by key in a Graph User's extension.
|
||||
/// </summary>
|
||||
/// <param name="userId">The id of the user.</param>
|
||||
/// <param name="extensionId">The id of the user extension.</param>
|
||||
/// <param name="key">The key for the target value.</param>
|
||||
/// <param name="value">The value to set.</param>
|
||||
/// <returns>A task upon completion.</returns>
|
||||
public static async Task Set(string userId, string extensionId, string key, object value)
|
||||
{
|
||||
await UserExtensionsDataSource.SetValue(userId, extensionId, key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new roaming settings extension on a Graph User.
|
||||
/// </summary>
|
||||
/// <param name="userId">The id of the user.</param>
|
||||
/// <param name="extensionId">The id of the user extension.</param>
|
||||
/// <returns>The newly created user extension.</returns>
|
||||
public static async Task<Extension> Create(string userId, string extensionId)
|
||||
{
|
||||
var userExtension = await UserExtensionsDataSource.CreateExtension(userId, extensionId);
|
||||
return userExtension;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes an extension by id on a Graph User.
|
||||
/// </summary>
|
||||
/// <param name="userId">The id of the user.</param>
|
||||
/// <param name="extensionId">The id of the user extension.</param>
|
||||
/// <returns>A task upon completion.</returns>
|
||||
public static async Task Delete(string userId, string extensionId)
|
||||
{
|
||||
await UserExtensionsDataSource.DeleteExtension(userId, extensionId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a user extension.
|
||||
/// </summary>
|
||||
/// <param name="userId">The id of the user.</param>
|
||||
/// <param name="extensionId">The id of the user extension.</param>
|
||||
/// <returns>The target extension.</returns>
|
||||
public static async Task<Extension> GetExtensionForUser(string userId, string extensionId)
|
||||
{
|
||||
var userExtension = await UserExtensionsDataSource.GetExtension(userId, extensionId);
|
||||
return userExtension;
|
||||
}
|
||||
|
||||
private static readonly IList<string> ReservedKeys = new List<string> { "responseHeaders", "statusCode", "@odata.context" };
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserExtensionDataStore"/> class.
|
||||
/// </summary>
|
||||
public UserExtensionDataStore(string userId, string extensionId, IObjectSerializer objectSerializer, bool autoSync = true)
|
||||
: base(userId, extensionId, objectSerializer, autoSync)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new roaming settings extension on the Graph User.
|
||||
/// </summary>
|
||||
/// <returns>The newly created Extension object.</returns>
|
||||
public override async Task Create()
|
||||
{
|
||||
await Create(UserId, Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the roamingSettings extension from the Graph User.
|
||||
/// </summary>
|
||||
/// <returns>A void task.</returns>
|
||||
public override async Task Delete()
|
||||
{
|
||||
// Delete the cache
|
||||
Cache.Clear();
|
||||
|
||||
// Delete the remote.
|
||||
await Delete(UserId, Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the remote extension to match the local cache and retrieve any new keys. Any existing remote values are replaced.
|
||||
/// </summary>
|
||||
/// <returns>The freshly synced user extension.</returns>
|
||||
public override async Task Sync()
|
||||
{
|
||||
try
|
||||
{
|
||||
IDictionary<string, object> 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<object>.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());
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<bool> FileExistsAsync(string filePath)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<T> ReadFileAsync<T>(string filePath, T @default = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<StorageFile> SaveFileAsync<T>(string filePath, T value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,10 +10,12 @@
|
|||
- ProviderExtensions: Extension on IProvider for accessing a pre-configured GraphServiceClient instance.
|
||||
</Description>
|
||||
<PackageTags>Windows Community Toolkit Graph Provider Extensions</PackageTags>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Graph" Version="4.0.0" />
|
||||
<PackageReference Include="Microsoft.Toolkit" Version="7.0.0-build.1396" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes a remote settings storage location with basic sync support.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">The type of keys to use for accessing values.</typeparam>
|
||||
public interface IRemoteSettingsStorageHelper<TKey> : ISettingsStorageHelper<TKey>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets an event that fires whenever a sync request has completed.
|
||||
/// </summary>
|
||||
EventHandler SyncCompleted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value an event that fires whenever a remote sync request has failed.
|
||||
/// </summary>
|
||||
EventHandler SyncFailed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Update the remote extension to match the local cache and retrieve any new keys. Any existing remote values are replaced.
|
||||
/// </summary>
|
||||
/// <returns>The freshly synced user extension.</returns>
|
||||
Task Sync();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Helpers for interacting with files in the special OneDrive AppRoot folder.
|
||||
/// </summary>
|
||||
internal static class OneDriveDataSource
|
||||
{
|
||||
private static GraphServiceClient Graph => ProviderManager.Instance.GlobalProvider?.GetClient();
|
||||
|
||||
/// <summary>
|
||||
/// Updates or create a new file on the remote with the provided content.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of object to save.</typeparam>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
public static async Task<DriveItem> SetFileAsync<T>(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<DriveItem>(stream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a file from the remote.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of object to return.</typeparam>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
public static async Task<T> GetFileAsync<T>(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<T>(streamContents);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete the file from the remote.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
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<IEnumerable<(DirectoryItemType, string)>> 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A base class for easily building roaming settings helper implementations.
|
||||
/// </summary>
|
||||
public class OneDriveStorageHelper : IFileStorageHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the id of the Graph user.
|
||||
/// </summary>
|
||||
public string UserId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an object serializer for converting objects in the data store.
|
||||
/// </summary>
|
||||
public IObjectSerializer Serializer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance using the userId retrieved from a Graph "Me" request.
|
||||
/// </summary>
|
||||
/// <param name="objectSerializer">A serializer used for converting stored objects.</param>
|
||||
/// <returns>A new instance of the <see cref="OneDriveStorageHelper"/> configured for the current Graph user.</returns>
|
||||
public static async Task<OneDriveStorageHelper> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OneDriveStorageHelper"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userId">The id of the target Graph user.</param>
|
||||
/// <param name="objectSerializer">A serializer used for converting stored objects.</param>
|
||||
public OneDriveStorageHelper(string userId, IObjectSerializer objectSerializer = null)
|
||||
{
|
||||
UserId = userId ?? throw new ArgumentNullException(nameof(userId));
|
||||
Serializer = objectSerializer ?? new SystemSerializer();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<T> ReadFileAsync<T>(string filePath, T @default = default)
|
||||
{
|
||||
return await OneDriveDataSource.GetFileAsync<T>(UserId, filePath, Serializer) ?? @default;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IEnumerable<(DirectoryItemType ItemType, string Name)>> ReadFolderAsync(string folderPath)
|
||||
{
|
||||
return OneDriveDataSource.ReadFolderAsync(UserId, folderPath);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task CreateFileAsync<T>(string filePath, T value)
|
||||
{
|
||||
await OneDriveDataSource.SetFileAsync<T>(UserId, filePath, value, Serializer);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task CreateFolderAsync(string folderName)
|
||||
{
|
||||
return OneDriveDataSource.CreateFolderAsync(UserId, folderName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure a folder exists at the path specified.
|
||||
/// </summary>
|
||||
/// <param name="folderName">The name of the new folder.</param>
|
||||
/// <param name="folderPath">The path to create the new folder in.</param>
|
||||
/// <returns>A task.</returns>
|
||||
public Task CreateFolderAsync(string folderName, string folderPath)
|
||||
{
|
||||
return OneDriveDataSource.CreateFolderAsync(UserId, folderName, folderPath);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DeleteItemAsync(string itemPath)
|
||||
{
|
||||
return OneDriveDataSource.DeleteItemAsync(UserId, itemPath);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,12 +10,12 @@ using CommunityToolkit.Authentication;
|
|||
using CommunityToolkit.Graph.Extensions;
|
||||
using Microsoft.Graph;
|
||||
|
||||
namespace CommunityToolkit.Graph.Uwp.Helpers.RoamingSettings
|
||||
namespace CommunityToolkit.Graph.Helpers.RoamingSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages Graph interaction with open extensions on the user.
|
||||
/// </summary>
|
||||
internal static class UserExtensionsDataSource
|
||||
internal static class UserExtensionDataSource
|
||||
{
|
||||
private static GraphServiceClient Graph => ProviderManager.Instance.GlobalProvider?.GetClient();
|
||||
|
||||
|
@ -91,7 +91,7 @@ namespace CommunityToolkit.Graph.Uwp.Helpers.RoamingSettings
|
|||
"\"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);
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// An IObjectStorageHelper implementation using open extensions on the Graph User for storing key/value pairs.
|
||||
/// </summary>
|
||||
public class UserExtensionStorageHelper : IRemoteSettingsStorageHelper<string>
|
||||
{
|
||||
private static readonly IList<string> ReservedKeys = new List<string> { "responseHeaders", "statusCode", "@odata.context" };
|
||||
private static readonly SemaphoreSlim SyncLock = new (1);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an event that fires whenever a sync request has completed.
|
||||
/// </summary>
|
||||
public EventHandler SyncCompleted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// gets or sets an event that fires whenever a remote sync request has failed.
|
||||
/// </summary>
|
||||
public EventHandler SyncFailed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the id for the target extension on a Graph user.
|
||||
/// </summary>
|
||||
public string ExtensionId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the id of the target Graph user.
|
||||
/// </summary>
|
||||
public string UserId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an object serializer for converting objects in the data store.
|
||||
/// </summary>
|
||||
public IObjectSerializer Serializer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a cache of the stored values, converted using the provided serializer.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, object> Cache => _cache;
|
||||
|
||||
private readonly Dictionary<string, object> _cache;
|
||||
private bool _cleared;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance using the userId retrieved from a Graph "Me" request.
|
||||
/// </summary>
|
||||
/// <param name="extensionId">The id for the target extension on a Graph user.</param>
|
||||
/// <param name="objectSerializer">A serializer used for converting stored objects.</param>
|
||||
/// <returns>A new instance of the <see cref="UserExtensionStorageHelper"/> configured for the current Graph user.</returns>
|
||||
public static async Task<UserExtensionStorageHelper> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An indexer for easily accessing key values.
|
||||
/// </summary>
|
||||
/// <param name="key">The key for the desired value.</param>
|
||||
/// <returns>The value found for the provided key.</returns>
|
||||
public object this[string key]
|
||||
{
|
||||
get => ISettingsStorageHelperExtensions.Read<string, object>(this, key);
|
||||
set => Save(key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserExtensionStorageHelper"/> class.
|
||||
/// </summary>
|
||||
/// <param name="extensionId">The id for the target extension on a Graph user.</param>
|
||||
/// <param name="userId">The id of the target Graph user.</param>
|
||||
/// <param name="objectSerializer">A serializer used for converting stored objects.</param>
|
||||
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<string, object>();
|
||||
_cleared = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Save<T>(string key, T value)
|
||||
{
|
||||
_cache[key] = SerializeValue(value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryRead<TValue>(string key, out TValue value)
|
||||
{
|
||||
if (_cache.TryGetValue(key, out object cachedValue))
|
||||
{
|
||||
value = DeserializeValue<TValue>(cachedValue);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryDelete(string key)
|
||||
{
|
||||
return _cache.Remove(key);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Clear()
|
||||
{
|
||||
_cache.Clear();
|
||||
_cleared = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <returns>The freshly synced user extension.</returns>
|
||||
public virtual async Task Sync()
|
||||
{
|
||||
await SyncLock.WaitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
IDictionary<string, object> 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<object>.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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use the serializer to deserialize a value appropriately for the type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of object expected.</typeparam>
|
||||
/// <param name="value">The value to deserialize.</param>
|
||||
/// <returns>An object of type T.</returns>
|
||||
protected T DeserializeValue<T>(object value)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Serializer.Deserialize<T>((string)value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Primitive types can't be deserialized.
|
||||
return (T)Convert.ChangeType(value, typeof(T));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use the serializer to serialize a value appropriately for the type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of object being serialized.</typeparam>
|
||||
/// <param name="value">The object to serialize.</param>
|
||||
/// <returns>The serialized object.</returns>
|
||||
protected object SerializeValue<T>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -52,9 +52,6 @@
|
|||
<PivotItem Header="PersonView">
|
||||
<samples:PersonViewSample />
|
||||
</PivotItem>
|
||||
<PivotItem Header="RoamingSettings">
|
||||
<samples:RoamingSettingsView />
|
||||
</PivotItem>
|
||||
<PivotItem Header="PeoplePicker">
|
||||
<StackPanel>
|
||||
<TextBlock Margin="0,0,0,16"
|
||||
|
|
|
@ -126,10 +126,6 @@
|
|||
<Compile Include="Samples\PersonView\PersonViewSample.xaml.cs">
|
||||
<DependentUpon>PersonViewSample.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Samples\RoamingSettings\RoamingSettingsView.xaml.cs">
|
||||
<DependentUpon>RoamingSettingsView.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Samples\RoamingSettings\RoamingSettingsViewModel.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AppxManifest Include="Package.appxmanifest">
|
||||
|
@ -161,10 +157,6 @@
|
|||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Samples\RoamingSettings\RoamingSettingsView.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Graph">
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
<Page
|
||||
x:Class="SampleTest.Samples.RoamingSettingsView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:SampleTest.Samples"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
|
||||
|
||||
<ScrollViewer VerticalScrollMode="Enabled" VerticalScrollBarVisibility="Visible">
|
||||
<StackPanel Spacing="24">
|
||||
<!-- Error text -->
|
||||
<TextBlock Text="{Binding ErrorText}" Foreground="Red" />
|
||||
|
||||
<!-- Create -->
|
||||
<StackPanel>
|
||||
<TextBlock Text="Creates a custom extension on your user object in Graph." />
|
||||
<TextBlock Text="(com.toolkit.roamingSettings.your-application-user-model-id)" />
|
||||
<Button
|
||||
Click="CreateButton_Click"
|
||||
Content="Create" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- View -->
|
||||
<StackPanel>
|
||||
<TextBlock Text="View the list of key value pairs stored in the roaming settings user extension." />
|
||||
<TextBlock Text="Note: Some keys cannot be modified (e.g. statusCode, responseHeaders, @odata.context, extensionName)" />
|
||||
<Button
|
||||
Click="ViewButton_Click"
|
||||
Content="View" />
|
||||
<ListView ItemsSource="{Binding AdditionalData}" IsItemClickEnabled="True" ItemClick="AdditionalData_ItemClick">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock>
|
||||
<Run Text="{Binding Key}" />
|
||||
<Run Text=" : " />
|
||||
<Run Text="{Binding Value}" />
|
||||
</TextBlock>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Get/Set -->
|
||||
<StackPanel>
|
||||
<TextBlock Text="Get or Set individual values on the roaming settings user extension." />
|
||||
|
||||
<TextBox
|
||||
Header="Key"
|
||||
PlaceholderText="Key"
|
||||
Name="KeyInputTextBox"
|
||||
Text="{Binding KeyInputText, Mode=TwoWay}" />
|
||||
<Button
|
||||
Click="GetButton_Click"
|
||||
Content="Get"
|
||||
VerticalAlignment="Stretch" />
|
||||
|
||||
<TextBox
|
||||
Header="Value"
|
||||
Name="ValueInputTextBox"
|
||||
PlaceholderText="Value"
|
||||
Text="{Binding ValueInputText, Mode=TwoWay}" />
|
||||
<Button
|
||||
Click="SetButton_Click"
|
||||
Content="Set"
|
||||
VerticalAlignment="Stretch" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Delete -->
|
||||
<StackPanel>
|
||||
<TextBlock Text="Deletes the custom extension from the user object in Graph." />
|
||||
<Button
|
||||
Click="DeleteButton_Click"
|
||||
Content="Delete" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Page>
|
|
@ -1,57 +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.Collections.Generic;
|
||||
using Windows.UI.Xaml.Controls;
|
||||
|
||||
namespace SampleTest.Samples
|
||||
{
|
||||
/// <summary>
|
||||
/// A sample for demonstrating features in the RoamingSettings namespace.
|
||||
/// </summary>
|
||||
public sealed partial class RoamingSettingsView : Page
|
||||
{
|
||||
private RoamingSettingsViewModel _vm => DataContext as RoamingSettingsViewModel;
|
||||
|
||||
public RoamingSettingsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new RoamingSettingsViewModel();
|
||||
}
|
||||
|
||||
private void CreateButton_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
|
||||
{
|
||||
_vm.CreateCustomRoamingSettings();
|
||||
}
|
||||
|
||||
private void DeleteButton_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
|
||||
{
|
||||
_vm.DeleteCustomRoamingSettings();
|
||||
}
|
||||
|
||||
private void SetButton_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
|
||||
{
|
||||
_vm.SetValue();
|
||||
}
|
||||
|
||||
private void GetButton_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
|
||||
{
|
||||
_vm.GetValue();
|
||||
}
|
||||
|
||||
private void ViewButton_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
|
||||
{
|
||||
_vm.SyncRoamingSettings();
|
||||
}
|
||||
|
||||
private void AdditionalData_ItemClick(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
if (e.ClickedItem is KeyValuePair<string, object> kvp)
|
||||
{
|
||||
_vm.KeyInputText = kvp.Key;
|
||||
_vm.ValueInputText = kvp.Value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,195 +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 CommunityToolkit.Authentication;
|
||||
using CommunityToolkit.Graph.Uwp.Helpers.RoamingSettings;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SampleTest.Samples
|
||||
{
|
||||
public class RoamingSettingsViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private IProvider GlobalProvider => ProviderManager.Instance.GlobalProvider;
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
private string _errorText;
|
||||
public string ErrorText
|
||||
{
|
||||
get => _errorText;
|
||||
set => Set(ref _errorText, value);
|
||||
}
|
||||
|
||||
private RoamingSettingsHelper _roamingSettings;
|
||||
|
||||
private ObservableCollection<KeyValuePair<string, object>> _additionalData;
|
||||
public ObservableCollection<KeyValuePair<string, object>> AdditionalData
|
||||
{
|
||||
get => _additionalData;
|
||||
set => Set(ref _additionalData, value);
|
||||
}
|
||||
|
||||
private string _keyInputText;
|
||||
public string KeyInputText
|
||||
{
|
||||
get => _keyInputText;
|
||||
set => Set(ref _keyInputText, value);
|
||||
}
|
||||
|
||||
private string _valueInputText;
|
||||
public string ValueInputText
|
||||
{
|
||||
get => _valueInputText;
|
||||
set => Set(ref _valueInputText, value);
|
||||
}
|
||||
|
||||
public RoamingSettingsViewModel()
|
||||
{
|
||||
_roamingSettings = null;
|
||||
_keyInputText = string.Empty;
|
||||
_valueInputText = string.Empty;
|
||||
|
||||
ProviderManager.Instance.ProviderStateChanged += (s, e) => CheckState();
|
||||
CheckState();
|
||||
}
|
||||
|
||||
public void GetValue()
|
||||
{
|
||||
try
|
||||
{
|
||||
ErrorText = string.Empty;
|
||||
ValueInputText = string.Empty;
|
||||
|
||||
string key = KeyInputText;
|
||||
string value = _roamingSettings.Read<string>(key);
|
||||
|
||||
ValueInputText = value;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ErrorText = e.Message;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetValue()
|
||||
{
|
||||
try
|
||||
{
|
||||
ErrorText = string.Empty;
|
||||
|
||||
_roamingSettings.Save(KeyInputText, ValueInputText);
|
||||
|
||||
SyncRoamingSettings();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ErrorText = e.Message;
|
||||
}
|
||||
}
|
||||
|
||||
public async void CreateCustomRoamingSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
ErrorText = string.Empty;
|
||||
|
||||
await _roamingSettings.Create();
|
||||
|
||||
AdditionalData = new ObservableCollection<KeyValuePair<string, object>>(_roamingSettings.Cache);
|
||||
|
||||
KeyInputText = string.Empty;
|
||||
ValueInputText = string.Empty;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ErrorText = e.Message;
|
||||
}
|
||||
}
|
||||
|
||||
public async void DeleteCustomRoamingSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
ErrorText = string.Empty;
|
||||
|
||||
await _roamingSettings.Delete();
|
||||
|
||||
AdditionalData?.Clear();
|
||||
KeyInputText = string.Empty;
|
||||
ValueInputText = string.Empty;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ErrorText = e.Message;
|
||||
}
|
||||
}
|
||||
|
||||
public async void SyncRoamingSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
ErrorText = string.Empty;
|
||||
AdditionalData?.Clear();
|
||||
|
||||
await _roamingSettings.Sync();
|
||||
if (_roamingSettings.Cache != null)
|
||||
{
|
||||
AdditionalData = new ObservableCollection<KeyValuePair<string, object>>(_roamingSettings.Cache);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ErrorText = e.Message;
|
||||
}
|
||||
}
|
||||
|
||||
private async void CheckState()
|
||||
{
|
||||
if (GlobalProvider != null && GlobalProvider.State == ProviderState.SignedIn)
|
||||
{
|
||||
await LoadState();
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearState();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadState()
|
||||
{
|
||||
try
|
||||
{
|
||||
ClearState();
|
||||
|
||||
_roamingSettings = await RoamingSettingsHelper.CreateForCurrentUser();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ErrorText = e.Message;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearState()
|
||||
{
|
||||
_roamingSettings = null;
|
||||
|
||||
KeyInputText = string.Empty;
|
||||
ValueInputText = string.Empty;
|
||||
}
|
||||
|
||||
private void Set<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
|
||||
{
|
||||
if (!EqualityComparer<T>.Default.Equals(field, value))
|
||||
{
|
||||
field = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,11 +3,13 @@
|
|||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CommunityToolkit.Authentication;
|
||||
using CommunityToolkit.Graph.Uwp.Helpers.RoamingSettings;
|
||||
using CommunityToolkit.Graph.Helpers.RoamingSettings;
|
||||
using Microsoft.Toolkit.Helpers;
|
||||
using Microsoft.Toolkit.Uwp;
|
||||
using Microsoft.Toolkit.Uwp.Helpers;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UnitTests.UWP.Helpers
|
||||
|
@ -28,17 +30,11 @@ namespace UnitTests.UWP.Helpers
|
|||
{
|
||||
try
|
||||
{
|
||||
string userId = "TestUserId";
|
||||
string dataStoreId = "RoamingData.json";
|
||||
IObjectSerializer serializer = new SystemSerializer();
|
||||
|
||||
IRoamingSettingsDataStore dataStore = new OneDriveDataStore(userId, dataStoreId, serializer, false);
|
||||
|
||||
var userId = "TestUserId";
|
||||
var storageHelper = new OneDriveStorageHelper(userId);
|
||||
|
||||
// Evaluate the default state is as expected
|
||||
Assert.IsFalse(dataStore.AutoSync);
|
||||
Assert.IsNotNull(dataStore.Cache);
|
||||
Assert.AreEqual(dataStoreId, dataStore.Id);
|
||||
Assert.AreEqual(userId, dataStore.UserId);
|
||||
Assert.AreEqual(userId, storageHelper.UserId);
|
||||
|
||||
tcs.SetResult(true);
|
||||
}
|
||||
|
@ -53,12 +49,9 @@ namespace UnitTests.UWP.Helpers
|
|||
await tcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test the dafault state of a new instance of the OneDriveDataStore.
|
||||
/// </summary>
|
||||
[TestCategory("RoamingSettings")]
|
||||
[TestMethod]
|
||||
public async Task Test_Sync()
|
||||
public async Task Test_FileCRUD()
|
||||
{
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
|
||||
|
@ -66,65 +59,92 @@ namespace UnitTests.UWP.Helpers
|
|||
{
|
||||
try
|
||||
{
|
||||
string userId = "TestUserId";
|
||||
string dataStoreId = "RoamingData.json";
|
||||
IObjectSerializer serializer = new SystemSerializer();
|
||||
var filePath = "TestFile.txt";
|
||||
var fileContents = "this is a test";
|
||||
var fileContents2 = "this is also a test";
|
||||
var storageHelper = await OneDriveStorageHelper.CreateForCurrentUserAsync();
|
||||
|
||||
IRoamingSettingsDataStore dataStore = new OneDriveDataStore(userId, dataStoreId, serializer, false);
|
||||
// Create a file
|
||||
await storageHelper.CreateFileAsync(filePath, fileContents);
|
||||
|
||||
try
|
||||
{
|
||||
// Attempt to delete the remote first.
|
||||
await dataStore.Delete();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
// Read a file
|
||||
var readContents = await storageHelper.ReadFileAsync<string>(filePath);
|
||||
Assert.AreEqual(fileContents, readContents);
|
||||
|
||||
dataStore.SyncCompleted += async (s, e) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// Create a second instance to ensure that the Cache doesn't yield a false positive.
|
||||
IRoamingSettingsDataStore dataStore2 = new OneDriveDataStore(userId, dataStoreId, serializer, false);
|
||||
await dataStore2.Sync();
|
||||
// Update a file
|
||||
await storageHelper.CreateFileAsync(filePath, fileContents2);
|
||||
var readContents2 = await storageHelper.ReadFileAsync<string>(filePath);
|
||||
Assert.AreEqual(fileContents2, readContents2);
|
||||
|
||||
var foo = dataStore.Read<string>("foo");
|
||||
Assert.AreEqual("bar", foo);
|
||||
// Delete a file
|
||||
await storageHelper.DeleteItemAsync(filePath);
|
||||
|
||||
tcs.SetResult(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.SetException(ex);
|
||||
}
|
||||
};
|
||||
|
||||
dataStore.SyncFailed = (s, e) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Assert.Fail("Sync Failed");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.SetException(ex);
|
||||
}
|
||||
};
|
||||
|
||||
dataStore.Save("foo", "bar");
|
||||
await dataStore.Sync();
|
||||
tcs.SetResult(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.SetException(ex);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
PrepareProvider(test);
|
||||
|
||||
var result = await tcs.Task;
|
||||
Assert.IsTrue(result);
|
||||
await tcs.Task;
|
||||
}
|
||||
|
||||
[TestCategory("RoamingSettings")]
|
||||
[TestMethod]
|
||||
public async Task Test_FolderCRUD()
|
||||
{
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
|
||||
async void test()
|
||||
{
|
||||
try
|
||||
{
|
||||
var subfolderName = "TestSubFolder";
|
||||
var folderName = "TestFolder";
|
||||
var fileName = "TestFile.txt";
|
||||
var filePath = $"{folderName}/{fileName}";
|
||||
var fileContents = "this is a test";
|
||||
var storageHelper = await OneDriveStorageHelper.CreateForCurrentUserAsync();
|
||||
|
||||
// Create a folder
|
||||
await storageHelper.CreateFolderAsync(folderName);
|
||||
|
||||
// Create a subfolder
|
||||
await storageHelper.CreateFolderAsync(subfolderName, folderName);
|
||||
|
||||
// Create a file in a folder
|
||||
await storageHelper.CreateFileAsync(filePath, fileContents);
|
||||
|
||||
// Read a file from a folder
|
||||
var readContents = await storageHelper.ReadFileAsync<string>(filePath);
|
||||
Assert.AreEqual(fileContents, readContents);
|
||||
|
||||
// List folder contents
|
||||
var folderItems = await storageHelper.ReadFolderAsync(folderName);
|
||||
var folderItemsList = folderItems.ToList();
|
||||
Assert.AreEqual(2, folderItemsList.Count());
|
||||
Assert.AreEqual(subfolderName, folderItemsList[0].Name);
|
||||
Assert.AreEqual(DirectoryItemType.Folder, folderItemsList[0].ItemType);
|
||||
Assert.AreEqual(fileName, folderItemsList[1].Name);
|
||||
Assert.AreEqual(DirectoryItemType.File, folderItemsList[1].ItemType);
|
||||
|
||||
// Delete a folder
|
||||
await storageHelper.DeleteItemAsync(folderName);
|
||||
|
||||
tcs.SetResult(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.SetException(ex);
|
||||
}
|
||||
};
|
||||
|
||||
PrepareProvider(test);
|
||||
|
||||
await tcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CommunityToolkit.Authentication;
|
||||
using CommunityToolkit.Graph.Uwp.Helpers.RoamingSettings;
|
||||
using CommunityToolkit.Graph.Helpers.RoamingSettings;
|
||||
using Microsoft.Toolkit.Extensions;
|
||||
using Microsoft.Toolkit.Helpers;
|
||||
using Microsoft.Toolkit.Uwp;
|
||||
using Microsoft.Toolkit.Uwp.Helpers;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -29,16 +30,14 @@ namespace UnitTests.UWP.Helpers
|
|||
try
|
||||
{
|
||||
string userId = "TestUserId";
|
||||
string dataStoreId = "RoamingData";
|
||||
IObjectSerializer serializer = new SystemSerializer();
|
||||
string extensionId = "RoamingData";
|
||||
|
||||
IRoamingSettingsDataStore dataStore = new UserExtensionDataStore(userId, dataStoreId, serializer, false);
|
||||
|
||||
// Evaluate the default state is as expected
|
||||
Assert.IsFalse(dataStore.AutoSync);
|
||||
Assert.IsNotNull(dataStore.Cache);
|
||||
Assert.AreEqual(dataStoreId, dataStore.Id);
|
||||
Assert.AreEqual(userId, dataStore.UserId);
|
||||
UserExtensionStorageHelper storageHelper = new UserExtensionStorageHelper(extensionId, userId);
|
||||
|
||||
Assert.AreEqual(extensionId, storageHelper.ExtensionId);
|
||||
Assert.AreEqual(userId, storageHelper.UserId);
|
||||
Assert.IsNotNull(storageHelper.Serializer);
|
||||
Assert.IsInstanceOfType(storageHelper.Serializer, typeof(SystemSerializer));
|
||||
|
||||
tcs.SetResult(true);
|
||||
}
|
||||
|
@ -66,31 +65,23 @@ namespace UnitTests.UWP.Helpers
|
|||
{
|
||||
try
|
||||
{
|
||||
string userId = "TestUserId";
|
||||
string dataStoreId = "RoamingData";
|
||||
IObjectSerializer serializer = new SystemSerializer();
|
||||
string extensionId = "RoamingData";
|
||||
|
||||
IRoamingSettingsDataStore dataStore = new UserExtensionDataStore(userId, dataStoreId, serializer, false);
|
||||
string testKey = "foo";
|
||||
string testValue = "bar";
|
||||
|
||||
try
|
||||
{
|
||||
// Attempt to delete the remote first.
|
||||
await dataStore.Delete();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
var dataStore = await UserExtensionStorageHelper.CreateForCurrentUserAsync(extensionId);
|
||||
|
||||
dataStore.SyncCompleted += async (s, e) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// Create a second instance to ensure that the Cache doesn't yield a false positive.
|
||||
IRoamingSettingsDataStore dataStore2 = new OneDriveDataStore(userId, dataStoreId, serializer, false);
|
||||
var dataStore2 = await UserExtensionStorageHelper.CreateForCurrentUserAsync(extensionId);
|
||||
await dataStore2.Sync();
|
||||
|
||||
var foo = dataStore.Read<string>("foo");
|
||||
Assert.AreEqual("bar", foo);
|
||||
Assert.IsTrue(dataStore.TryRead(testKey, out string storedValue));
|
||||
Assert.AreEqual(testValue, storedValue);
|
||||
|
||||
tcs.SetResult(true);
|
||||
}
|
||||
|
@ -112,7 +103,8 @@ namespace UnitTests.UWP.Helpers
|
|||
}
|
||||
};
|
||||
|
||||
dataStore.Save("foo", "bar");
|
||||
dataStore.Clear();
|
||||
dataStore.Save(testKey, testValue);
|
||||
await dataStore.Sync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -123,8 +115,7 @@ namespace UnitTests.UWP.Helpers
|
|||
|
||||
PrepareProvider(test);
|
||||
|
||||
var result = await tcs.Task;
|
||||
Assert.IsTrue(result);
|
||||
await tcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -159,12 +159,15 @@
|
|||
<PackageReference Include="FluentAssertions">
|
||||
<Version>5.10.3</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Graph.Core">
|
||||
<Version>2.0.0</Version>
|
||||
<PackageReference Include="Microsoft.Graph">
|
||||
<Version>4.0.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform">
|
||||
<Version>6.2.12</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Toolkit">
|
||||
<Version>7.0.0-build.1396</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Toolkit.Uwp">
|
||||
<Version>7.0.1</Version>
|
||||
</PackageReference>
|
||||
|
@ -193,9 +196,9 @@
|
|||
<Project>{2E4A708A-DF53-4863-B797-E14CDC6B90FA}</Project>
|
||||
<Name>CommunityToolkit.Authentication.Uwp</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\CommunityToolkit.Graph.Uwp\CommunityToolkit.Graph.Uwp.csproj">
|
||||
<Project>{42252EE8-7E68-428F-972B-6D2DD3AA12CC}</Project>
|
||||
<Name>CommunityToolkit.Graph.Uwp</Name>
|
||||
<ProjectReference Include="..\..\CommunityToolkit.Graph\CommunityToolkit.Graph.csproj">
|
||||
<Project>{B2246169-0CD8-473C-AFF6-172310E2C3F6}</Project>
|
||||
<Name>CommunityToolkit.Graph</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Condition=" '$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' < '14.0' ">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
|
|
Загрузка…
Ссылка в новой задаче