自己写一个Setting类,支持jason,ini格式。直接写入和读取对象,ISettingBackend用于配置加密,压缩等自定义。

using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;

namespace Shares
{
    public interface ISettingBackend
    {
        // 用户可配置:按添加顺序执行
        IList<Func<byte[], byte[]>> EncodePipeline { get; }
        IList<Func<byte[], byte[]>> DecodePipeline { get; }

        void Load(string text);
        string Save();

        void Set<T>(string sectionName, T value);
        T Get<T>(string sectionName, T defaultValue);
    }

    public sealed class Setting
    {
        private static readonly ConcurrentDictionary<string, object> fileLocks =
            new(StringComparer.OrdinalIgnoreCase);

        private static object Gate(string path) => fileLocks.GetOrAdd(path, _ => new object());

        private static readonly Dictionary<string, Func<ISettingBackend>> factories =
            new(StringComparer.OrdinalIgnoreCase)
            {
                [".json"] = () => new JsonSettingBackend(),
                [".ini"] = () => new IniSettingBackend(),
                [".key"]= () => new IniSettingBackend(),
            };

        public static void RegisterBackend(string extension, Func<ISettingBackend> factory)
        {
            if (string.IsNullOrWhiteSpace(extension))
                throw new ArgumentException(nameof(extension));

            if (!extension.StartsWith('.'))
                extension = "." + extension;

            factories[extension] = factory ?? throw new ArgumentNullException(nameof(factory));
        }

        private readonly string filepath;
        private readonly ISettingBackend backend;

        public Setting(string filePath,
                        IEnumerable<Func<byte[], byte[]>>? encodePipeline = null,
                        IEnumerable<Func<byte[], byte[]>>? decodePipeline = null)
        {
            if (string.IsNullOrWhiteSpace(filePath))
                throw new ArgumentException(nameof(filePath));

            filepath = Path.GetFullPath(filePath);
            var ext = Path.GetExtension(filepath);

            if (!factories.TryGetValue(ext, out var factory))
                throw new NotSupportedException($"Unsupported format: {ext}");

            backend = factory();

            if (encodePipeline != null)
                foreach (var f in encodePipeline) backend.EncodePipeline.Add(f);

            if (decodePipeline != null)
                foreach (var f in decodePipeline) backend.DecodePipeline.Add(f);

            lock (Gate(filepath))
                backend.Load(ReadText(filepath, backend.DecodePipeline));
        }

        public void Set<T>(string sectionName, T value)
        {
            if (string.IsNullOrWhiteSpace(sectionName))
                throw new ArgumentException(nameof(sectionName));

            lock (Gate(filepath))
            {
                backend.Set(sectionName, value);
                AtomicWrite(filepath, backend.Save(), backend.EncodePipeline);
            }
        }

        public T Get<T>(string sectionName) => Get(sectionName, default(T)!);

        public T Get<T>(string sectionName, T defaultValue)
        {
            if (string.IsNullOrWhiteSpace(sectionName))
                throw new ArgumentException(nameof(sectionName));

            lock (Gate(filepath))
                return backend.Get(sectionName, defaultValue);
        }

        private static string ReadText(string path, IList<Func<byte[], byte[]>> decodePipeline)
        {
            if (!File.Exists(path))
                return "";

            var bytes = File.ReadAllBytes(path);
            if (decodePipeline.Count > 0)
                bytes = ApplyPipeline(bytes, decodePipeline);

            return Encoding.UTF8.GetString(bytes);
        }

        private static void AtomicWrite(string path, string text, IList<Func<byte[], byte[]>> encodePipeline)
        {
            EnsureDir(path);

            var bytes = Encoding.UTF8.GetBytes(text);
            if (encodePipeline.Count > 0)
                bytes = ApplyPipeline(bytes, encodePipeline);

            var temp = path + ".tmp";
            File.WriteAllBytes(temp, bytes);

            if (File.Exists(path))
            {
                try { File.Replace(temp, path, null); return; }
                catch { }
            }

            File.Move(temp, path, true);
        }

        private static byte[] ApplyPipeline(byte[] data, IList<Func<byte[], byte[]>> pipeline)
        {
            for (int i = 0; i < pipeline.Count; i++)
                data = pipeline[i](data);
            return data;
        }

