Files
2026-06-08 12:18:58 +08:00

23 KiB
Raw Permalink Blame History

设置系统

本文档介绍阑山桌面的设置系统,包括配置管理、持久化、设置页面和最佳实践。

设置系统概览

阑山桌面提供了统一的设置系统,用于管理应用、插件和组件的配置数据。

核心特性

  • 💾 自动持久化 - 设置自动保存到本地
  • 🔔 变更通知 - 监听设置变更事件
  • 📁 分域管理 - 按命名空间组织设置
  • 🔒 类型安全 - 泛型 API 保证类型安全
  • 🎨 UI 集成 - 轻松创建设置页面

设置存储位置

%LOCALAPPDATA%\LanMountainDesktop\
└── settings\
    ├── app.json                    # 应用设置
    ├── appearance.json             # 外观设置
    ├── plugins\
    │   ├── com.example.plugin1.json
    │   └── com.example.plugin2.json
    └── components\
        └── com.example.plugin1.component1.json

使用设置服务

在插件中使用

public class MyPlugin : IPlugin
{
    private IPluginContext? _context;
    
    public async Task InitializeAsync(IPluginContext context)
    {
        _context = context;
        
        // 通过 context 访问设置
        var settings = context.Settings;
        
        // 读取设置
        var apiKey = settings.GetValue("ApiKey", "");
        var refreshRate = settings.GetValue("RefreshRate", 60);
        var enableNotifications = settings.GetValue("EnableNotifications", true);
        
        // 保存设置
        settings.SetValue("LastStartTime", DateTime.Now);
    }
}

在组件中使用

public class MyComponent : ComponentBase
{
    public override Task InitializeAsync()
    {
        // 组件有自己的设置域
        // 自动命名空间:{PluginId}.{ComponentId}
        
        // 读取设置
        var location = Settings.GetValue("Location", "北京");
        var useFahrenheit = Settings.GetValue("UseFahrenheit", false);
        
        // 读取复杂对象
        var config = Settings.GetValue<ComponentConfig>("Config", new ComponentConfig());
        
        return Task.CompletedTask;
    }
    
    public void UpdateLocation(string location)
    {
        Location = location;
        
        // 保存设置
        Settings.SetValue("Location", location);
    }
}

设置 API

ISettingsService 接口

public interface ISettingsService
{
    /// <summary>
    /// 获取设置值
    /// </summary>
    T GetValue<T>(string key, T defaultValue);
    
    /// <summary>
    /// 设置值
    /// </summary>
    void SetValue<T>(string key, T value);
    
    /// <summary>
    /// 删除设置
    /// </summary>
    void Remove(string key);
    
    /// <summary>
    /// 检查设置是否存在
    /// </summary>
    bool Contains(string key);
    
    /// <summary>
    /// 获取所有键
    /// </summary>
    IEnumerable<string> GetAllKeys();
    
    /// <summary>
    /// 清空所有设置
    /// </summary>
    void Clear();
    
    /// <summary>
    /// 设置变更事件
    /// </summary>
    event EventHandler<SettingChangedEventArgs>? SettingChanged;
}

基本用法

// 读取设置
var value = settings.GetValue<string>("Key", "DefaultValue");

// 保存设置
settings.SetValue("Key", "NewValue");

// 删除设置
settings.Remove("Key");

// 检查是否存在
if (settings.Contains("Key"))
{
    // ...
}

// 获取所有键
var keys = settings.GetAllKeys();

// 清空所有设置
settings.Clear();

支持的数据类型

基本类型

// 字符串
settings.SetValue("Name", "张三");
var name = settings.GetValue("Name", "");

// 数字
settings.SetValue("Age", 25);
var age = settings.GetValue("Age", 0);

settings.SetValue("Price", 99.99);
var price = settings.GetValue("Price", 0.0);

// 布尔值
settings.SetValue("Enabled", true);
var enabled = settings.GetValue("Enabled", false);

// 日期时间
settings.SetValue("LastUpdate", DateTime.Now);
var lastUpdate = settings.GetValue("LastUpdate", DateTime.MinValue);

// 枚举
settings.SetValue("Theme", AppTheme.Dark);
var theme = settings.GetValue("Theme", AppTheme.Light);

复杂对象

