IChangeToken通知属性用法

1、有些开发过程中没有属于数据库,使用本地json文件或者ini或者txt或者xml保存的数据
此时在本地资源管理改变了文件的内容,需要重启上位机,那么每次修改本地文件都需要重启软件,不符合客户的需求
2、此时我们需要使用到IChangeToken结合FileSystemWatcher在本地文件更新的时候,操作系统提醒我们文件内容改变,自动回调更改界面数据
以下是全部代码

 <StackPanel Margin="10">
     <TextBlock Text="Id:" FontWeight="Bold" />
     <TextBlock Text="{Binding AppConfig.Id}" Margin="0,5,0,15" />
     <TextBlock Text="Name:" FontWeight="Bold" />
     <TextBlock Text="{Binding AppConfig.Name}" Margin="0,5,0,15" />
 </StackPanel>
public class FileWatcherService : IDisposable
{
    private readonly string _configFilePath;
    private PhysicalFileProvider _fileProvider;
    private IDisposable _changeTokenRegistration;

    public AppConfig CurrentConfig { get; private set; } = new AppConfig();

    public FileWatcherService(string configFilePath)
    {
        _configFilePath = configFilePath;
        _fileProvider = new PhysicalFileProvider(Path.GetDirectoryName(configFilePath));

        // 初始加载配置
        LoadConfig();

        // 开始监听文件变化
        WatchFile();
    }

    private void LoadConfig()
    {
        try
        {
            string json = File.ReadAllText(_configFilePath);
            var newConfig = JsonConvert.DeserializeObject<AppConfig>(json);

            // 重要:更新现有实例的属性(而非替换实例)
            CurrentConfig.Id = newConfig.Id;
            CurrentConfig.Name = newConfig.Name;
        }
        catch
        {
            CurrentConfig.Id = 0;
            CurrentConfig.Name = "Default";
        }
    }

    private void WatchFile()
    {
        IChangeToken changeToken = _fileProvider.Watch(Path.GetFileName(_configFilePath));

        _changeTokenRegistration = changeToken.RegisterChangeCallback(_ =>
        {
            Thread.Sleep(300); // 等待文件写入完成
            LoadConfig();
            WatchFile(); // 重新注册
        }, null);
    }

    public void Dispose()
    {
        _changeTokenRegistration?.Dispose();
        _fileProvider?.Dispose();
    }
}

我使用了Prism框架,这里你们跟据自己的IOC框架自行修改

  public partial class App
  {
      protected override Window CreateShell()
      {
          return Container.Resolve<MainWindow>();
      }

      protected override void RegisterTypes(IContainerRegistry containerRegistry)
      {
          var configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json");
          containerRegistry.RegisterSingleton<FileWatcherService>(() => new FileWatcherService(configPath));
      }

  }
public class MainWindowViewModel : BindableBase
{
    private AppConfig _appConfig;

    public AppConfig AppConfig
    {
        get => _appConfig ?? (_appConfig = new AppConfig());
        set => SetProperty(ref _appConfig, value);
    }

    private readonly FileWatcherService _fileWatcher;

    public MainWindowViewModel(FileWatcherService fileWatcher)
    {
        _fileWatcher = fileWatcher;

        // 初始化时立即赋值
        AppConfig.Id = _fileWatcher.CurrentConfig.Id;
        AppConfig.Name = _fileWatcher.CurrentConfig.Name;

        // 监听后续变化
        _fileWatcher.CurrentConfig.PropertyChanged += OnConfigChanged;
        
    }

    private void OnConfigChanged(object sender, PropertyChangedEventArgs e)
    {
        // 确保在UI线程更新(WPF要求)
        Application.Current.Dispatcher.Invoke(() =>
        {
            AppConfig.Id = _fileWatcher.CurrentConfig.Id;
            AppConfig.Name = _fileWatcher.CurrentConfig.Name;
        });
    }
}

一、文件修改后的完整执行顺序
假设用户修改了 config.json 文件,流程如下:

mermaid
复制
sequenceDiagram
participant 用户操作
participant 操作系统
participant FileWatcherService
participant MainWindowViewModel
participant WPF界面

