WPF에서 실시간 변경값 바인딩 및 호출에 대한 문의드립니다.

WPF를 통해서 Uri를 통해 파일을 다운로드하면서, 실시간으로 다운로드 상태를 StatusFileSize에 보여주려고 합니다. 나중에 statusbar을 추가해서 실시간으로 변화하는 수치를 보여주려고 합니다.

문제점 및 개선 방향

  1. 당장 문제가 발생한 부분은 WindowsVM의 DownloadFile 함수에서 StatusFileSize= (int)(dProgressPercentage * 100); 를 OnPropertyChanged로 설정했음에도, View에서 StatusFileSize가 실시간으로 변경되지 않고 완료된 다음에서야 100으로 됩니다.

  2. 또한 현재는 WindowsVM에 다운로드로직(DownloadFile 함수)이 들어가 있는데 이를 별도 폴더(Helpers)를 생성해서 분리한 뒤, WindowsVM에서 호출해서 사용하려고 합니다. 그럴 경우, StatusFileSize에 대한 바인딩이 기존 View<-- WindowsVM 에서, View<--WindowsVM<--Helper 로 진행되기 때문에 Helper에서 변경되는 StatusFileSize를 어떻게 View까지 가져올지 고민됩니다.

관련해서 조언을 부탁드립니다.

<Window x:Class="WPF_DownloadFile.View.Driver_Page1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPF_DownloadFile.View"
        xmlns:vm="clr-namespace:WPF_DownloadFile.ViewModels"
        mc:Ignorable="d"
        Title="Driver_Page1"
        Height="450"
        Width="800">
    <Window.Resources>
        <vm:WindowsVM x:Key="vm" />    
    </Window.Resources>
    <DockPanel>        
        <StackPanel DockPanel.Dock="Top"
                    Orientation="Horizontal"
                    HorizontalAlignment="Left"
                    Margin="10"
                    DataContext="{StaticResource vm}">                    
            <Button Content="Download"
                    Margin="10"
                    Command="{Binding folderCommand}" />
            <TextBlock Text="{Binding FolderPath}"
                       FontSize="15" />
            <TextBlock Text="{Binding StatusFileSize, IsAsync=True}"
                       FontSize="15" />
        </StackPanel>        
    </DockPanel>
</Window>
namespace WPF_DownloadFile.ViewModels
{
    public class WindowsVM: INotifyPropertyChanged
    {     
        public FolderCommand folderCommand { get; set; }

        private string folderPath;
        public string FolderPath
        {
            get { return folderPath; }
            set
            {
                folderPath = value;
                OnPropertyChanged("FolderPath");
            }
        }
        private int _statusFileSize;
        public int StatusFileSize
        {
            get { return _statusFileSize; }
            set { _statusFileSize = value; OnPropertyChanged("StatusFileSize"); }
        } 

        public WindowsVM()
        {          
            folderCommand = new FolderCommand(this);
        }

        public event PropertyChangedEventHandler PropertyChanged;

        [NotifyPropertyChangedInvocator]
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {

            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        public void GetFolderPath()
        {
            string path = FolderHelper.GetFolderDirectory();
            FolderPath = path;
            foreach (var sw in SelectedSoftwares)
            {
                DownloadFile(sw, path);                
            }
        }      
        
        public void DownloadFile(File file, string path)
        {
            string orignalFilePath = file.OriginUri;
            string destinationFilePath = Path.Combine(path, file.FileName);
         
            Uri url = new Uri(orignalFilePath);
            System.Net.HttpWebRequest request = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(url);
            System.Net.HttpWebResponse response = (System.Net.HttpWebResponse)request.GetResponse();
            response.Close();          
            Int64 iSize = response.ContentLength;
            Int64 iRunningByteTotal = 0;
            
            using (System.Net.WebClient client = new System.Net.WebClient())
            {
                using (System.IO.Stream streamRemote = client.OpenRead(new Uri(orignalFilePath)))
                {
                    using (Stream streamLocal = new FileStream(destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None))
                    {
                        int iByteSize = 0;
                        byte[] byteBuffer = new byte[iSize];
                        while ((iByteSize = streamRemote.Read(byteBuffer, 0, byteBuffer.Length)) > 0)
                        {
                            streamLocal.Write(byteBuffer, 0, iByteSize);
                            iRunningByteTotal += iByteSize;

                            double dIndex = (double)(iRunningByteTotal);
                            double dTotal = (double)byteBuffer.Length;
                            double dProgressPercentage = (dIndex / dTotal);
                            int iProgressPercentage = (int)(dProgressPercentage * 100);     
                     
                            StatusFileSize= (int)(dProgressPercentage * 100);                         
                        
                            Console.WriteLine(iProgressPercentage);
                        }                     
                        streamLocal.Close();
                    }
                    streamRemote.Close();
                }
            }
        }
    }
}

namespace WPF_DownloadFile.ViewModels.Commands
{
    public class FolderCommand : ICommand
    {
        WindowsVM vm;
        public FolderCommand(WindowsVM _vm)
        {
            vm= _vm;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }


