반응형

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

반응형

관련글