title: Download multiple files using MVVM-WPF
body: <p>At first I made an application that downloads a file from the entered link and displays information about progress, speed, etc. When I decided to change the application to download several files at the same time, I ran into a problem. So, in the interface there is a listbox in which there are several objects. When you select one of the objects and enter the link to the file, it should start downloading. When selecting another object, information about the previous object should change to the information of the selected one. I can also enter a link to the file there and then track the downloads of the two files by switching between objects. However, when switching information does not change. How to implement it?</p>
  
  <p>Model:</p>
  
  <pre><code>public class Model
  {
      public WebClient webClient = new WebClient();
      public Stopwatch stopWatch = new Stopwatch();
      public event Action&lt;long&gt; FileSizeChanged;
      public event Action&lt;long, TimeSpan&gt; DownloadBytesChanged;
      public event Action&lt;double&gt; ProgressPercentageChanged;
      public event Action DownloadComplete;
  
      public string Name { get; set; }
  
      public void DownloadFile(string url, bool openAfterDownload)
      {
          if (webClient.IsBusy)
              throw new Exception("The client is busy");
          try
          {
              var startDownloading = DateTime.UtcNow;
              webClient.Proxy = null;
              if (!SelectFolder(Path.GetFileName(url)+Path.GetExtension(url), out var filePath))
                  throw DownloadingError();
              webClient.DownloadProgressChanged += (o, args) =&gt;
              {
                  ProgressPercentageChanged?.Invoke(args.ProgressPercentage);
                  FileSizeChanged?.Invoke(args.TotalBytesToReceive);
                  DownloadBytesChanged?.Invoke(args.BytesReceived, DateTime.UtcNow - startDownloading);
                  if (args.ProgressPercentage &gt;= 100 &amp;&amp; openAfterDownload)
                      Process.Start(filePath);
              };
              webClient.DownloadFileCompleted += (o, args) =&gt; DownloadComplete?.Invoke();
              stopWatch.Start();
              webClient.DownloadFileAsync(new Uri(url), filePath);
          }
          catch (Exception e)
          {
              throw DownloadingError();
          }
      }
  
      public void CancelDownloading()
      {
          webClient.CancelAsync();
          webClient.Dispose();
          DownloadComplete?.Invoke();
      }
  
      private static Exception DownloadingError()
          =&gt; new Exception("Downloading error!");
  
      private static bool SelectFolder(string fileName, out string filePath)
      {
          var saveFileDialog = new SaveFileDialog
          {
              InitialDirectory = "c:\\",
              FileName = fileName,
              Filter = "All files (*.*)|*.*"
          };
          filePath = "";
          if (saveFileDialog.ShowDialog() != true) return false;
          filePath = saveFileDialog.FileName;
          return true;
      }
  }
  </code></pre>
  
  <p>ViewModel: </p>
  
  <pre><code>class MainVM : INotifyPropertyChanged
  {
      private string url;
      private RelayCommand downloadCommand;
      private RelayCommand cancelCommand;
      private double progressBarValue;
      private string bytesReceived;
      private string bytesTotal;
      private string speed;
      private string time;
      private string error;
      private long totalBytes;
      private Model selectedGame;
      public ObservableCollection&lt;Model&gt; Games { get; set; }
  
      public MainVM()
      {
          Games = new ObservableCollection&lt;Model&gt;();
  
          Model Game1 = new Model { Name = "Name1" };
          Model Game2 = new Model { Name = "Name2" };
  
          Game1.FileSizeChanged += bytes =&gt; BytesTotal = PrettyBytes(totalBytes = bytes);
          Game1.DownloadBytesChanged += (bytes, time) =&gt;
          {
              BytesReceived = PrettyBytes(bytes);
              Speed = DownloadingSpeed(bytes, time);
              Time = DownloadingTime(bytes, totalBytes, time);
          };
          Game1.ProgressPercentageChanged += percentage =&gt; ProgressBarValue = percentage;
          Game1.DownloadComplete += () =&gt;
          {
              BytesReceived = "";
              BytesTotal = "";
              Speed = "";
              Time = "";
              ProgressBarValue = 0;
          };
  
          Game2.FileSizeChanged += bytes =&gt; BytesTotal = PrettyBytes(totalBytes = bytes);
          Game2.DownloadBytesChanged += (bytes, time) =&gt;
          {
              BytesReceived = PrettyBytes(bytes);
              Speed = DownloadingSpeed(bytes, time);
              Time = DownloadingTime(bytes, totalBytes, time);
          };
          Game2.ProgressPercentageChanged += percentage =&gt; ProgressBarValue = percentage;
          Game2.DownloadComplete += () =&gt;
          {
              BytesReceived = "";
              BytesTotal = "";
              Speed = "";
              Time = "";
              ProgressBarValue = 0;
          };
          Games.Add(Game1);
          Games.Add(Game2);
      }
  
      public Model SelectedGame
      {
          get =&gt; selectedGame;
          set
          {
              if (value == selectedGame) return; 
              selectedGame = value;
              OnPropertyChanged(nameof(SelectedGame));
          }
      }
  
      public string Error
      {
          get =&gt; error;
          private set
          {
              error = value;
              OnPropertyChanged(nameof(Error));
          }
      }
      public string URL
      {
          get =&gt; url;
          set
          {
              url = value;
              OnPropertyChanged(nameof(URL));
          }
      }
  
      public bool OpenDownloadedFile { get; set; }
  
      public double ProgressBarValue
      {
          get =&gt; progressBarValue;
          set
          {
              progressBarValue = value;
              OnPropertyChanged(nameof(ProgressBarValue));
          }
      }
  
      public string BytesTotal
      {
          get =&gt; bytesTotal;
          private set
          {
              bytesTotal = value;
              OnPropertyChanged(nameof(BytesTotal));
          }
      }
  
      public string BytesReceived
      {
          get =&gt; bytesReceived;
          private set
          {
              bytesReceived = value;
              OnPropertyChanged(nameof(BytesReceived));
          }
      }
  
      public string Speed
      {
          get =&gt; speed;
          private set
          {
              speed = value;
              OnPropertyChanged(nameof(Speed));
          }
      }
  
      public string Time
      {
          get =&gt; time;
          private set
          {
              time = value;
              OnPropertyChanged(nameof(Time));
          }
      }
  
      public RelayCommand DownloadCommand =&gt;
          downloadCommand ??
          (downloadCommand = new RelayCommand(DownloadButton_Click));
  
      public RelayCommand CancelCommand =&gt;
          cancelCommand ??
          (cancelCommand = new RelayCommand(CancelButton_Click));
  
      private void DownloadButton_Click(object obj)
      {
          if (url == null &amp;&amp; url == "") return;
          try
          {
              SelectedGame.DownloadFile(url, OpenDownloadedFile);
          }
          catch (Exception e)
          {
              Error = e.Message;
          }
      }
  
      private void CancelButton_Click(object obj)
      {
          if (url != null || url != "")
              SelectedGame.CancelDownloading();
      }
  
      public event PropertyChangedEventHandler PropertyChanged;
      protected virtual void OnPropertyChanged(string prop = "")
      {
          PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
      }
      private static string PrettyBytes(double bytes)
      {
          if (bytes &lt; 1024)
              return bytes + "Bytes";
          if (bytes &lt; Math.Pow(1024, 2))
              return (bytes / 1024).ToString("F" + 2) + "Kilobytes";
          if (bytes &lt; Math.Pow(1024, 3))
              return (bytes / Math.Pow(1024, 2)).ToString("F" + 2) + "Megabytes";
          if (bytes &lt; Math.Pow(1024, 4))
              return (bytes / Math.Pow(1024, 5)).ToString("F" + 2) + "Gygabytes";
          return (bytes / Math.Pow(1024, 4)).ToString("F" + 2) + "terabytes";
      }
  
      public static string DownloadingSpeed(long received, TimeSpan time)
      {
          return ((double)received / 1024 / 1024 / time.TotalSeconds).ToString("F" + 2) + " megabytes/sec";
      }
      public static string DownloadingTime(long received, long total, TimeSpan time)
      {
          var receivedD = (double) received;
          var totalD = (double) total;
          return ((totalD / (receivedD / time.TotalSeconds)) - time.TotalSeconds).ToString("F" + 1) + "sec";
      }
  }
  </code></pre>
  
  <p>View:</p>
  
  <pre><code>&lt;Window x:Class="DownloadingFiles.MainWindow"
      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:DownloadingFiles"
      mc:Ignorable="d"
      Title="MainWindow" Height="450" Width="800"&gt;
  &lt;Window.DataContext&gt;
      &lt;local:MainVM/&gt;
  &lt;/Window.DataContext&gt;
  &lt;Grid&gt;
      &lt;Grid.RowDefinitions&gt;
          &lt;RowDefinition&gt;&lt;/RowDefinition&gt;
          &lt;RowDefinition&gt;&lt;/RowDefinition&gt;
          &lt;RowDefinition&gt;&lt;/RowDefinition&gt;
          &lt;RowDefinition&gt;&lt;/RowDefinition&gt;
      &lt;/Grid.RowDefinitions&gt;
      &lt;Grid.ColumnDefinitions&gt;
          &lt;ColumnDefinition&gt;&lt;/ColumnDefinition&gt;
          &lt;ColumnDefinition&gt;&lt;/ColumnDefinition&gt;
          &lt;ColumnDefinition&gt;&lt;/ColumnDefinition&gt;
          &lt;ColumnDefinition&gt;&lt;/ColumnDefinition&gt;
      &lt;/Grid.ColumnDefinitions&gt;
      &lt;Canvas Grid.Column="1" Grid.ColumnSpan="3" Grid.RowSpan="4"&gt;
          &lt;TextBox Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2" Text="{Binding URL, UpdateSourceTrigger=PropertyChanged}"
              FontSize="40" Width="424"/&gt;
          &lt;Button Grid.Row="0" Grid.Column="3" Content="DOWNLOAD" FontSize="30" FontFamily="./#Sochi2014" Command="{Binding DownloadCommand}" Canvas.Left="429" Canvas.Top="-2" Width="157"/&gt;
          &lt;Label Grid.Row="1" Grid.Column="2" Content="{Binding Error, Mode=OneWay}" FontFamily="./#Sochi2014" Height="45" VerticalAlignment="Bottom" Canvas.Left="401" Canvas.Top="123" Width="184" /&gt;
          &lt;CheckBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" FontSize="30" Content="Open after downloading"
                    IsChecked="{Binding OpenDownloadedFile, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" FontFamily="./#Sochi2014" Canvas.Left="15" Canvas.Top="80"/&gt;
          &lt;Button Grid.Row="1" Grid.Column="3" Content="CANCEL" FontSize="30" FontFamily="./#Sochi2014" Command ="{Binding CancelCommand}" Canvas.Left="429" Canvas.Top="50" Width="157"/&gt;
          &lt;Label Grid.Row="2" Grid.Column="1" Content="{Binding Time, Mode=OneWay}" FontSize="30" FontFamily="./#Sochi2014" Height="40" Width="69" Canvas.Left="310" Canvas.Top="277" RenderTransformOrigin="2.284,1.56"/&gt;
          &lt;Label Grid.Row="2" Grid.Column="3" Content="{Binding Speed, Mode=OneWay}" FontSize="30" FontFamily="./#Sochi2014" Height="40" Width="193" Canvas.Left="15" Canvas.Top="277"/&gt;
          &lt;ProgressBar Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2" Value="{Binding ProgressBarValue}"  Foreground="#AAA1C8" Height="75" Width="424" Canvas.Left="15" Canvas.Top="335"/&gt;
          &lt;Label Grid.Row="3" FontSize="30" FontFamily="./#Sochi2014" Content="{Binding ProgressBarValue}" Grid.ColumnSpan="2" Canvas.Left="230" Canvas.Top="339"/&gt;
          &lt;Label Grid.Row="3" Grid.Column="3" Content="{Binding BytesReceived, Mode=OneWay}" FontSize="30" FontFamily="./#Sochi2014" Height="40" VerticalAlignment="Top" Canvas.Left="448" Canvas.Top="299" Width="137"/&gt;
          &lt;Label Grid.Row="3" Grid.Column="3" Content="{Binding BytesTotal, Mode=OneWay}" FontSize="30" FontFamily="./#Sochi2014" Height="44" Canvas.Left="448" Canvas.Top="344" Width="137" /&gt;
          &lt;Label Content="{Binding Name}" Height="40" Width="186" Canvas.Left="22" Canvas.Top="202"/&gt;
      &lt;/Canvas&gt;
  
      &lt;ListBox Grid.Row="0" Grid.Column="0" Grid.RowSpan="4" ItemsSource="{Binding Games}"
              SelectedItem="{Binding SelectedGame, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectedIndex="0" &gt;
          &lt;ListBox.ItemTemplate&gt;
              &lt;DataTemplate&gt;
                  &lt;StackPanel&gt;
                      &lt;TextBlock FontSize="20" Text="{Binding Name}"/&gt;
                  &lt;/StackPanel&gt;
              &lt;/DataTemplate&gt;
          &lt;/ListBox.ItemTemplate&gt;
      &lt;/ListBox&gt;
  &lt;/Grid&gt;
  </code></pre>
  
  <p></p>
  
  <p>RelayCommand:</p>
  
  <pre><code>public class RelayCommand : ICommand
  {
      private readonly Action&lt;object&gt; _execute;
      private readonly Predicate&lt;object&gt; _canExecute;
  
      public RelayCommand(Action&lt;object&gt; execute, Predicate&lt;object&gt; canExecute = null)
      {
          if (execute == null) throw new ArgumentNullException("execute");
  
          _execute = execute;
          _canExecute = canExecute;
      }
  
      public bool CanExecute(object parameter)
      {
          return _canExecute == null || _canExecute(parameter);
      }
  
      public event EventHandler CanExecuteChanged
      {
          add { CommandManager.RequerySuggested += value; }
          remove { CommandManager.RequerySuggested -= value; }
      }
  
      public void Execute(object parameter)
      {
          _execute(parameter ?? "&lt;N/A&gt;");
      }
  }
  </code></pre>