본문 바로가기

[C#] WPF MVVM 기초 #1 View와 ViewModel 연결하기 ViewModelBase클래스

I'm 영서 2024. 1. 23.
반응형

MVVM이 무엇인가에 대해서는 충분히 공부해봤다. 

간략히 복습하자면 느슨한 결합을 통해 View와 model, ViewModel간의 결합을 낮추어 UI와 코드를 한곳에서 처리할 필요가 없고 서로 영향을 주지않는 것 이라는걸 알 수 있다.

그렇다면 이제 실전을 해보자.

 

출퇴근 시간을 기록하기 위한 윈도우 어플리케이션을 만든다고 생각하여 진행할 것이다. 

다양한 종류의 변수들을 사용해볼 것이다.

프로젝트구조는 다음과 같이 빈 프로젝트를 생성하는것으로 시작한다. 

Calendar.xaml라는 이름으로 View와 CalendarViewModel.cs 라는 ViewModel을 만들었다.

 

 

나는 사용자 정보를 담는 LoginUser를 추가로 개발하였으나 설명에는 필요가 없다...

DevExpress를 사용했으므로 CalendarViewModel은 ViewModelBase를 상속받아 사용하였다.

    public class CalendarViewModel : ViewModelBase

 

상속받는 이 ViewModelBase를 알아보자. 

간략한 결론을 원하면 마지막에 3줄요약을 보면 된다.

public abstract class ViewModelBase : BindableBase, ISupportParentViewModel, ISupportServices, ISupportParameter, ICustomTypeDescriptor

 

선언을 보면 생각나는것들에 대해서 쭉 써보았다.

1. ViewModelBase는 추상클래스로 BindableBase를 상속받아 사용하는구나

   -> BindableBase는 뭐고 어떤 메서드를 구현해야하지?

2. ISupportParentViewModel, ISupportServices, ISupportParameter, ICustomTypeDescriptor를 구현해야하는구나! 

   -> 몇가지 인터페이스의 구현체를 포함하는데 이 인터페이스에서는 뭘 구현해야하지?

 

 

BindableBase의 구조

//
// 요약:
//     Provides support for the INotifyPropertyChanged interface and capabilities for
//     easy implementation of bindable properties with the GetProperty and SetProperty
//     methods.
[DataContract]
public abstract class BindableBase : INotifyPropertyChanged
아, 이제 드디어 아는 인터페이스가 나왔다. BindableBase는 INotifyPropertyChanged를 구현하는구나!
거기에 3줄 요약까지 있다.
INotifyPropertyChanged기능의 제공과 함께 메서드 GetPropertySetProperty와 같은 Bindable properties를 쉽게 구현하도록 지원한다. 

 

실제 코드를 한번 보면 

더보기
public abstract class BindableBase : INotifyPropertyChanged
{
    private Dictionary<string, object> _propertyBag;

    private Dictionary<string, object> PropertyBag => _propertyBag ?? (_propertyBag = new Dictionary<string, object>());

    //
    // 요약:
    //     Occurs when a property value changes.
    public event PropertyChangedEventHandler PropertyChanged;

    //
    // 요약:
    //     Returns the name of a property identified by a lambda expression.
    //
    // 매개 변수:
    //   expression:
    //     A lambda expression selecting the property.
    //
    // 반환 값:
    //     The name of the property accessed by expression.
    public static string GetPropertyName<T>(Expression<Func<T>> expression)
    {
        return GetPropertyNameFast(expression);
    }

    internal static string GetPropertyNameFast(LambdaExpression expression)
    {
        if (!(expression.Body is MemberExpression memberExpression))
        {
            throw new ArgumentException("MemberExpression is expected in expression.Body", "expression");
        }

        MemberInfo member = memberExpression.Member;
        if (member.MemberType == MemberTypes.Field && member.Name != null && member.Name.StartsWith("$VB$Local_"))
        {
            return member.Name.Substring("$VB$Local_".Length);
        }

        return member.Name;
    }

    protected bool SetProperty<T>(ref T storage, T value, Expression<Func<T>> expression, Action changedCallback)
    {
        return SetProperty(ref storage, value, GetPropertyName(expression), changedCallback);
    }

    protected bool SetProperty<T>(ref T storage, T value, Expression<Func<T>> expression)
    {
        return SetProperty(ref storage, value, expression, null);
    }

    protected void RaisePropertyChanged<T>(Expression<Func<T>> expression)
    {
        RaisePropertyChanged(GetPropertyName(expression));
    }

    protected void RaisePropertiesChanged<T1, T2>(Expression<Func<T1>> expression1, Expression<Func<T2>> expression2)
    {
        RaisePropertyChanged(expression1);
        RaisePropertyChanged(expression2);
    }

    protected void RaisePropertiesChanged<T1, T2, T3>(Expression<Func<T1>> expression1, Expression<Func<T2>> expression2, Expression<Func<T3>> expression3)
    {
        RaisePropertyChanged(expression1);
        RaisePropertyChanged(expression2);
        RaisePropertyChanged(expression3);
    }

    protected void RaisePropertiesChanged<T1, T2, T3, T4>(Expression<Func<T1>> expression1, Expression<Func<T2>> expression2, Expression<Func<T3>> expression3, Expression<Func<T4>> expression4)
    {
        RaisePropertyChanged(expression1);
        RaisePropertyChanged(expression2);
        RaisePropertyChanged(expression3);
        RaisePropertyChanged(expression4);
    }

    protected void RaisePropertiesChanged<T1, T2, T3, T4, T5>(Expression<Func<T1>> expression1, Expression<Func<T2>> expression2, Expression<Func<T3>> expression3, Expression<Func<T4>> expression4, Expression<Func<T5>> expression5)
    {
        RaisePropertyChanged(expression1);
        RaisePropertyChanged(expression2);
        RaisePropertyChanged(expression3);
        RaisePropertyChanged(expression4);
        RaisePropertyChanged(expression5);
    }

    protected T GetProperty<T>(Expression<Func<T>> expression)
    {
        return GetPropertyCore<T>(GetPropertyName(expression));
    }

    protected bool SetProperty<T>(Expression<Func<T>> expression, T value, Action<T> changedCallback)
    {
        string propertyName = GetPropertyName(expression);
        return SetPropertyCore(propertyName, value, changedCallback);
    }

    protected bool SetProperty<T>(Expression<Func<T>> expression, T value)
    {
        return SetProperty(expression, value, (Action)null);
    }

    protected bool SetProperty<T>(Expression<Func<T>> expression, T value, Action changedCallback)
    {
        string propertyName = GetPropertyName(expression);
        return SetPropertyCore(propertyName, value, changedCallback);
    }

    protected virtual bool SetProperty<T>(ref T storage, T value, string propertyName, Action changedCallback)
    {
        VerifyAccess();
        if (CompareValues(storage, value))
        {
            return false;
        }

        storage = value;
        RaisePropertyChanged(propertyName);
        changedCallback?.Invoke();
        return true;
    }

    protected bool SetProperty<T>(ref T storage, T value, string propertyName)
    {
        return SetProperty(ref storage, value, propertyName, null);
    }

    protected T GetValue<T>([CallerMemberName] string propertyName = null)
    {
        GuardPropertyName(propertyName);
        return GetPropertyCore<T>(propertyName);
    }

    protected bool SetValue<T>(T value, [CallerMemberName] string propertyName = null)
    {
        return SetValue(value, (Action)null, propertyName);
    }

    protected bool SetValue<T>(T value, Action changedCallback, [CallerMemberName] string propertyName = null)
    {
        return SetPropertyCore(propertyName, value, changedCallback);
    }

    protected bool SetValue<T>(T value, Action<T> changedCallback, [CallerMemberName] string propertyName = null)
    {
        return SetPropertyCore(propertyName, value, changedCallback);
    }

    protected bool SetValue<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
    {
        return SetValue(ref storage, value, null, propertyName);
    }

    protected bool SetValue<T>(ref T storage, T value, Action changedCallback, [CallerMemberName] string propertyName = null)
    {
        GuardPropertyName(propertyName);
        return SetProperty(ref storage, value, propertyName, changedCallback);
    }

    private static void GuardPropertyName(string propertyName)
    {
        if (string.IsNullOrEmpty(propertyName))
        {
            throw new ArgumentNullException("propertyName");
        }
    }

    protected void RaisePropertyChanged(string propertyName)
    {
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected void RaisePropertyChanged()
    {
        RaisePropertiesChanged((string[])null);
    }

    protected void RaisePropertiesChanged(params string[] propertyNames)
    {
        if (propertyNames == null || propertyNames.Length == 0)
        {
            RaisePropertyChanged(string.Empty);
            return;
        }

        foreach (string propertyName in propertyNames)
        {
            RaisePropertyChanged(propertyName);
        }
    }

    private T GetPropertyCore<T>(string propertyName)
    {
        if (PropertyBag.TryGetValue(propertyName, out var value))
        {
            return (T)value;
        }

        return default(T);
    }

    private bool SetPropertyCore<T>(string propertyName, T value, Action changedCallback)
    {
        T oldValue;
        bool flag = SetPropertyCore(propertyName, value, out oldValue);
        if (flag)
        {
            changedCallback?.Invoke();
        }

        return flag;
    }

    private bool SetPropertyCore<T>(string propertyName, T value, Action<T> changedCallback)
    {
        T oldValue;
        bool flag = SetPropertyCore(propertyName, value, out oldValue);
        if (flag)
        {
            changedCallback?.Invoke(oldValue);
        }

        return flag;
    }

    protected virtual bool SetPropertyCore<T>(string propertyName, T value, out T oldValue)
    {
        VerifyAccess();
        oldValue = default(T);
        if (PropertyBag.TryGetValue(propertyName, out var value2))
        {
            oldValue = (T)value2;
        }

        if (CompareValues(oldValue, value))
        {
            return false;
        }

        lock (PropertyBag)
        {
            PropertyBag[propertyName] = value;
        }

        RaisePropertyChanged(propertyName);
        return true;
    }

    protected virtual void VerifyAccess()
    {
    }

    private static bool CompareValues<T>(T storage, T value)
    {
        return EqualityComparer<T>.Default.Equals(storage, value);
    }
}

2개의 Dictionary, 1개의 Event, 32개의 매서드로 구성되어 있는 클래스이다. 

하나씩 파헤쳐보자.

 

두개의 변수 

_propertyBag :
private한 Dictionary<string, object> 타입의 필드로서, 속성의 값을 저장하는 데 사용된다. 
데이터 바인딩 및 속성 변경 알림을 처리하는 데 필요한 정보가 여기에 저장된다.

 

PropertyBag 
PropertyBag는 Dictionary<string, object> 타입을 반환하는 게터 속성으로서, 속성 값을 저장하고 검색하는데 사용된다.
이 속성은 지연 초기화 패턴을 구현하고, _propertyBag 필드가 null인 경우에만 새로운 Dictionary 인스턴스를 생성하여 반환한다. 

PropertyBag은 GetPropertyCoreSetPropertyCore 매서드에서 사용되는 것을 볼 수 있는데, 

private T GetPropertyCore<T>(string propertyName)
{
    if (PropertyBag.TryGetValue(propertyName, out var value))
    {
        return (T)value;
    }

    return default(T);
}

GetPropertyCore 는 PropertyBag Dictionary에서 파라미터로 전달받은 propertyName을 검색하여 value를 리턴해준다. 분기에 따라 <T>의 기본값을 리턴해준다. 

protected virtual bool SetPropertyCore<T>(string propertyName, T value, out T oldValue)
{
    VerifyAccess();
    oldValue = default(T);
    if (PropertyBag.TryGetValue(propertyName, out var value2))
    {
        oldValue = (T)value2;
    }

    if (CompareValues(oldValue, value))
    {
        return false;
    }

    lock (PropertyBag)
    {
        PropertyBag[propertyName] = value;
    }

    RaisePropertyChanged(propertyName);
    return true;
}
protected virtual void VerifyAccess()
{
}

private static bool CompareValues<T>(T storage, T value)
{
    return EqualityComparer<T>.Default.Equals(storage, value);
}

protected void RaisePropertyChanged(string propertyName)
{
    this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

 > VerifyAccess의 경우 현재 스레드가 UI스레드에서 실행 중인지를 확인한다고 하는것으로, 필요에 따라서 상속받은 클래스에서 작성하면 된다...

아무튼 SetPropertyCore는 default(T)를 통해 T 타입의 기본값으로 oldValue를 초기화 해주고 TryGetValue 분기를 통해 propertyName을 PropertyBag에서 조회하여 value2에 저장하며, 저장한 값을 oldValue로 초기화 해주고, 이제 value와 oldvalue를 비교하여 필요에 따라서 PropertyBag을 업데이트 해주고. RaisePropertyChanged를 통해 Property가 변경되었다는것을 알린다. 

당연하게도 그렇기 때문에 RaisePropertyChanged는 PropertyChagned 이벤트를 수행한다.

 

다음은 GetValueSetValue이다.

    protected T GetValue<T>([CallerMemberName] string propertyName = null)
    {
        GuardPropertyName(propertyName);
        return GetPropertyCore<T>(propertyName);
    }

    protected bool SetValue<T>(T value, [CallerMemberName] string propertyName = null)
    {
        return SetValue(value, (Action)null, propertyName);
    }
        private static void GuardPropertyName(string propertyName)
    {
        if (string.IsNullOrEmpty(propertyName))
        {
            throw new ArgumentNullException("propertyName");
        }
    }

 

요약하자면 

GetValue는 [CallerMemeberName] 속성으로 현재 메서드를 호출한 멤버의 이름(propertyName)을 가져와 속성의 이름 없이 메서드를 호출할 수 있다. GuardPropertyName은 이름이 유효한지 확인한다.

SetValue는 속성에 값을 설정하는것으로 결과에 따라 bool값을 리턴한다.

GetValue vs GetProperty

가장 큰 차이는 GetValue는 현재 메서드를 호출한 멤버의 이름을 자동으로 가져오고 GetProperty는 직접 전달한다. 

즉 해당 메서드를 호출한 멤버의 이름을 명시적으로 구현하고자 하면 GetProperty를 아닌경우 GetValue를 쓰면 된다.

ViewModelBase의 인터페이스들

ISupportParentViewModel

  > ViewModel간 통신 및 데이터 공유에 사용되는데, 부모 ViewModel에 대한 참조를 제공하고, 자식 ViewModel이 부모 ViewModel과 상호작용 할 수 있도록 돕는다.

 

예시

더보기
public class ChildViewModel : ISupportParentViewModel
{
    // 부모 ViewModel에 대한 참조
    public ParentViewModel ParentViewModel { get; private set; }

    // ISupportParentViewModel 인터페이스 메서드 구현
    public void SetParentViewModel(object parentViewModel)
    {
        if (parentViewModel is ParentViewModel)
        {
            this.ParentViewModel = (ParentViewModel)parentViewModel;
        }
    }

    // 자식 ViewModel에서 부모 ViewModel과 상호 작용하는 메서드
    public void DoSomethingWithParent()
    {
        if (this.ParentViewModel != null)
        {
            // 부모 ViewModel의 속성이나 메서드에 접근
            string parentName = this.ParentViewModel.Name;
            this.ParentViewModel.DoSomething();
        }
    }
}
public class App
{
    public void Run()
    {
        // 부모 ViewModel 초기화
        ParentViewModel parentViewModel = new ParentViewModel();
        parentViewModel.Name = "Parent";

        // 자식 ViewModel 초기화 및 부모 ViewModel 설정
        ChildViewModel childViewModel = new ChildViewModel();
        childViewModel.SetParentViewModel(parentViewModel);

        // 자식 ViewModel에서 부모 ViewModel과 상호 작용
        childViewModel.DoSomethingWithParent();

        // 어플리케이션 실행 로직 계속...
    }
}

 

부모 ViewModel과 자식 ViewModel간 상호작용한다. 쉬우므로 패스..

ISupportServices

 > ViewModel이 서비스를 사용하려는 경우에 사용되는데, 여기서 서비스는 비즈니스로직, 데이터액세스, 로깅 등과 같은 기능을 말하며, 서비스에 대한 참조를 받아와서 사용할 수 있다. 

예시

더보기
// 로깅 서비스 인터페이스
public interface ILoggingService
{
    void Log(string message);
}

// 로깅 서비스 구현
public class FileLoggingService : ILoggingService
{
    public void Log(string message)
    {
        // 로깅 메시지를 파일에 저장하는 로직 구현
    }
}
public class ServiceProvider
{
    // 서비스 프로바이더를 초기화하는 메서드
    public static IServiceProvider InitializeServices()
    {
        // 서비스 인스턴스를 생성하고 등록
        ILoggingService loggingService = new FileLoggingService();

        // 서비스 프로바이더를 생성하고 서비스 등록
        ServiceCollection services = new ServiceCollection();
        services.AddSingleton(loggingService);

        // 서비스 프로바이더 반환
        return services.BuildServiceProvider();
    }
}
public class MyViewModel : ISupportServices
{
    // 서비스에 대한 참조를 저장하는 멤버 변수
    private ILoggingService loggingService;

    // ISupportServices 인터페이스 메서드 구현
    public void InitializeServices(IServiceProvider serviceProvider)
    {
        // 서비스 프로바이더에서 로깅 서비스를 가져와서 저장
        this.loggingService = serviceProvider.GetService<ILoggingService>();
    }

    // ViewModel에서 로깅 서비스를 사용하는 메서드
    public void LogMessage(string message)
    {
        if (this.loggingService != null)
        {
            this.loggingService.Log(message);
        }
    }
}

 

ISupportParameter

> ViewModel이 다른 ViewModel로부터 파라미터를 전달받아야 하는 경우 사용, 

주로 다른 화면으로 이동하거나 특정 작업을 수행하기 위한 정보를 전달하는데 사용한다. 

이 인터페이스를 구현하였으므로 ViewModel은 파라미터를 받아서 처리할 수 있어야 한다.

더보기
public class App
{
    public void Run()
    {
        // 파라미터 값을 설정하여 ViewModel 초기화
        string parameterValue = "Hello, Parameter!";
        MyViewModel viewModel = new MyViewModel();
        viewModel.InitializeParameter(parameterValue);

        // ViewModel에서 파라미터 사용
        string parameter = viewModel.GetParameter();
        Console.WriteLine("Received Parameter: " + parameter);

        // 어플리케이션 실행 로직 계속...
    }
}

 

public class MyViewModel : ISupportParameter
{
    // 파라미터를 저장하는 멤버 변수
    private string parameter;

    // ISupportParameter 인터페이스 메서드 구현
    public void InitializeParameter(object parameter)
    {
        // 전달받은 파라미터를 멤버 변수에 저장
        if (parameter is string)
        {
            this.parameter = (string)parameter;
        }
    }

    // ViewModel에서 파라미터를 사용하는 메서드
    public string GetParameter()
    {
        return this.parameter;
    }
}

 

MyViewModel은 ISupportParameter 인터페이스를 구현하여 파라미터를 초기화하여 사용한다. 다른 ViewModel이 있다면 다른 ViewModel에서 해당 파라미터를 가져와서 사용할수 있다!

ICustomTypeDescriptor

> 닷넷 프레임워크의 고급 데이터 바인딩 및 메타데이터 처리를 지원하는 인터페이스로 해당 객체의 속성 및 이벤트에 대한 추가 정보를 제공할 수 있다. 

예시

더보기
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;

public class Person : ICustomTypeDescriptor
{
    public string Name { get; set; }
    public int Age { get; set; }

    // ICustomTypeDescriptor 인터페이스 메서드 구현
    public AttributeCollection GetAttributes()
    {
        return TypeDescriptor.GetAttributes(this, true);
    }

    public string GetClassName()
    {
        return TypeDescriptor.GetClassName(this, true);
    }

    public string GetComponentName()
    {
        return TypeDescriptor.GetComponentName(this, true);
    }

    public TypeConverter GetConverter()
    {
        return TypeDescriptor.GetConverter(this, true);
    }

    public EventDescriptor GetDefaultEvent()
    {
        return TypeDescriptor.GetDefaultEvent(this, true);
    }

    public PropertyDescriptor GetDefaultProperty()
    {
        return TypeDescriptor.GetDefaultProperty(this, true);
    }

    public object GetEditor(Type editorBaseType)
    {
        return TypeDescriptor.GetEditor(this, editorBaseType, true);
    }

    public EventDescriptorCollection GetEvents()
    {
        return TypeDescriptor.GetEvents(this, true);
    }

    public EventDescriptorCollection GetEvents(Attribute[] attributes)
    {
        return TypeDescriptor.GetEvents(this, attributes, true);
    }

    public PropertyDescriptorCollection GetProperties()
    {
        return ((ICustomTypeDescriptor)this).GetProperties(new Attribute[0]);
    }

    public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
    {
        List<PropertyDescriptor> properties = new List<PropertyDescriptor>();
        properties.Add(new CustomPropertyDescriptor(this, "Name", typeof(string), attributes));
        properties.Add(new CustomPropertyDescriptor(this, "Age", typeof(int), attributes));
        return new PropertyDescriptorCollection(properties.ToArray());
    }

    public object GetPropertyOwner(PropertyDescriptor pd)
    {
        return this;
    }
}

public class CustomPropertyDescriptor : PropertyDescriptor
{
    private Type propertyType;

    public CustomPropertyDescriptor(object component, string name, Type propertyType, Attribute[] attrs)
        : base(name, attrs)
    {
        this.propertyType = propertyType;
    }

    public override bool CanResetValue(object component)
    {
        return false;
    }

    public override Type ComponentType
    {
        get { return typeof(Person); }
    }

    public override object GetValue(object component)
    {
        Person person = (Person)component;
        if (Name == "Name")
        {
            return person.Name;
        }
        else if (Name == "Age")
        {
            return person.Age;
        }
        return null;
    }

    public override bool IsReadOnly
    {
        get { return false; }
    }

    public override Type PropertyType
    {
        get { return propertyType; }
    }

    public override void ResetValue(object component)
    {
    }

    public override void SetValue(object component, object value)
    {
        Person person = (Person)component;
        if (Name == "Name")
        {
            person.Name = (string)value;
        }
        else if (Name == "Age")
        {
            person.Age = (int)value;
        }
    }

    public override bool ShouldSerializeValue(object component)
    {
        return false;
    }
}

 

public class App
{
    public void Run()
    {
        Person person = new Person();
        person.Name = "John";
        person.Age = 30;

        // 속성 정보를 가져와서 출력
        PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(person);
        foreach (PropertyDescriptor property in properties)
        {
            Console.WriteLine($"{property.Name}: {property.GetValue(person)}");
        }

        // 어플리케이션 실행 로직 계속...
    }
}

서론이 상당히 긴데.. 아무튼 TypeDescriptor을 사용하여 등록해놓은 속성및 값을 TypeDescriptor.GetProperties를 통해 가져와 활용할 수 있다. 

3줄요약

1. ViewModelBase는 GetProperty와 SetProperty를 쉽게 사용하기 위한 클래스 BindableBase를 상속받는다

2. BindableBase는 부모자식간 파라미터 전달을 위한 인터페이스 ISupportParentViewModel, ViewModel이서비스를 사용하기 위한 인터페이스 ISupportServices, 다른 ViewModel간 파라미터를 전달하기 위한 ISupportParameter, 닷넷 안에서 고급 데이터 바인딩 및 메타데이터 처리를 위한 ICustomTypeDescriptor 를 구현한다.

3. 즉 Viewmodel은 INotifyChanged를 효율적으로 사용하기 위한 추상클래스 라고 판단할 수 있다. 

 

반응형

댓글