[c#] bindingList를 dataGridView에 바인딩하기 – INotifyPropertyChanged

다차원 구조의 대이터를 출력할 때 dataGridView를 쓴다. dataGridView에 쉽게 쓸 수 있는 대이터는 dataTable이지만 이건 다양한 기능들을 가지고 있어서 느리다. 다차원 구조의 대이터를 이리저리 정적으로 이용할 거면 dataTable이 좋지만 그저 동적으로 대이터만 빠르게 쓰고 읽으려면 class를 bindingList에 넣은 뒤 이걸 dataGridView에 바인딩하는 게 낫다.

일반적으로 dataGridView에 출력되는 내용을 빠르게 읽고 쓸 일은 없기 때문에 대부분의 경우에 dataTable을 쓰면 된다. 하지만 예를 들어 3천 개 정도의 주식 종목들이 각각 다차원 구조의 대이터를 가지고 있으며 이걸 빠르게 읽고 쓰는 경우를 생각해 볼 수 있다. 모든 종목들의 대이터를 한꺼번에 dataGridView에 출력하는 상황은 일반적이지 않다. 특정 종목을 고른 때 그 종목의 대이터만 dataGridView에 출력한면 된다. 이럴 때 한 종목의 대이터를 출력하자고 모든 종목들의 대이터 입출력을 dataTable에 하는 건 효율적이지 않다. 다차원 배열은 빠르긴 하지만 dataGridView에 바인딩을 할 수 없기 때문에 역시 적당하지 않다.

bindingList를 dataGridView에 바인딩하면 bindingList에 아이템을 넣거나 뺄 때 바로 dataGridView에 반영이 된다. 하지만 아이템의 값이 바뀐 때에는 그렇지 않다. 위 예에서 종목의 대이터가 바뀐 때 dataGridView에 바로 출력되게 하려면 INotifyPropertyChanged 인터패이스를 써야 한다. 무척 복잡하지만 달리 방법이 없다.

class Class1 : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    void NotifyPropertyChanged([CallerMemberName] string? propertyName = default)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    string? string1;

    public string String1
    {
        get
        {
            return string1!;
        }

        set
        {
            if (string1 != value)
            {
                string1 = value;

                NotifyPropertyChanged();
            }
        }
    }
}

BindingList<Class1> Class1s = new();

private void Form1_Load(object sender, EventArgs e)
{
    Class1 class1 = new();

    class1.String1 = "a";

    Class1s.Add(class1);

    dataGridView1.DataSource = Class1s;
}

private void Form1_Click(object sender, EventArgs e)
{
    Class1s[0].String1 = "b";
}

바인딩할 클래스 멤버는 프라퍼티여야 하고 필드는 안 되므로 get과 set 블록을 써야 한다.

INotifyPropertyChanged에는 PropertyChanged 이벤트 하나만 있다. 인터패이스의 이벤트는 선언을 하고 써야 한다.

To implement interface events in a class
Declare the event in your class and then invoke it in the appropriate areas.
How to implement interface events (C# Programming Guide)

NotifyPropertyChanged 메떠드를 따로 만든 건 프라퍼티들이 많을 때를 위한 거다.

CallerMemberName 애트리뷰트는 메떠드를 호출하는 콜러의 이름을 반환한다.

Allows you to obtain the method or property name of the caller to the method.
CallerMemberNameAttribute Class

위 예제에서 propertyName은 그냥 초기화했지만 아무 값이나 넣어도 된다. 어차피 프라퍼티의 이름으로 들어간다. 하지만 그냥 string? propertyName라고만 하면 안 된다. 이 애트리뷰트는 선언만 하고 값은 갖고 있지 않은 패러미터에는 쓸 수 없기 때문이다.

You apply the CallerMemberName attribute to an optional parameter that has a default value.
ibid.

클래스를 아무 것에도 바인딩하지 않고 그냥 인스턴스를 만들어 프라퍼티의 값을 바꾸면 PropertyChanged가 널이라며 예외로 처리된다. 이를 막기 위해 널이 아닌 때에만 작동하도록 PropertyChanged에 널 조건 연산자인 ?.를 붙였다.

invoke 메떠드를 이용하는 건 이벤트를 실행하는 일반적인 방법이다. 이 메떠드는 누가 이벤트를 실행하는 건지와 이벤트 실행에 필요한 내용을 담은 아규먼트를 패러미터들로 받는다. PropertyChangedEventArgs(propertyName)는 이벤트에 어느 프라퍼티가 바뀌었는지를 전달한다.