File: Preferences\Preferences.uwp.cs
Web Access
Project: src\src\Essentials\src\Essentials.csproj (Microsoft.Maui.Essentials)
using System;
using System.Globalization;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Maui.ApplicationModel;
using Windows.Storage;
 
using PreferencesDictionary = System.Collections.Concurrent.ConcurrentDictionary<string, System.Collections.Concurrent.ConcurrentDictionary<string, string>>;
using ShareNameDictionary = System.Collections.Concurrent.ConcurrentDictionary<string, string>;
 
namespace Microsoft.Maui.Storage
{
	class PreferencesImplementation : IPreferences
	{
		readonly IPreferences _preferences;
 
		public PreferencesImplementation()
		{
			_preferences = AppInfoUtils.IsPackagedApp
				? new PackagedPreferencesImplementation()
				: new UnpackagedPreferencesImplementation();
		}
 
		public bool ContainsKey(string key, string sharedName) =>
			_preferences.ContainsKey(key, sharedName);
 
		public void Remove(string key, string sharedName) =>
			_preferences.Remove(key, sharedName);
 
		public void Clear(string sharedName) =>
			_preferences.Clear(sharedName);
 
		public void Set<T>(string key, T value, string sharedName) =>
			_preferences.Set<T>(key, value, sharedName);
 
		public T Get<T>(string key, T defaultValue, string sharedName) =>
			_preferences.Get<T>(key, defaultValue, sharedName);
	}
 
	class PackagedPreferencesImplementation : IPreferences
	{
		static readonly object locker = new object();
 
		public bool ContainsKey(string key, string sharedName)
		{
			lock (locker)
			{
				var appDataContainer = GetApplicationDataContainer(sharedName);
				return appDataContainer.Values.ContainsKey(key);
			}
		}
 
		public void Remove(string key, string sharedName)
		{
			lock (locker)
			{
				var appDataContainer = GetApplicationDataContainer(sharedName);
				if (appDataContainer.Values.ContainsKey(key))
					appDataContainer.Values.Remove(key);
			}
		}
 
		public void Clear(string sharedName)
		{
			lock (locker)
			{
				var appDataContainer = GetApplicationDataContainer(sharedName);
				appDataContainer.Values.Clear();
			}
		}
 
		public void Set<T>(string key, T value, string sharedName)
		{
			Preferences.CheckIsSupportedType<T>();
 
			lock (locker)
			{
				var appDataContainer = GetApplicationDataContainer(sharedName);
 
				if (value == null)
				{
					if (appDataContainer.Values.ContainsKey(key))
						appDataContainer.Values.Remove(key);
					return;
				}
 
				if (value is DateTime dt)
				{
					appDataContainer.Values[key] = dt.ToBinary();
				}
				else if (value is DateTimeOffset dto)
				{
					appDataContainer.Values[key] = dto.ToString("O");
				}
				else
				{
					appDataContainer.Values[key] = value;
				}
			}
		}
 
		public T Get<T>(string key, T defaultValue, string sharedName)
		{
			lock (locker)
			{
				var appDataContainer = GetApplicationDataContainer(sharedName);
				if (appDataContainer.Values.ContainsKey(key))
				{
					var tempValue = appDataContainer.Values[key];
					if (tempValue != null)
					{
						if (defaultValue is DateTime dt)
						{
							return (T)(object)DateTime.FromBinary((long)tempValue);
						}
						else if (defaultValue is DateTimeOffset dto)
						{
							if (DateTimeOffset.TryParse((string)tempValue, out var dateTimeOffset))
							{
								return (T)(object)dateTimeOffset;
							}
						}
						else
						{
							return (T)tempValue;
						}
					}
				}
			}
 
			return defaultValue;
		}
 
		static ApplicationDataContainer GetApplicationDataContainer(string sharedName)
		{
			var localSettings = ApplicationData.Current.LocalSettings;
			if (string.IsNullOrWhiteSpace(sharedName))
				return localSettings;
 
			if (!localSettings.Containers.ContainsKey(sharedName))
				localSettings.CreateContainer(sharedName, ApplicationDataCreateDisposition.Always);
 
			return localSettings.Containers[sharedName];
		}
	}
 