        private static void EnsureDir(string path)
        {
            var dir = Path.GetDirectoryName(path);
            if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
                Directory.CreateDirectory(dir);
        }
    }

    internal abstract class SettingBackendBase : ISettingBackend
    {
        public IList<Func<byte[], byte[]>> EncodePipeline { get; } = [];
        public IList<Func<byte[], byte[]>> DecodePipeline { get; } = [];

        public abstract void Load(string text);
        public abstract string Save();
        public abstract void Set<T>(string sectionName, T value);
        public abstract T Get<T>(string sectionName, T defaultValue);
    }

    internal sealed class JsonSettingBackend : SettingBackendBase
    {
        private readonly JsonSerializerOptions options = new()
        {
            WriteIndented = true,
            PropertyNameCaseInsensitive = true,
            AllowTrailingCommas = true,
            ReadCommentHandling = JsonCommentHandling.Skip,
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
        };

        private JsonObject root = [];

        public override void Load(string text)
        {
            root = [];

            if (string.IsNullOrWhiteSpace(text))
                return;

            try
            {
                var node = JsonNode.Parse(text, documentOptions: new JsonDocumentOptions
                {
                    AllowTrailingCommas = true,
                    CommentHandling = JsonCommentHandling.Skip
                });

                if (node is JsonObject obj)
                    root = obj;
            }
            catch
            {
                root = [];
            }
        }

        public override string Save() => root.ToJsonString(options);

        public override void Set<T>(string sectionName, T value)
        {
            root[sectionName] = JsonSerializer.SerializeToNode(value, options);
        }

        public override T Get<T>(string sectionName, T defaultValue)
        {
            if (!root.TryGetPropertyValue(sectionName, out var node) || node is null)
                return defaultValue;

            try { return node.Deserialize<T>(options) ?? defaultValue; }
            catch { return defaultValue; }
        }
    }

    internal sealed class IniSettingBackend : SettingBackendBase
    {
        private Dictionary<string, Dictionary<string, string>> root =
            new(StringComparer.OrdinalIgnoreCase);

        public override void Load(string text)
        {
            root = ParseIni(text ?? "");
        }

        public override string Save() => BuildIni(root);

        public override void Set<T>(string sectionName, T value)
        {
            var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            if (value != null) FlattenAny(value, value.GetType(), dict, "");
            root[sectionName] = dict;
        }

        public override T Get<T>(string sectionName, T defaultValue)
        {
            if (!root.TryGetValue(sectionName, out var dict))
                return defaultValue;

            try
            {
                var t = typeof(T);

                if (t.IsValueType)
                {
                    object boxed = Activator.CreateInstance(t)!;
                    UnflattenInto(boxed, t, dict, "");
                    return (T)boxed;
                }

                var instance = Activator.CreateInstance<T>();
                if (instance == null) return defaultValue;

                UnflattenInto(instance, t, dict, "");
                return instance;
            }
            catch
            {
                return defaultValue;
            }
        }

        private static Dictionary<string, Dictionary<string, string>> ParseIni(string text)
        {
            var result = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
            string? current = null;

            using var reader = new StringReader(text);
            while (true)
            {
                var line = reader.ReadLine();
                if (line == null) break;

                line = line.Trim();
                if (line.Length == 0 || line.StartsWith(';') || line.StartsWith('#'))
                    continue;

                if (line.StartsWith('[') && line.EndsWith(']'))
                {
                    var name = line[1..^1].Trim();
                    current = name.Length == 0 ? null : name;

                    if (current != null && !result.ContainsKey(current))
                        result[current] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

                    continue;
                }

                if (current == null)
                    continue;

                var idx = line.IndexOf('=');
                if (idx <= 0) continue;

                result[current][line[..idx].Trim()] = UnescapeIni(line[(idx + 1)..].Trim());
            }

            return result;
        }

        private static string BuildIni(Dictionary<string, Dictionary<string, string>> data)
        {
            var sb = new StringBuilder();
            foreach (var sec in data)
            {
                sb.Append('[').Append(sec.Key).AppendLine("]");
                foreach (var kv in sec.Value)
                    sb.Append(kv.Key).Append('=').AppendLine(EscapeIni(kv.Value));
                sb.AppendLine();
            }
            return sb.ToString();
        }

        private static string EscapeIni(string value)
        {
            if (value == null) return "";
            return value.Replace("\\", "\\\\").Replace("\r", "\\r").Replace("\n", "\\n");
        }

