C#簡單配置類及數據綁定
簡介
本文實現一個簡單的配置類,原理比較簡單,適用於一些小型項目。主要實現以下功能:
- 保存配置到json文件
- 從文件或實例載入配置類的屬性值
- 數據綁定到介面控制項
一般情況下,項目都會提供配置的設置介面,很少手動更改配置文件,所以選擇以json文件保存配置數據。
配置基類
為了方便管理,項目中的配置一般是按用途劃分到不同的配置類中,保存時也是保存到多個配置文件。所以,我們需要實現一個配置基類,然後再派生出不同用途的配置類。
配置基類需要引用Json.NET,繼承數據綁定基類BindableBase,實現從其它實例載入數據的功能(參考C# 兩個具有相同屬性的類賦值),基類程式碼如下:
using Newtonsoft.Json;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
namespace TestConfigDll
{
/// <summary>
/// 配置基類,實現配置類的載入、保存功能
/// </summary>
/// <typeparam name="T"></typeparam>
public class ConfigBase<T> : BindableBase where T : class
{
/// <summary>
/// 文件保存路徑
/// </summary>
private string filePath = "";
/// <summary>
/// 設置文件保存路徑
/// </summary>
/// <param name="filePath"></param>
public virtual void SetPath(string filePath)
{
this.filePath = filePath;
}
/// <summary>
/// 保存到本地文件
/// </summary>
public virtual void Save()
{
string dirStr = Path.GetDirectoryName(filePath);
if (!Directory.Exists(dirStr))
{
Directory.CreateDirectory(dirStr);
}
string jsonStr = JsonConvert.SerializeObject(this);
File.WriteAllText(filePath, jsonStr);
}
/// <summary>
/// 從本地文件載入
/// </summary>
public virtual void Load()
{
if (File.Exists(filePath))
{
var config = JsonConvert.DeserializeObject<T>(File.ReadAllText(filePath));
foreach (PropertyInfo pro in typeof(T).GetProperties())
{
pro.SetValue(this, pro.GetValue(config));
}
}
}
/// <summary>
/// 從其它實例載入
/// </summary>
/// <param name="config"></param>
public virtual void Load(T config)
{
foreach (PropertyInfo pro in typeof(T).GetProperties())
{
pro.SetValue(this, pro.GetValue(config));
}
}
/// <summary>
/// 從其它實例載入,僅載入指定的屬性
/// </summary>
/// <param name="config"></param>
/// <param name="proName"></param>
public virtual void Load(T config, IEnumerable<string> proName = null)
{
foreach (PropertyInfo pro in typeof(T).GetProperties())
{
if (proName == null || proName.Contains(pro.Name))
{
pro.SetValue(this, pro.GetValue(config));
}
}
}
}
}
數據綁定基類BindableBase的實現參考WPF之數據綁定基類,程式碼如下:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace TestConfigDll
{
public class BindableBase : INotifyPropertyChanged
{
/// <summary>
/// 屬性值更改時發生
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// 檢查屬性是否已與設置值相等,設置屬性並僅在必要時通知偵聽器。
/// </summary>
/// <typeparam name="T">屬性的類型</typeparam>
/// <param name="storage">對同時具有getter和setter的屬性的引用</param>
/// <param name="value">屬性的所需值</param>
/// <param name="propertyName">用於通知偵聽器的屬性的名稱,此值是可選的,從支援CallerMemberName的編譯器調用時可以自動提供。</param>
/// <returns>如果值已更改,則為True;如果現有值與所需值匹配,則為false。</returns>
protected virtual bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(storage, value)) return false;
storage = value;
RaisePropertyChanged(propertyName);
return true;
}
/// <summary>
/// 檢查屬性是否已與設置值相等,設置屬性並僅在必要時通知偵聽器。
/// </summary>
/// <typeparam name="T">屬性的類型</typeparam>
/// <param name="storage">對同時具有getter和setter的屬性的引用</param>
/// <param name="value">屬性的所需值</param>
/// <param name="propertyName">用於通知偵聽器的屬性的名稱,此值是可選的,從支援CallerMemberName的編譯器調用時可以自動提供。</param>
/// <param name="onChanged">屬性值更改後調用的操作。</param>
/// <returns>如果值已更改,則為True;如果現有值與所需值匹配,則為false。</returns>
protected virtual bool SetProperty<T>(ref T storage, T value, Action onChanged, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(storage, value)) return false;
storage = value;
onChanged?.Invoke();
RaisePropertyChanged(propertyName);
return true;
}
/// <summary>
/// 引發此對象的PropertyChanged事件。
/// <param name="propertyName">用於通知偵聽器的屬性的名稱,此值是可選的,從支援CallerMemberName的編譯器調用時可以自動提供。</param>
protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
/// <summary>
/// 引發此對象的PropertyChanged事件。
/// </summary>
/// <param name="args">PropertyChangedEventArgs參數</param>
protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
{
PropertyChanged?.Invoke(this, args);
}
}
}
派生配置類
配置類的屬性定義不能使用縮寫形式,不用單例模式時可刪除「配置單例」的程式碼,配置類程式碼如下:
class TestConfig :ConfigBase<TestConfig>
{
#region 配置單例
/// <summary>
/// 唯一實例
/// </summary>
public static TestConfig Instance { get; private set; } = new TestConfig();
/// <summary>
/// 靜態構造函數
/// </summary>
static TestConfig()
{
Instance.SetPath("Config\\" + nameof(TestConfig) + ".json");
Instance.Load();
}
#endregion
#region 屬性定義
private string configStr = "";
public string ConfigStr
{
get
{
return this.configStr;
}
set
{
SetProperty(ref this.configStr, value);
}
}
private int configInt =0;
public int ConfigInt
{
get
{
return this.configInt;
}
set
{
if (value > 100)
{
SetProperty(ref this.configInt, value);
}
}
}
private bool configBool = false;
public bool ConfigBool
{
get
{
return this.configBool;
}
set
{
SetProperty(ref this.configBool, value);
}
}
#endregion
}
數據綁定
一般數據綁定會定義一個Model類、ViewModel類,本文為了演示方便使用配置類同時承擔兩者的角色。
Winform中的數據綁定
先設計一個簡單的介面,如下所示:
配置數據的載入、保存不用對每個控制項進行操作,後台程式碼如下:
public partial class Form1 : Form
{
private TestConfig testConfig = new TestConfig();
private List<string> proName = new List<string>();
public Form1()
{
InitializeComponent();
string textProName = nameof(textBox1.Text);
textBox1.DataBindings.Add(textProName, testConfig, nameof(testConfig.ConfigStr));
textBox2.DataBindings.Add(textProName, testConfig, nameof(testConfig.ConfigInt));
string checkedProName= nameof(checkBox1.Checked);
checkBox1.DataBindings.Add(checkedProName, testConfig, nameof(testConfig.ConfigBool));
proName.Add(textBox1.DataBindings[0].BindingMemberInfo.BindingField);
proName.Add(textBox2.DataBindings[0].BindingMemberInfo.BindingField);
proName.Add(checkBox1.DataBindings[0].BindingMemberInfo.BindingField);
}
private void button1_Click(object sender, EventArgs e)
{
testConfig.Load(TestConfig.Instance, proName);
}
private void button2_Click(object sender, EventArgs e)
{
TestConfig.Instance.Load(testConfig, proName);
TestConfig.Instance.Save();
}
}
如上所示,testConfig作為中轉,可以根據需求載入、保存配置類的部分或全部屬性。如果對Winform下的數據綁感興趣,可以參考Winform 普通控制項的雙向綁定。
WPF下的數據綁定
先設計一個簡單的介面,如下所示:
<Window x:Class="WpfApp1.MainWindow"
xmlns="//schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="//schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="//schemas.microsoft.com/expression/blend/2008"
xmlns:mc="//schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="208" Width="306.667">
<Grid>
<Label Content="ConfigStr:" HorizontalAlignment="Left" Margin="18,16,0,0" VerticalAlignment="Top"/>
<TextBox HorizontalAlignment="Left" Height="23" Margin="113,20,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="156" Text="{Binding Path=ConfigStr ,Mode=TwoWay}"/>
<Label Content="ConfigInt:" HorizontalAlignment="Left" Margin="18,44,0,0" VerticalAlignment="Top"/>
<TextBox HorizontalAlignment="Left" Height="23" Margin="113,48,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="156" Text="{Binding Path=ConfigInt ,Mode=TwoWay}"/>
<Label Content="ConfigBool:" HorizontalAlignment="Left" Margin="18,74,0,0" VerticalAlignment="Top"/>
<CheckBox Content="" HorizontalAlignment="Left" Margin="118,84,0,0" VerticalAlignment="Top" IsChecked="{Binding Path=ConfigBool ,Mode=TwoWay}"/>
<Button Content="載入" HorizontalAlignment="Left" Margin="24,129,0,0" VerticalAlignment="Top" Width="75" Click="Button_Click"/>
<Button Content="保存" HorizontalAlignment="Left" Margin="194,129,0,0" VerticalAlignment="Top" Width="75" Click="Button_Click_1"/>
</Grid>
</Window>
相對於Winform,WPF控制項綁定的操作由XAML實現,後台程式碼如下:
/// <summary>
/// MainWindow.xaml 的交互邏輯
/// </summary>
public partial class MainWindow : Window
{
private TestConfig testConfig = new TestConfig();
public MainWindow()
{
InitializeComponent();
this.DataContext = testConfig;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
testConfig.Load(TestConfig.Instance);
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
TestConfig.Instance.Load(testConfig);
TestConfig.Instance.Save();
}
}
上面的程式碼比較粗糙,沒有記錄已經綁定的屬性,實際使用時可以進一步優化。