Загрузка нескольких файлов с помощью MVVM-WPF

#c# #wpf #mvvm #webclient

#c# #wpf #mvvm #webclient

Вопрос:

Сначала я создал приложение, которое загружает файл по введенной ссылке и отображает информацию о ходе выполнения, скорости и т.д. Когда я решил изменить приложение, чтобы загружать несколько файлов одновременно, я столкнулся с проблемой. Итак, в интерфейсе есть поле списка, в котором есть несколько объектов. Когда вы выбираете один из объектов и вводите ссылку на файл, он должен начать загрузку. При выборе другого объекта информация о предыдущем объекте должна измениться на информацию о выбранном объекте. Я также могу ввести ссылку на файл там, а затем отслеживать загрузки двух файлов, переключаясь между объектами. Однако при переключении информация не меняется. Как это реализовать?

Модель:

 public class Model
{
    public WebClient webClient = new WebClient();
    public Stopwatch stopWatch = new Stopwatch();
    public event Action<long> FileSizeChanged;
    public event Action<long, TimeSpan> DownloadBytesChanged;
    public event Action<double> 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) =>
            {
                ProgressPercentageChanged?.Invoke(args.ProgressPercentage);
                FileSizeChanged?.Invoke(args.TotalBytesToReceive);
                DownloadBytesChanged?.Invoke(args.BytesReceived, DateTime.UtcNow - startDownloading);
                if (args.ProgressPercentage >= 100 amp;amp; openAfterDownload)
                    Process.Start(filePath);
            };
            webClient.DownloadFileCompleted  = (o, args) => 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()
        => 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;
    }
}
  

ViewModel:

 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<Model> Games { get; set; }

    public MainVM()
    {
        Games = new ObservableCollection<Model>();

        Model Game1 = new Model { Name = "Name1" };
        Model Game2 = new Model { Name = "Name2" };

        Game1.FileSizeChanged  = bytes => BytesTotal = PrettyBytes(totalBytes = bytes);
        Game1.DownloadBytesChanged  = (bytes, time) =>
        {
            BytesReceived = PrettyBytes(bytes);
            Speed = DownloadingSpeed(bytes, time);
            Time = DownloadingTime(bytes, totalBytes, time);
        };
        Game1.ProgressPercentageChanged  = percentage => ProgressBarValue = percentage;
        Game1.DownloadComplete  = () =>
        {
            BytesReceived = "";
            BytesTotal = "";
            Speed = "";
            Time = "";
            ProgressBarValue = 0;
        };

        Game2.FileSizeChanged  = bytes => BytesTotal = PrettyBytes(totalBytes = bytes);
        Game2.DownloadBytesChanged  = (bytes, time) =>
        {
            BytesReceived = PrettyBytes(bytes);
            Speed = DownloadingSpeed(bytes, time);
            Time = DownloadingTime(bytes, totalBytes, time);
        };
        Game2.ProgressPercentageChanged  = percentage => ProgressBarValue = percentage;
        Game2.DownloadComplete  = () =>
        {
            BytesReceived = "";
            BytesTotal = "";
            Speed = "";
            Time = "";
            ProgressBarValue = 0;
        };
        Games.Add(Game1);
        Games.Add(Game2);
    }

    public Model SelectedGame
    {
        get => selectedGame;
        set
        {
            if (value == selectedGame) return; 
            selectedGame = value;
            OnPropertyChanged(nameof(SelectedGame));
        }
    }

    public string Error
    {
        get => error;
        private set
        {
            error = value;
            OnPropertyChanged(nameof(Error));
        }
    }
    public string URL
    {
        get => url;
        set
        {
            url = value;
            OnPropertyChanged(nameof(URL));
        }
    }

    public bool OpenDownloadedFile { get; set; }

    public double ProgressBarValue
    {
        get => progressBarValue;
        set
        {
            progressBarValue = value;
            OnPropertyChanged(nameof(ProgressBarValue));
        }
    }

    public string BytesTotal
    {
        get => bytesTotal;
        private set
        {
            bytesTotal = value;
            OnPropertyChanged(nameof(BytesTotal));
        }
    }

    public string BytesReceived
    {
        get => bytesReceived;
        private set
        {
            bytesReceived = value;
            OnPropertyChanged(nameof(BytesReceived));
        }
    }

    public string Speed
    {
        get => speed;
        private set
        {
            speed = value;
            OnPropertyChanged(nameof(Speed));
        }
    }

    public string Time
    {
        get => time;
        private set
        {
            time = value;
            OnPropertyChanged(nameof(Time));
        }
    }

    public RelayCommand DownloadCommand =>
        downloadCommand ??
        (downloadCommand = new RelayCommand(DownloadButton_Click));

    public RelayCommand CancelCommand =>
        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 < 1024)
            return bytes   "Bytes";
        if (bytes < Math.Pow(1024, 2))
            return (bytes / 1024).ToString("F"   2)   "Kilobytes";
        if (bytes < Math.Pow(1024, 3))
            return (bytes / Math.Pow(1024, 2)).ToString("F"   2)   "Megabytes";
        if (bytes < 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";
    }
}
  

