Favicon

INotifyPropertyChanged interface

Peponi1/16/20256m

C#
Typeinterface

1. Introduction

INotifyPropertyChanged 인터페이스는 클라이언트에 속성 값이 변경됨을 알리는데 사용한다. PropertyChanged 이벤트만 정의된 간단한 인터페이스로, 프로퍼티 값 변경 시 이벤트를 통해 각 클라이언트에 전파할 수 있도록 한다. 구현이 쉽고 간결하며 수동으로 멤버를 바인딩 하는 경우에 비해 제작 편의성 및 재사용성이 대폭 향상된다.

INotifyPropertyChanged 인터페이스를 적절히 구현하면 다음과 같은 동작 특성을 가질 수 있다.

  • 이벤트를 통해 프로퍼티 값 변경을 자동으로 추적할 수 있다.
  • UI가 존재하는 경우, 프로퍼티 값 수정 시 자동으로 컨트롤에 반영이 되도록 할 수 있다.
  • UI가 존재하는 경우, 컨트롤의 값을 수정할 때 프로퍼티와 동일한 형식의 값인 경우 자동 반영, 잘못된 값을 입력하는 경우 이전 값으로 돌아간다.

2. Example

하단 샘플은 INotifyPropertyChanged 인터페이스에 대한 간단한 사용 방법이다.

Data class
public class TestClass : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    
    private bool _testBool;
 
    public bool TestBool
    {
        get => _testBool;
        set
        {
            if (_testBool != value)
            {
                _testBool = value;
                OnPropertyChanged();
            }
        }
    }
 
    // 바인딩된 컴포넌트 반환. new로 반환하여 이 클래스의 데이터를 표시하는 여러개의 컴포넌트 생성 가능
    public UserControl GetComponent() => new TestControl(this);
 
    // 프로퍼티 변경 전파
    private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
Control class
public partial class TestControl : UserControl
{
    private TestClass _testClass { get; set; }
 
    public TestControl(TestClass testClass)
    {
        InitializeComponent();
        _testClass = testClass;
        BindData();
    }
 
    private void BindData()
    {
        // TestBool : boolean 바인딩 테스트를 위한 CheckBox
        TestBool.DataBindings.Add(new Binding(nameof(TestBool.Checked), _testClass, nameof(TestClass.TestBool)));
    }
}

내용을 요약하자면,

  • Data class

    1. INotifyPropertyChanged 상속 및 구현
  • Control class

    1. Bindable 클라이언트에 데이터 바인딩

정도로 요약할 수 있다.

만약 추가 예제가 필요한 경우, 마지막 섹션을 참고한다.

3. 참조 자료

A. Additional example

Data class
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Windows.Forms;
 
namespace INotifyPropertyChangedExample
{
    public enum TestEnum
    {
        A, B, C
    }
 
    public class TestClass : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
 
        private bool _testBool = false;
 
        public bool TestBool
        {
            get => _testBool;
            set
            {
                if (_testBool != value)
                {
                    _testBool = value;
                    OnPropertyChanged();
                }
            }
        }
 
        private int _testInt = 0;
 
        public int TestInt
        {
            get => _testInt;
            set
            {
                if (_testInt != value)
                {
                    _testInt = value;
                    OnPropertyChanged();
                }
            }
        }
 
        private double _testDouble = 0;
 
        public double TestDouble
        {
            get => _testDouble;
            set
            {
                if (_testDouble != value)
                {
                    _testDouble = value;
                    OnPropertyChanged();
                }
            }
        }
 
        private string _testString = string.Empty;
 
        public string TestString
        {
            get => _testString;
            set
            {
                if (_testString != value)
                {
                    _testString = value;
                    OnPropertyChanged();
                }
            }
        }
 
        private TestEnum _testEnum = TestEnum.A;
 
        public TestEnum TestEnum
        {
            get => _testEnum;
            set
            {
                if (_testEnum != value)
                {
                    _testEnum = value;
                    OnPropertyChanged();
                }
            }
        }
 
        // 바인딩된 컴포넌트 반환. new로 반환하여 이 클래스의 데이터를 표시하는 여러개의 컴포넌트 생성 가능
        public UserControl GetComponent() => new TestControl(this);
 
        // 프로퍼티 변경 전파
        private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}
Control class
using System;
using System.Windows.Forms;
 
namespace INotifyPropertyChangedExample
{
    public partial class TestControl : UserControl
    {
        private TestClass _testClass { get; set; }
 
        public TestControl()
        {
            InitializeComponent();
        }
 
        public TestControl(TestClass testClass)
        {
            InitializeComponent();
            EnumSelect.Items.AddRange(Enum.GetNames(typeof(TestEnum)));
 
            _testClass = testClass;
 
            BindData();
        }
 
        // TestClass 바인딩 : 양방향 바인딩
        // 1. 프로퍼티 값을 수정하면 UI에 자동 반영
        // 2. 바인딩된 UI의 값을 바꿀 시 자동으로 프로퍼티에 반영
        private void BindData()
        {
            // CheckBox
            TestBool.DataBindings.Add(new Binding(nameof(TestBool.Checked), _testClass, nameof(TestClass.TestBool)));
 
            // NumericUpDown
            TestInt.DataBindings.Add(new Binding(nameof(TestInt.Value), _testClass, nameof(TestClass.TestInt)));
 
            // NumericUpDown
            TestDouble.DataBindings.Add(new Binding(nameof(TestDouble.Value), _testClass, nameof(TestClass.TestDouble)));
 
            // TextBox
            TestString.DataBindings.Add(new Binding(nameof(TestString.Text), _testClass, nameof(TestClass.TestString)));
 
            // Enum은 양방향 바인딩을 원할 경우 ComboBox를 고려하는 것이 좋다.
            TestEnum.DataBindings.Add(new Binding(nameof(TestEnum.Text), _testClass, nameof(TestClass.TestEnum)));
        }
    }
}
Form class
using System.Threading;
using System.Windows.Forms;
 
namespace INotifyPropertyChangedExample
{
    public partial class MainFrame : Form
    {
        public MainFrame()
        {
            InitializeComponent();
            SetUI();
        }
 
        private void SetUI()
        {
            TestClass testClass = new TestClass();
            TableLayoutPanel tableLayoutPanel = new TableLayoutPanel();
            tableLayoutPanel.ColumnCount = 3;
 
            // 같은 데이터를 원본으로 하는 컴포넌트 세개 추가. 어느 한 곳에서 프로퍼티 변경 시 나머지 컴포넌트 또한 값이 자동 변경
            tableLayoutPanel.Controls.Add(testClass.GetComponent(), 0, 0);
            tableLayoutPanel.Controls.Add(testClass.GetComponent(), 1, 0);
            tableLayoutPanel.Controls.Add(testClass.GetComponent(), 2, 0);
            tableLayoutPanel.Dock = DockStyle.Fill;
            this.Controls.Add(tableLayoutPanel);
        }
    }
}