        private static string UnescapeIni(string value)
        {
            if (value == null) return "";
            return value.Replace("\\n", "\n").Replace("\\r", "\r").Replace("\\\\", "\\");
        }

        private static string EscapeToken(string token)
        {
            if (token == null) return "";
            return token.Replace("\\", "\\\\").Replace("]", "\\]");
        }

        private static string UnescapeToken(string token)
        {
            if (token == null) return "";
            return token.Replace("\\]", "]").Replace("\\\\", "\\");
        }

        private static bool IsSimple(Type type)
        {
            type = Nullable.GetUnderlyingType(type) ?? type;
            return type.IsEnum
                   || type.IsPrimitive
                   || type == typeof(string)
                   || type == typeof(decimal)
                   || type == typeof(DateTime)
                   || type == typeof(DateTimeOffset)
                   || type == typeof(Guid)
                   || type == typeof(TimeSpan);
        }

        private static string ToStringInvariant(object value, Type type)
        {
            type = Nullable.GetUnderlyingType(type) ?? type;

            if (type == typeof(DateTime))
                return ((DateTime)value).ToString("O", CultureInfo.InvariantCulture);

            if (type == typeof(DateTimeOffset))
                return ((DateTimeOffset)value).ToString("O", CultureInfo.InvariantCulture);

            if (type == typeof(TimeSpan))
                return ((TimeSpan)value).ToString("c", CultureInfo.InvariantCulture);

            if (type == typeof(bool))
                return (bool)value ? "true" : "false";

            return Convert.ToString(value, CultureInfo.InvariantCulture) ?? "";
        }

        private static object? FromStringInvariant(string raw, Type targetType)
        {
            var underlying = Nullable.GetUnderlyingType(targetType);
            var type = underlying ?? targetType;

            if (type == typeof(string)) return raw;
            if (type == typeof(bool)) return raw.Equals("true", StringComparison.OrdinalIgnoreCase) || raw == "1";
            if (type.IsEnum) return Enum.Parse(type, raw, true);

            if (type == typeof(DateTime))
                return DateTime.Parse(raw, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);

            if (type == typeof(DateTimeOffset))
                return DateTimeOffset.Parse(raw, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);

            if (type == typeof(TimeSpan))
                return TimeSpan.Parse(raw, CultureInfo.InvariantCulture);

            if (type == typeof(Guid))
                return Guid.Parse(raw);

            return Convert.ChangeType(raw, type, CultureInfo.InvariantCulture);
        }

        private static bool IsListOrArray(Type type, out Type elementType, out bool isArray)
        {
            if (type.IsArray)
            {
                elementType = type.GetElementType()!;
                isArray = true;
                return true;
            }

            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
            {
                elementType = type.GetGenericArguments()[0];
                isArray = false;
                return true;
            }

            elementType = typeof(object);
            isArray = false;
            return false;
        }

        private static bool IsStringKeyDictionary(Type type, out Type valueType)
        {
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
            {
                var args = type.GetGenericArguments();
                if (args[0] == typeof(string))
                {
                    valueType = args[1];
                    return true;
                }
            }

            valueType = typeof(object);
            return false;
        }

        private static void FlattenAny(object obj, Type objType, Dictionary<string, string> dict, string prefix)
        {
            if (IsStringKeyDictionary(objType, out var valueType) && obj is IDictionary map)
            {
                foreach (DictionaryEntry e in map)
                {
                    var key = EscapeToken(e.Key?.ToString() ?? "");
                    var baseKey = prefix + "[" + key + "]";
                    var val = e.Value;

                    if (val == null) { dict[baseKey] = ""; continue; }

                    var vt = Nullable.GetUnderlyingType(valueType) ?? valueType;
                    if (IsSimple(vt))
                        dict[baseKey] = ToStringInvariant(val, vt);
                    else
                        FlattenAny(val, val.GetType(), dict, baseKey);
                }
                return;
            }

            if (IsListOrArray(objType, out var elementType, out _) && obj is IEnumerable en && objType != typeof(string))
            {
                var i = 0;
                foreach (var item in en)
                {
                    var baseKey = prefix + "[" + i.ToString(CultureInfo.InvariantCulture) + "]";
                    if (item == null) { dict[baseKey] = ""; i++; continue; }

                    var et = Nullable.GetUnderlyingType(elementType) ?? elementType;
                    if (IsSimple(et))
                        dict[baseKey] = ToStringInvariant(item, et);
                    else
                        FlattenAny(item, item.GetType(), dict, baseKey);

                    i++;
                }
                return;
            }

            foreach (var prop in objType.GetProperties(BindingFlags.Instance | BindingFlags.Public))
            {
                if (!prop.CanRead) continue;
                if (prop.GetIndexParameters().Length != 0) continue;

                var value = prop.GetValue(obj);
                var key = prefix.Length == 0 ? prop.Name : prefix + "." + prop.Name;

                if (value == null) { dict[key] = ""; continue; }

                var pt = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;

                if (IsSimple(pt))
                    dict[key] = ToStringInvariant(value, pt);
                else
                    FlattenAny(value, value.GetType(), dict, key);
            }
        }