Вид:

 <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">
<Window.DataContext>
    <local:MainVM/>
</Window.DataContext>
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition></ColumnDefinition>
        <ColumnDefinition></ColumnDefinition>
        <ColumnDefinition></ColumnDefinition>
        <ColumnDefinition></ColumnDefinition>
    </Grid.ColumnDefinitions>
    <Canvas Grid.Column="1" Grid.ColumnSpan="3" Grid.RowSpan="4">
        <TextBox Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2" Text="{Binding URL, UpdateSourceTrigger=PropertyChanged}"
            FontSize="40" Width="424"/>
        <Button Grid.Row="0" Grid.Column="3" Content="DOWNLOAD" FontSize="30" FontFamily="./#Sochi2014" Command="{Binding DownloadCommand}" Canvas.Left="429" Canvas.Top="-2" Width="157"/>
        <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" />
        <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"/>
        <Button Grid.Row="1" Grid.Column="3" Content="CANCEL" FontSize="30" FontFamily="./#Sochi2014" Command ="{Binding CancelCommand}" Canvas.Left="429" Canvas.Top="50" Width="157"/>
        <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"/>
        <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"/>
        <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"/>
        <Label Grid.Row="3" FontSize="30" FontFamily="./#Sochi2014" Content="{Binding ProgressBarValue}" Grid.ColumnSpan="2" Canvas.Left="230" Canvas.Top="339"/>
        <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"/>
        <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" />
        <Label Content="{Binding Name}" Height="40" Width="186" Canvas.Left="22" Canvas.Top="202"/>
    </Canvas>

    <ListBox Grid.Row="0" Grid.Column="0" Grid.RowSpan="4" ItemsSource="{Binding Games}"
            SelectedItem="{Binding SelectedGame, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectedIndex="0" >
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel>
                    <TextBlock FontSize="20" Text="{Binding Name}"/>
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Grid>
  

RelayCommand:

 public class RelayCommand : ICommand
{
    private readonly Action<object> _execute;
    private readonly Predicate<object> _canExecute;

    public RelayCommand(Action<object> execute, Predicate<object> 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 ?? "<N/A>");
    }
}
  

Комментарии:

1. github.com/markodt/SGet

Ответ №1:

Вы должны выполнить привязку к SelectedGame свойству. Но чтобы полностью включить переключение между элементами загрузки, вам пришлось бы провести рефакторинг вашего кода и перенести определенные атрибуты загрузки (например, прогресс, скорость) в отдельный класс для каждой загрузки (потому что SelectedGame не предоставляет все требуемые атрибуты). Таким образом, каждая игра или загружаемый элемент имеет свою собственную информацию, связанную с загрузкой, в представлении.

Итак, я представил DownloadItem класс, который инкапсулирует атрибуты или данные, связанные с donwnload. Этот класс представляет вашу игру или загружаемые элементы, которые вы можете выбрать в ListView :

 class DownloadItem : INotifyPropertyChanged
{
  public DownloadItem()
  {
    this.DisplayBytesTotal = string.Empty;
    this.Url = string.Empty;
    this.DownloadSpeed = string.Empty;
    this.ErrorMessage = string.Empty;
    this.Name = string.Empty;
    this.ProgressBytesRead = string.Empty;
  }

  [NotifyPropertyChangedInvocator]
  protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
  {
    this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  }

  public event PropertyChangedEventHandler PropertyChanged;

  private string name;    
  public string Name
  {
    get => this.name;
    set
    {
      if (value == this.name) return;
      this.name = value;
      OnPropertyChanged();
    }
  }

  private string url;    
  public string Url
  {
    get => this.url;
    set
    {
      if (value == this.url) return;
      this.url = value;
      OnPropertyChanged();
    }
  }

  private double progress;    
  public double Progress
  {
    get => this.progress;
    set
    {
      this.progress = value;
      OnPropertyChanged();
    }
  }

