티스토리 뷰

반응형

2022.04.11 - [WPF .NET] - 대용량 데이터처리시 발생되는 UI Freeze 문제 해결 part1

2022.04.12 - [WPF .NET] - 대용량 데이터처리시 발생되는 UI Freeze 문제 해결 part2

 

이전 포스트에서 구현한 코드를 MVVM 패턴을 사용하는 경우에는 어떻게 사용해야할까요?

쉬운 문제였죠? Behavior을 하나 만들어서 사용하면 쉽게 구현할 수 있습니다~

1. MvvmViewModel.cs

GetModels에서 반환된 IEnumerable<SampleData>를 SyncModels라는 이름의 프로퍼티로 노출 시켜줍니다.

using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
using Microsoft.Win32;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace LargeFileReadSample
{
    public class MvvmViewModel : ObservableObject
    {
        public IRelayCommand OpenCommand { get; set; }

        private IEnumerable<SampleData> _sysnModels;
        public IEnumerable<SampleData> SyncModels
        {
            get { return _sysnModels; }
            set { SetProperty(ref _sysnModels, value); }
        }

        public MvvmViewModel()
        {
            OpenCommand = new RelayCommand(OnOpen);
        }

        private void OnOpen()
        {
            var dialog = new OpenFileDialog
            {
                Filter = "All files (*.*)|*.*"
            };
            var result = dialog.ShowDialog();
            if (result == false)
            {
                return;
            }
            var fileName = dialog.FileName;
            if (string.IsNullOrEmpty(fileName))
            {
                return;
            }

            SyncModels = GetModels(fileName);
        }

        private IEnumerable<SampleData> GetModels(string fileName)
        {
            bool isFirstLine = true;
            using (var reader = new StreamReader(fileName))
            {
                while (!reader.EndOfStream)
                {
                    var line = reader.ReadLine();
                    if (isFirstLine == false && line != null)
                    {
                        var model = GetModelFromString(line);
                        yield return model;
                    }
                    else
                    {
                        isFirstLine = false;
                    }
                }
                reader.Close();
            }
        }

        private SampleData GetModelFromString(string item)
        {
            var columns = item.Split(",");
            var model = new SampleData();
            var propertys = model.GetType().GetProperties();
            for (int i = 0; i < propertys.Count(); i++)
            {
                var property = propertys[i];
                property.SetValue(model, columns[i], null);
            }
            return model;
        }
    }
}

2. ItemsControlBehavior.cs

Behavior를 하나 추가합니다.

Base Control은 ItemsControl을 이용합니다. 우리가 사용하려는 Items 프로퍼티는 ItemsControl에 기본 프로퍼티로 ListBox, ListView, DataGrid 등의 컨트롤은 모두 ItemsControl을 기본으로 만들어져있기 때문입니다.

여기에 EnumerableDatas라는 이름의 DependencyProperty를 하나 추가해서 Binding이 가능하도록 만들어 줍니다. 또한, Count 프로퍼티를 추가해서 추가되는 카운트를 외부에 노출하도록 만들어 줍니다.

using Microsoft.Xaml.Behaviors;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;

namespace LargeFileReadSample
{
    public class ItemsControlBehavior : Behavior<ItemsControl>
    {
        protected override void OnAttached()
        {
            base.OnAttached();
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();
        }

        public IEnumerable<object> EnumerableDatas
        {
            get { return (IEnumerable<object>)GetValue(EnumerableDatasProperty); }
            set { SetValue(EnumerableDatasProperty, value); }
        }

        // Using a DependencyProperty as the backing store for EnumerableDatas.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty EnumerableDatasProperty =
            DependencyProperty.Register(nameof(EnumerableDatas), typeof(IEnumerable<object>),
                typeof(ItemsControlBehavior), new PropertyMetadata(null, EnumerableDatasChanged));

        private static void EnumerableDatasChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var behavior = (ItemsControlBehavior)d;
            behavior.SetEnumerableDatas();
        }