        private static void UnflattenInto(object obj, Type objType, Dictionary<string, string> dict, string prefix)
        {
            foreach (var prop in objType.GetProperties(BindingFlags.Instance | BindingFlags.Public))
            {
                if (!prop.CanWrite) continue;
                if (prop.GetIndexParameters().Length != 0) continue;

                var key = prefix.Length == 0 ? prop.Name : prefix + "." + prop.Name;
                var pType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;

                if (IsSimple(pType))
                {
                    if (dict.TryGetValue(key, out var raw))
                    {
                        try { prop.SetValue(obj, FromStringInvariant(raw, prop.PropertyType)); } catch { }
                    }
                    continue;
                }

                if (IsStringKeyDictionary(pType, out var valueType))
                {
                    var keys = dict.Keys.Where(k => k.StartsWith(key + "[", StringComparison.OrdinalIgnoreCase)).ToArray();
                    if (keys.Length == 0) continue;

                    object? mapObj;
                    try { mapObj = Activator.CreateInstance(pType); } catch { mapObj = null; }
                    if (mapObj is not IDictionary map) continue;

                    foreach (var token in keys.Select(k => ExtractBracketToken(k, key)).Where(t => t != null).Distinct(StringComparer.OrdinalIgnoreCase)!)
                    {
                        var realKey = UnescapeToken(token!);
                        var baseKey = key + "[" + token + "]";
                        var vt = Nullable.GetUnderlyingType(valueType) ?? valueType;

                        if (IsSimple(vt))
                        {
                            if (dict.TryGetValue(baseKey, out var raw))
                            {
                                try { map[realKey] = FromStringInvariant(raw, valueType); } catch { }
                            }
                        }
                        else
                        {
                            object child = Activator.CreateInstance(valueType)!;
                            UnflattenInto(child, valueType, dict, baseKey);
                            map[realKey] = child;
                        }
                    }

                    try { prop.SetValue(obj, mapObj); } catch { }
                    continue;
                }

                if (IsListOrArray(pType, out var elementType, out var isArray))
                {
                    var idxs = dict.Keys
                        .Where(k => k.StartsWith(key + "[", StringComparison.OrdinalIgnoreCase))
                        .Select(k => ExtractBracketIndex(k, key))
                        .Where(i => i >= 0)
                        .Distinct()
                        .OrderBy(i => i)
                        .ToArray();

                    if (idxs.Length == 0) continue;

                    var list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType))!;
                    var et = Nullable.GetUnderlyingType(elementType) ?? elementType;

                    foreach (var i in idxs)
                    {
                        var baseKey = key + "[" + i.ToString(CultureInfo.InvariantCulture) + "]";

                        if (IsSimple(et))
                        {
                            if (dict.TryGetValue(baseKey, out var raw))
                            {
                                try { list.Add(FromStringInvariant(raw, elementType)); }
                                catch { list.Add(elementType.IsValueType ? Activator.CreateInstance(elementType) : null); }
                            }
                            else
                            {
                                list.Add(elementType.IsValueType ? Activator.CreateInstance(elementType) : null);
                            }
                        }
                        else
                        {
                            object child = Activator.CreateInstance(elementType)!;
                            UnflattenInto(child, elementType, dict, baseKey);
                            list.Add(child);
                        }
                    }

                    try
                    {
                        if (isArray)
                        {
                            var arr = Array.CreateInstance(elementType, list.Count);
                            list.CopyTo(arr, 0);
                            prop.SetValue(obj, arr);
                        }
                        else
                        {
                            prop.SetValue(obj, list);
                        }
                    }
                    catch { }

                    continue;
                }