  private bool isOpenAfterDownloadEnabled;    
  public bool IsOpenAfterDownloadEnabled
  {
    get => this.isOpenAfterDownloadEnabled;
    set
    {
      this.isOpenAfterDownloadEnabled = value;
      OnPropertyChanged();
    }
  }

  private string progressBytesRead;    
  public string ProgressBytesRead
  {
    get => this.progressBytesRead;
    set
    {
      if (value == this.progressBytesRead) return;
      this.progressBytesRead = value;
      OnPropertyChanged();
    }
  }

  private long bytesTotal;    
  public long BytesTotal
  {
    get => this.bytesTotal;
    set
    {
      if (value == this.bytesTotal) return;
      this.bytesTotal = value;
      OnPropertyChanged();
    }
  }

  private string displayBytesTotal;    
  public string DisplayBytesTotal
  {
    get => this.displayBytesTotal;
    set
    {
      if (value == this.displayBytesTotal) return;
      this.displayBytesTotal = value;
      OnPropertyChanged();
    }
  }

  private string downloadSpeed;    
  public string DownloadSpeed
  {
    get => this.downloadSpeed;
    set
    {
      if (value == this.downloadSpeed) return;
      this.downloadSpeed = value;
      OnPropertyChanged();
    }
  }

  private string timeElapsed;    
  public string TimeElapsed
  {
    get => this.timeElapsed;
    set
    {
      if (value == this.timeElapsed) return;
      this.timeElapsed = value;
      OnPropertyChanged();
    }
  }

  private string errorMessage;    
  public string ErrorMessage
  {
    get => this.errorMessage;
    set
    {
      if (value == this.errorMessage) return;
      this.errorMessage = value;
      OnPropertyChanged();
    }
  }
}
  

Затем, чтобы инкапсулировать поведение загрузки, я изменил ваш Model класс и переименовал его в Downloader . Каждый DownloadItem связан с одним Downloader . Поэтому Downloader теперь сам обрабатывает ход выполнения связанных с ним DownloadItem операций и соответствующим образом обновляет DownloadItem :

 class Downloader
{
  public DownloadItem CurrentDownloadItem { get; set; }
  public WebClient webClient = new WebClient();
  public Stopwatch stopWatch = new Stopwatch();
  public event Action<long> FileSizeChanged;
  public event Action<long, TimeSpan> DownloadBytesChanged;
  public event Action<double> ProgressPercentageChanged;
  public event Action DownloadComplete;


  public void DownloadFile(DownloadItem gameToDownload)
  {
    this.CurrentDownloadItem = gameToDownload;
    if (webClient.IsBusy)
      throw new Exception("The client is busy");

    var startDownloading = DateTime.UtcNow;
    webClient.Proxy = null;
    if (!SelectFolder(
      Path.GetFileName(this.CurrentDownloadItem.Url)   Path.GetExtension(this.CurrentDownloadItem.Url),
      out var filePath))
    {
      DownloadingError();
      return;
    }

    webClient.DownloadProgressChanged  = (o, args) =>
    {
      UpdateProgressPercentage(args.ProgressPercentage);
      UpdateFileSize(args.TotalBytesToReceive);
      UpdateProgressBytesRead(args.BytesReceived, DateTime.UtcNow - startDownloading);
      if (args.ProgressPercentage >= 100 amp;amp; this.CurrentDownloadItem.IsOpenAfterDownloadEnabled)
        Process.Start(filePath);
    };
    webClient.DownloadFileCompleted  = OnDownloadCompleted;
    stopWatch.Start();
    webClient.DownloadFileAsync(new Uri(this.CurrentDownloadItem.Url), filePath);
  }

  public void CancelDownloading()
  {
    webClient.CancelAsync();
    webClient.Dispose();
    DownloadComplete?.Invoke();
  }

  private string PrettyBytes(double bytes)
  {
    if (bytes < 1024)
      return bytes   "Bytes";
    if (bytes < Math.Pow(1024, 2))
      return (bytes / 1024).ToString("F"   2)   "Kilobytes";
    if (bytes < Math.Pow(1024, 3))
      return (bytes / Math.Pow(1024, 2)).ToString("F"   2)   "Megabytes";
    if (bytes < Math.Pow(1024, 4))
      return (bytes / Math.Pow(1024, 5)).ToString("F"   2)   "Gygabytes";
    return (bytes / Math.Pow(1024, 4)).ToString("F"   2)   "terabytes";
  }

  private string DownloadingSpeed(long received, TimeSpan time)
  {
    return ((double) received / 1024 / 1024 / time.TotalSeconds).ToString("F"   2)   " megabytes/sec";
  }

