바인딩(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"로 지정하면 됩니다.