        public bool CanExecute(object parameter)
        {
            return true;
        }

        public void Execute(object parameter)
        {
            vm.GetFolderPath();
        }
    }
}

감사합니다.

2개의 좋아요

질문 1번에 대한 문제점

DownloadFile 메서드에서 실제 http웹으로 파일을 다운로드 받는 로직 자체가
파일 사이즈를 계산해서 스트림을 이용해 파일을 쓰는 동기 방식으로 진행 되고 있습니다.
이는 while구문이 처리되는 동안은 메세지 펌핑이 안되기 때문에 렌더링 처리가 안되고 있는 것 입니다.
(뷰에서 IsAsync=True를 하셨는데 이는 실제 내부 동작이 비동기로 처리 되는 것은 아닙니다.)

그래서 모두 다운로드가 완료된 이후 진행율 표시가 되는 것 입니다.

해결방법은
다운로드 로직을 별도 스레드로 처리하여 비동기로 처리 하면 될 것 같습니다.
위 로직을 직접 System.Threading.Tasks로 처리 하시거나
특별히 문제가 되지 않는다면 System.Net.WebClient 클래스에서 제공되는 DownloadFileAsync 비동기 메서드를 사용해 보시면 될 것 같습니다.

Dispatcher 호출로 메세지 펌핑을 시켜줘도 될것 같지만 위 비동기 처리 방식이 더 좋을 것 같습니다.

질문 2번에 대한 개선 방향

현재 생각나는 방법으로는 두가지 방법으로 생각드는데
첫번째는
다운로드 로직을 별도의 서비스로 구현해서 뷰모델에 해당 서비스를 주입 시켜 주어서
실제 다운로드 처리는 서비스 내부에서 담당하고
뷰모델은 뷰에 관련된 부분만 바인딩으로 뷰에 통보 하는 방식 입니다.
이러한 처리를 '의존성 주입’이라 불리우는데 뷰모델을 IoC같은 구조로 관리를 해야 합니다.
의존성 주입(Dependency Injection)에 대해서 찾아보시면 될 것 같습니다.

두번째는
다운로드 처리를 비지니스 로직으로 판단해서 모델에 포함 시키는 것입니다.
즉 다운로드 로직과 진행율 정보가 되는 모델 하나를 만들어서
뷰에서는 모델의 데이터를 바인딩 처리하도록 합니다. 그럼 뷰모델에서는 모델을 이용해서 다운로드 처리 를 할 수 있습니다.

5개의 좋아요

친절한 댓글 감사드립니다. 우선 질문 1번은 말씀하신대로 DownloadFileAsync에 대한 문서를 보던 중 WebClient.DownloadProgressChanged Event발견하여, 한번 적용해보려고 합니다. 적용해보고 다시 공유드리겠습니다.

3개의 좋아요

@aroooong 조언주신대로 DownloadFileAsync와 DI를 사용하여 적용하였습니다. 말씀하신대로 View에서 이제 정상적으로 보입니다. 다만, 아래 작성한 코드에 대한 개선이 필요한 점이 있다면 피드백을 주실 수 있을까요?

namespace WPF_DownloadFile.ViewModels
{
    public class WindowsVM: INotifyPropertyChanged
    {     
        public FolderCommand folderCommand { get; set; }

