티스토리 뷰

반응형

UI Freeze은 화면이 사용자의 입력에 반응하지 않고, 멈추는 현상을 이야기 합니다. 옛날에는 이 문제를 해결하기 위한 여러가지 꼼수를 사용해야 했지만, 이제는 async/await를 이용하면 쉽게 해결할 수 있습니다.

다들 알고 계신가요? 흐흐흐 

우선 이 현상이 발생되는 예제를 만들어 보도록 하겠습니다.

1. MainWindow.xaml (UI Freeze 발생)

CategoryComboBox1, CategoryListBox1, CategoryComboBox2, CategoryListBox2 이렇게 4개의 컨트롤을 배치하고, 각 컨트롤의 SelectionChanged 이벤트를 연결했습니다.

        <StackPanel>
            <TextBlock Text="Category1"/>
            <ComboBox x:Name="CategoryComboBox1" SelectionChanged="CategoryComboBox1_SelectionChanged">
                <ComboBoxItem Content="0_Category_1"/>
                <ComboBoxItem Content="0_Category_2"/>
                <ComboBoxItem Content="0_Category_3"/>
                <ComboBoxItem Content="0_Category_4"/>
                <ComboBoxItem Content="0_Category_5"/>
            </ComboBox>
        </StackPanel>
        <ListBox x:Name="CategoryListBox1" Grid.Row="1"
                 SelectionChanged="CategoryListBox1_SelectionChanged">
        </ListBox>
        <StackPanel Grid.Column="1">
            <TextBlock Text="Category2"/>
            <ComboBox x:Name="CategoryComboBox2" SelectionChanged="CategoryComboBox2_SelectionChanged"/>
        </StackPanel>
        <ListBox x:Name="CategoryListBox2" Grid.Column="1" Grid.Row="1"
                 SelectionChanged="CategoryListBox2_SelectionChanged"/>

콤보박스나, 리스트박스의 SelectionChanged 이벤트가 발생되면, GetDatas 메소드를 호출해서 데이터를 조회합니다. 

물론 진짜 데이터를 조회하는 것은 아니지만요~

조회된 데이터를 ItemsSource에 넣어고, SelectedIndex에 0을 넣어서 첫번째 아이템이 선택되도록 합니다. 이렇게하면, 다른 컨트롤의 SelectionChanged 이벤트가 다시 발생하여 연쇄반응을 일으키게 됩니다.

SelectionChanged이벤트는 ItemsSource가 변경될 때와 SelectedItem이 변경될 때 발생합니다. 그래서, Control의 SelectedItem이 null인지 확인하는 코드를 추가했습니다.
        private void CategoryComboBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if(CategoryComboBox1.SelectedItem == null)
            {
                return;
            }
            BusyPopup.IsOpen = true;
            var datas = GetDatas(_sleepConst, 
                ((ComboBoxItem)CategoryComboBox1.SelectedItem).Content as string ?? "0_Category_0");
            CategoryListBox1.ItemsSource = datas;
            CategoryListBox1.SelectedIndex = 0;
        }
        
        private void CategoryListBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if(CategoryListBox1.SelectedItem == null)
            {
                return;
            }
            BusyPopup.IsOpen = true;
            var datas = GetDatas(_sleepConst, CategoryListBox1.SelectedItem as string ?? "CategoryListBox1");
            CategoryComboBox2.ItemsSource = datas;
            CategoryComboBox2.SelectedIndex = 0;
        }

GetDatas메소드는 Task.Delay()를 이용해서 강제 지연을 발생 시킵니다. 반환되는 데이터는 selectedString과 연결되어있습니다.

        private IList<string> GetDatas(int sleep, string selectedString)
        {
            Task.Delay(sleep).Wait();
            _count++;
            var words = selectedString.Split('_');
            return new List<string> 
            {
                $"{_count}_{words[1] + words[2]}_01",
                $"{_count}_{words[1] + words[2]}_02",
                $"{_count}_{words[1] + words[2]}_03",
                $"{_count}_{words[1] + words[2]}_04",
                $"{_count}_{words[1] + words[2]}_05",
                $"{_count}_{words[1] + words[2]}_06",
                $"{_count}_{words[1] + words[2]}_07",
                $"{_count}_{words[1] + words[2]}_08",
                $"{_count}_{words[1] + words[2]}_09",
                $"{_count}_{words[1] + words[2]}_10",
            };
        }

