C# 基于DI和JSON序列化的界面配置参数方案
基于DI和JSON序列化的界面配置参数方案
如题,该功能的实现是基于Microsoft.Extensions.DependencyInjection
和Newtonsoft.Json
实现WPF界面参数的保存和加载。具体的DI(依赖注入)工具可以有很多选择,同样数据序列化和反序列化工具也有很多。
- DI
- Ninject
- Unity
- Autofac
- …
- 对象序列化和反序列化
System.Text.Json
Json文件System.Xml.Serialization
Xml文件- …
本文基于Microsoft.Extensions.DependencyInjection
和Newtonsoft.Json
进行界面参数(ViewModel)的保存和加载。
MVVM中的DI说明
详情参考mvvm.toolkit依赖注入说明:
learn.microsoft.com/zh-cn/dotnet/communitytoolkit/mvvm/ioc
ViewModel注册
public sealed partial class App : Application
{public App(){Services = ConfigureServices();this.InitializeComponent();}/// <summary>/// Gets the current <see cref="App"/> instance in use/// </summary>public new static App Current => (App)Application.Current;/// <summary>/// Gets the <see cref="IServiceProvider"/> instance to resolve application services./// </summary>public IServiceProvider Services { get; }/// <summary>/// Configures the services for the application./// </summary>private static IServiceProvider ConfigureServices(){var services = new ServiceCollection();services.AddSingleton<IFilesService, FilesService>();services.AddSingleton<MainViewModel>();return services.BuildServiceProvider();}
}
ViewModel获取
MainViewModel mainVm = App.Current.Services.GetService<MainViewModel>();
参数保存
/// <summary>/// 保存配置文件/// 将当前DI容器中的实例序列化保存到配置文件中 /// 注意:不需要保存的属性请添加<property: JsonIgnore>特性/// </summary>/// <typeparam name="T">需要保存的ViewModel类型,例如<see cref="ViewModels.Main.BenchParamViewModel"/></typeparam>public static void Save<T>() where T : class{if (!Directory.Exists(CONFIG_UI)){Directory.CreateDirectory(CONFIG_UI);}Type type = typeof(T);var appRunModel = App.Current.Services.GetService<T>();var contect = JsonConvert.SerializeObject(appRunModel);var fileNameFull = System.IO.Path.Combine(CONFIG_UI, type.Name + ".json");File.WriteAllText(fileNameFull, contect);}
保存原理:
App.Current.Services.GetService<T>()
获取到当前的实例对象- 然后将对象通过
JsonConvert.SerializeObject(appRunModel)
序列化为Json文本。
为了简化保存的信息内容,在ViewModel中对部分属性进行忽略操作。主要的方式是在属性上添加[JsonIgnore]
特性说明。值得注意的是,如果ViewModel中采用[ObservableProperty]
定义通知属性时,为了将[JsonIgnore]
特性传递到公共的属性上,需要添加property
字段属性传递说明,如下所示:
[ObservableProperty][property: JsonIgnore]private int blowingTime = 55;
参数加载
/// <summary>/// 根据类型加载配置文件/// 自动赋值修改DI容器中的实例/// </summary>/// <typeparam name="T">需要加载ViewModel类型,例如<see cref="ViewModels.Main.BenchParamViewModel"/></typeparam>/// <returns></returns>public static T? Load<T>() where T : class{Type type = typeof(T);var fileNameFull = System.IO.Path.Combine(CONFIG_UI, type.Name + ".json");if (!File.Exists(fileNameFull)) // 当前配置不存在 {// 加载默认配置文件fileNameFull = System.IO.Path.Combine(CONFIG_UI_DEFAULT, type.Name + ".json");if (!File.Exists(fileNameFull)) // 默认配置也不存在return null;}var content = File.ReadAllText(fileNameFull);var jsonFileModel = JsonConvert.DeserializeObject<T>(content);if (jsonFileModel == null) return null;var appRunModel = App.Current.Services.GetService<T>();SetProperties(appRunModel, jsonFileModel);return appRunModel;}
参数加载的原理:
- 读取Json文件内容,直接反序列化为对象
JsonConvert.DeserializeObject<T>(content)
。 - 然后从DI
App.Current.Services.GetService<T>()
获取当前软件正在运行对象。最后把Json反序列化对象赋值给正在运行的对象。
对ViewModel进行Json序列化时,需要注意:ViewModel中大部分情况下,默认构造函数会存在很多对信息的初始化的代码,因此如果序列化时,采用冲突的默认构造函数,会导致很多参数信息的多次构造,导致与加载的Json文件的内容不一致。
解决办法需要在ViewModel中指明Json序列化时的使用的构造函数,如下所示:
partial class InformationInputViewModel : ObservableObject{public InformationInputViewModel(){WeakReferenceMessenger.Default.Register<string>(this, (r, m) =>{if (m == "DbUpdate"){Gcus = PetSetDbBll.SqliteDb.Queryable<Can>().Where(o => o.Temp == "GCU").ToList();Ecus = PetSetDbBll.SqliteDb.Queryable<Can>().Where(o => o.Temp == "ECU").ToList();Rules = PetSetDbBll.SqliteDb.Queryable<RunRule>().ToList();if (gcu != null)Gcu = gcus.Where(t => t.Name == gcu.Name).FirstOrDefault();if (ecu != null)Ecu = ecus.Where(t => t.Name == ecu.Name).FirstOrDefault();if (Rule != null)Rule = rules.Where(t => t.NameVer == rule.NameVer).FirstOrDefault();}});}[JsonConstructor]public InformationInputViewModel(string json) { }}
由于正在运行的对象是由DI进行管理的,所以赋值只能修改值。为了赋值的通用性,采用反射进行属性的赋值。如果有一些赋值的特殊处理,都可以在赋值进行筛选。
/// <summary>/// 设置对象属性值/// 让target = source /// </summary>/// <param name="target">需要设置的目标,被修改的对象</param>/// <param name="source">数据源,被修改对象的修改信息获取的数据源</param>private static void SetProperties(object target, object source){var targetType = target.GetType();var sourceType = source.GetType();var properties = targetType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty);foreach (var prop in properties){var jsonIgnoreAttribute = prop.GetCustomAttribute<JsonIgnoreAttribute>();if (jsonIgnoreAttribute != null){continue; // 跳过带有JsonIgnore特性的属性}if (!prop.CanWrite) continue;var sourceProp = sourceType.GetProperty(prop.Name);if (sourceProp == null) continue;var sourceValue = sourceProp.GetValue(source);// 如果属性值不为 null,赋值给目标对象if (sourceValue != null){// 特殊处理 ObservableCollection 类型的属性if (prop.PropertyType.IsGenericType && (prop.PropertyType.GetGenericTypeDefinition() == typeof(ObservableCollection<>) || prop.PropertyType.GetGenericTypeDefinition() == typeof(List<>))){var collection = prop.GetValue(target) as System.Collections.IList;if (collection != null){collection.Clear(); // 清空原有集合foreach (var item in (System.Collections.IEnumerable)sourceValue){collection.Add(item); // 添加新元素}}}else if (prop.PropertyType.IsClass && prop.PropertyType != typeof(string) && prop.PropertyType != typeof(Brush)){// 如果属性是类类型(非字符串),递归处理var targetValue = prop.GetValue(target);if (targetValue != null){SetProperties(targetValue, sourceValue);}}else{if (prop.CanWrite)prop.SetValue(target, sourceValue);}}}}
总结
以上就是基于DI、JSON序列化、反射实现的ViewModel参数配置参数保存和加载功能。可以普遍的应用到各个项目中,需要轻微改造:
- ViewModel中明确哪些不需要保存,添加
[JsonIgnore]
特性 - ViewModel中明确序列化和反序列时的构造函数
[JsonConstructor]
SetProperties
反射赋值函数可能需要面相项目进行稍微修改
其他未采用方案
方案1(使用IMemeryParam接口)
配置定义
定义IMemeryParam
接口,封装保存、加载和配置文件名接口内容,如果需要进行参数保存的ViewModel继承这个接口,并实现对应的函数接口和配置文件名。
每个ViewModel的配置参数独立的存储到一个文件中。
本地config_ui
文件夹中存储每一个需要保存的参数,一类参数存储到一个独立的文件中,以ViewModel进行分类。
本地config_ui\default
文件夹中备份了默认参数,系统发布的版本中,默认情况下会有当前版本的最新默认配置,该配置用于恢复配置时使用。
配置应用
外部定义一个静态类,用于管理参数保存接口。
配置记忆的加载:在ViewModel的构造函数中,自动调用Load函数,实现配置记忆获取。
默认配置说明
如果Load时,配置记忆信息为空,首先从default
中查找默认配置,并加载。
如果default
中不存在默认配置,则使用程序预定义参数信息作为默认配置。
配置文件格式说明
文件的格式采用json,使用Newtonsoft.Json的序列化和反序列化,可以很方便的实现文本内容与实例对象的转换。
示例
PETDomain
public static class PETDomain{public static string BaseDirectory => AppDomain.CurrentDomain.BaseDirectory;public static string CONFIG_UI => System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config_ui");public static string CONFIG_UI_DEFAULT => System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config_ui", "default");public static void Save(){App.Current.Services.GetService<BenchParamViewModel>().Save();}}
BenchParamViewModel 示例
partial class BenchParamViewModel : ObservableObject, IMemeryParam{[ObservableProperty]string header = string.Empty;[ObservableProperty]private ParamDisplayGridModel paramDisplayGridModel = new ParamDisplayGridModel(){Title = "台架参数",GridModelColl = new ObservableCollection<ParamDisplayModel>{new ParamDisplayModel{Title = "进水水压Kpa",Value =null,ValueSource="进水水压"},new ParamDisplayModel{Title ="汽油压力kPa",Value =null,ValueSource= "汽油压力"},new ParamDisplayModel{Title ="电瓶电压V", Value =null,ValueSource="电瓶电压"},new ParamDisplayModel{Title = "水箱温度℃",Value =null,ValueSource="水箱温度"},new ParamDisplayModel{Title ="进水温度℃",Value =null,ValueSource="进水温度"},new ParamDisplayModel{Title ="出水温度℃", Value =null,ValueSource="出水温度"}}};//[ObservableProperty]//private ParamDisplayGridModel paramDisplayGridModel = ParamSaving.Load(ParamType.台架参数);#region IMemeryParampublic void Save(string path = null){if (path == null) path = System.IO.Path.Combine(PETDomain.CONFIG_UI, ParamName);FileInfo file = new FileInfo(path);if (!Directory.Exists(file.Directory.FullName)){Directory.CreateDirectory(file.Directory.FullName);}var contect = JsonConvert.SerializeObject(paramDisplayGridModel);var fileNameFull = System.IO.Path.Combine(path);File.WriteAllText(fileNameFull, contect);}public bool Load(string path = null){if (path == null) path = System.IO.Path.Combine(PETDomain.CONFIG_UI, ParamName);// 当前配置if (!File.Exists(path)) // 当前配置不存在 {path = System.IO.Path.Combine(PETDomain.CONFIG_UI_DEFAULT, ParamName); // 默认配置if (!File.Exists(path)) // 默认配置也不存在{return false;}}var content = File.ReadAllText(path);ParamDisplayGridModel = JsonConvert.DeserializeObject<ParamDisplayGridModel>(content);return true;}public string ParamName => "台架参数.json";#endregionpublic BenchParamViewModel(){// 注册一个消息处理程序,监听 DataSourceModel<double?> 类型的消息WeakReferenceMessenger.Default.Register<DataSourceModel<double?>>(this, (r, m) =>{// 当接收到消息时,调用 UpdataValue 方法更新参数值UpdataValue(m);});// 加载保存参数Load();}// UpdataValue 方法用于更新参数值private void UpdataValue(DataSourceModel<double?> dataSource){// 从 ParamDisplayGridModel 的 GridModelColl 中筛选出与 dataSource.paraname 匹配的 ParamDisplayModel 对象var models = ParamDisplayGridModel.GridModelColl.Where(o => o.ValueSource == dataSource.paraname).ToList();// 遍历筛选出的 ParamDisplayModel 对象foreach (var displayModel in models){// 如果当前模型的值与数据源的值不同,则更新当前模型的值if (displayModel.Value != dataSource.value){displayModel.Value = dataSource.value;}}}}
}
方案2(使用ParamSaving公共管理配置)
配置定义
ParamSaving
提供Load() 和Save()函数,这两个函数可以集中的管理整个项目所有的需要保存的参数信息。
Load()
Load()函数需要传递一个ParamType的参数,用于标识加载的参数类型,该参数类型唯一对应一个配置文件,获取后的内容转换成对应的参数内容返回对象。
在ViewModel中需要记忆功能的参数需要调用Load函数获取配置记忆的对象作为当前程序启动的配置信息。
Save()
Save()函数需要传入需要保存的参数对象和参数类型ParamType,直接将参数对象转换成Json文本直接存储到ParamType对应的配置文件即可。
同时为了对外方便,提供一个无参的*Save()*函数,用于集中存储所有参数内容。
配置文件格式说明
文件的格式采用json,使用Newtonsoft.Json的序列化和反序列化,可以很方便的实现文本内容与实例对象的转换。
示例
public static class ParamSaving{static string basePath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config_ui");/// <summary>/// 根据文件名加载参数信息/// </summary>/// <param name="fileName"></param>/// <returns></returns>public static T? Load<T>(ParamType fileName) where T : class{var fileNameFull = System.IO.Path.Combine(basePath, fileName + ".json");var content = File.ReadAllText(fileNameFull);return JsonConvert.DeserializeObject<T>(content);}public static void ClearDirectory(string directoryPath){// 检查目录是否存在if (Directory.Exists(directoryPath)){// 获取目录中的所有文件并删除string[] files = Directory.GetFiles(directoryPath);foreach (string file in files){File.Delete(file);}// 如果需要删除子目录中的文件,可以递归调用string[] subDirectories = Directory.GetDirectories(directoryPath);foreach (string subDirectory in subDirectories){ClearDirectory(subDirectory);}}else{Console.WriteLine("指定的目录不存在!");}}/// <summary>/// 对外统一管理所有参数存储接口/// </summary>public static void Save(){Save(App.Current.Services.GetService<BenchParamViewModel>().ParamDisplayGridModel, ParamType.台架参数);Save(App.Current.Services.GetService<BenchParamViewModel>().ParamDisplayGridModel, ParamType.其他啊啊参数);}/// <summary>/// 将参数信息保存到指定文件/// </summary>/// <param name="model"></param>/// <param name="fileName"></param>public static void Save(ParamDisplayGridModel model, ParamType fileName){if (!Directory.Exists(basePath)){Directory.CreateDirectory(basePath);}ClearDirectory(basePath);var contect = JsonConvert.SerializeObject(model);var fileNameFull = System.IO.Path.Combine(basePath, fileName + ".json");File.WriteAllText(fileNameFull, contect);}}
ViewModel中加载参数
[ObservableProperty]private ParamDisplayGridModel paramDisplayGridModel = ParamSaving.Load<ParamDisplayGridModel>(ParamType.台架参数);