diff --git a/Wabbajack.Lib/Extensions/ReactiveUIExt.cs b/Wabbajack.Lib/Extensions/ReactiveUIExt.cs index c18543da..eb2a79bc 100644 --- a/Wabbajack.Lib/Extensions/ReactiveUIExt.cs +++ b/Wabbajack.Lib/Extensions/ReactiveUIExt.cs @@ -2,10 +2,12 @@ using System.Linq; using System.Linq.Expressions; using System.Reactive; +using System.Reactive.Disposables; using System.Reactive.Linq; using DynamicData; using DynamicData.Kernel; using ReactiveUI; +using Wabbajack.Lib; namespace Wabbajack { @@ -78,6 +80,29 @@ namespace Wabbajack return new ChangeSet(changes); } + public static ObservableAsPropertyHelper ToGuiProperty( + this IObservable source, + ViewModel vm, + string property, + TRet initialValue = default, + bool deferSubscription = false) + { + return source + .ToProperty(vm, property, initialValue, deferSubscription, RxApp.MainThreadScheduler) + .DisposeWith(vm.CompositeDisposable); + } + + public static void ToGuiProperty( + this IObservable source, + ViewModel vm, + string property, + out ObservableAsPropertyHelper result, + TRet initialValue = default, + bool deferSubscription = false) + { + source.ToProperty(vm, property, out result, initialValue, deferSubscription, RxApp.MainThreadScheduler) + .DisposeWith(vm.CompositeDisposable); + } internal static Optional> Reduce(Optional> previous, Change next) { diff --git a/Wabbajack/UI/FilePickerVM.cs b/Wabbajack/UI/FilePickerVM.cs index 81c462de..1351e47e 100644 --- a/Wabbajack/UI/FilePickerVM.cs +++ b/Wabbajack/UI/FilePickerVM.cs @@ -147,9 +147,8 @@ namespace Wabbajack } }) .DistinctUntilChanged() - .ObserveOnGuiThread() .StartWith(false) - .ToProperty(this, nameof(Exists)); + .ToGuiProperty(this, nameof(Exists)); var passesFilters = Observable.CombineLatest( this.WhenAny(x => x.TargetPath), @@ -218,12 +217,11 @@ namespace Wabbajack if (filter.Failed) return filter; return ErrorResponse.Convert(err); }) - .ObserveOnGuiThread() - .ToProperty(this, nameof(ErrorState)); + .ToGuiProperty(this, nameof(ErrorState)); _inError = this.WhenAny(x => x.ErrorState) .Select(x => !x.Succeeded) - .ToProperty(this, nameof(InError)); + .ToGuiProperty(this, nameof(InError)); // Doesn't derive from ErrorState, as we want to bubble non-empty tooltips, // which is slightly different logic @@ -244,8 +242,7 @@ namespace Wabbajack if (!string.IsNullOrWhiteSpace(filters)) return filters; return err?.Reason; }) - .ObserveOnGuiThread() - .ToProperty(this, nameof(ErrorTooltip)); + .ToGuiProperty(this, nameof(ErrorTooltip)); } public ICommand ConstructTypicalPickerCommand() diff --git a/Wabbajack/View Models/BackNavigatingVM.cs b/Wabbajack/View Models/BackNavigatingVM.cs index bf89054a..ec1a16ac 100644 --- a/Wabbajack/View Models/BackNavigatingVM.cs +++ b/Wabbajack/View Models/BackNavigatingVM.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reactive; @@ -36,7 +36,7 @@ namespace Wabbajack .ObserveOnGuiThread()); _IsActive = this.ConstructIsActive(mainWindowVM) - .ToProperty(this, nameof(IsActive)); + .ToGuiProperty(this, nameof(IsActive)); } } diff --git a/Wabbajack/View Models/Compilers/CompilerVM.cs b/Wabbajack/View Models/Compilers/CompilerVM.cs index d1c53559..65a579bb 100644 --- a/Wabbajack/View Models/Compilers/CompilerVM.cs +++ b/Wabbajack/View Models/Compilers/CompilerVM.cs @@ -115,11 +115,11 @@ namespace Wabbajack pair.Previous?.Unload(); }) .Select(p => p.Current) - .ToProperty(this, nameof(Compiler)); + .ToGuiProperty(this, nameof(Compiler)); // Let sub VM determine what settings we're displaying and when _currentModlistSettings = this.WhenAny(x => x.Compiler.ModlistSettings) - .ToProperty(this, nameof(CurrentModlistSettings)); + .ToGuiProperty(this, nameof(CurrentModlistSettings)); _image = this.WhenAny(x => x.CurrentModlistSettings.ImagePath.TargetPath) // Throttle so that it only loads image after any sets of swaps have completed @@ -135,12 +135,11 @@ namespace Wabbajack } return null; }) - .ToProperty(this, nameof(Image)); + .ToGuiProperty(this, nameof(Image)); _compiling = this.WhenAny(x => x.Compiler.ActiveCompilation) .Select(compilation => compilation != null) - .ObserveOnGuiThread() - .ToProperty(this, nameof(Compiling)); + .ToGuiProperty(this, nameof(Compiling)); BackCommand = ReactiveCommand.Create( execute: () => @@ -176,7 +175,7 @@ namespace Wabbajack }) .Switch() .Debounce(TimeSpan.FromMilliseconds(25)) - .ToProperty(this, nameof(PercentCompleted)); + .ToGuiProperty(this, nameof(PercentCompleted)); BeginCommand = ReactiveCommand.CreateFromTask( canExecute: this.WhenAny(x => x.Compiler.CanCompile) @@ -217,8 +216,7 @@ namespace Wabbajack _ActiveGlobalUserIntervention = activeInterventions.Connect() .Filter(x => x.CpuID == WorkQueue.UnassignedCpuId) .QueryWhenChanged(query => query.FirstOrDefault()) - .ObserveOnGuiThread() - .ToProperty(this, nameof(ActiveGlobalUserIntervention)); + .ToGuiProperty(this, nameof(ActiveGlobalUserIntervention)); CloseWhenCompleteCommand = ReactiveCommand.Create( canExecute: this.WhenAny(x => x.Completed) @@ -263,12 +261,11 @@ namespace Wabbajack return "Configuring"; } }) - .ToProperty(this, nameof(ProgressTitle)); + .ToGuiProperty(this, nameof(ProgressTitle)); _CurrentCpuCount = this.WhenAny(x => x.Compiler.ActiveCompilation.Queue.CurrentCpuCount) .Switch() - .ObserveOnGuiThread() - .ToProperty(this, nameof(CurrentCpuCount)); + .ToGuiProperty(this, nameof(CurrentCpuCount)); } } } diff --git a/Wabbajack/View Models/Compilers/MO2CompilerVM.cs b/Wabbajack/View Models/Compilers/MO2CompilerVM.cs index d993df53..3c5191d6 100644 --- a/Wabbajack/View Models/Compilers/MO2CompilerVM.cs +++ b/Wabbajack/View Models/Compilers/MO2CompilerVM.cs @@ -68,7 +68,7 @@ namespace Wabbajack return null; } }) - .ToProperty(this, nameof(Mo2Folder)); + .ToGuiProperty(this, nameof(Mo2Folder)); _moProfile = this.WhenAny(x => x.ModListLocation.TargetPath) .Select(loc => { @@ -82,7 +82,7 @@ namespace Wabbajack return null; } }) - .ToProperty(this, nameof(MOProfile)); + .ToGuiProperty(this, nameof(MOProfile)); // Wire missing Mo2Folder to signal error state for ModList Location ModListLocation.AdditionalError = this.WhenAny(x => x.Mo2Folder) @@ -116,9 +116,7 @@ namespace Wabbajack pair.Current?.Init(); }) .Select(x => x.Current) - // Save to property - .ObserveOnGuiThread() - .ToProperty(this, nameof(ModlistSettings)); + .ToGuiProperty(this, nameof(ModlistSettings)); CanCompile = Observable.CombineLatest( this.WhenAny(x => x.ModListLocation.InError), diff --git a/Wabbajack/View Models/Compilers/VortexCompilerVM.cs b/Wabbajack/View Models/Compilers/VortexCompilerVM.cs index e3498651..08dcc7b8 100644 --- a/Wabbajack/View Models/Compilers/VortexCompilerVM.cs +++ b/Wabbajack/View Models/Compilers/VortexCompilerVM.cs @@ -94,9 +94,7 @@ namespace Wabbajack current?.Init(); }) .Select(x => x.Current) - // Save to property - .ObserveOnGuiThread() - .ToProperty(this, nameof(ModlistSettings)); + .ToGuiProperty(this, nameof(ModlistSettings)); CanCompile = Observable.CombineLatest( this.WhenAny(x => x.GameLocation.InError), diff --git a/Wabbajack/View Models/Installers/InstallerVM.cs b/Wabbajack/View Models/Installers/InstallerVM.cs index 6d229346..aae2f24a 100644 --- a/Wabbajack/View Models/Installers/InstallerVM.cs +++ b/Wabbajack/View Models/Installers/InstallerVM.cs @@ -1,4 +1,4 @@ -using Syroot.Windows.IO; +using Syroot.Windows.IO; using System; using ReactiveUI; using System.Diagnostics; @@ -147,7 +147,7 @@ namespace Wabbajack pair.Previous?.Unload(); }) .Select(p => p.Current) - .ToProperty(this, nameof(Installer)); + .ToGuiProperty(this, nameof(Installer)); // Load settings MWVM.Settings.SaveSignal @@ -158,7 +158,7 @@ namespace Wabbajack .DisposeWith(CompositeDisposable); _IsActive = this.ConstructIsActive(MWVM) - .ToProperty(this, nameof(IsActive)); + .ToGuiProperty(this, nameof(IsActive)); // Active path represents the path to currently have loaded // If we're not actively showing, then "unload" the active path @@ -188,7 +188,7 @@ namespace Wabbajack .DisposeOld() .ObserveOnGuiThread() .StartWith(default(ModListVM)) - .ToProperty(this, nameof(ModList)); + .ToGuiProperty(this, nameof(ModList)); // Force GC collect when modlist changes, just to make sure we clean up any loose large items immediately this.WhenAny(x => x.ModList) @@ -205,17 +205,16 @@ namespace Wabbajack // When the resulting modlist comes in, mark it as done this.WhenAny(x => x.ModList) .Select(_ => false)) - .ToProperty(this, nameof(LoadingModlist)); + .ToGuiProperty(this, nameof(LoadingModlist)); _htmlReport = this.WhenAny(x => x.ModList) .Select(modList => modList?.ReportHTML) - .ToProperty(this, nameof(HTMLReport)); + .ToGuiProperty(this, nameof(HTMLReport)); _installing = this.WhenAny(x => x.Installer.ActiveInstallation) .Select(i => i != null) - .ObserveOnGuiThread() - .ToProperty(this, nameof(Installing)); + .ToGuiProperty(this, nameof(Installing)); _TargetManager = this.WhenAny(x => x.ModList) .Select(modList => modList?.ModManager) - .ToProperty(this, nameof(TargetManager)); + .ToGuiProperty(this, nameof(TargetManager)); // Add additional error check on ModList ModListLocation.AdditionalError = this.WhenAny(x => x.ModList) @@ -254,7 +253,7 @@ namespace Wabbajack }) .Switch() .Debounce(TimeSpan.FromMilliseconds(25)) - .ToProperty(this, nameof(PercentCompleted)); + .ToGuiProperty(this, nameof(PercentCompleted)); Slideshow = new SlideShow(this); @@ -280,7 +279,7 @@ namespace Wabbajack return installing ? slideshow : modList; }) .Select(x => x) - .ToProperty(this, nameof(Image)); + .ToGuiProperty(this, nameof(Image)); _titleText = Observable.CombineLatest( this.WhenAny(x => x.ModList) .Select(modList => modList?.Name ?? string.Empty), @@ -288,7 +287,7 @@ namespace Wabbajack .StartWith(default(string)), this.WhenAny(x => x.Installing), resultSelector: (modList, mod, installing) => installing ? mod : modList) - .ToProperty(this, nameof(TitleText)); + .ToGuiProperty(this, nameof(TitleText)); _authorText = Observable.CombineLatest( this.WhenAny(x => x.ModList) .Select(modList => modList?.Author ?? string.Empty), @@ -296,7 +295,7 @@ namespace Wabbajack .StartWith(default(string)), this.WhenAny(x => x.Installing), resultSelector: (modList, mod, installing) => installing ? mod : modList) - .ToProperty(this, nameof(AuthorText)); + .ToGuiProperty(this, nameof(AuthorText)); _description = Observable.CombineLatest( this.WhenAny(x => x.ModList) .Select(modList => modList?.Description ?? string.Empty), @@ -304,7 +303,7 @@ namespace Wabbajack .StartWith(default(string)), this.WhenAny(x => x.Installing), resultSelector: (modList, mod, installing) => installing ? mod : modList) - .ToProperty(this, nameof(Description)); + .ToGuiProperty(this, nameof(Description)); _modListName = Observable.CombineLatest( this.WhenAny(x => x.ModList.Error) .Select(x => x != null), @@ -315,7 +314,7 @@ namespace Wabbajack if (err) return "Corrupted Modlist"; return name; }) - .ToProperty(this, nameof(ModListName)); + .ToGuiProperty(this, nameof(ModListName)); // Define commands ShowReportCommand = ReactiveCommand.Create(ShowReport); @@ -350,7 +349,7 @@ namespace Wabbajack return "Configuring"; } }) - .ToProperty(this, nameof(ProgressTitle)); + .ToGuiProperty(this, nameof(ProgressTitle)); UIUtils.BindCpuStatus( this.WhenAny(x => x.Installer.ActiveInstallation) @@ -405,8 +404,7 @@ namespace Wabbajack _ActiveGlobalUserIntervention = activeInterventions.Connect() .Filter(x => x.CpuID == WorkQueue.UnassignedCpuId) .QueryWhenChanged(query => query.FirstOrDefault()) - .ObserveOnGuiThread() - .ToProperty(this, nameof(ActiveGlobalUserIntervention)); + .ToGuiProperty(this, nameof(ActiveGlobalUserIntervention)); CloseWhenCompleteCommand = ReactiveCommand.Create( canExecute: this.WhenAny(x => x.Completed) @@ -429,8 +427,7 @@ namespace Wabbajack _CurrentCpuCount = this.WhenAny(x => x.Installer.ActiveInstallation.Queue.CurrentCpuCount) .Switch() - .ObserveOnGuiThread() - .ToProperty(this, nameof(CurrentCpuCount)); + .ToGuiProperty(this, nameof(CurrentCpuCount)); } private void ShowReport() diff --git a/Wabbajack/View Models/Installers/MO2InstallerVM.cs b/Wabbajack/View Models/Installers/MO2InstallerVM.cs index 5d52714a..8ced7dd8 100644 --- a/Wabbajack/View Models/Installers/MO2InstallerVM.cs +++ b/Wabbajack/View Models/Installers/MO2InstallerVM.cs @@ -88,7 +88,7 @@ namespace Wabbajack // Load settings _CurrentSettings = installerVM.WhenAny(x => x.ModListLocation.TargetPath) .Select(path => path == null ? null : installerVM.MWVM.Settings.Installer.Mo2ModlistSettings.TryCreate(path)) - .ToProperty(this, nameof(CurrentSettings)); + .ToGuiProperty(this, nameof(CurrentSettings)); this.WhenAny(x => x.CurrentSettings) .Pairwise() .Subscribe(settingsPair => diff --git a/Wabbajack/View Models/Installers/VortexInstallerVM.cs b/Wabbajack/View Models/Installers/VortexInstallerVM.cs index f4606f07..352aa7b2 100644 --- a/Wabbajack/View Models/Installers/VortexInstallerVM.cs +++ b/Wabbajack/View Models/Installers/VortexInstallerVM.cs @@ -39,7 +39,7 @@ namespace Wabbajack Parent = installerVM; _TargetGame = installerVM.WhenAny(x => x.ModList.SourceModList.GameType) - .ToProperty(this, nameof(TargetGame)); + .ToGuiProperty(this, nameof(TargetGame)); CanInstall = Observable.CombineLatest( this.WhenAny(x => x.TargetGame) diff --git a/Wabbajack/View Models/ModListMetadataVM.cs b/Wabbajack/View Models/ModListMetadataVM.cs index 6355da64..47a1cc68 100644 --- a/Wabbajack/View Models/ModListMetadataVM.cs +++ b/Wabbajack/View Models/ModListMetadataVM.cs @@ -119,11 +119,11 @@ namespace Wabbajack return true; } }) - .ToProperty(this, nameof(Exists)); + .ToGuiProperty(this, nameof(Exists)); _Image = Observable.Return(Metadata.Links.ImageUri) .DownloadBitmapImage((ex) => Utils.Log($"Error downloading modlist image {Metadata.Title}")) - .ToProperty(this, nameof(Image)); + .ToGuiProperty(this, nameof(Image)); } private Task Download() diff --git a/Wabbajack/View Models/Settings/LoginManagerVM.cs b/Wabbajack/View Models/Settings/LoginManagerVM.cs index a30cd5e4..1487adf7 100644 --- a/Wabbajack/View Models/Settings/LoginManagerVM.cs +++ b/Wabbajack/View Models/Settings/LoginManagerVM.cs @@ -36,8 +36,7 @@ namespace Wabbajack { Login = login; _MetaInfo = (login.MetaInfo ?? Observable.Return("")) - .ObserveOnGuiThread() - .ToProperty(this, nameof(MetaInfo)); + .ToGuiProperty(this, nameof(MetaInfo)); } } } diff --git a/Wabbajack/View Models/SlideShow.cs b/Wabbajack/View Models/SlideShow.cs index 74bf9a80..03099841 100644 --- a/Wabbajack/View Models/SlideShow.cs +++ b/Wabbajack/View Models/SlideShow.cs @@ -109,15 +109,14 @@ namespace Wabbajack return query.Items.ElementAtOrDefault(index); }) .StartWith(default(ModVM)) - .ObserveOnGuiThread() - .ToProperty(this, nameof(TargetMod)); + .ToGuiProperty(this, nameof(TargetMod)); // Mark interest and materialize image of target mod _image = this.WhenAny(x => x.TargetMod) // We want to Switch here, not SelectMany, as we want to hotswap to newest target without waiting on old ones .Select(x => x?.ImageObservable ?? Observable.Return(default(BitmapImage))) .Switch() - .ToProperty(this, nameof(Image)); + .ToGuiProperty(this, nameof(Image)); VisitNexusSiteCommand = ReactiveCommand.Create( execute: () => Process.Start(TargetMod.ModURL),