  private 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";
  }

  private void OnDownloadCompleted(object sender, AsyncCompletedEventArgs asyncCompletedEventArgs)
  {
  }

  private void UpdateProgressPercentage(double percentage)
  {
    this.CurrentDownloadItem.Progress = percentage;
  }

  private void UpdateProgressBytesRead(long bytes, TimeSpan time)
  {
    this.CurrentDownloadItem.ProgressBytesRead = PrettyBytes(bytes);
    this.CurrentDownloadItem.DownloadSpeed = DownloadingSpeed(bytes, time);
    this.CurrentDownloadItem.TimeElapsed = DownloadingTime(bytes, this.CurrentDownloadItem.BytesTotal, time);
  }

  protected virtual void UpdateFileSize(long bytes)
  {
    this.CurrentDownloadItem.DisplayBytesTotal = PrettyBytes(bytes);
  }

  private void DownloadingError()
    => this.CurrentDownloadItem.ErrorMessage = "Downloading Error";

  private static bool SelectFolder(string fileName, out string filePath)
  {
    var saveFileDialog = new SaveFileDialog
    {
      InitialDirectory = @"C:UsersMusicMonkeyDownloads",
      FileName = fileName,
      Filter = "All files (*.*)|*.*",
    };
    filePath = "";
    if (saveFileDialog.ShowDialog() != true)
      return false;
    filePath = saveFileDialog.FileName;
    return true;
  }
}
  

Я настоятельно рекомендую перенести SaveFileDialog и взаимодействие в представление. Таким образом, вы исключили бы зависимости модели просмотра для просмотра связанных операций или логики.

Переработанная модель представления будет выглядеть следующим образом:

 class TestViewModel : INotifyPropertyChanged
{
  private RelayCommand downloadCommand;
  private RelayCommand cancelCommand;
  private DownloadItem selectedGame;
  public ObservableCollection<DownloadItem> Games { get; set; }

  private Dictionary<DownloadItem, Downloader> DownloaderMap { get; set; }

  public TestViewModel()
  {
    this.Games = new ObservableCollection<DownloadItem>();
    this.DownloaderMap = new Dictionary<DownloadItem, Downloader>();

    var game1 = new DownloadItem() {Name = "Name1"};
    this.Games.Add(game1);
    this.DownloaderMap.Add(game1, new Downloader());
    var game2 = new DownloadItem() {Name = "Name2"};
    this.Games.Add(game2);
    this.DownloaderMap.Add(game2, new Downloader());
  }

  public DownloadItem SelectedGame
  {
    get => selectedGame;
    set
    {
      if (value == selectedGame)
        return;
      selectedGame = value;
      OnPropertyChanged(nameof(SelectedGame));
    }
  }

  public RelayCommand DownloadCommand =>
    downloadCommand ??
    (downloadCommand = new RelayCommand((param) => DownloadButton_Click(param), (param) => true));

  public RelayCommand CancelCommand =>
    cancelCommand ??
    (cancelCommand = new RelayCommand((param) => CancelButton_Click(param), (param) => true));

  private void DownloadButton_Click(object obj)
  {
    if (string.IsNullOrWhiteSpace(this.SelectedGame.Url))
      return;

    if (this.DownloaderMap.TryGetValue(this.SelectedGame, out Downloader downloader))
    {
      downloader.DownloadFile(this.SelectedGame);
    }
  }

  private void CancelButton_Click(object obj)
  {
    if (!string.IsNullOrWhiteSpace(this.SelectedGame.Url) amp;amp;
        this.DownloaderMap.TryGetValue(this.SelectedGame, out Downloader downloader))
    {
      downloader.CancelDownloading();
    }
  }
}
  