        private string folderPath;
        public string FolderPath
        {
            get { return folderPath; }
            set
            {
                folderPath = value;
                OnPropertyChanged("FolderPath");
            }
        }
        private int _statusFileSize;
        public int StatusFileSize
        {
            get { return _statusFileSize; }
            set { _statusFileSize = value; OnPropertyChanged("StatusFileSize"); }
        } 

        public WindowsVM()
        {          
            folderCommand = new FolderCommand(this);
            DownloadHelper dw = new DownloadHelper(this);
        }

        public event PropertyChangedEventHandler PropertyChanged;

        [NotifyPropertyChangedInvocator]
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {

            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        public void GetFolderPath()
        {
            string path = FolderHelper.GetFolderDirectory();
            FolderPath = path;
            foreach (var sw in SelectedSoftwares)
            {
                DownloadHelper.DownloadFile(sw, path);                
            }
        }            
    }
}

namespace WPF_ParsingXML.ViewModels.Helper
{
    public class DownloadHelper
    {
        static WindowsVM vm;
        public DownloadHelper(WindowsVM _vm)
        {
            vm= _vm;
        }
        public static void DownloadDriver(File sw, string path)
        {
            string fileName = Path.Combine(path, sw.FileName);
            Uri uri = new Uri(sw.OriginUri);

            using (WebClient client = new WebClient())
            {
                client.DownloadProgressChanged += new DownloadProgressChangedEventHandler(DownloadProgressCallback);
                client.DownloadFileAsync(uri, fileName);
            }
        }

        private static void DownloadProgressCallback(object sender, DownloadProgressChangedEventArgs e)
        {
            vm.StatusFileSize = e.ProgressPercentage;
        }      
    }
}

1개의 좋아요

일단 기술해 주신 코드는 제가 제시해 드린 Dependency Injection이 아닙니다.

DownloadHelper는 WindowsVM과 강력한 커플링으로 의존하고 있습니다.

사실상 저렇게 분리 하는 것은 그냥 DownloadHelper가 뷰 모델의 역할을 대신 하는 것 같아 보입니다.

아래 블로그를 한번 참고해 보셔서 다시 설계를 해보는 것은 어떨까요?
https://blog.qmatteoq.com/the-mvvm-pattern-dependency-injection/

2개의 좋아요

말씀 감사드립니다. 관련 자료를 살펴보던 중, 우선 예제에서 사용된 MVVM Light 라이브러리가 더이상 서비스를 하지 않아서 MVVM Toolkit 으로 적용해보고 다시 이곳에 포스팅하겠습니다.

1개의 좋아요

@aroooong 말씀해주신대로 수정을 해보았습니다. 관련하여 조언을 부탁드립니다.

초기에는 MVVM Toolkit을 적용하려고 하였으나, 정식 문서에서에는 app.xaml.cs 파일는 물론 View에서도 설정을 해줘야 하는 것으로 이해하였습니다. 하지만, 유사 사례 경우, Testing이전에는 해당 설정을 별도로 추가하지 않은 것 같아서는 우선을 설정없이 정상으로 동작확인하였습니다.

namespace WPF_DownloadFile.ViewModels
{
    public class WindowsVM: INotifyPropertyChanged
    {   
        private readonly IDownloadHelper _downloadHelper;  // 수정 부분!!
        public FolderCommand folderCommand { get; set; }
        private string folderPath;
        public string FolderPath
        {
            get { return folderPath; }
            set
            {
                folderPath = value;
                OnPropertyChanged("FolderPath");
            }
        }
        private int _statusFileSize;
        public int StatusFileSize
        {
            get { return _statusFileSize; }
            set { _statusFileSize = value; OnPropertyChanged("StatusFileSize"); }
        } 
        private SelectableCollection<File> _selectedFiles;
        public SelectableCollection<File> SelectedFiles
        {
            get { return _selectedFiles; }
            set
            {
                _selectedFiles= value;
                OnPropertyChanged("SelectedFiles");
            }
        }
        public WindowsVM()
        { 
            _downloadHelper = new DownloadHelper();         // 수정 부분!!
            folderCommand = new FolderCommand(this);
            DownloadHelper dw = new DownloadHelper(this);
        }

        public event PropertyChangedEventHandler PropertyChanged;

