#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:
Вы должны выполнить привязку к 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
, вы сможете отображать ход каждой загрузки одновременно без какого-либо переключения.
Подводя итог: ваш дизайн не позволяет вам достичь вашей цели или делает ее слишком сложной. После разделения вашего кода на обязанности и инкапсуляции определенного поведения и атрибутов вашей цели достичь намного проще. Это всего лишь пример того, как улучшенный дизайн может помочь быть более гибким при реализации требований.