用户操作->>操作系统: 保存config.json
操作系统->>FileWatcherService: 触发FileSystemWatcher事件
FileWatcherService->>FileWatcherService: IChangeToken回调执行
FileWatcherService->>FileWatcherService: LoadConfig()
FileWatcherService->>AppConfig: 更新Id/Name属性
AppConfig->>MainWindowViewModel: 触发PropertyChanged事件
MainWindowViewModel->>WPF界面: Dispatcher.Invoke更新UI

二、关键步骤详解

  1. 操作系统检测文件变化
    当文件被修改时,Windows 内核的 文件系统过滤器驱动 会检测到更改,并通知所有订阅了该目录/文件的监听器(包括 PhysicalFileProvider 底层的 FileSystemWatcher)。

  2. IChangeToken 回调触发
    PhysicalFileProvider.Watch() 创建的 IChangeToken 内部封装了 FileSystemWatcher,其回调触发条件:

// 伪代码展示PhysicalFileProvider的核心逻辑

public IChangeToken Watch(string filter)
{
    var watcher = new FileSystemWatcher
    {
        Path = _rootPath,
        Filter = filter,
        NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName
    };
    
    watcher.Changed += (s, e) => _changeToken.NotifyChanged();
    return _changeToken;
}

延迟问题:某些编辑器(如记事本)会先创建临时文件再替换原文件,此时需 Thread.Sleep(300) 等待写入完成。

三、IChangeToken 的通知机制深度解析
1、 底层原理
PhysicalFileProvider 的实现:
使用 FileSystemWatcher 监听文件系统的 Changed/Created/Deleted/Renamed 事件。
当事件触发时,调用 IChangeToken 注册的回调。

public IDisposable RegisterChangeCallback(Action<object> callback, object state)
{
    // 将回调存入列表,当FileSystemWatcher事件触发时遍历执行
    _callbacks.Add(new CallbackRegistration(callback, state));
    return new DisposableRegistration(this, callback);
}

2、 文件系统事件的局限性
不可靠事件:某些情况下(如磁盘满、病毒扫描),事件可能丢失。
重复触发:部分编辑器会触发多次事件(如VS Code保存时触发2次)。
解决方案:

private DateTime _lastEventTime;
private void WatchFile()
{
    IChangeToken changeToken = _fileProvider.Watch(Path.GetFileName(_configFilePath));
    _changeTokenRegistration = changeToken.RegisterChangeCallback(_ =>
    {
        if ((DateTime.Now - _lastEventTime).TotalMilliseconds < 500) return;
        _lastEventTime = DateTime.Now;
        LoadConfig();
        WatchFile();
    }, null);
}

四、性能优化建议
1、 减少文件读取开销

private void LoadConfig()
{
    // 使用FileStream+FileShare.ReadWrite避免文件锁定问题
    using (var stream = new FileStream(_configFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
    using (var reader = new StreamReader(stream))
    {
        string json = reader.ReadToEnd();
        // 其余逻辑不变...
    }
}

2、 选择性反序列化
// 使用JsonTextReader仅读取需要的字段

using (var reader = new JsonTextReader(new StringReader(json)))
{
    while (reader.Read())
    {
        if (reader.TokenType == JsonToken.PropertyName && reader.Value.ToString() == "Name")
        {
            reader.Read();
            CurrentConfig.Name = reader.Value.ToString();
        }
    }
}

3、异步处理(防止UI卡顿)

_changeTokenRegistration = changeToken.RegisterChangeCallback(async _ =>
{
    await Task.Delay(300); // 异步等待
    LoadConfig();
    WatchFile();
}, null);

五、常见问题排查
回调未触发:
检查文件路径是否包含非法字符。
确认文件修改后是否触发了 LastWrite 事件(可用 Process Monitor 工具监控)。
UI未更新:
确保 PropertyChanged 事件被触发(调试断点检查)。
验证 Dispatcher.Invoke 是否执行。
多次触发:
添加时间戳判断(如上文去重逻辑)。

posted @ 2025-04-16 22:27  孤沉  阅读(35)  评论(0)    收藏  举报