실행하면 어떻게 될까요?

따란~ 15초간 아무것도 보이지도, 사용자 입력도 불가능한 상태를 지속하다가...

한번에 모든 데이터가 표시됩니다. 여기서, 콤보박스나 리스트박스의 아이템을 하나 변경하면, 또 다시 UI Freeze가 발생하고, 시간이 지나면 화면이 갑자기 갱신됩니다.

옛날 옛날에는 이렇게 되는 것을 막아 볼려고 별짓을 다 했었는데... 물론 지금도 괴로워하는 개발자가... 있겠죠?

2. SecondWindow.xaml (async/await 버전)

애플리케이션 실행을 SecondWindow.xaml로 변경하기 위해서는 App.xaml.cs에 StartupUri를 수정합니다.

    public partial class App : Application
    {
        public App()
        {
            //StartupUri = new Uri("/MainWindow.xaml", UriKind.RelativeOrAbsolute);
            StartupUri = new Uri("/SecondWindow.xaml", UriKind.RelativeOrAbsolute);
        }
    }

GetDatasAsync 메소드에서는 await Task.Delay(sleep);를 이용해서 강제로 대기시간을 발생시키고 있습니다.

나머지는 위에서 사용한 GetDatas 메소드와 동일합니다.

HttpClient나 서비스 등의 호출 후 대기하는 부분을 async/await로 만들어서 활용하시면 됩니다.
        private async Task<IList<string>> GetDatasAsync(int sleep, string selectedString)
        {
            await Task.Delay(sleep);
            _count++;
            var words = selectedString.Split('_');
            return new List<string>
            {
                $"{_count}_{words[1] + words[2]}_01",
                $"{_count}_{words[1] + words[2]}_02",
                $"{_count}_{words[1] + words[2]}_03",
                $"{_count}_{words[1] + words[2]}_04",
                $"{_count}_{words[1] + words[2]}_05",
                $"{_count}_{words[1] + words[2]}_06",
                $"{_count}_{words[1] + words[2]}_07",
                $"{_count}_{words[1] + words[2]}_08",
                $"{_count}_{words[1] + words[2]}_09",
                $"{_count}_{words[1] + words[2]}_10",
            };
        }

SelectionChanged 이벤트가 발생할 때마다, GetDatasAsync 메소드를 await 키워드를 추가해서 호출해주기만 하면됩니다. 참 쉽죠?

        private async void CategoryComboBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (CategoryComboBox1.SelectedItem == null)
            {
                return;
            }
            BusyPopup.IsOpen = true;
            var datas = await GetDatasAsync(_sleepConst, ((ComboBoxItem)CategoryComboBox1.SelectedItem).Content as string ?? "0_Category_0");
            CategoryListBox1.ItemsSource = datas;
            CategoryListBox1.SelectedIndex = 0;
        }

        private async void CategoryListBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (CategoryListBox1.SelectedItem == null)
            {
                return;
            }
            BusyPopup.IsOpen = true;
            var datas = await GetDatasAsync(_sleepConst, CategoryListBox1.SelectedItem as string ?? "CategoryListBox1");
            CategoryComboBox2.ItemsSource = datas;
            CategoryComboBox2.SelectedIndex = 0;
        }

실행을 해보겠습니다.

첫 실행화면에서 아까는 볼 수 없었던 Busy 컨트롤이 보입니다.

리스트박스와 콤보박스가 시간이 지나면서 채워지는 것을 바로바로 확인할 수 있습니다. 물론 Busy 컨트롤은 계속 떠있는 중이구요

모든 데이터를 출력한 후에는 Busy 컨트롤이 숨겨집니다.

이렇게 간단하게 async/await를 이용하도록 메소드를 변경하면, UI Freeze 문제를 쉽게 해결할 수 있습니다.

이 방법은 WPF, WinForm 모두 사용할 수 있으니 꼭 활용하시면 좋겠습니다.

 

Async/await 프로그래밍에 대한 더 자세한 사항은 여기를 참고하시기 바랍니다.

3. 소스

WpfTest/EventAsyncSample at master · kaki104/WpfTest (github.com)

 

GitHub - kaki104/WpfTest

Contribute to kaki104/WpfTest development by creating an account on GitHub.

github.com

 

반응형
댓글