C#, 컨트롤 값을 바인딩하여 JSON 파일과 동기화하기
바인딩(DataBindings) 사용 목적
데이터 바인딩은 컨트롤 속성을 데이터 클래스 속성과 동기화하기 위해 사용됩니다.
여기서는 컨트롤 값을 변경할 때마다 자동으로 파일로 저장하기 위해 사용합니다. 앱을 실행하면 파일을 읽어서 컨트롤 값 속성을 변경합니다.
이때, 컨트롤 값이 변경되는 이벤트를 사용한다면 코드가 복잡해지고, 컨트롤마다 이벤트 이름이 다르며, 유지보수가 어렵습니다.
선택을 변경할 때마다
JSON 형식 파일로 저장됩니다.
딥시크가 알려주는 C# 프로그래밍
텍스트 박스의 문자열 또는 체크박스 선택 상태를 파일로 저장하기
(참조)는 별도의 라이브러리로 만들어 참조(reference)할 수 있는 클래스를 말합니다.
(앱)은 앱에 포함되어 사용자정의를 해야 하는 클래스를 말합니다.
특정 기능만 하는 것이 아닌, 범용 클래스는 <형식명>으로 일반화(Generic)를 사용하는 것이 좋습니다. T는 원하는 이름을 사용할 수 있으며, T가 기본 문자입니다.
파일 읽기·저장용 클래스(참조)
T 형식의 속성을 파일로 저장하고, 파일을 읽어서 T 형식으로 전달하는 클래스입니다.
Microsoft.Extensions NuGet 패키지를 사용합니다. Microsoft.Extensions.Configuration.Json 패키지와 업데이트된 System.Text.Json 패키지가 포함되기 때문에 .NETFramework 시절에서 유용했던 Newtonsoft.Json은 필요 없습니다.😉
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
public class SettingsManager<T> where T : class, new()
{
private readonly string _configFilePath;
public SettingsManager(string configFilePath)
{
_configFilePath = configFilePath;
}
public T Load()
{
try
{
var configuration = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile(_configFilePath, optional: true, reloadOnChange: true)
.Build();
var settings = new T();
configuration.Bind(settings);
return settings;
}
catch (Exception ex)
{
MessageBox.Show($"Failed to load settings: {ex.Message}");
return null;
}
}
public void Save(T settings)
{
try
{
JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions { WriteIndented = true };
var jsonString = JsonSerializer.Serialize(settings, _jsonSerializerOptions);
File.WriteAllText(_configFilePath, jsonString);
}
catch (Exception ex)
{
throw new($"Failed to save settings: {ex.Message}");
}
}
}
설정 클래스의 부모 클래스(참조)
설정 클래스들의 공통 코드를 상위 클래스에 정의합니다.
public abstract class SettingsBase<T> : INotifyPropertyChanged where T : class, new()
{
private static readonly Lazy<T> defaultInstance = new(() =>
{
var instance = new T();
var settingsBase = instance as SettingsBase<T>;
settingsBase?.Load();
settingsBase?.Init();
return instance;
});
public static T Default => defaultInstance.Value;
private readonly string _configFilePath;
private readonly SettingsManager<T> _settingsManager;
protected SettingsBase(string configFilePath)
{
_configFilePath = configFilePath;
_settingsManager = new SettingsManager<T>(_configFilePath);
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new(propertyName));
}
public void Load()
{
try
{
var loadedSettings = _settingsManager.Load();
if (loadedSettings != null)
{
CopySettings(loadedSettings);
}
}
catch (Exception ex)
{
throw new($"Failed to load settings: {ex.Message}");
}
}
public void Save()
{
try
{
_settingsManager.Save(this as T);
}
catch (Exception ex)
{
throw new($"Failed to save settings: {ex.Message}");
}
}
protected void CopySettings(T source)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
if (GetType() != source.GetType())
throw new ArgumentException("Source and target types must be the same.");
var properties = GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var property in properties)
{
if (property.CanRead && property.CanWrite)
{
var value = property.GetValue(source);
property.SetValue(this, value);
}
}
}
protected virtual void Init()
{
}
}
INotifyPropertyChanged 인터페이스를 상속받아서 속성 변경 이벤트를 사용합니다.
where T : class, new() 조건으로 개체화 가능한 클래스만 상속받도록 합니다.
싱글톤 인스턴스를 사용하여 메모리 상에서 한 개의 개체만 생성되도록 합니다.
설정 클래스(앱)
설정 클래스들은 상위 클래스를 상속받아서 코드를 간결하게 유지할 수 있습니다.
이 클래스는 앱에 포함되며 사용자 정의하는 클래스입니다. 필요에 따라 코드를 편집합니다.
public class AppSettings : SettingsBase<AppSettings>
{
public string WorkRootPath { get; set; } = string.Empty;
public string OriginalRootPath { get; set; } = string.Empty;
private ObservableCollection<BindingDataModel<bool>> _booleanValues;
public ObservableCollection<BindingDataModel<bool>> BooleanValues
{
get => _booleanValues;
set
{
if (_booleanValues != value)
{
foreach (var item in _booleanValues)
{
item.PropertyChanged -= OnBooleanValueChanged;
}
_booleanValues = value;
if (_booleanValues != null)
{
foreach (var item in _booleanValues)
{
item.PropertyChanged += OnBooleanValueChanged;
}
}
}
}
}
public AppSettings() : base("appSettings.json")
{
_booleanValues = new ObservableCollection<BindingDataModel<bool>>();
}
public void Init()
{
var controlNames = new[]
{
ControlNames.checkBox_Country.ToString(),
ControlNames.checkBox_Responsiveness.ToString(),
ControlNames.checkBox_UnlockByExploration.ToString(),
ControlNames.checkBox_UnlockByRank.ToString(),
ControlNames.checkBox_BodyFriction.ToString(),
ControlNames.checkBox_BodyFrictionAsphalt.ToString(),
ControlNames.checkBox_SubstanceFriction.ToString(),
ControlNames.checkBox_IsIgnoreIce.ToString(),
};
foreach (var name in controlNames)
{
if (!BooleanValues.Any(item => item.Name == name))
{
_booleanValues.Add(new BindingDataModel<bool>
{
Name = name,
Value = false
});
}
}
foreach (var item in _booleanValues)
{
item.PropertyChanged += OnBooleanValueChanged;
}
}
private void OnBooleanValueChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(BindingDataModel<bool>.Value))
{
Save();
}
}
}
string으로 개별 속성을 저장하거나 ObservableCollection<형식>으로 컬렉션으로 저장할 수 있습니다.
ObservableCollection<T> 클래스는 Collection<T> 클래스를 상속받으면서 INotifyPropertyChanged 인터페이스가 구현되어 있습니다. 그래서 위 코드의 BooleanValues set 정의에서 OnPropertyChanged(nameof(BooleanValues)); 코드가 없어도 이벤트가 발행됩니다.
controlNames 변수에는 컨트롤의 이름을 문자열로 지정합니다. 위 코드는 컨트롤 이름을 서드파티 플러그인에서 참조하기 위해 미리 열거형으로 만들어놨습니다.😉
바인딩 데이터 클래스(참조)
컨트롤의 속성 중 파일로 저장할 항목을 담는 클래스입니다.
public class BindingDataModel<T> : INotifyPropertyChanged
{
private string _name;
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged("Name");
}
}
}
private T _value;
public T Value
{
get => _value;
set
{
if (!EqualityComparer<T>.Default.Equals(_value, value))
{
_value = value;
OnPropertyChanged("Value");
}
}
}
private ObservableCollection<T> _values;
public ObservableCollection<T> Values
{
get => _values;
set
{
if (_values != value)
{
_values = value;
OnPropertyChanged("Values");
}
}
}
// INotifyPropertyChanged 구현
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new(propertyName));
}
}
MainForm 클래스(앱)
앱을 실행하면 보여지는 WinForm 클래스입니다. 폼 생성자에서 아래 메서드를 호출합니다.
private void LoadAppSettings()
{
foreach (var bindingModel in AppSettings.Default.BooleanValues)
{
var control = Controls.Find(bindingModel.Name, true).FirstOrDefault();
if (control is CheckBox checkBox)
{
checkBox.DataBindings.Clear();
checkBox.DataBindings.Add("Checked", bindingModel, "Value", false, DataSourceUpdateMode.OnPropertyChanged);
}
}
}
이런식으로 여러 종류의 컨트롤을 바인딩하면 됩니다.
DataBindings.Add에서 첫번째 매개변수는 값으로 바인딩할 속성 이름입니다. 체크박스의 값은 Checked 속성에 저장됩니다. 텍스트박스는 "Text"로 지정하면 됩니다.