На последнем шаге я обновил привязки представления к новым свойствам:

 <Grid>
  <Grid.RowDefinitions>
    <RowDefinition></RowDefinition>
    <RowDefinition></RowDefinition>
    <RowDefinition></RowDefinition>
    <RowDefinition></RowDefinition>
  </Grid.RowDefinitions>
  <Grid.ColumnDefinitions>
    <ColumnDefinition></ColumnDefinition>
    <ColumnDefinition></ColumnDefinition>
    <ColumnDefinition></ColumnDefinition>
    <ColumnDefinition></ColumnDefinition>
  </Grid.ColumnDefinitions>
  <Canvas Grid.Column="1"
          Grid.ColumnSpan="3"
          Grid.RowSpan="4">
    <TextBox Grid.Row="0"
             Grid.Column="1"
             Grid.ColumnSpan="2"
             Text="{Binding SelectedGame.Url, UpdateSourceTrigger=PropertyChanged}"
             FontSize="40"
             Width="424" />
    <Button Grid.Row="0"
            Grid.Column="3"
            Content="DOWNLOAD"
            FontSize="30"
            FontFamily="./#Sochi2014"
            Command="{Binding DownloadCommand}"
            Canvas.Left="429"
            Canvas.Top="-2"
            Width="157" />
    <Label Grid.Row="1"
           Grid.Column="2"
           Content="{Binding SelectedGame.ErrorMessage, Mode=OneWay}"
           FontFamily="./#Sochi2014"
           Height="45"
           VerticalAlignment="Bottom"
           Canvas.Left="401"
           Canvas.Top="123"
           Width="184" />
    <CheckBox Grid.Row="1"
              Grid.Column="1"
              Grid.ColumnSpan="2"
              FontSize="30"
              Content="Open after downloading"
              IsChecked="{Binding SelectedGame.IsOpenAfterDownloadEnabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
              FontFamily="./#Sochi2014"
              Canvas.Left="15"
              Canvas.Top="80" />
    <Button Grid.Row="1"
            Grid.Column="3"
            Content="CANCEL"
            FontSize="30"
            FontFamily="./#Sochi2014"
            Command="{Binding CancelCommand}"
            Canvas.Left="429"
            Canvas.Top="50"
            Width="157" />
    <Label Grid.Row="2"
           Grid.Column="1"
           Content="{Binding SelectedGame.TimeElapsed, Mode=OneWay}"
           FontSize="30"
           FontFamily="./#Sochi2014"
           Height="40"
           Width="69"
           Canvas.Left="310"
           Canvas.Top="277"
           RenderTransformOrigin="2.284,1.56" />
    <Label Grid.Row="2"
           Grid.Column="3"
           Content="{Binding SelectedGame.DownloadSpeed, Mode=OneWay}"
           FontSize="30"
           FontFamily="./#Sochi2014"
           Height="40"
           Width="193"
           Canvas.Left="15"
           Canvas.Top="277" />
    <ProgressBar Grid.Row="3"
                 Grid.Column="1"
                 Grid.ColumnSpan="2"
                 Value="{Binding SelectedGame.Progress}"
                 Foreground="#AAA1C8"
                 Height="75"
                 Width="424"
                 Canvas.Left="15"
                 Canvas.Top="335" />
    <Label Grid.Row="3"
           FontSize="30"
           FontFamily="./#Sochi2014"
           Content="{Binding SelectedGame.Progress}"
           Grid.ColumnSpan="2"
           Canvas.Left="230"
           Canvas.Top="339" />
    <Label Grid.Row="3"
           Grid.Column="3"
           Content="{Binding SelectedGame.ProgressBytesRead, Mode=OneWay}"
           FontSize="30"
           FontFamily="./#Sochi2014"
           Height="40"
           VerticalAlignment="Top"
           Canvas.Left="448"
           Canvas.Top="299"
           Width="137" />
    <Label Grid.Row="3"
           Grid.Column="3"
           Content="{Binding SelectedGame.DisplayBytesTotal, Mode=OneWay}"
           FontSize="30"
           FontFamily="./#Sochi2014"
           Height="44"
           Canvas.Left="448"
           Canvas.Top="344"
           Width="137" />
    <Label Content="{Binding SelectedGame.Name}"
           Height="40"
           Width="186"
           Canvas.Left="22"
           Canvas.Top="202" />
  </Canvas>

  <ListBox x:Name="ListBox" Grid.Row="0"
           Grid.Column="0"
           Grid.RowSpan="4"
           ItemsSource="{Binding Games}"
           SelectedItem="{Binding SelectedGame, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
           SelectedIndex="0">
    <ListBox.ItemTemplate>
      <DataTemplate>
        <StackPanel>
          <TextBlock FontSize="20"
                     Text="{Binding Name}" />
        </StackPanel>
      </DataTemplate>
    </ListBox.ItemTemplate>
  </ListBox>
</Grid>
  

Для улучшения представления вы могли бы подумать о создании коллекции с текущими активными загрузками и привязать ее к ItemsControl . Теперь, когда вы переместите макет в ItemTemplate , вы сможете отображать ход каждой загрузки одновременно без какого-либо переключения.

Подводя итог: ваш дизайн не позволяет вам достичь вашей цели или делает ее слишком сложной. После разделения вашего кода на обязанности и инкапсуляции определенного поведения и атрибутов вашей цели достичь намного проще. Это всего лишь пример того, как улучшенный дизайн может помочь быть более гибким при реализации требований.