使用MVVM-WPF下载多个文件

时间:2019-03-22 13:35:52

标签: 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 && 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 && 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 个答案:

答案 0 :(得分:0)

您必须绑定到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 && 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:\Users\MusicMonkey\Downloads",
      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) &&
        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时,无需进行任何切换即可同时显示每次下载的进度。

总而言之:您的设计不允许您实现目标或使其过于复杂。将代码分为职责并封装某些行为和属性后,您的目标就容易实现了。这只是一个改进的设计如何在实现需求时如何帮助提高灵活性的原始示例。