// 定义配置类
public class WeatherConfig
{
    public string City { get; set; } = "北京";
    public string Unit { get; set; } = "Celsius";
    public int RefreshInterval { get; set; } = 10;
    public List<string> FavoriteCities { get; set; } = new();
}

// 保存对象
var config = new WeatherConfig
{
    City = "上海",
    Unit = "Celsius",
    RefreshInterval = 15,
    FavoriteCities = new List<string> { "北京", "上海", "广州" }
};
settings.SetValue("WeatherConfig", config);

// 读取对象
var savedConfig = settings.GetValue<WeatherConfig>(
    "WeatherConfig",
    new WeatherConfig()
);

集合类型

// 列表
var favoriteColors = new List<string> { "红色", "蓝色", "绿色" };
settings.SetValue("FavoriteColors", favoriteColors);
var colors = settings.GetValue<List<string>>("FavoriteColors", new List<string>());

// 字典
var preferences = new Dictionary<string, string>
{
    ["Language"] = "zh-CN",
    ["Timezone"] = "Asia/Shanghai"
};
settings.SetValue("Preferences", preferences);
var prefs = settings.GetValue<Dictionary<string, string>>(
    "Preferences",
    new Dictionary<string, string>()
);

监听设置变更

订阅变更事件

public class MyPlugin : IPlugin
{
    private ISettingsService? _settings;
    
    public async Task InitializeAsync(IPluginContext context)
    {
        _settings = context.Settings;
        
        // 订阅设置变更事件
        _settings.SettingChanged += OnSettingChanged;
    }
    
    private void OnSettingChanged(object? sender, SettingChangedEventArgs e)
    {
        // e.Key - 变更的设置键
        // e.OldValue - 旧值
        // e.NewValue - 新值
        
        if (e.Key == "ApiKey")
        {
            var newApiKey = e.NewValue as string;
            _logger.LogInformation($"API Key changed to: {newApiKey}");
            
            // 重新初始化服务
            ReinitializeService(newApiKey);
        }
    }
    
    public async Task ShutdownAsync()
    {
        // 取消订阅(防止内存泄漏)
        if (_settings != null)
        {
            _settings.SettingChanged -= OnSettingChanged;
        }
    }
}

在组件中监听

public class MyComponent : ComponentBase
{
    public override Task InitializeAsync()
    {
        // 监听设置变更
        Settings.SettingChanged += OnSettingChanged;
        return Task.CompletedTask;
    }
    
    private void OnSettingChanged(object? sender, SettingChangedEventArgs e)
    {
        switch (e.Key)
        {
            case "Location":
                Location = e.NewValue as string ?? "北京";
                _ = RefreshWeatherAsync();
                break;
                
            case "UseFahrenheit":
                UseFahrenheit = (bool)(e.NewValue ?? false);
                OnPropertyChanged(nameof(DisplayTemperature));
                break;
        }
    }
    
    public override void Dispose()
    {
        // 取消订阅
        Settings.SettingChanged -= OnSettingChanged;
        base.Dispose();
    }
}

创建设置页面

步骤 1: 创建设置页视图

创建 Settings/MyPluginSettingsPage.axaml