        private async void SetEnumerableDatas()
        {
            AssociatedObject.Items.Clear();
            if (EnumerableDatas != null)
            {
                int count = 0;
                foreach (var model in EnumerableDatas)
                {
                    count++;
                    if (count % 2000 == 0)
                    {
                        Count = count;
                        await Task.Delay(1);
                    }
                    AssociatedObject.Items.Add(model);
                }
            }
        }

        public int Count
        {
            get { return (int)GetValue(CountProperty); }
            set { SetValue(CountProperty, value); }
        }

        /// <summary>
        /// Count
        /// </summary>
        public static readonly DependencyProperty CountProperty =
            DependencyProperty.Register(nameof(Count), typeof(int), typeof(ItemsControlBehavior), new PropertyMetadata(0));


    }
}

3. MvvmWindow.xaml

Behavior를 이용하기 위해서는 xmlns:b="http://schemas.microsoft.com/xaml/behaviors" 네임 스페이스를 추가해야합니다. 물론 이 네임스페이스를 사용하기전에 Microsoft.Xaml.Behaviors.Wpf nuget package를 추가해야 합니다.

 

ListBox에 ItemsControlBehavior를 추가해 줍니다. 이 Behavior의 이름을 ICB로 정의하고, TextBlock에서 Count를 출력하도록 바인딩해줍니다.

<Window
    x:Class="LargeFileReadSample.MvvmWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:LargeFileReadSample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MvvmWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <StackPanel Orientation="Horizontal">
            <Button
                Width="80"
                Command="{Binding OpenCommand}"
                Content="File Open" />
            <TextBlock Margin="10,0,0,0">
                Count :<Run x:Name="CountTextBlock" Text="{Binding ElementName=ICB, Path=Count}" />
            </TextBlock>
        </StackPanel>
        <ProgressBar
            Grid.Column="1"
            Grid.ColumnSpan="2"
            Margin="5,0"
            IsIndeterminate="True" />

        <ListBox x:Name="listBox" Grid.Row="1">
            <b:Interaction.Behaviors>
                <local:ItemsControlBehavior
                    x:Name="ICB"
                    AsyncEnumerableDatas="{Binding AsyncModels}"
                    EnumerableDatas="{Binding SyncModels}" />
            </b:Interaction.Behaviors>
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding ID}" />
                        <TextBlock Margin="5,0" Text="{Binding CaseNumber}" />
                        <TextBlock Margin="5,0" Text="{Binding Date}" />
                        <TextBlock Margin="5,0" Text="{Binding Block}" />
                        <TextBlock Margin="5,0" Text="{Binding IUCR}" />
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

        <DataGrid
            x:Name="dataGrid"
            Grid.Row="1"
            Grid.Column="1">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding ID}" Header="Id" />
                <DataGridTextColumn Binding="{Binding CaseNumber}" Header="CaseNumber" />
                <DataGridTextColumn Binding="{Binding Date}" Header="Date" />
                <DataGridTextColumn Binding="{Binding Block}" Header="Block" />
                <DataGridTextColumn Binding="{Binding IUCR}" Header="IUCR" />
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

MvvmWindow가 생성될 때 DataContext에 MvvmViewModel을 인스턴스해서 넣어줍니다. 이 방법은 일반적인 MVVM 생성 방법은 아니고, 셈플 프로젝트에서 간단하게 사용하는 방법이니 참고하시기 바랍니다.

using System.Windows;

namespace LargeFileReadSample
{
    /// <summary>
    /// Interaction logic for MvvmWindow.xaml
    /// </summary>
    public partial class MvvmWindow : Window
    {
        public MvvmWindow()
        {
            InitializeComponent();
            DataContext = new MvvmViewModel();
        }
    }
}

4. 실행 화면

App.xaml.cs에서 MvvmWindow.xaml을 시작주소로 변경하고 실행합니다.

Memory : 8.4GB

완료시간 : 약 2분

시간은 2000개당 Delay(1)을 사용하기 때문에 좀 줄어든 것으로 보입니다.

5. 기타

소스를 보시면 Async로 처리하는 방법에 대해서도 작업이 되어있습니다. 그런데, 사용해보면 IEnumerable를 사용하는 것보다 오래 걸립니다.;;; 참고하세요

6. 소스

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

 

GitHub - kaki104/WpfTest

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

github.com

 

반응형
댓글