From 3fedf5c94d86524c0267788923e121b75f988ccf Mon Sep 17 00:00:00 2001
From: Lamparter <71598437+Lamparter@users.noreply.github.com>
Date: Tue, 7 Jan 2025 18:40:53 +0000
Subject: [PATCH 1/4] Port `StorageFileHelper`
---
components/Helpers/src/StorageFileHelper.cs | 747 ++++++++++++++++++++
1 file changed, 747 insertions(+)
create mode 100644 components/Helpers/src/StorageFileHelper.cs
diff --git a/components/Helpers/src/StorageFileHelper.cs b/components/Helpers/src/StorageFileHelper.cs
new file mode 100644
index 00000000..d40b9e41
--- /dev/null
+++ b/components/Helpers/src/StorageFileHelper.cs
@@ -0,0 +1,747 @@
+// 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.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Windows.ApplicationModel;
+using Windows.Storage;
+using Windows.Storage.Search;
+using Windows.Storage.Streams;
+
+namespace CommunityToolkit.WinUI.Helpers;
+
+///
+/// This class provides static helper methods for .
+///
+public static class StorageFileHelper
+{
+ ///
+ /// Saves a string value to a in application local folder/>.
+ ///
+ ///
+ /// The value to save to the file.
+ ///
+ ///
+ /// The name for the file.
+ ///
+ ///
+ /// The creation collision options. Default is ReplaceExisting.
+ ///
+ ///
+ /// The saved containing the text.
+ ///
+ ///
+ /// Exception thrown if the file location or file name are null or empty.
+ ///
+ public static Task WriteTextToLocalFileAsync(
+ string text,
+ string fileName,
+ CreationCollisionOption options = CreationCollisionOption.ReplaceExisting)
+ {
+ if (string.IsNullOrWhiteSpace(fileName))
+ {
+ throw new ArgumentNullException(nameof(fileName));
+ }
+
+ var folder = ApplicationData.Current.LocalFolder;
+ return folder.WriteTextToFileAsync(text, fileName, options);
+ }
+
+ ///
+ /// Saves a string value to a in application local cache folder/>.
+ ///
+ ///
+ /// The value to save to the file.
+ ///
+ ///
+ /// The name for the file.
+ ///
+ ///
+ /// The creation collision options. Default is ReplaceExisting.
+ ///
+ ///
+ /// The saved containing the text.
+ ///
+ ///
+ /// Exception thrown if the file location or file name are null or empty.
+ ///
+ public static Task WriteTextToLocalCacheFileAsync(
+ string text,
+ string fileName,
+ CreationCollisionOption options = CreationCollisionOption.ReplaceExisting)
+ {
+ if (string.IsNullOrWhiteSpace(fileName))
+ {
+ throw new ArgumentNullException(nameof(fileName));
+ }
+
+ var folder = ApplicationData.Current.LocalCacheFolder;
+ return folder.WriteTextToFileAsync(text, fileName, options);
+ }
+
+#if !HAS_UNO
+ ///
+ /// Saves a string value to a in well known folder/>.
+ ///
+ ///
+ /// The well known folder ID to use.
+ ///
+ ///
+ /// The value to save to the file.
+ ///
+ ///
+ /// The name for the file.
+ ///
+ ///
+ /// The creation collision options. Default is ReplaceExisting.
+ ///
+ ///
+ /// The saved containing the text.
+ ///
+ ///
+ /// Exception thrown if the file location or file name are null or empty.
+ ///
+ public static Task WriteTextToKnownFolderFileAsync(
+ KnownFolderId knownFolderId,
+ string text,
+ string fileName,
+ CreationCollisionOption options = CreationCollisionOption.ReplaceExisting)
+ {
+ if (string.IsNullOrWhiteSpace(fileName))
+ {
+ throw new ArgumentNullException(nameof(fileName));
+ }
+
+ var folder = GetFolderFromKnownFolderId(knownFolderId);
+ return folder.WriteTextToFileAsync(text, fileName, options);
+ }
+#endif
+
+ ///
+ /// Saves a string value to a in the given .
+ ///
+ ///
+ /// The to save the file in.
+ ///
+ ///
+ /// The value to save to the file.
+ ///
+ ///
+ /// The name for the file.
+ ///
+ ///
+ /// The creation collision options. Default is ReplaceExisting.
+ ///
+ ///
+ /// The saved containing the text.
+ ///
+ ///
+ /// Exception thrown if the file location or file name are null or empty.
+ ///
+ public static async Task WriteTextToFileAsync(
+ this StorageFolder fileLocation,
+ string text,
+ string fileName,
+ CreationCollisionOption options = CreationCollisionOption.ReplaceExisting)
+ {
+ if (fileLocation == null)
+ {
+ throw new ArgumentNullException(nameof(fileLocation));
+ }
+
+ if (string.IsNullOrWhiteSpace(fileName))
+ {
+ throw new ArgumentNullException(nameof(fileName));
+ }
+
+ var storageFile = await fileLocation.CreateFileAsync(fileName, options);
+ await FileIO.WriteTextAsync(storageFile, text);
+
+ return storageFile;
+ }
+
+ ///
+ /// Saves an array of bytes to a to application local folder/>.
+ ///
+ ///
+ /// The array to save to the file.
+ ///
+ ///
+ /// The name for the file.
+ ///
+ ///
+ /// The creation collision options. Default is ReplaceExisting.
+ ///
+ ///
+ /// The saved containing the bytes.
+ ///
+ ///
+ /// Exception thrown if the file location or file name are null or empty.
+ ///
+ public static Task WriteBytesToLocalFileAsync(
+ byte[] bytes,
+ string fileName,
+ CreationCollisionOption options = CreationCollisionOption.ReplaceExisting)
+ {
+ if (string.IsNullOrWhiteSpace(fileName))
+ {
+ throw new ArgumentNullException(nameof(fileName));
+ }
+
+ var folder = ApplicationData.Current.LocalFolder;
+ return folder.WriteBytesToFileAsync(bytes, fileName, options);
+ }
+
+ ///
+ /// Saves an array of bytes to a to application local cache folder/>.
+ ///
+ ///
+ /// The array to save to the file.
+ ///
+ ///
+ /// The name for the file.
+ ///
+ ///
+ /// The creation collision options. Default is ReplaceExisting.
+ ///
+ ///
+ /// The saved containing the bytes.
+ ///
+ ///
+ /// Exception thrown if the file location or file name are null or empty.
+ ///
+ public static Task WriteBytesToLocalCacheFileAsync(
+ byte[] bytes,
+ string fileName,
+ CreationCollisionOption options = CreationCollisionOption.ReplaceExisting)
+ {
+ if (string.IsNullOrWhiteSpace(fileName))
+ {
+ throw new ArgumentNullException(nameof(fileName));
+ }
+
+ var folder = ApplicationData.Current.LocalCacheFolder;
+ return folder.WriteBytesToFileAsync(bytes, fileName, options);
+ }
+
+#if !HAS_UNO
+ ///
+ /// Saves an array of bytes to a to well known folder/>.
+ ///
+ ///
+ /// The well known folder ID to use.
+ ///
+ ///
+ /// The array to save to the file.
+ ///
+ ///
+ /// The name for the file.
+ ///
+ ///
+ /// The creation collision options. Default is ReplaceExisting.
+ ///
+ ///
+ /// The saved containing the bytes.
+ ///
+ ///
+ /// Exception thrown if the file location or file name are null or empty.
+ ///
+ public static Task WriteBytesToKnownFolderFileAsync(
+ KnownFolderId knownFolderId,
+ byte[] bytes,
+ string fileName,
+ CreationCollisionOption options = CreationCollisionOption.ReplaceExisting)
+ {
+ if (string.IsNullOrWhiteSpace(fileName))
+ {
+ throw new ArgumentNullException(nameof(fileName));
+ }
+
+ var folder = GetFolderFromKnownFolderId(knownFolderId);
+ return folder.WriteBytesToFileAsync(bytes, fileName, options);
+ }
+#endif
+
+ ///
+ /// Saves an array of bytes to a in the given .
+ ///
+ ///
+ /// The to save the file in.
+ ///
+ ///
+ /// The array to save to the file.
+ ///
+ ///
+ /// The name for the file.
+ ///
+ ///
+ /// The creation collision options. Default is ReplaceExisting.
+ ///
+ ///
+ /// The saved containing the bytes.
+ ///
+ ///
+ /// Exception thrown if the file location or file name are null or empty.
+ ///
+ public static async Task WriteBytesToFileAsync(
+ this StorageFolder fileLocation,
+ byte[] bytes,
+ string fileName,
+ CreationCollisionOption options = CreationCollisionOption.ReplaceExisting)
+ {
+ if (fileLocation == null)
+ {
+ throw new ArgumentNullException(nameof(fileLocation));
+ }
+
+ if (string.IsNullOrWhiteSpace(fileName))
+ {
+ throw new ArgumentNullException(nameof(fileName));
+ }
+
+ var storageFile = await fileLocation.CreateFileAsync(fileName, options);
+ await FileIO.WriteBytesAsync(storageFile, bytes);
+
+ return storageFile;
+ }
+
+ ///
+ /// Gets a string value from a located in the application installation folder.
+ ///
+ ///
+ /// The relative file path.
+ ///
+ ///
+ /// The stored value.
+ ///
+ ///
+ /// Exception thrown if the is null or empty.
+ ///
+ public static Task ReadTextFromPackagedFileAsync(string fileName)
+ {
+ if (string.IsNullOrWhiteSpace(fileName))
+ {
+ throw new ArgumentNullException(nameof(fileName));
+ }
+
+ var folder = Package.Current.InstalledLocation;
+ return folder.ReadTextFromFileAsync(fileName);
+ }
+
+ ///
+ /// Gets a string value from a located in the application local cache folder.
+ ///
+ ///
+ /// The relative file path.
+ ///
+ ///
+ /// The stored value.
+ ///
+ ///
+ /// Exception thrown if the is null or empty.
+ ///
+ public static Task ReadTextFromLocalCacheFileAsync(string fileName)
+ {
+ if (string.IsNullOrWhiteSpace(fileName))
+ {
+ throw new ArgumentNullException(nameof(fileName));
+ }
+
+ var folder = ApplicationData.Current.LocalCacheFolder;
+ return folder.ReadTextFromFileAsync(fileName);
+ }
+
+ ///
+ /// Gets a string value from a located in the application local folder.
+ ///
+ ///
+ /// The relative file path.
+ ///
+ ///
+ /// The stored value.
+ ///
+ ///
+ /// Exception thrown if the is null or empty.
+ ///
+ public static Task ReadTextFromLocalFileAsync(string fileName)
+ {
+ if (string.IsNullOrWhiteSpace(fileName))
+ {
+ throw new ArgumentNullException(nameof(fileName));
+ }
+
+ var folder = ApplicationData.Current.LocalFolder;
+ return folder.ReadTextFromFileAsync(fileName);
+ }
+
+#if !HAS_UNO
+ ///
+ /// Gets a string value from a located in a well known folder.
+ ///
+ ///
+ /// The well known folder ID to use.
+ ///
+ ///
+ /// The relative file path.
+ ///
+ ///
+ /// The stored value.
+ ///
+ ///
+ /// Exception thrown if the is null or empty.
+ ///
+ public static Task ReadTextFromKnownFoldersFileAsync(
+ KnownFolderId knownFolderId,
+ string fileName)
+ {
+ if (string.IsNullOrWhiteSpace(fileName))
+ {
+ throw new ArgumentNullException(nameof(fileName));
+ }
+
+ var folder = GetFolderFromKnownFolderId(knownFolderId);
+ return folder.ReadTextFromFileAsync(fileName);
+ }
+#endif
+
+ ///
+ /// Gets a string value from a located in the given .
+ ///
+ ///
+ /// The to save the file in.
+ ///
+ ///
+ /// The relative file path.
+ ///
+ ///
+ /// The stored value.
+ ///
+ ///
+ /// Exception thrown if the is null or empty.
+ ///
+ public static async Task ReadTextFromFileAsync(
+ this StorageFolder fileLocation,
+ string fileName)
+ {
+ if (string.IsNullOrWhiteSpace(fileName))
+ {
+ throw new ArgumentNullException(nameof(fileName));
+ }
+
+ var file = await fileLocation.GetFileAsync(fileName);
+ return await FileIO.ReadTextAsync(file);
+ }
+
+#if !HAS_UNO
+ ///
+ /// Gets an array of bytes from a located in the application installation folder.
+ ///
+ ///
+ /// The relative file path.
+ ///
+ ///
+ /// The stored array.
+ ///
+ ///
+ /// Exception thrown if the is null or empty.
+ ///
+ public static Task ReadBytesFromPackagedFileAsync(string fileName)
+ {
+ if (string.IsNullOrWhiteSpace(fileName))
+ {
+ throw new ArgumentNullException(nameof(fileName));
+ }
+
+ var folder = Package.Current.InstalledLocation;
+ return folder.ReadBytesFromFileAsync(fileName);
+ }
+
+ ///
+ /// Gets an array of bytes from a located in the application local cache folder.
+ ///
+ ///
+ /// The relative file path.
+ ///
+ ///
+ /// The stored array.
+ ///
+ ///
+ /// Exception thrown if the is null or empty.
+ ///
+ public static Task ReadBytesFromLocalCacheFileAsync(string fileName)
+ {
+ if (string.IsNullOrWhiteSpace(fileName))
+ {
+ throw new ArgumentNullException(nameof(fileName));
+ }
+
+ var folder = ApplicationData.Current.LocalCacheFolder;
+ return folder.ReadBytesFromFileAsync(fileName);
+ }
+
+ ///
+ /// Gets an array of bytes from a located in the application local folder.
+ ///
+ ///
+ /// The relative file path.
+ ///
+ ///
+ /// The stored array.
+ ///
+ ///
+ /// Exception thrown if the is null or empty.
+ ///
+ public static Task ReadBytesFromLocalFileAsync(string fileName)
+ {
+ if (string.IsNullOrWhiteSpace(fileName))
+ {
+ throw new ArgumentNullException(nameof(fileName));
+ }
+
+ var folder = ApplicationData.Current.LocalFolder;
+ return folder.ReadBytesFromFileAsync(fileName);
+ }
+
+ ///
+ /// Gets an array of bytes from a located in a well known folder.
+ ///
+ ///
+ /// The well known folder ID to use.
+ ///
+ ///
+ /// The relative file path.
+ ///
+ ///
+ /// The stored array.
+ ///
+ ///
+ /// Exception thrown if the is null or empty.
+ ///
+ public static Task ReadBytesFromKnownFoldersFileAsync(
+ KnownFolderId knownFolderId,
+ string fileName)
+ {
+ if (string.IsNullOrWhiteSpace(fileName))
+ {
+ throw new ArgumentNullException(nameof(fileName));
+ }
+
+ var folder = GetFolderFromKnownFolderId(knownFolderId);
+ return folder.ReadBytesFromFileAsync(fileName);
+ }
+
+ ///
+ /// Gets an array of bytes from a located in the given .
+ ///
+ ///
+ /// The to save the file in.
+ ///
+ ///
+ /// The relative file path.
+ ///
+ ///
+ /// The stored array.
+ ///
+ ///
+ /// Exception thrown if the is null or empty.
+ ///
+ public static async Task ReadBytesFromFileAsync(
+ this StorageFolder fileLocation,
+ string fileName)
+ {
+ if (string.IsNullOrWhiteSpace(fileName))
+ {
+ throw new ArgumentNullException(nameof(fileName));
+ }
+
+ var file = await fileLocation.GetFileAsync(fileName).AsTask().ConfigureAwait(false);
+ return await file.ReadBytesAsync();
+ }
+
+ ///
+ /// Gets an array of bytes from a .
+ ///
+ ///
+ /// The .
+ ///
+ ///
+ /// The stored array.
+ ///
+ public static async Task ReadBytesAsync(this StorageFile file)
+ {
+ if (file == null)
+ {
+ throw new ArgumentNullException(nameof(file));
+ }
+
+ using (IRandomAccessStream stream = await file.OpenReadAsync())
+ {
+ using (var reader = new DataReader(stream.GetInputStreamAt(0)))
+ {
+ await reader.LoadAsync((uint)stream.Size);
+ var bytes = new byte[stream.Size];
+ reader.ReadBytes(bytes);
+ return bytes;
+ }
+ }
+ }
+#endif
+
+ ///
+ /// Gets a value indicating whether a file exists in the current folder.
+ ///
+ ///
+ /// The to look for the file in.
+ ///
+ ///
+ /// The filename of the file to search for. Must include the file extension and is not case-sensitive.
+ ///
+ ///
+ /// The , indicating if the subfolders should also be searched through.
+ ///
+ ///
+ /// true if the file exists; otherwise, false.
+ ///
+ public static Task FileExistsAsync(this StorageFolder folder, string fileName, bool isRecursive = false)
+#if HAS_UNO
+ => FileExistsInFolderAsync(folder, fileName);
+#else
+ => isRecursive
+ ? FileExistsInSubtreeAsync(folder, fileName)
+ : FileExistsInFolderAsync(folder, fileName);
+#endif
+
+ ///
+ /// Gets a value indicating whether a filename is correct or not using the Storage feature.
+ ///
+ /// The filename to test. Must include the file extension and is not case-sensitive.
+ /// true if the filename is valid; otherwise, false.
+ public static bool IsFileNameValid(string fileName)
+ {
+ var illegalChars = Path.GetInvalidFileNameChars();
+ return fileName.All(c => !illegalChars.Contains(c));
+ }
+
+ ///
+ /// Gets a value indicating whether a file path is correct or not using the Storage feature.
+ ///
+ /// The file path to test. Must include the file extension and is not case-sensitive.
+ /// true if the file path is valid; otherwise, false.
+ public static bool IsFilePathValid(string filePath)
+ {
+ var illegalChars = Path.GetInvalidPathChars();
+ return filePath.All(c => !illegalChars.Contains(c));
+ }
+
+ ///
+ /// Gets a value indicating whether a file exists in the current folder.
+ ///
+ ///
+ /// The to look for the file in.
+ ///
+ ///
+ /// The filename of the file to search for. Must include the file extension and is not case-sensitive.
+ ///
+ ///
+ /// true if the file exists; otherwise, false.
+ ///
+ internal static async Task FileExistsInFolderAsync(StorageFolder folder, string fileName)
+ {
+ var item = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false);
+ return item != null && item.IsOfType(StorageItemTypes.File);
+ }
+
+#if !HAS_UNO
+ ///
+ /// Gets a value indicating whether a file exists in the current folder or in one of its subfolders.
+ ///
+ ///
+ /// The to look for the file in.
+ ///
+ ///
+ /// The filename of the file to search for. Must include the file extension and is not case-sensitive.
+ ///
+ ///
+ /// true if the file exists; otherwise, false.
+ ///
+ ///
+ /// Exception thrown if the contains a quotation mark.
+ ///
+ internal static async Task FileExistsInSubtreeAsync(StorageFolder rootFolder, string fileName)
+ {
+ if (fileName.IndexOf('"') >= 0)
+ {
+ throw new ArgumentException(nameof(fileName));
+ }
+
+ var options = new QueryOptions
+ {
+ FolderDepth = FolderDepth.Deep,
+ UserSearchFilter = $"filename:=\"{fileName}\"" // “:=” is the exact-match operator
+ };
+
+ var files = await rootFolder.CreateFileQueryWithOptions(options).GetFilesAsync().AsTask().ConfigureAwait(false);
+ return files.Count > 0;
+ }
+
+ ///
+ /// Returns a from a
+ ///
+ /// Folder Id
+ /// The
+ internal static StorageFolder GetFolderFromKnownFolderId(KnownFolderId knownFolderId)
+ {
+ StorageFolder workingFolder;
+
+ switch (knownFolderId)
+ {
+ case KnownFolderId.AppCaptures:
+ workingFolder = KnownFolders.AppCaptures;
+ break;
+ case KnownFolderId.CameraRoll:
+ workingFolder = KnownFolders.CameraRoll;
+ break;
+ case KnownFolderId.DocumentsLibrary:
+ workingFolder = KnownFolders.DocumentsLibrary;
+ break;
+ case KnownFolderId.HomeGroup:
+ workingFolder = KnownFolders.HomeGroup;
+ break;
+ case KnownFolderId.MediaServerDevices:
+ workingFolder = KnownFolders.MediaServerDevices;
+ break;
+ case KnownFolderId.MusicLibrary:
+ workingFolder = KnownFolders.MusicLibrary;
+ break;
+ case KnownFolderId.Objects3D:
+ workingFolder = KnownFolders.Objects3D;
+ break;
+ case KnownFolderId.PicturesLibrary:
+ workingFolder = KnownFolders.PicturesLibrary;
+ break;
+ case KnownFolderId.Playlists:
+ workingFolder = KnownFolders.Playlists;
+ break;
+ case KnownFolderId.RecordedCalls:
+ workingFolder = KnownFolders.RecordedCalls;
+ break;
+ case KnownFolderId.RemovableDevices:
+ workingFolder = KnownFolders.RemovableDevices;
+ break;
+ case KnownFolderId.SavedPictures:
+ workingFolder = KnownFolders.SavedPictures;
+ break;
+ case KnownFolderId.VideosLibrary:
+ workingFolder = KnownFolders.VideosLibrary;
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(knownFolderId), knownFolderId, null);
+ }
+
+ return workingFolder;
+ }
+#endif
+}
From 546c6ed6934e0d0d7e30d2080d8a7d8173dda92e Mon Sep 17 00:00:00 2001
From: Lamparter <71598437+Lamparter@users.noreply.github.com>
Date: Tue, 7 Jan 2025 18:45:36 +0000
Subject: [PATCH 2/4] Port `ApplicationDataStorageHelper`
very difficult
---
.../ApplicationDataStorageHelper.cs | 344 ++++++++++++++++++
1 file changed, 344 insertions(+)
create mode 100644 components/Helpers/src/ObjectStorage/ApplicationDataStorageHelper.cs
diff --git a/components/Helpers/src/ObjectStorage/ApplicationDataStorageHelper.cs b/components/Helpers/src/ObjectStorage/ApplicationDataStorageHelper.cs
new file mode 100644
index 00000000..2912716d
--- /dev/null
+++ b/components/Helpers/src/ObjectStorage/ApplicationDataStorageHelper.cs
@@ -0,0 +1,344 @@
+// 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.Linq;
+using System.Threading.Tasks;
+using CommunityToolkit.Common.Helpers;
+using Windows.Storage;
+using Windows.System;
+using CommunityToolkit.Helpers;
+
+namespace CommunityToolkit.WinUI.Helpers.ObjectStorage;
+
+///
+/// Storage helper for files and folders living in Windows.Storage.ApplicationData storage endpoints.
+///
+/// The data store to interact with.
+/// Serializer for converting stored values. Defaults to .
+public partial class ApplicationDataStorageHelper(ApplicationData appData, IObjectSerializer? objectSerializer = null) : IFileStorageHelper, ISettingsStorageHelper
+{
+ ///
+ /// Gets the settings container.
+ ///
+ public ApplicationDataContainer Settings => AppData.LocalSettings;
+
+ ///
+ /// Gets the storage folder.
+ ///
+ public StorageFolder Folder => AppData.LocalFolder;
+
+ ///
+ /// Gets the storage host.
+ ///
+ protected ApplicationData AppData { get; } = appData ?? throw new ArgumentNullException(nameof(appData));
+
+ ///
+ /// Gets the serializer for converting stored values.
+ ///
+ protected IObjectSerializer Serializer { get; } = objectSerializer ?? new SystemSerializer();
+
+ ///
+ /// Get a new instance using ApplicationData.Current and the provided serializer.
+ ///
+ /// Serializer for converting stored values. Defaults to .
+ /// A new instance of ApplicationDataStorageHelper.
+ public static ApplicationDataStorageHelper GetCurrent(IObjectSerializer? objectSerializer = null)
+ {
+ var appData = ApplicationData.Current;
+ return new ApplicationDataStorageHelper(appData, objectSerializer);
+ }
+
+#if !HAS_UNO
+ ///
+ /// Get a new instance using the ApplicationData for the provided user and serializer.
+ ///
+ /// App data user owner.
+ /// Serializer for converting stored values. Defaults to .
+ /// A new instance of ApplicationDataStorageHelper.
+ public static async Task GetForUserAsync(User user, IObjectSerializer? objectSerializer = null)
+ {
+ var appData = await ApplicationData.GetForUserAsync(user);
+ return new ApplicationDataStorageHelper(appData, objectSerializer);
+ }
+#endif
+
+ ///
+ /// Determines whether a setting already exists.
+ ///
+ /// Key of the setting (that contains object).
+ /// True if a value exists.
+ public bool KeyExists(string key)
+ {
+ return Settings.Values.ContainsKey(key);
+ }
+
+ ///
+ /// Retrieves a single item by its key.
+ ///
+ /// Type of object retrieved.
+ /// Key of the object.
+ /// Default value of the object.
+ /// The TValue object.
+ public T? Read(string key, T? @default = default)
+ {
+ if (Settings.Values.TryGetValue(key, out var valueObj) && valueObj is string valueString)
+ {
+ return Serializer.Deserialize(valueString);
+ }
+
+ return @default;
+ }
+
+ ///
+ public bool TryRead(string key, out T? value)
+ {
+ if (Settings.Values.TryGetValue(key, out var valueObj) && valueObj is string valueString)
+ {
+ value = Serializer.Deserialize(valueString);
+ return true;
+ }
+
+ value = default;
+ return false;
+ }
+
+ ///
+ public void Save(string key, T value)
+ {
+ Settings.Values[key] = Serializer.Serialize(value);
+ }
+
+ ///
+ public bool TryDelete(string key)
+ {
+ return Settings.Values.Remove(key);
+ }
+
+ ///
+ public void Clear()
+ {
+ Settings.Values.Clear();
+ }
+
+ ///
+ /// Determines whether a setting already exists in composite.
+ ///
+ /// Key of the composite (that contains settings).
+ /// Key of the setting (that contains object).
+ /// True if a value exists.
+ public bool KeyExists(string compositeKey, string key)
+ {
+ if (TryRead(compositeKey, out ApplicationDataCompositeValue? composite) && composite != null)
+ {
+ return composite.ContainsKey(key);
+ }
+
+ return false;
+ }
+
+ ///
+ /// Attempts to retrieve a single item by its key in composite.
+ ///
+ /// Type of object retrieved.
+ /// Key of the composite (that contains settings).
+ /// Key of the object.
+ /// The value of the object retrieved.
+ /// The T object.
+ public bool TryRead(string compositeKey, string key, out T? value)
+ {
+ if (TryRead(compositeKey, out ApplicationDataCompositeValue? composite) && composite != null)
+ {
+ string compositeValue = (string)composite[key];
+ if (compositeValue != null)
+ {
+ value = Serializer.Deserialize(compositeValue);
+ return true;
+ }
+ }
+
+ value = default;
+ return false;
+ }
+
+ ///
+ /// Retrieves a single item by its key in composite.
+ ///
+ /// Type of object retrieved.
+ /// Key of the composite (that contains settings).
+ /// Key of the object.
+ /// Default value of the object.
+ /// The T object.
+ public T? Read(string compositeKey, string key, T? @default = default)
+ {
+ if (TryRead(compositeKey, out ApplicationDataCompositeValue? composite) && composite != null)
+ {
+ if (composite.TryGetValue(key, out var valueObj) && valueObj is string value)
+ {
+ return Serializer.Deserialize(value);
+ }
+ }
+
+ return @default;
+ }
+
+ ///
+ /// 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
+ /// and for groups of settings which need to be treated in an atomic way.
+ ///
+ /// Type of object saved.
+ /// Key of the composite (that contains settings).
+ /// Objects to save.
+ public void Save(string compositeKey, IDictionary values)
+ {
+ if (TryRead(compositeKey, out ApplicationDataCompositeValue? composite) && composite != null)
+ {
+ foreach (KeyValuePair setting in values)
+ {
+ var serializedValue = Serializer.Serialize(setting.Value) ?? string.Empty;
+ if (composite.ContainsKey(setting.Key))
+ {
+ composite[setting.Key] = serializedValue;
+ }
+ else
+ {
+ composite.Add(setting.Key, serializedValue);
+ }
+ }
+ }
+ else
+ {
+ composite = [];
+ foreach (KeyValuePair setting in values)
+ {
+ var serializedValue = Serializer.Serialize(setting.Value) ?? string.Empty;
+ composite.Add(setting.Key, serializedValue);
+ }
+
+ Settings.Values[compositeKey] = composite;
+ }
+ }
+
+ ///
+ /// Deletes a single item by its key in composite.
+ ///
+ /// Key of the composite (that contains settings).
+ /// Key of the object.
+ /// A boolean indicator of success.
+ public bool TryDelete(string compositeKey, string key)
+ {
+ if (TryRead(compositeKey, out ApplicationDataCompositeValue? composite) && composite != null)
+ {
+ return composite.Remove(key);
+ }
+
+ return false;
+ }
+
+ ///
+ public Task ReadFileAsync(string filePath, T? @default = default)
+ {
+ return ReadFileAsync(Folder, filePath, @default);
+ }
+
+ ///
+ public Task> ReadFolderAsync(string folderPath)
+ {
+ return ReadFolderAsync(Folder, folderPath);
+ }
+
+ ///
+ public Task CreateFileAsync(string filePath, T value)
+ {
+ return CreateFileAsync(Folder, filePath, value);
+ }
+
+ ///
+ public Task CreateFolderAsync(string folderPath)
+ {
+ return CreateFolderAsync(Folder, folderPath);
+ }
+
+ ///
+ public Task TryDeleteItemAsync(string itemPath)
+ {
+ return TryDeleteItemAsync(Folder, itemPath);
+ }
+
+ ///
+ public Task TryRenameItemAsync(string itemPath, string newName)
+ {
+ return TryRenameItemAsync(Folder, itemPath, newName);
+ }
+
+ private async Task ReadFileAsync(StorageFolder folder, string filePath, T? @default = default)
+ {
+ string value = await StorageFileHelper.ReadTextFromFileAsync(folder, NormalizePath(filePath));
+ return (value != null) ? Serializer.Deserialize(value) : @default;
+ }
+
+ private async Task> ReadFolderAsync(StorageFolder folder, string folderPath)
+ {
+ var targetFolder = await folder.GetFolderAsync(NormalizePath(folderPath));
+ var items = await targetFolder.GetItemsAsync();
+
+ return items.Select((item) =>
+ {
+ var itemType = item.IsOfType(StorageItemTypes.File) ? DirectoryItemType.File
+ : item.IsOfType(StorageItemTypes.Folder) ? DirectoryItemType.Folder
+ : DirectoryItemType.None;
+
+ return (itemType, item.Name);
+ });
+ }
+
+ private async Task CreateFileAsync(StorageFolder folder, string filePath, T value)
+ {
+ var serializedValue = Serializer.Serialize(value)?.ToString() ?? string.Empty;
+ return await StorageFileHelper.WriteTextToFileAsync(folder, serializedValue, NormalizePath(filePath), CreationCollisionOption.ReplaceExisting);
+ }
+
+ private async Task CreateFolderAsync(StorageFolder folder, string folderPath)
+ {
+ await folder.CreateFolderAsync(NormalizePath(folderPath), CreationCollisionOption.OpenIfExists);
+ }
+
+ private async Task TryDeleteItemAsync(StorageFolder folder, string itemPath)
+ {
+ try
+ {
+ var item = await folder.GetItemAsync(NormalizePath(itemPath));
+ await item.DeleteAsync();
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private async Task TryRenameItemAsync(StorageFolder folder, string itemPath, string newName)
+ {
+ try
+ {
+ var item = await folder.GetItemAsync(NormalizePath(itemPath));
+ await item.RenameAsync(newName, NameCollisionOption.FailIfExists);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private string NormalizePath(string path)
+ {
+ var directoryName = Path.GetDirectoryName(path) ?? string.Empty;
+ var fileName = Path.GetFileName(path);
+ return Path.Combine(directoryName, fileName);
+ }
+}
From edb94116895d1deaff31b17cb99c3dba5cb98ee8 Mon Sep 17 00:00:00 2001
From: Lamparter <71598437+Lamparter@users.noreply.github.com>
Date: Tue, 7 Jan 2025 18:50:47 +0000
Subject: [PATCH 3/4] Port `OSVersion`
---
components/Helpers/src/OSVersion.cs | 38 +++++++++++++++++++++++++++++
1 file changed, 38 insertions(+)
create mode 100644 components/Helpers/src/OSVersion.cs
diff --git a/components/Helpers/src/OSVersion.cs b/components/Helpers/src/OSVersion.cs
new file mode 100644
index 00000000..26a04627
--- /dev/null
+++ b/components/Helpers/src/OSVersion.cs
@@ -0,0 +1,38 @@
+// 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.
+
+namespace CommunityToolkit.WinUI.Helpers;
+
+///
+/// Defines Operating System version
+///
+public struct OSVersion
+{
+ ///
+ /// Value describing major version
+ ///
+ public ushort Major;
+
+ ///
+ /// Value describing minor version
+ ///
+ public ushort Minor;
+
+ ///
+ /// Value describing build
+ ///
+ public ushort Build;
+
+ ///
+ /// Value describing revision
+ ///
+ public ushort Revision;
+
+ ///
+ /// Converts OSVersion to string
+ ///
+ /// Major.Minor.Build.Revision as a string
+ public override readonly string ToString()
+ => $"{Major}.{Minor}.{Build}.{Revision}";
+}
From 4778a9d6d6ea9a6fc4ffe1ccf53f60adb2ffe389 Mon Sep 17 00:00:00 2001
From: Lamparter <71598437+Lamparter@users.noreply.github.com>
Date: Tue, 7 Jan 2025 18:51:07 +0000
Subject: [PATCH 4/4] Port `SystemInformation`
---
components/Helpers/src/SystemInformation.cs | 435 ++++++++++++++++++++
1 file changed, 435 insertions(+)
create mode 100644 components/Helpers/src/SystemInformation.cs
diff --git a/components/Helpers/src/SystemInformation.cs b/components/Helpers/src/SystemInformation.cs
new file mode 100644
index 00000000..56891ad9
--- /dev/null
+++ b/components/Helpers/src/SystemInformation.cs
@@ -0,0 +1,435 @@
+// 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.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+using CommunityToolkit.Common.Helpers;
+using Microsoft.UI.Xaml;
+using Windows.ApplicationModel;
+using Windows.ApplicationModel.Activation;
+using Windows.Security.ExchangeActiveSyncProvisioning;
+using Windows.System;
+using Windows.System.Profile;
+using Windows.System.UserProfile;
+using Windows.UI.Core;
+using CommunityToolkit.WinUI.Helpers;
+using CommunityToolkit.Common;
+using CommunityToolkit.WinUI.Helpers.ObjectStorage;
+
+namespace CommunityToolkit.WinUI.Helpers;
+
+///
+/// This class provides info about the app and the system.
+///
+public sealed class SystemInformation
+{
+ ///
+ /// The instance used to save and retrieve application settings.
+ ///
+ private readonly ApplicationDataStorageHelper _settingsStorage = ApplicationDataStorageHelper.GetCurrent();
+
+ ///
+ /// The starting time of the current application session (since app launch or last move to foreground).
+ ///
+ private DateTime _sessionStart;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ private SystemInformation()
+ {
+ ApplicationName = Package.Current.DisplayName;
+ ApplicationVersion = Package.Current.Id.Version;
+
+#if !HAS_UNO
+ try
+ {
+ Culture = GlobalizationPreferences.Languages.Count > 0 ? new CultureInfo(GlobalizationPreferences.Languages[0]) : null;
+ }
+ catch
+ {
+ Culture = null;
+ }
+#endif
+
+ DeviceFamily = AnalyticsInfo.VersionInfo.DeviceFamily;
+
+#if !HAS_UNO
+ ulong version = ulong.Parse(AnalyticsInfo.VersionInfo.DeviceFamilyVersion);
+
+ OperatingSystemVersion = new OSVersion
+ {
+ Major = (ushort)((version & 0xFFFF000000000000L) >> 48),
+ Minor = (ushort)((version & 0x0000FFFF00000000L) >> 32),
+ Build = (ushort)((version & 0x00000000FFFF0000L) >> 16),
+ Revision = (ushort)(version & 0x000000000000FFFFL)
+ };
+
+ OperatingSystemArchitecture = Package.Current.Id.Architecture;
+#endif
+
+ EasClientDeviceInformation deviceInfo = new();
+
+ OperatingSystem = deviceInfo.OperatingSystem;
+ DeviceManufacturer = deviceInfo.SystemManufacturer;
+ DeviceModel = deviceInfo.SystemProductName;
+ IsFirstRun = DetectIfFirstUse();
+ (IsAppUpdated, PreviousVersionInstalled) = DetectIfAppUpdated();
+ FirstUseTime = DetectFirstUseTime();
+ FirstVersionInstalled = DetectFirstVersionInstalled();
+
+ InitializeValuesSetWithTrackAppUse();
+ }
+
+ ///
+ /// Gets the unique instance of .
+ ///
+ public static SystemInformation Instance { get; } = new();
+
+ ///
+ /// Gets the application's name.
+ ///
+ public string ApplicationName { get; }
+
+ ///
+ /// Gets the application's version.
+ ///
+ public PackageVersion ApplicationVersion { get; }
+
+#if !HAS_UNO
+ ///
+ /// Gets the user's most preferred culture.
+ ///
+ public CultureInfo? Culture { get; }
+#endif
+
+ ///
+ /// Gets the device's family.
+ ///
+ /// Common values include:
+ ///
+ /// - "Windows.Desktop"
+ /// - "Windows.Mobile"
+ /// - "Windows.Xbox"
+ /// - "Windows.Holographic"
+ /// - "Windows.Team"
+ /// - "Windows.IoT"
+ ///
+ ///
+ /// Prepare your code for other values.
+ ///
+ public string DeviceFamily { get; }
+
+ ///
+ /// Gets the operating system's name.
+ ///
+ public string OperatingSystem { get; }
+
+#if !HAS_UNO
+ ///
+ /// Gets the operating system's version.
+ ///
+ public OSVersion OperatingSystemVersion { get; }
+
+ ///
+ /// Gets the processor architecture.
+ ///
+ public Windows.System.ProcessorArchitecture OperatingSystemArchitecture { get; }
+#endif
+
+ ///
+ /// Gets the available memory.
+ ///
+ public static float AvailableMemory => (float)MemoryManager.AppMemoryUsageLimit / 1024 / 1024;
+
+ ///
+ /// Gets the device's model.
+ /// Will be empty if the model couldn't be determined (For example: when running in a virtual machine).
+ ///
+ public string DeviceModel { get; }
+
+ ///
+ /// Gets the device's manufacturer.
+ /// Will be empty if the manufacturer couldn't be determined (For example: when running in a virtual machine).
+ ///
+ public string DeviceManufacturer { get; }
+
+ ///
+ /// Gets a value indicating whether the app is being used for the first time since it was installed.
+ /// Use this to tell if you should do or display something different for the app's first use.
+ ///
+ public bool IsFirstRun { get; }
+
+ ///
+ /// Gets a value indicating whether the app is being used for the first time since being upgraded from an older version.
+ /// Use this to tell if you should display details about what has changed.
+ ///
+ public bool IsAppUpdated { get; }
+
+ ///
+ /// Gets the first version of the app that was installed.
+ /// This will be the current version if a previous version of the app was installed before accessing this property.
+ ///
+ public PackageVersion FirstVersionInstalled { get; }
+
+ ///
+ /// Gets the previous version of the app that was installed.
+ /// This will be the current version if a previous version of the app was installed
+ /// before using or if the app is not updated.
+ ///
+ public PackageVersion PreviousVersionInstalled { get; }
+
+ ///
+ /// Gets the DateTime (in UTC) when the app was launched for the first time.
+ ///
+ public DateTime FirstUseTime { get; }
+
+ ///
+ /// Gets the DateTime (in UTC) when the app was last launched, not including this instance.
+ /// Will be if has not been called yet.
+ ///
+ public DateTime LastLaunchTime { get; private set; }
+
+ ///
+ /// Gets the number of times the app has been launched.
+ /// Will be 0 if has not been called yet.
+ ///
+ public long LaunchCount { get; private set; }
+
+ ///
+ /// Gets the number of times the app has been launched.
+ /// Will be 0 if has not been called yet.
+ ///
+ public long TotalLaunchCount { get; private set; }
+
+ ///
+ /// Gets the DateTime (in UTC) that this instance of the app was launched.
+ /// Will be if has not been called yet.
+ ///
+ public DateTime LaunchTime { get; private set; }
+
+ ///
+ /// Gets the DateTime (in UTC) when the launch count was last reset.
+ /// Will be if has not been called yet.
+ ///
+ public DateTime LastResetTime { get; private set; }
+
+ ///
+ /// Gets the length of time this instance of the app has been running.
+ /// Will be if has not been called yet.
+ ///
+ public TimeSpan AppUptime
+ {
+ get
+ {
+ if (LaunchCount > 0)
+ {
+ var subSessionLength = DateTime.UtcNow.Subtract(_sessionStart).Ticks;
+ var uptimeSoFar = _settingsStorage.Read(nameof(AppUptime));
+
+ return new(uptimeSoFar + subSessionLength);
+ }
+
+ return TimeSpan.MinValue;
+ }
+ }
+
+ ///
+ /// Adds to the record of how long the app has been running.
+ /// Use this to optionally include time spent in background tasks or extended execution.
+ ///
+ /// The amount to time to add
+ public void AddToAppUptime(TimeSpan duration)
+ {
+ var uptimeSoFar = _settingsStorage.Read(nameof(AppUptime));
+
+ _settingsStorage.Save(nameof(AppUptime), uptimeSoFar + duration.Ticks);
+ }
+
+ ///
+ /// Resets the launch count.
+ ///
+ public void ResetLaunchCount()
+ {
+ LastResetTime = DateTime.UtcNow;
+ LaunchCount = 0;
+
+ _settingsStorage.Save(nameof(LastResetTime), LastResetTime.ToFileTimeUtc());
+ _settingsStorage.Save(nameof(LaunchCount), LaunchCount);
+ }
+
+ ///
+ /// Tracks information about the app's launch.
+ ///
+ /// Details about the launch request and process.
+ /// The XamlRoot object from your visual tree.
+ public void TrackAppUse(IActivatedEventArgs args, XamlRoot? xamlRoot = null)
+ {
+ if (args.PreviousExecutionState is ApplicationExecutionState.ClosedByUser or ApplicationExecutionState.NotRunning)
+ {
+ LaunchCount = _settingsStorage.Read(nameof(LaunchCount)) + 1;
+ TotalLaunchCount = _settingsStorage.Read(nameof(TotalLaunchCount)) + 1;
+
+ // In case we upgraded the properties, make TotalLaunchCount is correct
+ if (TotalLaunchCount < LaunchCount)
+ {
+ TotalLaunchCount = LaunchCount;
+ }
+
+ _settingsStorage.Save(nameof(LaunchCount), LaunchCount);
+ _settingsStorage.Save(nameof(TotalLaunchCount), TotalLaunchCount);
+
+ LaunchTime = DateTime.UtcNow;
+
+ var lastLaunch = _settingsStorage.Read(nameof(LastLaunchTime));
+
+ LastLaunchTime = lastLaunch != 0
+ ? DateTime.FromFileTimeUtc(lastLaunch)
+ : LaunchTime;
+
+ _settingsStorage.Save(nameof(LastLaunchTime), LaunchTime.ToFileTimeUtc());
+ _settingsStorage.Save(nameof(AppUptime), 0L);
+
+ var lastResetTime = _settingsStorage.Read(nameof(LastResetTime));
+
+ LastResetTime = lastResetTime != 0
+ ? DateTime.FromFileTimeUtc(lastResetTime)
+ : DateTime.MinValue;
+ }
+
+ if (xamlRoot != null)
+ {
+ void XamlRoot_Changed(XamlRoot sender, XamlRootChangedEventArgs e)
+ {
+ UpdateVisibility(sender.IsHostVisible);
+ }
+
+ xamlRoot.Changed -= XamlRoot_Changed;
+ xamlRoot.Changed += XamlRoot_Changed;
+ }
+ else
+ {
+ void App_VisibilityChanged(CoreWindow sender, VisibilityChangedEventArgs e)
+ {
+ UpdateVisibility(e.Visible);
+ }
+
+ var windowForCurrentThread = CoreWindow.GetForCurrentThread();
+ if (windowForCurrentThread != null)
+ {
+ windowForCurrentThread.VisibilityChanged -= App_VisibilityChanged;
+ windowForCurrentThread.VisibilityChanged += App_VisibilityChanged;
+ }
+ }
+ }
+
+ private void UpdateVisibility(bool visible)
+ {
+ if (visible)
+ {
+ _sessionStart = DateTime.UtcNow;
+ }
+ else
+ {
+ var subSessionLength = DateTime.UtcNow.Subtract(_sessionStart).Ticks;
+ var uptimeSoFar = _settingsStorage.Read(nameof(AppUptime));
+
+ _settingsStorage.Save(nameof(AppUptime), uptimeSoFar + subSessionLength);
+ }
+ }
+
+ private bool DetectIfFirstUse()
+ {
+ if (_settingsStorage.KeyExists(nameof(IsFirstRun)))
+ {
+ return false;
+ }
+
+ _settingsStorage.Save(nameof(IsFirstRun), true);
+
+ return true;
+ }
+
+ private (bool IsUpdated, PackageVersion PreviousVersion) DetectIfAppUpdated()
+ {
+ var currentVersion = ApplicationVersion.ToFormattedString();
+
+ // If the "currentVersion" key does not exist, it means that this is the first time this method
+ // is ever called. That is, this is either the first time the app has been launched, or the first
+ // time a previously existing app has run this method (or has run it after a new update of the app).
+ // In this case, save the current version and report the same version as previous version installed.
+ if (!_settingsStorage.KeyExists(nameof(currentVersion)))
+ {
+ _settingsStorage.Save(nameof(currentVersion), currentVersion);
+ }
+ else
+ {
+ var previousVersion = _settingsStorage.Read(nameof(currentVersion));
+
+ // There are two possible cases if the "currentVersion" key exists:
+ // 1) The previous version is different than the current one. This means that the application
+ // has been updated since the last time this method was called. We can overwrite the saved
+ // setting for "currentVersion" to bring that value up to date, and return its old value.
+ // 2) The previous version matches the current one: the app has just been reopened without updates.
+ // In this case we have nothing to do and just return the previous version installed to be the same.
+ if (currentVersion != previousVersion && previousVersion != null)
+ {
+ _settingsStorage.Save(nameof(currentVersion), currentVersion);
+ return (true, previousVersion.ToPackageVersion());
+ }
+ }
+
+ return (false, currentVersion.ToPackageVersion());
+ }
+
+ private DateTime DetectFirstUseTime()
+ {
+ if (_settingsStorage.KeyExists(nameof(FirstUseTime)))
+ {
+ var firstUse = _settingsStorage.Read(nameof(FirstUseTime));
+
+ return DateTime.FromFileTimeUtc(firstUse);
+ }
+
+ DateTime utcNow = DateTime.UtcNow;
+
+ _settingsStorage.Save(nameof(FirstUseTime), utcNow.ToFileTimeUtc());
+
+ return utcNow;
+ }
+
+ private PackageVersion DetectFirstVersionInstalled()
+ {
+ var firstVersionInstalled = _settingsStorage.Read(nameof(FirstVersionInstalled));
+ if (firstVersionInstalled != null)
+ {
+ return firstVersionInstalled.ToPackageVersion();
+ }
+
+ _settingsStorage.Save(nameof(FirstVersionInstalled), ApplicationVersion.ToFormattedString());
+
+ return ApplicationVersion;
+ }
+
+ private void InitializeValuesSetWithTrackAppUse()
+ {
+ LaunchTime = DateTime.MinValue;
+ LaunchCount = 0;
+ TotalLaunchCount = 0;
+ LastLaunchTime = DateTime.MinValue;
+ LastResetTime = DateTime.MinValue;
+ }
+
+ ///
+ /// Launches the store app so the user can leave a review.
+ ///
+ /// A representing the asynchronous operation.
+ /// This method needs to be called from your UI thread.
+ public static Task LaunchStoreForReviewAsync()
+ {
+ return Launcher.LaunchUriAsync(new Uri(string.Format("ms-windows-store://review/?PFN={0}", Package.Current.Id.FamilyName))).AsTask();
+ }
+}