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:
Shane Weaver 2021-08-02 15:44:35 -07:00 коммит произвёл GitHub
Родитель aa57a6f045
Коммит 6fdcc7dbe2
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
21 изменённых файлов: 615 добавлений и 1421 удалений

Просмотреть файл

@ -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,31 +59,25 @@ 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);
}
@ -100,11 +87,54 @@ namespace UnitTests.UWP.Helpers
}
};
dataStore.SyncFailed = (s, e) =>
PrepareProvider(test);
await tcs.Task;
}
[TestCategory("RoamingSettings")]
[TestMethod]
public async Task Test_FolderCRUD()
{
var tcs = new TaskCompletionSource<bool>();
async void test()
{
try
{
Assert.Fail("Sync Failed");
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)
{
@ -112,19 +142,9 @@ namespace UnitTests.UWP.Helpers
}
};
dataStore.Save("foo", "bar");
await dataStore.Sync();
}
catch (Exception ex)
{
tcs.SetException(ex);
}
}
PrepareProvider(test);
var result = await tcs.Task;
Assert.IsTrue(result);
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);
UserExtensionStorageHelper storageHelper = new UserExtensionStorageHelper(extensionId, 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(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)' &lt; '14.0' ">

Просмотреть файл

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />