diff --git a/Wabbajack/View Models/CompilerVM.cs b/Wabbajack/View Models/CompilerVM.cs index 68164fc1..5b1ad8ed 100644 --- a/Wabbajack/View Models/CompilerVM.cs +++ b/Wabbajack/View Models/CompilerVM.cs @@ -1,4 +1,5 @@ -using ReactiveUI; +using Microsoft.WindowsAPICodePack.Dialogs; +using ReactiveUI; using ReactiveUI.Fody.Helpers; using System; using System.IO; @@ -25,8 +26,7 @@ namespace Wabbajack [Reactive] public string ModListName { get; set; } - [Reactive] - public string ModlistLocation { get; set; } + public FilePickerVM ModlistLocation { get; } [Reactive] public bool Compiling { get; set; } @@ -37,8 +37,7 @@ namespace Wabbajack [Reactive] public string Description { get; set; } - [Reactive] - public string ImagePath { get; set; } + public FilePickerVM ImagePath { get; } private readonly ObservableAsPropertyHelper _Image; public BitmapImage Image => _Image.Value; @@ -46,14 +45,12 @@ namespace Wabbajack [Reactive] public string Website { get; set; } - [Reactive] - public string ReadMeText { get; set; } + public FilePickerVM ReadMeText { get; } [Reactive] public string HTMLReport { get; set; } - [Reactive] - public string DownloadLocation { get; set; } + public FilePickerVM DownloadLocation { get; } [Reactive] public bool ModlistLocationInError { get; set; } @@ -66,7 +63,33 @@ namespace Wabbajack public CompilerVM(MainWindowVM mainWindowVM, string source) { this.MWVM = mainWindowVM; - this.ModlistLocation = source; + this.ModlistLocation = new FilePickerVM() + { + TargetPath = source, + DoExistsCheck = true, + PathType = FilePickerVM.PathTypeOptions.File, + PromptTitle = "Select Modlist" + }; + this.DownloadLocation = new FilePickerVM() + { + DoExistsCheck = true, + PathType = FilePickerVM.PathTypeOptions.Folder, + PromptTitle = "Select Download Location", + }; + this.ImagePath = new FilePickerVM() + { + DoExistsCheck = false, + PathType = FilePickerVM.PathTypeOptions.File, + Filters = + { + new CommonFileDialogFilter("Banner image", "*.png") + } + }; + this.ReadMeText = new FilePickerVM() + { + PathType = FilePickerVM.PathTypeOptions.File, + DoExistsCheck = true, + }; this.BeginCommand = ReactiveCommand.CreateFromTask( execute: this.ExecuteBegin, @@ -74,7 +97,7 @@ namespace Wabbajack .Select(compiling => !compiling) .ObserveOnGuiThread()); - this._Image = this.WhenAny(x => x.ImagePath) + this._Image = this.WhenAny(x => x.ImagePath.TargetPath) .Select(path => { if (string.IsNullOrWhiteSpace(path)) return UIUtils.BitmapImageFromResource("Wabbajack.Resources.Banner_Dark.png"); @@ -93,16 +116,16 @@ namespace Wabbajack this.AuthorText = settings.Author; this.ModListName = settings.ModListName; this.Description = settings.Description; - this.ReadMeText = settings.Readme; - this.ImagePath = settings.SplashScreen; + this.ReadMeText.TargetPath = settings.Readme; + this.ImagePath.TargetPath = settings.SplashScreen; this.Website = settings.Website; if (!string.IsNullOrWhiteSpace(settings.DownloadLocation)) { - this.DownloadLocation = settings.DownloadLocation; + this.DownloadLocation.TargetPath = settings.DownloadLocation; } if (!string.IsNullOrWhiteSpace(settings.Location)) { - this.ModlistLocation = settings.Location; + this.ModlistLocation.TargetPath = settings.Location; } this.MWVM.Settings.SaveSignal .Subscribe(_ => @@ -110,11 +133,11 @@ namespace Wabbajack settings.Author = this.AuthorText; settings.ModListName = this.ModListName; settings.Description = this.Description; - settings.Readme = this.ReadMeText; - settings.SplashScreen = this.ImagePath; + settings.Readme = this.ReadMeText.TargetPath; + settings.SplashScreen = this.ImagePath.TargetPath; settings.Website = this.Website; - settings.Location = this.ModlistLocation; - settings.DownloadLocation = this.DownloadLocation; + settings.Location = this.ModlistLocation.TargetPath; + settings.DownloadLocation = this.DownloadLocation.TargetPath; }) .DisposeWith(this.CompositeDisposable); } @@ -132,7 +155,7 @@ namespace Wabbajack this.ModListName = this.MOProfile; var tmp_compiler = new Compiler(this.Mo2Folder); - this.DownloadLocation = tmp_compiler.MO2DownloadsFolder; + this.DownloadLocation.TargetPath = tmp_compiler.MO2DownloadsFolder; } private async Task ExecuteBegin() @@ -145,9 +168,9 @@ namespace Wabbajack ModListName = this.ModListName, ModListAuthor = this.AuthorText, ModListDescription = this.Description, - ModListImage = this.ImagePath, + ModListImage = this.ImagePath.TargetPath, ModListWebsite = this.Website, - ModListReadme = this.ReadMeText, + ModListReadme = this.ReadMeText.TargetPath, }; await Task.Run(() => { diff --git a/Wabbajack/View Models/FilePickerVM.cs b/Wabbajack/View Models/FilePickerVM.cs new file mode 100644 index 00000000..2f9aeda7 --- /dev/null +++ b/Wabbajack/View Models/FilePickerVM.cs @@ -0,0 +1,164 @@ +using Microsoft.WindowsAPICodePack.Dialogs; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reactive.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Input; +using Wabbajack.Lib; + +namespace Wabbajack +{ + public class FilePickerVM : ViewModel + { + public enum PathTypeOptions + { + Off, + Either, + File, + Folder + } + + public object Parent { get; } + + [Reactive] + public ICommand SetTargetPathCommand { get; set; } + + [Reactive] + public string TargetPath { get; set; } + + [Reactive] + public string PromptTitle { get; set; } + + [Reactive] + public PathTypeOptions PathType { get; set; } + + [Reactive] + public bool DoExistsCheck { get; set; } + + [Reactive] + public IObservable AdditionalError { get; set; } + + private readonly ObservableAsPropertyHelper _Exists; + public bool Exists => _Exists.Value; + + private readonly ObservableAsPropertyHelper _InError; + public bool InError => _InError.Value; + + private readonly ObservableAsPropertyHelper _ErrorTooltip; + public string ErrorTooltip => _ErrorTooltip.Value; + + public List Filters { get; } = new List(); + + public FilePickerVM(object parentVM = null) + { + this.Parent = parentVM; + this.SetTargetPathCommand = ConstructTypicalPickerCommand(); + + // Check that file exists + this._Exists = Observable.Interval(TimeSpan.FromSeconds(3)) + .FilterSwitch( + Observable.CombineLatest( + this.WhenAny(x => x.PathType), + this.WhenAny(x => x.DoExistsCheck), + resultSelector: (type, doExists) => type != PathTypeOptions.Off && doExists)) + .Unit() + // Also do it when fields change + .Merge(this.WhenAny(x => x.PathType).Unit()) + .Merge(this.WhenAny(x => x.DoExistsCheck).Unit()) + .CombineLatest( + this.WhenAny(x => x.DoExistsCheck), + this.WhenAny(x => x.PathType), + this.WhenAny(x => x.TargetPath) + .Throttle(TimeSpan.FromMilliseconds(200)), + resultSelector: (_, DoExists, Type, Path) => (DoExists, Type, Path)) + // Refresh exists + .Select(t => + { + if (!t.DoExists) return true; + switch (t.Type) + { + case PathTypeOptions.Either: + return File.Exists(t.Path) || Directory.Exists(t.Path); + case PathTypeOptions.File: + return File.Exists(t.Path); + case PathTypeOptions.Folder: + return Directory.Exists(t.Path); + default: + return true; + } + }) + .DistinctUntilChanged() + .ObserveOn(RxApp.MainThreadScheduler) + .ToProperty(this, nameof(this.Exists)); + + this._InError = Observable.CombineLatest( + this.WhenAny(x => x.Exists), + this.WhenAny(x => x.AdditionalError) + .Select(x => x ?? Observable.Return(ErrorResponse.Success)) + .Switch() + .Select(err => !err?.Succeeded ?? false), + resultSelector: (exist, err) => !exist || err) + .ToProperty(this, nameof(this.InError)); + + this._ErrorTooltip = Observable.CombineLatest( + this.WhenAny(x => x.Exists) + .Select(exists => exists ? default(string) : "Path does not exist"), + this.WhenAny(x => x.AdditionalError) + .Select(x => x ?? Observable.Return(ErrorResponse.Success)) + .Switch(), + resultSelector: (exists, err) => + { + if ((!err?.Succeeded ?? false) + && !string.IsNullOrWhiteSpace(err.Reason)) + { + return err.Reason; + } + return exists; + }) + .ToProperty(this, nameof(this.ErrorTooltip)); + } + + public ICommand ConstructTypicalPickerCommand() + { + return ReactiveCommand.Create( + execute: () => + { + string dirPath; + if (File.Exists(this.TargetPath)) + { + dirPath = System.IO.Path.GetDirectoryName(this.TargetPath); + } + else + { + dirPath = this.TargetPath; + } + var dlg = new CommonOpenFileDialog + { + Title = this.PromptTitle, + IsFolderPicker = this.PathType == PathTypeOptions.Folder, + InitialDirectory = this.TargetPath, + AddToMostRecentlyUsedList = false, + AllowNonFileSystemItems = false, + DefaultDirectory = this.TargetPath, + EnsureFileExists = true, + EnsurePathExists = true, + EnsureReadOnly = false, + EnsureValidNames = true, + Multiselect = false, + ShowPlacesList = true, + }; + foreach (var filter in this.Filters) + { + dlg.Filters.Add(filter); + } + if (dlg.ShowDialog() != CommonFileDialogResult.Ok) return; + this.TargetPath = dlg.FileName; + }); + } + } +} diff --git a/Wabbajack/View Models/InstallerVM.cs b/Wabbajack/View Models/InstallerVM.cs index 2dd58746..0506b307 100644 --- a/Wabbajack/View Models/InstallerVM.cs +++ b/Wabbajack/View Models/InstallerVM.cs @@ -50,17 +50,9 @@ namespace Wabbajack [Reactive] public bool InstallingMode { get; set; } - [Reactive] - public string Location { get; set; } + public FilePickerVM Location { get; } - private readonly ObservableAsPropertyHelper _LocationError; - public IErrorResponse LocationError => _LocationError.Value; - - [Reactive] - public string DownloadLocation { get; set; } - - private readonly ObservableAsPropertyHelper _DownloadLocationError; - public IErrorResponse DownloadLocationError => _DownloadLocationError.Value; + public FilePickerVM DownloadLocation { get; } private readonly ObservableAsPropertyHelper _ProgressPercent; public float ProgressPercent => _ProgressPercent.Value; @@ -105,15 +97,32 @@ namespace Wabbajack this.MWVM = mainWindowVM; this.ModListPath = source; + this.Location = new FilePickerVM() + { + DoExistsCheck = false, + PathType = FilePickerVM.PathTypeOptions.Folder, + PromptTitle = "Select Installation Directory", + }; + this.Location.AdditionalError = this.WhenAny(x => x.Location.TargetPath) + .Select(x => Utils.IsDirectoryPathValid(x)); + this.DownloadLocation = new FilePickerVM() + { + DoExistsCheck = false, + PathType = FilePickerVM.PathTypeOptions.Folder, + PromptTitle = "Select a location for MO2 downloads", + }; + this.DownloadLocation.AdditionalError = this.WhenAny(x => x.DownloadLocation.TargetPath) + .Select(x => Utils.IsDirectoryPathValid(x)); + // Load settings InstallationSettings settings = this.MWVM.Settings.InstallationSettings.TryCreate(source); - this.Location = settings.InstallationLocation; - this.DownloadLocation = settings.DownloadLocation; + this.Location.TargetPath = settings.InstallationLocation; + this.DownloadLocation.TargetPath = settings.DownloadLocation; this.MWVM.Settings.SaveSignal .Subscribe(_ => { - settings.InstallationLocation = this.Location; - settings.DownloadLocation = this.DownloadLocation; + settings.InstallationLocation = this.Location.TargetPath; + settings.DownloadLocation = this.DownloadLocation.TargetPath; }) .DisposeWith(this.CompositeDisposable); @@ -197,14 +206,6 @@ namespace Wabbajack .Select(x => x?.Name) .ToProperty(this, nameof(this.ModListName)); - this._LocationError = this.WhenAny(x => x.Location) - .Select(x => Utils.IsDirectoryPathValid(x)) - .ToProperty(this, nameof(this.LocationError)); - - this._DownloadLocationError = this.WhenAny(x => x.DownloadLocation) - .Select(x => Utils.IsDirectoryPathValid(x)) - .ToProperty(this, nameof(this.DownloadLocationError)); - // Define commands this.ShowReportCommand = ReactiveCommand.Create(ShowReport); this.OpenReadmeCommand = ReactiveCommand.Create( @@ -216,13 +217,12 @@ namespace Wabbajack execute: this.ExecuteBegin, canExecute: Observable.CombineLatest( this.WhenAny(x => x.Installing), - this.WhenAny(x => x.LocationError), - this.WhenAny(x => x.DownloadLocationError), + this.WhenAny(x => x.Location.InError), + this.WhenAny(x => x.DownloadLocation.InError), resultSelector: (installing, loc, download) => { if (installing) return false; - return (loc?.Succeeded ?? false) - && (download?.Succeeded ?? false); + return !loc && !download; }) .ObserveOnGuiThread()); this.VisitWebsiteCommand = ReactiveCommand.Create( @@ -232,13 +232,13 @@ namespace Wabbajack .ObserveOnGuiThread()); // Have Installation location updates modify the downloads location if empty - this.WhenAny(x => x.Location) + this.WhenAny(x => x.Location.TargetPath) .Skip(1) // Don't do it initially .Subscribe(installPath => { - if (string.IsNullOrWhiteSpace(this.DownloadLocation)) + if (string.IsNullOrWhiteSpace(this.DownloadLocation.TargetPath)) { - this.DownloadLocation = Path.Combine(installPath, "downloads"); + this.DownloadLocation.TargetPath = Path.Combine(installPath, "downloads"); } }) .DisposeWith(this.CompositeDisposable); @@ -291,9 +291,9 @@ namespace Wabbajack { this.Installing = true; this.InstallingMode = true; - var installer = new Installer(this.ModListPath, this.ModList.SourceModList, Location) + var installer = new Installer(this.ModListPath, this.ModList.SourceModList, Location.TargetPath) { - DownloadFolder = DownloadLocation + DownloadFolder = DownloadLocation.TargetPath }; var th = new Thread(() => { diff --git a/Wabbajack/Views/CompilerView.xaml b/Wabbajack/Views/CompilerView.xaml index 8db042a2..da062ba7 100644 --- a/Wabbajack/Views/CompilerView.xaml +++ b/Wabbajack/Views/CompilerView.xaml @@ -71,11 +71,7 @@ Margin="{StaticResource TitleMargin}" Text="Readme Path" ToolTip="Path to a readme file." /> - + + DataContext="{Binding ModlistLocation}" + FontSize="14" /> + DataContext="{Binding DownloadLocation}" + FontSize="14" /> + Text="{Binding TargetPath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" + Visibility="{Binding ShowTextBoxInput}" /> + ToolTip="{Binding ErrorTooltip}" + Visibility="{Binding InError, Converter={StaticResource bool2VisibilityConverter}}" /> - + diff --git a/Wabbajack/Views/FilePicker.xaml.cs b/Wabbajack/Views/FilePicker.xaml.cs index 502f0ae0..b8136e80 100644 --- a/Wabbajack/Views/FilePicker.xaml.cs +++ b/Wabbajack/Views/FilePicker.xaml.cs @@ -1,216 +1,15 @@ -using Microsoft.WindowsAPICodePack.Dialogs; -using ReactiveUI; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; +using System.Windows.Controls; namespace Wabbajack { /// /// Interaction logic for FilePicker.xaml /// - public partial class FilePicker : UserControlRx + public partial class FilePicker : UserControl { - public enum PathTypeOptions - { - Off, - Either, - File, - Folder - } - - public ICommand SetTargetPathCommand - { - get => (ICommand)GetValue(SetTargetPathCommandProperty); - set => SetValue(SetTargetPathCommandProperty, value); - } - public static readonly DependencyProperty SetTargetPathCommandProperty = DependencyProperty.Register(nameof(SetTargetPathCommand), typeof(ICommand), typeof(FilePicker), - new FrameworkPropertyMetadata(default(ICommand))); - - public string TargetPath - { - get { return (string)GetValue(TargetPathProperty); } - set { SetValue(TargetPathProperty, value); } - } - public static readonly DependencyProperty TargetPathProperty = DependencyProperty.Register(nameof(TargetPath), typeof(string), typeof(FilePicker), - new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, WireNotifyPropertyChanged)); - - public bool ShowTextBoxInput - { - get => (bool)GetValue(ShowTextBoxInputProperty); - set => SetValue(ShowTextBoxInputProperty, value); - } - public static readonly DependencyProperty ShowTextBoxInputProperty = DependencyProperty.Register(nameof(ShowTextBoxInput), typeof(bool), typeof(FilePicker), - new FrameworkPropertyMetadata(true)); - - public PathTypeOptions PathType - { - get => (PathTypeOptions)GetValue(PathTypeProperty); - set => SetValue(PathTypeProperty, value); - } - public static readonly DependencyProperty PathTypeProperty = DependencyProperty.Register(nameof(PathType), typeof(PathTypeOptions), typeof(FilePicker), - new FrameworkPropertyMetadata(PathTypeOptions.Off, WireNotifyPropertyChanged)); - - public bool Exists - { - get => (bool)GetValue(ExistsProperty); - set => SetValue(ExistsProperty, value); - } - public static readonly DependencyProperty ExistsProperty = DependencyProperty.Register(nameof(Exists), typeof(bool), typeof(FilePicker), - new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, WireNotifyPropertyChanged)); - - public bool DoExistsCheck - { - get => (bool)GetValue(DoExistsCheckProperty); - set => SetValue(DoExistsCheckProperty, value); - } - public static readonly DependencyProperty DoExistsCheckProperty = DependencyProperty.Register(nameof(DoExistsCheck), typeof(bool), typeof(FilePicker), - new FrameworkPropertyMetadata(true, WireNotifyPropertyChanged)); - - public string PromptTitle - { - get => (string)GetValue(PromptTitleProperty); - set => SetValue(PromptTitleProperty, value); - } - public static readonly DependencyProperty PromptTitleProperty = DependencyProperty.Register(nameof(PromptTitle), typeof(string), typeof(FilePicker), - new FrameworkPropertyMetadata(default(string), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); - - public string Filter - { - get => (string)GetValue(FilterProperty); - set => SetValue(FilterProperty, value); - } - public static readonly DependencyProperty FilterProperty = DependencyProperty.Register(nameof(Filter), typeof(string), typeof(FilePicker), - new FrameworkPropertyMetadata(default(string))); - - public IErrorResponse AdditionalError - { - get => (IErrorResponse)GetValue(AdditionalErrorProperty); - set => SetValue(AdditionalErrorProperty, value); - } - public static readonly DependencyProperty AdditionalErrorProperty = DependencyProperty.Register(nameof(AdditionalError), typeof(IErrorResponse), typeof(FilePicker), - new FrameworkPropertyMetadata(default(IErrorResponse), WireNotifyPropertyChanged)); - - private readonly ObservableAsPropertyHelper _InError; - public bool InError => _InError.Value; - - private readonly ObservableAsPropertyHelper _ErrorTooltip; - public string ErrorTooltip => _ErrorTooltip.Value; - public FilePicker() { InitializeComponent(); - this.SetTargetPathCommand = ReactiveCommand.Create( - execute: () => - { - string dirPath; - if (File.Exists(this.TargetPath)) - { - dirPath = System.IO.Path.GetDirectoryName(this.TargetPath); - } - else - { - dirPath = this.TargetPath; - } - var dlg = new CommonOpenFileDialog(); - dlg.Title = this.PromptTitle; - dlg.IsFolderPicker = this.PathType == PathTypeOptions.Folder; - dlg.InitialDirectory = this.TargetPath; - - dlg.AddToMostRecentlyUsedList = false; - dlg.AllowNonFileSystemItems = false; - dlg.DefaultDirectory = this.TargetPath; - dlg.EnsureFileExists = true; - dlg.EnsurePathExists = true; - dlg.EnsureReadOnly = false; - if (!string.IsNullOrWhiteSpace(this.Filter)) - { - var split = this.Filter.Split('|'); - if (split.Length == 2) - { - dlg.Filters.Add(new CommonFileDialogFilter(split[0], split[1])); - } - } - dlg.EnsureValidNames = true; - dlg.Multiselect = false; - dlg.ShowPlacesList = true; - if (dlg.ShowDialog() != CommonFileDialogResult.Ok) return; - this.TargetPath = dlg.FileName; - }); - - // Check that file exists - Observable.Interval(TimeSpan.FromSeconds(3)) - .FilterSwitch( - Observable.CombineLatest( - this.WhenAny(x => x.PathType), - this.WhenAny(x => x.DoExistsCheck), - resultSelector: (type, doExists) => type != PathTypeOptions.Off && doExists)) - .Unit() - // Also do it when fields change - .Merge(this.WhenAny(x => x.PathType).Unit()) - .Merge(this.WhenAny(x => x.DoExistsCheck).Unit()) - .CombineLatest( - this.WhenAny(x => x.DoExistsCheck), - this.WhenAny(x => x.PathType), - this.WhenAny(x => x.TargetPath) - .Throttle(TimeSpan.FromMilliseconds(200)), - resultSelector: (_, DoExists, Type, Path) => (DoExists, Type, Path)) - // Refresh exists - .Select(t => - { - if (!t.DoExists) return true; - switch (t.Type) - { - case PathTypeOptions.Either: - return File.Exists(t.Path) || Directory.Exists(t.Path); - case PathTypeOptions.File: - return File.Exists(t.Path); - case PathTypeOptions.Folder: - return Directory.Exists(t.Path); - default: - return true; - } - }) - .DistinctUntilChanged() - .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(exists => this.Exists = exists) - .DisposeWith(this.CompositeDisposable); - - this._InError = Observable.CombineLatest( - this.WhenAny(x => x.Exists), - this.WhenAny(x => x.AdditionalError) - .Select(err => !err?.Succeeded ?? false), - resultSelector: (exist, err) => !exist || err) - .ToProperty(this, nameof(this.InError)); - - this._ErrorTooltip = Observable.CombineLatest( - this.WhenAny(x => x.Exists) - .Select(exists => exists ? default(string) : "Path does not exist"), - this.WhenAny(x => x.AdditionalError), - resultSelector: (exists, err) => - { - if ((!err?.Succeeded ?? false) - && !string.IsNullOrWhiteSpace(err.Reason)) - { - return err.Reason; - } - return exists; - }) - .ToProperty(this, nameof(this.ErrorTooltip)); } } } diff --git a/Wabbajack/Views/InstallationView.xaml b/Wabbajack/Views/InstallationView.xaml index 1e2122ed..3e850853 100644 --- a/Wabbajack/Views/InstallationView.xaml +++ b/Wabbajack/Views/InstallationView.xaml @@ -301,12 +301,8 @@ Grid.Column="2" Height="30" VerticalAlignment="Center" - AdditionalError="{Binding LocationError}" - DoExistsCheck="False" - FontSize="14" - PathType="Folder" - PromptTitle="Select Installation directory" - TargetPath="{Binding Location}" /> + DataContext="{Binding Location}" + FontSize="14" /> + DataContext="{Binding DownloadLocation}" + FontSize="14" /> TopProgressView.xaml +