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
二、关键步骤详解
-
操作系统检测文件变化
当文件被修改时,Windows 内核的 文件系统过滤器驱动 会检测到更改,并通知所有订阅了该目录/文件的监听器(包括 PhysicalFileProvider 底层的 FileSystemWatcher)。 -
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 是否执行。
多次触发:
添加时间戳判断(如上文去重逻辑)。

浙公网安备 33010602011771号