본문 바로가기

.NET/WPF

[WPF] 크로스 스레드 문제 해결

WPF에서 역시 크로스 스레드 문제가 발생할 수 있는데 보통 메인 스레드가 아닌 다른 스레드에서 뷰모델의 속성 값을 변경하려고 시도하거나 코드 비하인드에서 컨트롤을 수정하려 할 때 발생할 수 있다.

이러한 경우 WinForm에서의 Invoke() 대신 Dispatcher.Invoke() 를 사용하여 해결할 수 있다.

 

public class View1 : Window
{
    public View1()
    {
        InitializeComponent();
    }
    
    public void button_Click(object sender, RoutedEventArgs e)
    {
        new Thread(myThread) { IsBackground = true }.Start();
    }
    
    private void myThread()
    {
        for( var i = 0; i < 100; i++ )
        {
            Thread.Sleep(1000);
            Dispatcher.Invoke( () => 
            {
                textBlock.Text = i.ToString();
            } );
        }
    }
}

 

실제로 위와 같이 코드를 작성한 뒤에 Dispatcher.Invoke() 부분을 제거하고 textBlock의 Text 속성을 직접 변경하려고 하면 아래와 같이 오류가 발생한다.

 

 

Dispatcher는 현재 호출 스레드가 액세스 권한이 없는 경우 액세스 권한을 가진 스레드를 찾아 대리자를 전달하여 대신 처리하도록 한다.

 

액세스 권한이 있는지 없는지 확인한 다음, 없는 경우에만 대리자를 호출하려면 아래와 같이 코드를 수정할 수 있다.

(다만 동일한 스레드에서 매번 값을 변경할 때마다 changeValueSafe() 메서드로 액세스 권한을 확인하는 것은 비효율적인 로직이다.)

public partial class View1 : Window
{
    public View1()
    {
        InitializeComponent();
    }

    private void Button_Click( object sender, RoutedEventArgs e )
    {
        new Thread( myThread ) { IsBackground = true }.Start();
    }
    private void myThread()
    {
        for( var i = 0; i < 100; i++ )
        {
            Thread.Sleep( 1000 );
            changeValueSafe( textBlock, i.ToString() );
        }
    }

    private void changeValueSafe(TextBlock textBlock, string text )
    {
        if( !textBlock.CheckAccess() )
        {
            // 대리자에게 현재 메서드를 직접 다시 호출하도록 매개변수와 함께 넘긴다.
            Dispatcher.Invoke( new Action<TextBlock, string>( changeValueSafe ), textBlock, text );

            // 또는
            // 대리자에게 TextBlock의 속성을 직접 바꾸는 익명 함수를 바로 전달한다.
            Dispatcher.Invoke( () =>
            {
                textBlock.Text = text;
            } );
        }
        else
        {
            textBlock.Text = text;
        }
    }
}

 

CheckAccess() 메서드는 이유는 모르겠지만 EditorBrowsableState 특성이 Never로 지정되어 있기 때문에 Visual Studio 자동 완성에 뜨지 않는다. 직접 전부 입력하면 정상적으로 인식하기 때문에 당황하지 말고 입력하자.

 


 

위에서 알아봤듯이 코드 비하인드에서 발생하는 크로스 스레드 문제 해결은 상당히 간단하다.

하지만 MVVM 디자인 패턴으로 개발을 진행하면서 코드 비하인드가 아닌 ViewModel에서 크로스 스레드 문제가 발생하면 난감하기 짝이 없다. Dispatcher.Invoke()가 없기 때문.

위 사진과 같이 동일한 내용의 코드가 ViewModel로 이동하면 Dispatcher를 찾을 수 없다고 나온다.

 

해결 방법은 의외로 간단한데 ViewModel이 DependancyObject를 상속하도록 하면 된다.

internal abstract class ViewModelBase : DependencyObject, INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

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

    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;

        var b = new Binding();


        NotifyPropertyChanged( propertyName );
        return true;
    }
}

 

일전에 올렸던 ViewModelBase 클래스에 DependencyObject를 상속하도록 수정했다.

 

ViewModelBase를 상속한 ViewModel에서도 Dispatcher를 사용할 수 있게 되었다.