<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:vm="using:MyPlugin.ViewModels"
             x:Class="MyPlugin.Settings.MyPluginSettingsPage"
             x:DataType="vm:MyPluginSettingsViewModel">
  
  <ScrollViewer>
    <StackPanel Spacing="16" Margin="24">
      
      <!-- 页面标题 -->
      <TextBlock Text="天气插件设置"
                 FontSize="24"
                 FontWeight="Bold"
                 Margin="0,0,0,8" />
      
      <!-- 基本设置 -->
      <Border Background="{DynamicResource CardBackgroundBrush}"
              CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
              Padding="16"
              BorderBrush="{DynamicResource CardBorderBrush}"
              BorderThickness="1">
        <StackPanel Spacing="12">
          
          <!-- 分组标题 -->
          <TextBlock Text="基本设置"
                     FontSize="16"
                     FontWeight="SemiBold" />
          
          <!-- 城市设置 -->
          <StackPanel Spacing="8">
            <TextBlock Text="城市:" />
            <TextBox Text="{Binding Location, Mode=TwoWay}"
                     Watermark="输入城市名称"
                     Width="300"
                     HorizontalAlignment="Left" />
          </StackPanel>
          
          <!-- API Key -->
          <StackPanel Spacing="8">
            <TextBlock Text="API Key:" />
            <TextBox Text="{Binding ApiKey, Mode=TwoWay}"
                     Watermark="输入 API Key"
                     PasswordChar="●"
                     Width="300"
                     HorizontalAlignment="Left" />
            <TextBlock Text="从 https://api.weather.com 获取"
                       FontSize="12"
                       Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
          </StackPanel>
          
        </StackPanel>
      </Border>
      
      <!-- 显示设置 -->
      <Border Background="{DynamicResource CardBackgroundBrush}"
              CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
              Padding="16"
              BorderBrush="{DynamicResource CardBorderBrush}"
              BorderThickness="1">
        <StackPanel Spacing="12">
          
          <TextBlock Text="显示设置"
                     FontSize="16"
                     FontWeight="SemiBold" />
          
          <!-- 温度单位 -->
          <StackPanel Spacing="8">
            <TextBlock Text="温度单位:" />
            <ComboBox SelectedIndex="{Binding TemperatureUnitIndex, Mode=TwoWay}"
                      Width="200"
                      HorizontalAlignment="Left">
              <ComboBoxItem Content="摄氏度 (°C)" />
              <ComboBoxItem Content="华氏度 (°F)" />
            </ComboBox>
          </StackPanel>
          
          <!-- 刷新间隔 -->
          <StackPanel Spacing="8">
            <TextBlock Text="刷新间隔 (分钟):" />
            <NumericUpDown Value="{Binding RefreshInterval, Mode=TwoWay}"
                           Minimum="5"
                           Maximum="60"
                           Increment="5"
                           Width="200"
                           HorizontalAlignment="Left" />
          </StackPanel>
          
          <!-- 开关选项 -->
          <CheckBox IsChecked="{Binding ShowIcon, Mode=TwoWay}"
                    Content="显示天气图标" />
          
          <CheckBox IsChecked="{Binding EnableNotifications, Mode=TwoWay}"
                    Content="启用天气预警通知" />
          
        </StackPanel>
      </Border>
      
      <!-- 高级设置 -->
      <Border Background="{DynamicResource CardBackgroundBrush}"
              CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
              Padding="16"
              BorderBrush="{DynamicResource CardBorderBrush}"
              BorderThickness="1">
        <StackPanel Spacing="12">
          
          <TextBlock Text="高级设置"
                     FontSize="16"
                     FontWeight="SemiBold" />
          
          <!-- 收藏城市 -->
          <StackPanel Spacing="8">
            <TextBlock Text="收藏城市:" />
            <ListBox ItemsSource="{Binding FavoriteCities}"
                     Height="150"
                     Width="300"
                     HorizontalAlignment="Left" />
            <StackPanel Orientation="Horizontal" Spacing="8">
              <TextBox x:Name="NewCityTextBox"
                       Watermark="添加城市"
                       Width="200" />
              <Button Content="添加"
                      Command="{Binding AddCityCommand}"
                      CommandParameter="{Binding #NewCityTextBox.Text}" />
            </StackPanel>
          </StackPanel>
          
        </StackPanel>
      </Border>
      
      <!-- 操作按钮 -->
      <StackPanel Orientation="Horizontal" Spacing="12">
        <Button Content="保存"
                Command="{Binding SaveCommand}"
                IsDefault="True" />
        <Button Content="重置"
                Command="{Binding ResetCommand}" />
        <Button Content="测试连接"
                Command="{Binding TestConnectionCommand}" />
      </StackPanel>
      
      <!-- 状态提示 -->
      <TextBlock Text="{Binding StatusMessage}"
                 Foreground="{Binding StatusColor}"
                 IsVisible="{Binding !!StatusMessage}" />
      
    </StackPanel>
  </ScrollViewer>
  
</UserControl>

步骤 2: 创建设置页视图模型

创建 ViewModels/MyPluginSettingsViewModel.cs

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;

namespace MyPlugin.ViewModels;

public partial class MyPluginSettingsViewModel : ObservableObject
{
    private readonly ISettingsService _settings;
    private readonly ILogger _logger;
    
    public MyPluginSettingsViewModel(
        ISettingsService settings,
        ILogger logger)
    {
        _settings = settings;
        _logger = logger;
        
        // 加载设置
        LoadSettings();
    }
    
    // === 属性 ===
    
    [ObservableProperty]
    private string _location = "北京";
    
    [ObservableProperty]
    private string _apiKey = "";
    