                var childPrefix = key + ".";
                if (!dict.Keys.Any(k => k.StartsWith(childPrefix, StringComparison.OrdinalIgnoreCase)))
                    continue;

                if (prop.PropertyType.IsValueType)
                {
                    object boxed = Activator.CreateInstance(prop.PropertyType)!;
                    UnflattenInto(boxed, prop.PropertyType, dict, key);
                    try { prop.SetValue(obj, boxed); } catch { }
                    continue;
                }

                object? childObj;
                try { childObj = prop.GetValue(obj) ?? Activator.CreateInstance(prop.PropertyType); }
                catch { continue; }

                if (childObj == null) continue;

                try { prop.SetValue(obj, childObj); } catch { continue; }
                UnflattenInto(childObj, prop.PropertyType, dict, key);
            }
        }

        private static string? ExtractBracketToken(string fullKey, string baseKey)
        {
            var start = baseKey.Length;
            if (fullKey.Length <= start + 2) return null;
            if (fullKey[start] != '[') return null;

            var end = FindBracketEnd(fullKey, start);
            if (end < 0) return null;

            return fullKey.Substring(start + 1, end - start - 1);
        }

        private static int ExtractBracketIndex(string fullKey, string baseKey)
        {
            var token = ExtractBracketToken(fullKey, baseKey);
            if (token == null) return -1;
            return int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) ? i : -1;
        }

        private static int FindBracketEnd(string s, int bracketStartIndex)
        {
            for (var i = bracketStartIndex + 1; i < s.Length; i++)
            {
                if (s[i] == ']' && s[i - 1] != '\\')
                    return i;
            }
            return -1;
        }
    }
}

SavePosition.axaml代码

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Height="300" Width="300"
        x:Class="AvaloniaUI.SavePosition"
        Title="SavePosition">
    <StackPanel Margin="10" HorizontalAlignment="Center" Spacing="5">
        <Button Click="cmdSave_Click">Save Position and Size</Button>
        <Button Click="cmdRestore_Click">Restore Position and Size</Button>
    </StackPanel>
</Window>

SavePosition.axaml.cs代码

using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Shares;
using System;
using System.IO;

namespace AvaloniaUI;
public sealed class WindowPlacement
{
    public int X { get; set; }
    public int Y { get; set; }
    public double Width { get; set; } = 300;
    public double Height { get; set; } = 300;
    public bool IsMaximized { get; set; }
}
public partial class SavePosition : Window
{
    private readonly Setting setting;
    private const string sectionName = "MainWindow";
    public SavePosition()
    {
        InitializeComponent();
        var path = Path.Combine(AppContext.BaseDirectory, "settings.json");
        //Console.WriteLine(path);
        setting = new Setting(path);
    }
    private void cmdSave_Click(object? sender, RoutedEventArgs e)
    {
        var placement = CapturePlacement();
        setting.Set(sectionName, placement);
    }
    private void cmdRestore_Click(object? sender, RoutedEventArgs e)
    {
        var placement = setting.Get<WindowPlacement>(sectionName);
        ApplyPlacement(placement);
    }
    private WindowPlacement CapturePlacement()
    {
        // 最大化时保存当前位置和 ClientSize 意义不大,但可以保存一个 IsMaximized 供恢复使用
        var pos = Position;
        var size = ClientSize;

        return new WindowPlacement
        {
            X = pos.X,
            Y = pos.Y,
            Width = Math.Max(200, size.Width),
            Height = Math.Max(200, size.Height),
            IsMaximized = WindowState == WindowState.Maximized
        };
    }
    private void ApplyPlacement(WindowPlacement placement)
    {
        // 先还原,避免最大化状态下设置尺寸/位置无效
        WindowState = WindowState.Normal;

        Width = placement.Width > 200 ? placement.Width : 300;
        Height = placement.Height > 200 ? placement.Height : 300;

        Position = new PixelPoint(placement.X, placement.Y);

        if (placement.IsMaximized)
            WindowState = WindowState.Maximized;
    }
} 

运行效果

image

 

posted on 2026-03-19 11:19  dalgleish  阅读(2)  评论(0)    收藏  举报