본문 바로가기

.NET/WPF

[WPF] MVVM 디자인 패턴 (3) - ViewModelBase

MVVM 디자인 패턴으로 개발을 할 때 필수적인 요소 중 하나로 INotifyPropertyChanged 인터페이스를 구현하는 것이 있다.

View와 ViewModel 간에 결합을 낮추면서 데이터를 주고받기 위해서 Binding을 사용하는데, 이 Binding이 데이터가 변화되는 시점을 감지하고 데이터를 가져가기 위해서는 데이터가 변화되었음을 알려줘야 하는 것이다.

 

XAML 코드 상에서 컨트롤의 속성과 ViewModel의 속성을 Binding 하게 되면 내부적으로 속성의 변화를 감지하기 위해 ViewModel의 INotifyPropertyChanged 인터페이스에 선언된 PropertyChanged 이벤트에 감지 메서드를 등록할 것이다. 즉, ViewModel은 INotifyPropertyChanged 인터페이스를 구현해야 한다.

 

기본적으로 아래와 같은 형태를 띄게 될 것이다.

public class MainWindowViewModel : INotifyPropertyChanged
{
    // INotifyPropertyChanged의 요소
    public event PropertyChangedEventHandler PropertyChanged;
    
    // 속성값이 변화될 때 이 메서드를 '직접' 호출해줘야 한다.
    private void notifyPropertyChanged(string propertyName)
    {
        // PropertyChanged에 등록된 메서드들을 Invoke 해준다.
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    
    
    private string _name;
    public string Name
    {
        get => _name;
        set
        {
            if (_name != value)
            {
                _name = value;
                notifyPropertyChanged(nameof(Name));
            }
        }
    }
}

 

그러나 ViewModel은 View의 종류나 확장을 위해 여러 가지가 존재할 수 있고, 매번 저렇게 INotifyPropertyChanged를 구현하기는 번거로울 것이다. 따라서 INotifyPropertyChanged를 위한 기능들만을 구현한 ViewModelBase 추상 클래스를 만들어 사용한다.

 

아래 코드를 보자.

using System.ComponentModel;
using System.CompilerServices;

public abstract class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    
    protected void NotifyPropertyChanged([CallerMemberName]string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

이제 ViewModel은 ViewModelBase 클래스로부터 파생되는 것으로 INotifyPropertyChanged에 필요한 기본 기능을 상속 받아 사용할 수 있다. 또한 NotifyPropertyChanged 메서드에 속성 이름을 직접 인수로 지정해줘야 하는 번거로움을 해결하기 위해 CallerMemberName 특성을 적용하여 해당 메서드를 호출한 멤버의 이름이 propertyName 매개변수의 기본값으로 지정되도록 하였다.

ViewModelBase를 적용한 ViewModel 코드를 보자.

public class MainWindowViewModel : ViewModelBase
{
    private string _name;
    public string Name
    {
        get => _name;
        set
        {
            if (_name != value)
            {
                _name = value;
                NotifyPropertyChanged();
            }
        }
    }
}

 

그러나 여전히 속성 하나를 구현하기 위해 저런 행위를 반복하는 것은 쉽지 않다.

그렇기 때문에 if ~ NotifyPropertyChanged() 영역을 대신해줄 또 하나의 메서드를 ViewModelBase에 추가한다.

// 아래 코드를 실제로 MVVM 디자인 패턴으로 개발할 프로젝트에서 ViewModelBase로 사용할 수 있다.

using System.ComponentModel;
using System.CompilerServices;

public abstract class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    
    protected void NotifyPropertyChanged([CallerMemberName]string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    
    // 파생 클래스의 속성 setter에서 사용할 메서드
    protected virtual bool SetProperty<T>(ref T member, T value, [CallerMemberName]string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(member, value)) return false;
        
        member = value;
        NotifyPropertyChanged(propertyName);
        return true;
    }
}

SetProperty() 메서드는 속성의 Set 부분을 대체할 수 있다.

EqualityComparer<T>는 제네릭 타입에 대한 비교자를 제공하여 값이 같은지를 판단할 수 있게 해 준다. 간략히 설명하면 제네릭 타입 T가 기본 형식(int, double, byte 등)인 경우 해당 타입에 대한 비교자를 사용하고, 그렇지 않은 경우 Object 형식 비교자를 사용하여 값을 비교한다.

 

값이 서로 다르다면 ref 형식으로 받아온 member에 값을 대입하고 역시 마찬가지로 CallerMemberName 특성으로 가져온 속성의 이름을 사용하여 NotifyPropertyChanged() 메서드를 호출한다.

 

이로 인해 ViewModel의 코드는 더 간략해진다.

public class MainWindowViewModel : ViewModelBase
{
    private string _name;
    public string Name
    {
        get => _name;
        set => SetProperty(ref _name, value);
    }
}

 

MVVM 디자인 패턴으로 개발할 때 ViewModelBase를 위와 같이 준비해두고 ViewModel을 만들기 시작하면 정말 편하다!

'.NET > WPF' 카테고리의 다른 글

[WPF] XAML 코드 상에서 DataContext 지정하기  (0) 2023.01.03
[WPF] Binding 기초  (0) 2022.11.25
[WPF] DependencyProperty에 대해서  (1) 2022.11.24
[WPF] MVVM 디자인 패턴 (2)  (0) 2022.11.13
[WPF] MVVM 디자인 패턴 (1)  (0) 2022.11.10