Вы должны привязать к свойству SelectedGame
. Но чтобы полностью включить переключение между элементами загрузки, вам нужно было бы реорганизовать свой код и переместить определенные атрибуты загрузки (например, прогресс, скорость) в отдельный класс для каждой загрузки (поскольку SelectedGame
не предоставляет все необходимые атрибуты). Таким образом, каждая игра или элемент загрузки имеет свою собственную информацию, которая предоставляет свою собственную информацию, связанную с загрузкой.
Итак, я представил класс DownloadItem
, который инкапсулирует атрибуты или данные, связанные с загрузкой. Этот класс представляет вашу игру или загружаемые предметы, которые вы можете выбрать в 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
, вы сможете одновременно отображать ход выполнения каждой загрузки без необходимости переключения.
Подводя итог: ваш дизайн не позволяет вам достичь своей цели или делает ее слишком сложной. После разделения вашего кода на обязанности и инкапсулирование определенного поведения и атрибутов вашей цели гораздо легче достичь. Это просто пример того, как улучшенный дизайн помогает повысить гибкость при выполнении требований.