diff --git a/CommunityToolkit.Uwp.Graph.Controls/CommunityToolkit.Uwp.Graph.Controls.csproj b/CommunityToolkit.Uwp.Graph.Controls/CommunityToolkit.Uwp.Graph.Controls.csproj
index ecff297..a4a3235 100644
--- a/CommunityToolkit.Uwp.Graph.Controls/CommunityToolkit.Uwp.Graph.Controls.csproj
+++ b/CommunityToolkit.Uwp.Graph.Controls/CommunityToolkit.Uwp.Graph.Controls.csproj
@@ -36,7 +36,7 @@
-
+
diff --git a/CommunityToolkit.Uwp.Graph.Controls/Helpers/RoamingSettings/BaseRoamingSettingsDataStore.cs b/CommunityToolkit.Uwp.Graph.Controls/Helpers/RoamingSettings/BaseRoamingSettingsDataStore.cs
new file mode 100644
index 0000000..3b034da
--- /dev/null
+++ b/CommunityToolkit.Uwp.Graph.Controls/Helpers/RoamingSettings/BaseRoamingSettingsDataStore.cs
@@ -0,0 +1,242 @@
+// 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.Uwp.Graph.Helpers.RoamingSettings
+{
+ ///
+ /// A base class for easily building roaming settings helper implementations.
+ ///
+ public abstract class BaseRoamingSettingsDataStore : IRoamingSettingsDataStore
+ {
+ ///
+ public EventHandler SyncCompleted { get; set; }
+
+ ///
+ public EventHandler SyncFailed { get; set; }
+
+ ///
+ public bool AutoSync { get; }
+
+ ///
+ public string Id { get; }
+
+ ///
+ public string UserId { get; }
+
+ ///
+ public IDictionary Cache { get; private set; }
+
+ ///
+ /// Gets an object serializer for converting objects in the data store.
+ ///
+ protected IObjectSerializer Serializer { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The id of the target Graph user.
+ /// A unique id for the data store.
+ /// An IObjectSerializer used for serializing objects.
+ /// Determines if the data store should sync for every interaction.
+ public BaseRoamingSettingsDataStore(string userId, string dataStoreId, IObjectSerializer objectSerializer, bool autoSync = true)
+ {
+ AutoSync = autoSync;
+ Id = dataStoreId;
+ UserId = userId;
+ Serializer = objectSerializer;
+
+ Cache = null;
+ }
+
+ ///
+ /// Create a new instance of the data storage container.
+ ///
+ /// A task.
+ public abstract Task Create();
+
+ ///
+ /// Delete the instance of the data storage container.
+ ///
+ /// A task.
+ public abstract Task Delete();
+
+ ///
+ public bool KeyExists(string key)
+ {
+ return Cache != null && Cache.ContainsKey(key);
+ }
+
+ ///
+ public bool KeyExists(string compositeKey, string key)
+ {
+ if (KeyExists(compositeKey))
+ {
+ ApplicationDataCompositeValue composite = (ApplicationDataCompositeValue)Cache[compositeKey];
+ if (composite != null)
+ {
+ return composite.ContainsKey(key);
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ public T Read(string key, T @default = default)
+ {
+ if (Cache != null && Cache.TryGetValue(key, out object value))
+ {
+ try
+ {
+ return Serializer.Deserialize((string)value);
+ }
+ catch
+ {
+ // Primitive types can't be deserialized.
+ return (T)Convert.ChangeType(value, typeof(T));
+ }
+ }
+
+ return @default;
+ }
+
+ ///
+ public T Read(string compositeKey, string key, T @default = default)
+ {
+ if (Cache != null)
+ {
+ ApplicationDataCompositeValue composite = (ApplicationDataCompositeValue)Cache[compositeKey];
+ if (composite != null)
+ {
+ object value = composite[key];
+ if (value != null)
+ {
+ try
+ {
+ return Serializer.Deserialize((string)value);
+ }
+ catch
+ {
+ // Primitive types can't be deserialized.
+ return (T)Convert.ChangeType(value, typeof(T));
+ }
+ }
+ }
+ }
+
+ return @default;
+ }
+
+ ///
+ public void Save(string key, T value)
+ {
+ InitCache();
+
+ // Skip serialization for primitives.
+ if (typeof(T) == typeof(object) || Type.GetTypeCode(typeof(T)) != TypeCode.Object)
+ {
+ // Update the cache
+ Cache[key] = value;
+ }
+ else
+ {
+ // Update the cache
+ Cache[key] = Serializer.Serialize(value);
+ }
+
+ if (AutoSync)
+ {
+ // Update the remote
+ Task.Run(() => Sync());
+ }
+ }
+
+ ///
+ public void Save(string compositeKey, IDictionary values)
+ {
+ InitCache();
+
+ if (KeyExists(compositeKey))
+ {
+ ApplicationDataCompositeValue composite = (ApplicationDataCompositeValue)Cache[compositeKey];
+
+ foreach (KeyValuePair setting in values.ToList())
+ {
+ if (composite.ContainsKey(setting.Key))
+ {
+ composite[setting.Key] = Serializer.Serialize(setting.Value);
+ }
+ else
+ {
+ composite.Add(setting.Key, Serializer.Serialize(setting.Value));
+ }
+ }
+
+ // Update the cache
+ Cache[compositeKey] = composite;
+
+ if (AutoSync)
+ {
+ // Update the remote
+ Task.Run(() => Sync());
+ }
+ }
+ else
+ {
+ ApplicationDataCompositeValue composite = new ApplicationDataCompositeValue();
+ foreach (KeyValuePair setting in values.ToList())
+ {
+ composite.Add(setting.Key, Serializer.Serialize(setting.Value));
+ }
+
+ // Update the cache
+ Cache[compositeKey] = composite;
+
+ if (AutoSync)
+ {
+ // Update the remote
+ Task.Run(() => Sync());
+ }
+ }
+ }
+
+ ///
+ public abstract Task FileExistsAsync(string filePath);
+
+ ///
+ public abstract Task ReadFileAsync(string filePath, T @default = default);
+
+ ///
+ public abstract Task SaveFileAsync(string filePath, T value);
+
+ ///
+ public abstract Task Sync();
+
+ ///
+ /// Initialize the internal cache.
+ ///
+ protected void InitCache()
+ {
+ if (Cache == null)
+ {
+ Cache = new Dictionary();
+ }
+ }
+
+ ///
+ /// Delete the internal cache.
+ ///
+ protected void DeleteCache()
+ {
+ Cache = null;
+ }
+ }
+}
diff --git a/CommunityToolkit.Uwp.Graph.Controls/Helpers/RoamingSettings/OneDriveDataSource.cs b/CommunityToolkit.Uwp.Graph.Controls/Helpers/RoamingSettings/OneDriveDataSource.cs
new file mode 100644
index 0000000..cbf3cf9
--- /dev/null
+++ b/CommunityToolkit.Uwp.Graph.Controls/Helpers/RoamingSettings/OneDriveDataSource.cs
@@ -0,0 +1,66 @@
+// 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.Net.Authentication;
+using CommunityToolkit.Net.Graph.Extensions;
+using Microsoft.Graph;
+
+namespace CommunityToolkit.Uwp.Graph.Helpers.RoamingSettings
+{
+ ///
+ /// Helpers for interacting with files in the special OneDrive AppRoot folder.
+ ///
+ internal static class OneDriveDataSource
+ {
+ private static GraphServiceClient Graph => ProviderManager.Instance.GlobalProvider?.Graph();
+
+ // Create a new file.
+ // This fails, because OneDrive doesn't like empty files. Use Update instead.
+ // public static async Task Create(string fileWithExt)
+ // {
+ // var driveItem = new DriveItem()
+ // {
+ // Name = fileWithExt,
+ // };
+ // await Graph.Users[userId].Drive.Special.AppRoot.ItemWithPath(fileWithExt).Request().CreateAsync(driveItem);
+ // }
+
+ ///
+ /// Updates or create a new file on the remote with the provided content.
+ ///
+ /// The type of object to save.
+ /// A representing the asynchronous operation.
+ public static async Task Update(string userId, string fileWithExt, T fileContents)
+ {
+ var json = Graph.HttpProvider.Serializer.SerializeObject(fileContents);
+ using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json));
+
+ return await Graph.Users[userId].Drive.Special.AppRoot.ItemWithPath(fileWithExt).Content.Request().PutAsync(stream);
+ }
+
+ ///
+ /// Get a file from the remote.
+ ///
+ /// The type of object to return.
+ /// A representing the asynchronous operation.
+ public static async Task Retrieve(string userId, string fileWithExt)
+ {
+ Stream stream = await Graph.Users[userId].Drive.Special.AppRoot.ItemWithPath(fileWithExt).Content.Request().GetAsync();
+
+ return Graph.HttpProvider.Serializer.DeserializeObject(stream);
+ }
+
+ ///
+ /// Delete the file from the remote.
+ ///
+ /// A representing the asynchronous operation.
+ public static async Task Delete(string userId, string fileWithExt)
+ {
+ await Graph.Users[userId].Drive.Special.AppRoot.ItemWithPath(fileWithExt).Request().DeleteAsync();
+ }
+ }
+}
diff --git a/CommunityToolkit.Uwp.Graph.Controls/Helpers/RoamingSettings/OneDriveDataStore.cs b/CommunityToolkit.Uwp.Graph.Controls/Helpers/RoamingSettings/OneDriveDataStore.cs
new file mode 100644
index 0000000..46c433e
--- /dev/null
+++ b/CommunityToolkit.Uwp.Graph.Controls/Helpers/RoamingSettings/OneDriveDataStore.cs
@@ -0,0 +1,159 @@
+// 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.Uwp.Graph.Helpers.RoamingSettings
+{
+ ///
+ /// A DataStore for managing roaming settings in OneDrive.
+ ///
+ public class OneDriveDataStore : BaseRoamingSettingsDataStore
+ {
+ ///
+ /// Retrieve an object stored in a OneDrive file.
+ ///
+ /// The type of object to retrieve.
+ /// The id of the target Graph user.
+ /// The name of the file.
+ /// The deserialized file contents.
+ public static async Task Get(string userId, string fileName)
+ {
+ return await OneDriveDataSource.Retrieve(userId, fileName);
+ }
+
+ ///
+ /// Update the contents of a OneDrive file.
+ ///
+ /// The type of object being stored.
+ /// The id of the target Graph user.
+ /// The name of the file.
+ /// The object to store.
+ /// A task.
+ public static async Task Set(string userId, string fileName, T fileContents)
+ {
+ await OneDriveDataSource.Update(userId, fileName, fileContents);
+ }
+
+ ///
+ /// Delete a file from OneDrive by name.
+ ///
+ /// The id of the target Graph user.
+ /// The name of the file.
+ /// A task.
+ public static async Task Delete(string userId, string fileName)
+ {
+ await OneDriveDataSource.Delete(userId, fileName);
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public OneDriveDataStore(string userId, string syncDataFileName, IObjectSerializer objectSerializer, bool autoSync = true)
+ : base(userId, syncDataFileName, objectSerializer, autoSync)
+ {
+ }
+
+ ///
+ public override Task Create()
+ {
+ InitCache();
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ public override async Task Delete()
+ {
+ // Clear the cache
+ DeleteCache();
+
+ // Delete the remote.
+ await Delete(UserId, Id);
+ }
+
+ ///
+ public override async Task FileExistsAsync(string filePath)
+ {
+ var roamingSettings = await Get
+
+ 4.0.0-preview.1
+
6.2.12
diff --git a/UnitTests/UnitTests.UWP/RoamingSettings/Test_OneDriveDataStore.cs b/UnitTests/UnitTests.UWP/RoamingSettings/Test_OneDriveDataStore.cs
new file mode 100644
index 0000000..0e46dc4
--- /dev/null
+++ b/UnitTests/UnitTests.UWP/RoamingSettings/Test_OneDriveDataStore.cs
@@ -0,0 +1,155 @@
+// 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.Net.Authentication;
+using CommunityToolkit.Uwp.Authentication;
+using CommunityToolkit.Uwp.Graph.Helpers.RoamingSettings;
+using Microsoft.Toolkit.Uwp;
+using Microsoft.Toolkit.Uwp.Helpers;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System;
+using System.Threading.Tasks;
+
+namespace UnitTests.UWP.Helpers
+{
+ [TestClass]
+ public class Test_OneDriveDataStore : VisualUITestBase
+ {
+ ///
+ /// Test the dafault state of a new instance of the OneDriveDataStore.
+ ///
+ [TestCategory("RoamingSettings")]
+ [TestMethod]
+ public async Task Test_Default()
+ {
+ var tcs = new TaskCompletionSource();
+
+ void test()
+ {
+ try
+ {
+ string userId = "TestUserId";
+ string dataStoreId = "RoamingData.json";
+ IObjectSerializer serializer = new SystemSerializer();
+
+ IRoamingSettingsDataStore dataStore = new OneDriveDataStore(userId, dataStoreId, serializer, false);
+
+ // Evaluate the default state is as expected
+ Assert.IsFalse(dataStore.AutoSync);
+ Assert.IsNull(dataStore.Cache);
+ Assert.AreEqual(dataStoreId, dataStore.Id);
+ Assert.AreEqual(userId, dataStore.UserId);
+
+ tcs.SetResult(true);
+ }
+ catch (Exception ex)
+ {
+ tcs.SetException(ex);
+ }
+ };
+
+ PrepareProvider(test);
+
+ await tcs.Task;
+ }
+
+ ///
+ /// Test the dafault state of a new instance of the OneDriveDataStore.
+ ///
+ [TestCategory("RoamingSettings")]
+ [TestMethod]
+ public async Task Test_Sync()
+ {
+ var tcs = new TaskCompletionSource();
+
+ async void test()
+ {
+ try
+ {
+ string userId = "TestUserId";
+ string dataStoreId = "RoamingData.json";
+ IObjectSerializer serializer = new SystemSerializer();
+
+ IRoamingSettingsDataStore dataStore = new OneDriveDataStore(userId, dataStoreId, serializer, false);
+
+ try
+ {
+ // Attempt to delete the remote first.
+ await dataStore.Delete();
+ }
+ catch
+ {
+ }
+
+ 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();
+
+ var foo = dataStore.Read("foo");
+ Assert.AreEqual("bar", foo);
+
+ 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();
+ }
+ catch (Exception ex)
+ {
+ tcs.SetException(ex);
+ }
+ }
+
+ PrepareProvider(test);
+
+ var result = await tcs.Task;
+ Assert.IsTrue(result);
+ }
+
+ ///
+ /// Create a new instance of IProvider and check that it has the proper default state, then execute the provided action.
+ ///
+ private async void PrepareProvider(Action test)
+ {
+ await App.DispatcherQueue.EnqueueAsync(async () =>
+ {
+ var provider = new WindowsProvider(new string[] { "User.Read", "Files.ReadWrite" }, autoSignIn: false);
+
+ ProviderManager.Instance.ProviderUpdated += (s, e) =>
+ {
+ var providerManager = s as ProviderManager;
+ if (providerManager.GlobalProvider.State == ProviderState.SignedIn)
+ {
+ test.Invoke();
+ }
+ };
+
+ ProviderManager.Instance.GlobalProvider = provider;
+
+ await provider.LoginAsync();
+ });
+ }
+ }
+}
diff --git a/UnitTests/UnitTests.UWP/RoamingSettings/Test_UserExtensionDataStore.cs b/UnitTests/UnitTests.UWP/RoamingSettings/Test_UserExtensionDataStore.cs
index 11116df..c4160cb 100644
--- a/UnitTests/UnitTests.UWP/RoamingSettings/Test_UserExtensionDataStore.cs
+++ b/UnitTests/UnitTests.UWP/RoamingSettings/Test_UserExtensionDataStore.cs
@@ -1,5 +1,11 @@
-using CommunityToolkit.Net.Authentication;
+// 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.Net.Authentication;
+using CommunityToolkit.Uwp.Authentication;
using CommunityToolkit.Uwp.Graph.Helpers.RoamingSettings;
+using Microsoft.Toolkit.Uwp;
using Microsoft.Toolkit.Uwp.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
@@ -15,31 +21,79 @@ public class Test_UserExtensionDataStore
///
[TestCategory("RoamingSettings")]
[TestMethod]
- public async Task Test_MockProvider_Default()
+ public async Task Test_Default()
{
var tcs = new TaskCompletionSource();
- Action test = async () =>
+ void test()
{
try
{
string userId = "TestUserId";
- string dataStoreId = "TestExtensionId";
+ string dataStoreId = "RoamingData";
IObjectSerializer serializer = new SystemSerializer();
- UserExtensionDataStore dataStore = new UserExtensionDataStore(userId, dataStoreId, serializer);
+ IRoamingSettingsDataStore dataStore = new UserExtensionDataStore(userId, dataStoreId, serializer, false);
// Evaluate the default state is as expected
- Assert.IsTrue(dataStore.AutoSync);
+ Assert.IsFalse(dataStore.AutoSync);
Assert.IsNull(dataStore.Cache);
Assert.AreEqual(dataStoreId, dataStore.Id);
Assert.AreEqual(userId, dataStore.UserId);
- dataStore.SyncCompleted += (s, e) =>
+ tcs.SetResult(true);
+ }
+ catch (Exception ex)
+ {
+ tcs.SetException(ex);
+ }
+ };
+
+ PrepareProvider(test);
+
+ await tcs.Task;
+ }
+
+ ///
+ /// Test the dafault state of a new instance of the UserExtensionDataStore.
+ ///
+ [TestCategory("RoamingSettings")]
+ [TestMethod]
+ public async Task Test_Sync()
+ {
+ var tcs = new TaskCompletionSource();
+
+ async void test()
+ {
+ try
+ {
+ string userId = "TestUserId";
+ string dataStoreId = "RoamingData";
+ IObjectSerializer serializer = new SystemSerializer();
+
+ IRoamingSettingsDataStore dataStore = new UserExtensionDataStore(userId, dataStoreId, serializer, false);
+
+ try
+ {
+ // Attempt to delete the remote first.
+ await dataStore.Delete();
+ }
+ catch
+ {
+ }
+
+ dataStore.SyncCompleted += async (s, e) =>
{
try
{
- Assert.Fail("Sync should have failed because we are using the MockProvider.");
+ // 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();
+
+ var foo = dataStore.Read("foo");
+ Assert.AreEqual("bar", foo);
+
+ tcs.SetResult(true);
}
catch (Exception ex)
{
@@ -51,8 +105,7 @@ public async Task Test_MockProvider_Default()
{
try
{
- Assert.IsNull((s as UserExtensionDataStore).Cache);
- tcs.SetResult(true);
+ Assert.Fail("Sync Failed");
}
catch (Exception ex)
{
@@ -60,33 +113,43 @@ public async Task Test_MockProvider_Default()
}
};
+ dataStore.Save("foo", "bar");
await dataStore.Sync();
}
catch (Exception ex)
{
tcs.SetException(ex);
}
- };
+ }
- PrepareMockProvider(test);
+ PrepareProvider(test);
- await tcs.Task;
+ var result = await tcs.Task;
+ Assert.IsTrue(result);
}
///
- /// Create a new instance of the MockProvider and check that it has the proper default state, then execute the provided action.
+ /// Create a new instance of IProvider and check that it has the proper default state, then execute the provided action.
///
- private void PrepareMockProvider(Action test)
+ private async void PrepareProvider(Action test)
{
- ProviderManager.Instance.ProviderUpdated += (s, e) =>
+ await App.DispatcherQueue.EnqueueAsync(async () =>
{
- var providerManager = s as ProviderManager;
- if (providerManager.GlobalProvider.State == ProviderState.SignedIn)
+ var provider = new WindowsProvider(new string[] { "User.ReadWrite" }, autoSignIn: false);
+
+ ProviderManager.Instance.ProviderUpdated += (s, e) =>
{
- test.Invoke();
- }
- };
- ProviderManager.Instance.GlobalProvider = new MockProvider();
+ var providerManager = s as ProviderManager;
+ if (providerManager.GlobalProvider.State == ProviderState.SignedIn)
+ {
+ test.Invoke();
+ }
+ };
+
+ ProviderManager.Instance.GlobalProvider = provider;
+
+ await provider.LoginAsync();
+ });
}
}
}
diff --git a/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj b/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj
index 6e55334..ade8dc8 100644
--- a/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj
+++ b/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj
@@ -123,6 +123,7 @@
+
UnitTestApp.xaml
@@ -177,7 +178,7 @@
2.1.2
- 13.0.1
+ 10.0.3