	class UnpackagedPreferencesImplementation : IPreferences
	{
		static readonly string AppPreferencesPath = Path.Combine(FileSystem.AppDataDirectory, "..", "Settings", "preferences.dat");
 
		readonly PreferencesDictionary _preferences = new();
 
		public UnpackagedPreferencesImplementation()
		{
			Load();
 
			_preferences.GetOrAdd(string.Empty, _ => new ShareNameDictionary());
		}
 
		public bool ContainsKey(string key, string sharedName = null)
		{
			if (_preferences.TryGetValue(CleanSharedName(sharedName), out var inner))
			{
				return inner.ContainsKey(key);
			}
 
			return false;
		}
 
		public void Remove(string key, string sharedName = null)
		{
			if (_preferences.TryGetValue(CleanSharedName(sharedName), out var inner))
			{
				inner.TryRemove(key, out _);
				Save();
			}
		}
 
		public void Clear(string sharedName = null)
		{
			if (_preferences.TryGetValue(CleanSharedName(sharedName), out var prefs))
			{
				prefs.Clear();
				Save();
			}
		}
 
		public void Set<T>(string key, T value, string sharedName = null)
		{
			Preferences.CheckIsSupportedType<T>();
 
			var prefs = _preferences.GetOrAdd(CleanSharedName(sharedName), _ => new ShareNameDictionary());
 
			if (value is null)
				prefs.TryRemove(key, out _);
			else if (value is DateTime dt)
				prefs[key] = string.Format(CultureInfo.InvariantCulture, "{0}", dt.ToBinary());
			else if (value is DateTimeOffset dto)
				prefs[key] = dto.ToString("O");
			else
				prefs[key] = string.Format(CultureInfo.InvariantCulture, "{0}", value);
 
			Save();
		}
 
		public T Get<T>(string key, T defaultValue, string sharedName = null)
		{
			if (_preferences.TryGetValue(CleanSharedName(sharedName), out var inner))
			{
				if (inner.TryGetValue(key, out var value) && value is not null)
				{
					if (defaultValue is DateTime dt)
					{
						// long for the .NET 9+ format
						if (long.TryParse(value, CultureInfo.InvariantCulture, out var longValue))
							return (T)(object)DateTime.FromBinary(longValue);
						// DateTime string for the .NET 8 format
						if (DateTime.TryParse(value, CultureInfo.InvariantCulture, out var datetimeValue))
							return (T)(object)datetimeValue;
					}
					else if (defaultValue is DateTimeOffset dto)
					{
						if (DateTimeOffset.TryParse((string)value, CultureInfo.InvariantCulture, out var dateTimeOffset))
						{
							return (T)(object)dateTimeOffset;
						}
					}
 
					try
					{
						return (T)Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture);
					}
					catch (FormatException)
					{
						// bad get, fall back to default
					}
				}
			}
 
			return defaultValue;
		}
 
		void Load()
		{
			if (!File.Exists(AppPreferencesPath))
				return;
 
			try
			{
				using var stream = File.OpenRead(AppPreferencesPath);
 
				PreferencesDictionary readPreferences = JsonSerializer.Deserialize(stream, PreferencesJsonSerializerContext.Default.PreferencesDictionary);
 
				if (readPreferences != null)
				{
					_preferences.Clear();
					foreach (var pair in readPreferences)
						_preferences.TryAdd(pair.Key, pair.Value);
				}
			}
			catch (JsonException)
			{
				// if deserialization fails proceed with empty settings
			}
		}
 
		void Save()
		{
			var dir = Path.GetDirectoryName(AppPreferencesPath);
			Directory.CreateDirectory(dir);
 
			using var stream = File.Create(AppPreferencesPath);
			JsonSerializer.Serialize(stream, _preferences, PreferencesJsonSerializerContext.Default.PreferencesDictionary);
		}
 
		static string CleanSharedName(string sharedName) =>
			string.IsNullOrEmpty(sharedName) ? string.Empty : sharedName;
	}
}
 
[JsonSerializable(typeof(PreferencesDictionary), TypeInfoPropertyName = nameof(PreferencesDictionary))]
internal partial class PreferencesJsonSerializerContext : JsonSerializerContext
{
}