    [ObservableProperty]
    private int _temperatureUnitIndex = 0;
    
    [ObservableProperty]
    private int _refreshInterval = 10;
    
    [ObservableProperty]
    private bool _showIcon = true;
    
    [ObservableProperty]
    private bool _enableNotifications = true;
    
    [ObservableProperty]
    private ObservableCollection<string> _favoriteCities = new();
    
    [ObservableProperty]
    private string? _statusMessage;
    
    [ObservableProperty]
    private string _statusColor = "Green";
    
    // === 命令 ===
    
    /// <summary>
    /// 保存命令
    /// </summary>
    [RelayCommand]
    private void Save()
    {
        try
        {
            // 保存所有设置
            _settings.SetValue("Location", Location);
            _settings.SetValue("ApiKey", ApiKey);
            _settings.SetValue("UseFahrenheit", TemperatureUnitIndex == 1);
            _settings.SetValue("RefreshInterval", RefreshInterval);
            _settings.SetValue("ShowIcon", ShowIcon);
            _settings.SetValue("EnableNotifications", EnableNotifications);
            _settings.SetValue("FavoriteCities", FavoriteCities.ToList());
            
            ShowStatus("设置已保存", "Green");
            _logger.LogInformation("Settings saved successfully");
        }
        catch (Exception ex)
        {
            ShowStatus($"保存失败: {ex.Message}", "Red");
            _logger.LogError(ex, "Failed to save settings");
        }
    }
    
    /// <summary>
    /// 重置命令
    /// </summary>
    [RelayCommand]
    private void Reset()
    {
        // 重新加载设置
        LoadSettings();
        ShowStatus("已重置到上次保存的值", "Orange");
    }
    
    /// <summary>
    /// 添加城市命令
    /// </summary>
    [RelayCommand]
    private void AddCity(string? city)
    {
        if (string.IsNullOrWhiteSpace(city))
            return;
        
        if (!FavoriteCities.Contains(city))
        {
            FavoriteCities.Add(city);
            ShowStatus($"已添加城市: {city}", "Green");
        }
        else
        {
            ShowStatus("城市已存在", "Orange");
        }
    }
    
    /// <summary>
    /// 测试连接命令
    /// </summary>
    [RelayCommand]
    private async Task TestConnectionAsync()
    {
        ShowStatus("正在测试连接...", "Blue");
        
        try
        {
            // 测试 API 连接
            var result = await TestWeatherApiAsync(ApiKey, Location);
            
            if (result)
            {
                ShowStatus("连接成功!", "Green");
            }
            else
            {
                ShowStatus("连接失败,请检查 API Key 和城市名称", "Red");
            }
        }
        catch (Exception ex)
        {
            ShowStatus($"测试失败: {ex.Message}", "Red");
            _logger.LogError(ex, "Connection test failed");
        }
    }
    
    // === 辅助方法 ===
    
    private void LoadSettings()
    {
        Location = _settings.GetValue("Location", "北京");
        ApiKey = _settings.GetValue("ApiKey", "");
        
        var useFahrenheit = _settings.GetValue("UseFahrenheit", false);
        TemperatureUnitIndex = useFahrenheit ? 1 : 0;
        
        RefreshInterval = _settings.GetValue("RefreshInterval", 10);
        ShowIcon = _settings.GetValue("ShowIcon", true);
        EnableNotifications = _settings.GetValue("EnableNotifications", true);
        
        var cities = _settings.GetValue<List<string>>("FavoriteCities", new List<string>());
        FavoriteCities = new ObservableCollection<string>(cities);
    }
    
    private void ShowStatus(string message, string color)
    {
        StatusMessage = message;
        StatusColor = color;
        
        // 3 秒后清除状态
        Task.Delay(3000).ContinueWith(_ =>
        {
            StatusMessage = null;
        });
    }
    
    private async Task<bool> TestWeatherApiAsync(string apiKey, string location)
    {
        // 实际实现中测试 API 连接
        await Task.Delay(1000);
        return !string.IsNullOrEmpty(apiKey);
    }
}

步骤 3: 注册设置页

在插件入口注册:

public class MyPlugin : IPlugin
{
    public async Task InitializeAsync(IPluginContext context)
    {
        var settingsRegistry = context.Services
            .GetService<ISettingsPageRegistry>();
        
        if (settingsRegistry != null)
        {
            // 注册设置页
            settingsRegistry.RegisterPage(
                title: "天气插件",
                category: "插件",
                icon: "avares://MyPlugin/Assets/settings-icon.png",
                pageFactory: () =>
                {
                    var viewModel = new MyPluginSettingsViewModel(
                        context.Settings,
                        context.Logger
                    );
                    
                    return new MyPluginSettingsPage
                    {
                        DataContext = viewModel
                    };
                }
            );
            
            context.Logger.LogInformation("Settings page registered");
        }
    }
}

设置最佳实践

提供默认值

// ✅ 好:提供合理的默认值
var timeout = settings.GetValue("Timeout", 30);
var apiUrl = settings.GetValue("ApiUrl", "https://api.example.com");

// ❌ 差:不提供默认值
var timeout = settings.GetValue<int>("Timeout", 0); // 0 可能不合理

验证设置值

// ✅ 好:验证设置值
public void SetRefreshInterval(int minutes)
{
    if (minutes < 1 || minutes > 60)
    {
        throw new ArgumentOutOfRangeException(
            nameof(minutes),
            "刷新间隔必须在 1-60 分钟之间"
        );
    }
    
    RefreshInterval = minutes;
    Settings.SetValue("RefreshInterval", minutes);
}

// ❌ 差:不验证
public void SetRefreshInterval(int minutes)
{
    Settings.SetValue("RefreshInterval", minutes); // 可能是非法值
}

使用类型化配置

// ✅ 好:使用强类型配置类
public class PluginConfig
{
    public string ApiKey { get; set; } = "";
    public string Location { get; set; } = "北京";
    public int RefreshInterval { get; set; } = 10;
    public bool EnableNotifications { get; set; } = true;
    
    public void Validate()
    {
        if (string.IsNullOrEmpty(ApiKey))
            throw new InvalidOperationException("API Key is required");
        
        if (RefreshInterval < 1 || RefreshInterval > 60)
            throw new ArgumentOutOfRangeException(nameof(RefreshInterval));
    }
}

// 使用
var config = settings.GetValue<PluginConfig>("Config", new PluginConfig());
config.Validate();

// ❌ 差:分散的设置键
var apiKey = settings.GetValue<string>("ApiKey", "");
var location = settings.GetValue<string>("Location", "");
var interval = settings.GetValue<int>("RefreshInterval", 10);

取消事件订阅

// ✅ 好:在 Dispose 中取消订阅
public class MyComponent : ComponentBase
{
    public override Task InitializeAsync()
    {
        Settings.SettingChanged += OnSettingChanged;
        return Task.CompletedTask;
    }
    
    public override void Dispose()
    {
        Settings.SettingChanged -= OnSettingChanged;
        base.Dispose();
    }
}

// ❌ 差:忘记取消订阅(内存泄漏)
public class MyComponent : ComponentBase
{
    public override Task InitializeAsync()
    {
        Settings.SettingChanged += OnSettingChanged;
        return Task.CompletedTask;
    }
    // 没有 Dispose导致内存泄漏
}

设置迁移

版本升级时的设置迁移

public class MyPlugin : IPlugin
{
    public async Task InitializeAsync(IPluginContext context)
    {
        var settings = context.Settings;
        
        // 检查设置版本
        var settingsVersion = settings.GetValue("SettingsVersion", 1);
        
        if (settingsVersion < 2)
        {
            // 迁移到版本 2
            MigrateToV2(settings);
            settings.SetValue("SettingsVersion", 2);
        }
        
        if (settingsVersion < 3)
        {
            // 迁移到版本 3
            MigrateToV3(settings);
            settings.SetValue("SettingsVersion", 3);
        }
    }
    
    private void MigrateToV2(ISettingsService settings)
    {
        // 例如:重命名设置键
        if (settings.Contains("OldKey"))
        {
            var value = settings.GetValue<string>("OldKey", "");
            settings.SetValue("NewKey", value);
            settings.Remove("OldKey");
        }
    }
    
    private void MigrateToV3(ISettingsService settings)
    {
        // 例如:更改数据格式
        var oldFormat = settings.GetValue<string>("Location", "");
        var newFormat = new LocationConfig
        {
            City = oldFormat,
            Country = "中国"
        };
        settings.SetValue("LocationConfig", newFormat);
        settings.Remove("Location");
    }
}

下一步