        [NotifyPropertyChangedInvocator]
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        public void GetFolderPath()
        {
            string path = FolderHelper.GetFolderDirectory();
            FolderPath = path;
            _downloadHelper.DownloadDriver(SelectedFiles, path);   // 수정 부분!!
        }   

       //   SelectedFiles 관리 로직 별도      
       ...
    }
}
namespace WPF_DownloadFile.ViewModels.Helper
{
    public interface IDownloadHelper
    {
        Task DownloadDriver(SelectableCollection<File> SelectedFiles, string path);
    }
}

namespace WPF_DownloadFile.ViewModels.Helper
{
    public class DownloadHelper : IDownloadHelper
    {
        public async Task DownloadDriver(SelectableCollection<File> SelectedFiles, string path)
        {
            foreach (var sw in File )
            {
                string fileName = Path.Combine(path, sw.FileName);
                Uri uri = new Uri(sw.OriginUri);

                using (WebClient client = new WebClient())
                {
                    client.DownloadProgressChanged += (s, e) =>
                    {
                        sw.StatusFilesize = e.ProgressPercentage;
                    };
                   await client.DownloadFileTaskAsync(uri, fileName);
                }
            }
        }
    }
}

이후, 상기 작성한 코드에서 MVVM Toolkit을 적용하려고 하였습니다.

namespace WPF_DownloadFile
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
       
        public App()
        {
            Services = ConfigureServices();
        }

        /// <summary>
        /// Gets the <see cref="IServiceProvider"/> instance to resolve application services.
        /// </summary>
        public IServiceProvider Services { get; }

        /// <summary>
        /// Gets the current <see cref="App"/> instance in use
        /// </summary>
        public new static App Current => (App)Application.Current;

        /// <summary>
        /// Configures the services for the application.
        /// </summary>
        private static IServiceProvider ConfigureServices()
        {
            var services = new ServiceCollection();
            services.AddSingleton<IDownloadHelper, DownloadHelper>();

            // Viewmodels
            services.AddTransient<WindowsVM>();
            services.AddTransient<File_Page1>();
            return services.BuildServiceProvider();
        }
        public WindowsVM MainVM => Services.GetService<WindowsVM>();
    }
}

namespace WPF_DownloadFile.ViewModels
{
    public class WindowsVM: INotifyPropertyChanged
    {   
        private readonly IDownloadHelper _downloadHelper;  

        public FolderCommand folderCommand { get; set; }
        private string folderPath;
        public string FolderPath
        {
            get { return folderPath; }
            set
            {
                folderPath = value;
                OnPropertyChanged("FolderPath");
            }
        }
        private int _statusFileSize;
        public int StatusFileSize
        {
            get { return _statusFileSize; }
            set { _statusFileSize = value; OnPropertyChanged("StatusFileSize"); }
        } 
        private SelectableCollection<File> _selectedFiles;
        public SelectableCollection<File> SelectedFiles
        {
            get { return _selectedFiles; }
            set
            {
                _selectedFiles= value;
                OnPropertyChanged("SelectedFiles");
            }
        }
        public WindowsVM(IDownloadHelper downloadHelper)   // 수정 부분!!
        { 
            _downloadHelper = downloadHelper;         // 수정 부분!!
            folderCommand = new FolderCommand(this);
            DownloadHelper dw = new DownloadHelper(this);
        }

        public event PropertyChangedEventHandler PropertyChanged;

        [NotifyPropertyChangedInvocator]
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        public void GetFolderPath()
        {
            string path = FolderHelper.GetFolderDirectory();
            FolderPath = path;
            _downloadHelper.DownloadDriver(SelectedFiles, path);   
        }   

       //   SelectedFiles 관리 로직 별도      
       ...
    }
}

하지만 상기대로 진행시, InitializeComponent()에서 WindowsVM에서 기본 생성자가 없다는 오류가 발생하게 됩니다. 관련해서 제가 빠뜨린 부분이 있다면 의견을 부탁드립니다.

namespace WPF_DownloadFile.View
{
    public partial class File_Page1: Window
    {
        public Driver_Page1()
        {
            InitializeComponent();  // 오류 발생 부분!!
        }     
    }
}
1개의 좋아요