diff --git a/CHANGELOG.md b/CHANGELOG.md
index d9729dd5..b9b54098 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
### Changelog
+* Progress ring displays when downloading modlist images
+* GUI releases memory of download modlists better when navigating around
+* Fixed phrasing after failed installations to say "failed".
+
#### Version - 1.0 beta 15 - 1/6/2020
* Don't delete the download folder when deleting empty folders during an update
* If `Game Folder Files` exists in the MO2 folder during compilation the Game folder will be ignored as a file source
diff --git a/Wabbajack.Common/Extensions/RxExt.cs b/Wabbajack.Common/Extensions/RxExt.cs
index bdefd987..e0ad02ce 100644
--- a/Wabbajack.Common/Extensions/RxExt.cs
+++ b/Wabbajack.Common/Extensions/RxExt.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Linq;
using System.Reactive;
using System.Reactive.Concurrency;
@@ -67,7 +67,7 @@ namespace Wabbajack
/// On/Off signal of whether to subscribe to source observable
/// Value to fire when switching off
/// Observable that publishes data from source, if the switch is on.
- public static IObservable FilterSwitch(this IObservable source, IObservable filterSwitch, T valueWhenOff)
+ public static IObservable FlowSwitch(this IObservable source, IObservable filterSwitch, T valueWhenOff)
{
return filterSwitch
.DistinctUntilChanged()
@@ -88,7 +88,7 @@ namespace Wabbajack
/// Inspiration:
/// http://reactivex.io/documentation/operators/debounce.html
/// https://stackoverflow.com/questions/20034476/how-can-i-use-reactive-extensions-to-throttle-events-using-a-max-window-size
- public static IObservable Debounce(this IObservable source, TimeSpan interval, IScheduler scheduler = null)
+ public static IObservable Debounce(this IObservable source, TimeSpan interval, IScheduler scheduler)
{
scheduler = scheduler ?? Scheduler.Default;
return Observable.Create(o =>
@@ -209,15 +209,6 @@ namespace Wabbajack
});
}
- public static IObservable DelayInitial(this IObservable source, TimeSpan delay)
- {
- return source.FlowSwitch(
- Observable.Return(System.Reactive.Unit.Default)
- .Delay(delay)
- .Select(_ => true)
- .StartWith(false));
- }
-
public static IObservable DelayInitial(this IObservable source, TimeSpan delay, IScheduler scheduler)
{
return source.FlowSwitch(
@@ -226,5 +217,21 @@ namespace Wabbajack
.Select(_ => true)
.StartWith(false));
}
+
+ public static IObservable DisposeOld(this IObservable source)
+ where T : IDisposable
+ {
+ return source
+ .StartWith(default(T))
+ .Pairwise()
+ .Do(x =>
+ {
+ if (x.Previous != null)
+ {
+ x.Previous.Dispose();
+ }
+ })
+ .Select(x => x.Current);
+ }
}
}
diff --git a/Wabbajack.Common/Utils.cs b/Wabbajack.Common/Utils.cs
index 721a4d62..8da3094a 100644
--- a/Wabbajack.Common/Utils.cs
+++ b/Wabbajack.Common/Utils.cs
@@ -1131,9 +1131,7 @@ namespace Wabbajack.Common
public static HashSet ToHashSet(this IEnumerable coll)
{
- var hs = new HashSet();
- coll.Do(v => hs.Add(v));
- return hs;
+ return new HashSet(coll);
}
public static HashSet ToHashSet(this T[] coll)
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..4f20675a 100644
--- a/Wabbajack/UI/FilePickerVM.cs
+++ b/Wabbajack/UI/FilePickerVM.cs
@@ -80,7 +80,7 @@ namespace Wabbajack
this.WhenAny(x => x.TargetPath)
// Dont want to debounce the initial value, because we know it's null
.Skip(1)
- .Debounce(TimeSpan.FromMilliseconds(200), RxApp.TaskpoolScheduler)
+ .Debounce(TimeSpan.FromMilliseconds(200), RxApp.MainThreadScheduler)
.StartWith(default(string)),
resultSelector: (existsOption, type, path) => (ExistsOption: existsOption, Type: type, Path: path))
.StartWith((ExistsOption: ExistCheckOption, Type: PathType, Path: TargetPath))
@@ -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 59408f39..ec1a16ac 100644
--- a/Wabbajack/View Models/BackNavigatingVM.cs
+++ b/Wabbajack/View Models/BackNavigatingVM.cs
@@ -35,9 +35,8 @@ namespace Wabbajack
canExecute: this.ConstructCanNavigateBack()
.ObserveOnGuiThread());
- _IsActive = mainWindowVM.WhenAny(x => x.ActivePane)
- .Select(x => object.ReferenceEquals(this, x))
- .ToProperty(this, nameof(IsActive));
+ _IsActive = this.ConstructIsActive(mainWindowVM)
+ .ToGuiProperty(this, nameof(IsActive));
}
}
@@ -48,5 +47,11 @@ namespace Wabbajack
return vm.WhenAny(x => x.NavigateBackTarget)
.Select(x => x != null);
}
+
+ public static IObservable ConstructIsActive(this IBackNavigatingVM vm, MainWindowVM mwvm)
+ {
+ return mwvm.WhenAny(x => x.ActivePane)
+ .Select(x => object.ReferenceEquals(vm, x));
+ }
}
}
diff --git a/Wabbajack/View Models/Compilers/CompilerVM.cs b/Wabbajack/View Models/Compilers/CompilerVM.cs
index 066b2ba0..48d5dfdc 100644
--- a/Wabbajack/View Models/Compilers/CompilerVM.cs
+++ b/Wabbajack/View Models/Compilers/CompilerVM.cs
@@ -115,15 +115,15 @@ 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
- .Throttle(TimeSpan.FromMilliseconds(50), RxApp.TaskpoolScheduler)
+ .Throttle(TimeSpan.FromMilliseconds(50), RxApp.MainThreadScheduler)
.DistinctUntilChanged()
.ObserveOnGuiThread()
.Select(path =>
@@ -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: () =>
@@ -175,8 +174,8 @@ namespace Wabbajack
return compiler.PercentCompleted.StartWith(0);
})
.Switch()
- .Debounce(TimeSpan.FromMilliseconds(25))
- .ToProperty(this, nameof(PercentCompleted));
+ .Debounce(TimeSpan.FromMilliseconds(25), RxApp.MainThreadScheduler)
+ .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)
@@ -243,26 +241,31 @@ namespace Wabbajack
}
});
- _progressTitle = Observable.CombineLatest(
- this.WhenAny(x => x.Compiling),
- this.WhenAny(x => x.StartedCompilation),
- resultSelector: (compiling, started) =>
+ _progressTitle = this.WhenAnyValue(
+ x => x.Compiling,
+ x => x.StartedCompilation,
+ x => x.Completed,
+ selector: (compiling, started, completed) =>
{
if (compiling)
{
return "Compiling";
}
+ else if (started)
+ {
+ if (completed == null) return "Compiling";
+ return completed.Value.Succeeded ? "Compiled" : "Failed";
+ }
else
{
- return started ? "Compiled" : "Configuring";
+ 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 78128111..4f924ef7 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)
@@ -98,7 +98,7 @@ namespace Wabbajack
(this).WhenAny(x => x.ModListLocation.TargetPath),
resultSelector: (state, path) => (State: state, Path: path))
// A short throttle is a quick hack to make the above changes "atomic"
- .Throttle(TimeSpan.FromMilliseconds(25))
+ .Throttle(TimeSpan.FromMilliseconds(25), RxApp.MainThreadScheduler)
.Select(u =>
{
if (u.State.Failed) return null;
@@ -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),
@@ -143,8 +141,8 @@ namespace Wabbajack
.DisposeWith(CompositeDisposable);
// If Mo2 folder changes and download location is empty, set it for convenience
- (this).WhenAny(x => x.Mo2Folder)
- .DelayInitial(TimeSpan.FromMilliseconds(100))
+ this.WhenAny(x => x.Mo2Folder)
+ .DelayInitial(TimeSpan.FromMilliseconds(100), RxApp.MainThreadScheduler)
.Where(x => Directory.Exists(x))
.FlowSwitch(
(this).WhenAny(x => x.DownloadLocation.Exists)
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 8378c9be..f1dbf785 100644
--- a/Wabbajack/View Models/Installers/InstallerVM.cs
+++ b/Wabbajack/View Models/Installers/InstallerVM.cs
@@ -30,8 +30,8 @@ namespace Wabbajack
public MainWindowVM MWVM { get; }
- public BitmapImage WabbajackLogo { get; } = UIUtils.BitmapImageFromStream(Application.GetResourceStream(new Uri("pack://application:,,,/Wabbajack;component/Resources/Wabba_Mouth_No_Text.png")).Stream);
- public BitmapImage WabbajackErrLogo { get; } = UIUtils.BitmapImageFromStream(Application.GetResourceStream(new Uri("pack://application:,,,/Wabbajack;component/Resources/Wabba_Ded.png")).Stream);
+ public static BitmapImage WabbajackLogo { get; } = UIUtils.BitmapImageFromStream(Application.GetResourceStream(new Uri("pack://application:,,,/Wabbajack;component/Resources/Wabba_Mouth_No_Text.png")).Stream);
+ public static BitmapImage WabbajackErrLogo { get; } = UIUtils.BitmapImageFromStream(Application.GetResourceStream(new Uri("pack://application:,,,/Wabbajack;component/Resources/Wabba_Ded.png")).Stream);
private readonly ObservableAsPropertyHelper _modList;
public ModListVM ModList => _modList.Value;
@@ -89,6 +89,12 @@ namespace Wabbajack
private readonly ObservableAsPropertyHelper<(int CurrentCPUs, int DesiredCPUs)> _CurrentCpuCount;
public (int CurrentCPUs, int DesiredCPUs) CurrentCpuCount => _CurrentCpuCount.Value;
+ private readonly ObservableAsPropertyHelper _LoadingModlist;
+ public bool LoadingModlist => _LoadingModlist.Value;
+
+ private readonly ObservableAsPropertyHelper _IsActive;
+ public bool IsActive => _IsActive.Value;
+
// Command properties
public IReactiveCommand ShowReportCommand { get; }
public IReactiveCommand OpenReadmeCommand { get; }
@@ -141,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
@@ -151,27 +157,64 @@ namespace Wabbajack
})
.DisposeWith(CompositeDisposable);
- _modList = this.WhenAny(x => x.ModListLocation.TargetPath)
+ _IsActive = this.ConstructIsActive(MWVM)
+ .ToGuiProperty(this, nameof(IsActive));
+
+ // Active path represents the path to currently have loaded
+ // If we're not actively showing, then "unload" the active path
+ var activePath = Observable.CombineLatest(
+ this.WhenAny(x => x.ModListLocation.TargetPath),
+ this.WhenAny(x => x.IsActive),
+ resultSelector: (path, active) => (path, active))
+ .Select(x =>
+ {
+ if (!x.active) return default(string);
+ return x.path;
+ })
+ // Throttle slightly so changes happen more atomically
+ .Throttle(TimeSpan.FromMilliseconds(50), RxApp.MainThreadScheduler)
+ .Replay(1)
+ .RefCount();
+
+ _modList = activePath
.ObserveOn(RxApp.TaskpoolScheduler)
+ // Convert from active path to modlist VM
.Select(modListPath =>
{
if (modListPath == null) return default(ModListVM);
if (!File.Exists(modListPath)) return default(ModListVM);
return new ModListVM(modListPath);
})
+ .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)
+ .Delay(TimeSpan.FromMilliseconds(50), RxApp.MainThreadScheduler)
+ .Subscribe(x =>
+ {
+ GC.Collect();
+ });
+
+ _LoadingModlist = Observable.Merge(
+ // When active path changes, mark as loading
+ activePath
+ .Select(_ => true),
+ // When the resulting modlist comes in, mark it as done
+ this.WhenAny(x => x.ModList)
+ .Select(_ => false))
+ .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)
@@ -209,8 +252,8 @@ namespace Wabbajack
return installer.PercentCompleted.StartWith(0f);
})
.Switch()
- .Debounce(TimeSpan.FromMilliseconds(25))
- .ToProperty(this, nameof(PercentCompleted));
+ .Debounce(TimeSpan.FromMilliseconds(25), RxApp.MainThreadScheduler)
+ .ToGuiProperty(this, nameof(PercentCompleted));
Slideshow = new SlideShow(this);
@@ -219,23 +262,24 @@ namespace Wabbajack
_image = Observable.CombineLatest(
this.WhenAny(x => x.ModList.Error),
this.WhenAny(x => x.ModList)
- .Select(x => x?.ImageObservable ?? Observable.Return(WabbajackLogo))
+ .Select(x => x?.ImageObservable ?? Observable.Return(default(BitmapImage)))
.Switch()
- .StartWith(WabbajackLogo),
+ .StartWith(default(BitmapImage)),
this.WhenAny(x => x.Slideshow.Image)
.StartWith(default(BitmapImage)),
this.WhenAny(x => x.Installing),
- resultSelector: (err, modList, slideshow, installing) =>
+ this.WhenAny(x => x.LoadingModlist),
+ resultSelector: (err, modList, slideshow, installing, loading) =>
{
if (err != null)
{
return WabbajackErrLogo;
}
- var ret = installing ? slideshow : modList;
- return ret ?? WabbajackLogo;
+ if (loading) return default;
+ 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),
@@ -243,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),
@@ -251,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),
@@ -259,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),
@@ -270,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);
@@ -285,21 +329,27 @@ namespace Wabbajack
.Select(x => x?.StartsWith("https://") ?? false)
.ObserveOnGuiThread());
- _progressTitle = Observable.CombineLatest(
- this.WhenAny(x => x.Installing),
- this.WhenAny(x => x.StartedInstallation),
- resultSelector: (installing, started) =>
+ _progressTitle = this.WhenAnyValue(
+ x => x.Installing,
+ x => x.StartedInstallation,
+ x => x.Completed,
+ selector: (installing, started, completed) =>
{
if (installing)
{
return "Installing";
}
+ else if (started)
+ {
+ if (completed == null) return "Installing";
+ return completed.Value.Succeeded ? "Installed" : "Failed";
+ }
else
{
- return started ? "Installed" : "Configuring";
+ return "Configuring";
}
})
- .ToProperty(this, nameof(ProgressTitle));
+ .ToGuiProperty(this, nameof(ProgressTitle));
UIUtils.BindCpuStatus(
this.WhenAny(x => x.Installer.ActiveInstallation)
@@ -354,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)
@@ -378,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/ModListGalleryVM.cs b/Wabbajack/View Models/ModListGalleryVM.cs
index 84ff0a96..5204b99c 100644
--- a/Wabbajack/View Models/ModListGalleryVM.cs
+++ b/Wabbajack/View Models/ModListGalleryVM.cs
@@ -21,30 +21,43 @@ namespace Wabbajack
public ObservableCollectionExtended ModLists { get; } = new ObservableCollectionExtended();
- public IReactiveCommand RefreshCommand { get; }
-
private int missingHashFallbackCounter;
public ModListGalleryVM(MainWindowVM mainWindowVM)
: base(mainWindowVM)
{
MWVM = mainWindowVM;
- RefreshCommand = ReactiveCommand.Create(() => { });
- RefreshCommand.StartingExecution()
- .StartWith(Unit.Default)
+ Observable.Return(Unit.Default)
.ObserveOn(RxApp.TaskpoolScheduler)
.SelectTask(async _ =>
{
return (await ModlistMetadata.LoadFromGithub())
.AsObservableChangeSet(x => x.DownloadMetadata?.Hash ?? $"Fallback{missingHashFallbackCounter++}");
})
+ // Unsubscribe and release when not active
+ .FlowSwitch(
+ this.WhenAny(x => x.IsActive),
+ valueWhenOff: Observable.Return(ChangeSet.Empty))
+ // Convert to VM and bind to resulting list
.Switch()
.ObserveOnGuiThread()
.Transform(m => new ModListMetadataVM(this, m))
+ .DisposeMany()
.Bind(ModLists)
.Subscribe()
.DisposeWith(CompositeDisposable);
+
+ // Extra GC when navigating away, just to immediately clean up modlist metadata
+ this.WhenAny(x => x.IsActive)
+ .Where(x => !x)
+ .Skip(1)
+ .Delay(TimeSpan.FromMilliseconds(50), RxApp.MainThreadScheduler)
+ .Subscribe(_ =>
+ {
+ GC.Collect();
+ })
+ .DisposeWith(CompositeDisposable);
}
}
}
diff --git a/Wabbajack/View Models/ModListMetadataVM.cs b/Wabbajack/View Models/ModListMetadataVM.cs
index 6355da64..9b72bed2 100644
--- a/Wabbajack/View Models/ModListMetadataVM.cs
+++ b/Wabbajack/View Models/ModListMetadataVM.cs
@@ -45,6 +45,9 @@ namespace Wabbajack
private readonly ObservableAsPropertyHelper _Image;
public BitmapImage Image => _Image.Value;
+ private readonly ObservableAsPropertyHelper _LoadingImage;
+ public bool LoadingImage => _LoadingImage.Value;
+
public ModListMetadataVM(ModListGalleryVM parent, ModlistMetadata metadata)
{
_parent = parent;
@@ -119,11 +122,18 @@ 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));
+ var imageObs = Observable.Return(Metadata.Links.ImageUri)
+ .DownloadBitmapImage((ex) => Utils.Log($"Error downloading modlist image {Metadata.Title}"));
+
+ _Image = imageObs
+ .ToGuiProperty(this, nameof(Image));
+
+ _LoadingImage = imageObs
+ .Select(x => false)
+ .StartWith(true)
+ .ToGuiProperty(this, nameof(LoadingImage));
}
private Task Download()
diff --git a/Wabbajack/View Models/ModListVM.cs b/Wabbajack/View Models/ModListVM.cs
index b08f933f..4bdb4009 100644
--- a/Wabbajack/View Models/ModListVM.cs
+++ b/Wabbajack/View Models/ModListVM.cs
@@ -13,7 +13,7 @@ namespace Wabbajack
{
public class ModListVM : ViewModel
{
- public ModList SourceModList { get; }
+ public ModList SourceModList { get; private set; }
public Exception Error { get; }
public string ModListPath { get; }
public string Name => SourceModList?.Name;
@@ -42,6 +42,7 @@ namespace Wabbajack
}
ImageObservable = Observable.Return(Unit.Default)
+ // Download and retrieve bytes on background thread
.ObserveOn(RxApp.TaskpoolScheduler)
.Select(filePath =>
{
@@ -66,6 +67,7 @@ namespace Wabbajack
return default(MemoryStream);
}
})
+ // Create Bitmap image on GUI thread
.ObserveOnGuiThread()
.Select(memStream =>
{
@@ -80,6 +82,11 @@ namespace Wabbajack
return default(BitmapImage);
}
})
+ // If ever would return null, show WJ logo instead
+ .Select(x =>
+ {
+ return x ?? InstallerVM.WabbajackLogo;
+ })
.Replay(1)
.RefCount();
}
@@ -116,5 +123,13 @@ namespace Wabbajack
}
}
}
+
+ public override void Dispose()
+ {
+ base.Dispose();
+ // Just drop reference explicitly, as it's large, so it can be GCed
+ // Even if someone is holding a stale reference to the VM
+ this.SourceModList = null;
+ }
}
}
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 625eb603..7a64cacd 100644
--- a/Wabbajack/View Models/SlideShow.cs
+++ b/Wabbajack/View Models/SlideShow.cs
@@ -66,7 +66,7 @@ namespace Wabbajack
this.WhenAny(x => x.Installer.Installing),
resultSelector: (enabled, installing) => enabled && installing))
// Block spam
- .Debounce(TimeSpan.FromMilliseconds(250))
+ .Debounce(TimeSpan.FromMilliseconds(250), RxApp.MainThreadScheduler)
.Scan(
seed: 0,
accumulator: (i, _) => i + 1)
@@ -80,19 +80,20 @@ namespace Wabbajack
{
if (modList?.SourceModList?.Archives == null)
{
- return Observable.Empty()
+ return Observable.Empty()
.ToObservableChangeSet(x => x.ModID);
}
return modList.SourceModList.Archives
.Select(m => m.State)
.OfType()
- .Select(nexus => new ModVM(nexus))
// Shuffle it
.Shuffle(_random)
.AsObservableChangeSet(x => x.ModID);
})
// Switch to the new list after every ModList change
.Switch()
+ .Transform(nexus => new ModVM(nexus))
+ .DisposeMany()
// Filter out any NSFW slides if we don't want them
.AutoRefreshOnObservable(slide => this.WhenAny(x => x.ShowNSFW))
.Filter(slide => !slide.IsNSFW || ShowNSFW)
@@ -108,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),
diff --git a/Wabbajack/Views/Common/DetailImageView.xaml b/Wabbajack/Views/Common/DetailImageView.xaml
index 43bd46f1..476a69ef 100644
--- a/Wabbajack/Views/Common/DetailImageView.xaml
+++ b/Wabbajack/Views/Common/DetailImageView.xaml
@@ -4,10 +4,12 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:icon="http://metro.mahapps.com/winfx/xaml/iconpacks"
+ xmlns:lib="clr-namespace:Wabbajack.Lib;assembly=Wabbajack.Lib"
xmlns:local="clr-namespace:Wabbajack"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="450"
d:DesignWidth="800"
+ x:TypeArguments="lib:ViewModel"
ClipToBounds="True"
mc:Ignorable="d">
diff --git a/Wabbajack/Views/Common/DetailImageView.xaml.cs b/Wabbajack/Views/Common/DetailImageView.xaml.cs
index 4fe4a7f4..96357d9f 100644
--- a/Wabbajack/Views/Common/DetailImageView.xaml.cs
+++ b/Wabbajack/Views/Common/DetailImageView.xaml.cs
@@ -1,15 +1,20 @@
using ReactiveUI;
+using ReactiveUI.Fody.Helpers;
+using System;
using System.Linq;
+using System.Reactive;
+using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Media;
+using Wabbajack.Lib;
namespace Wabbajack
{
///
/// Interaction logic for DetailImageView.xaml
///
- public partial class DetailImageView : UserControlRx
+ public partial class DetailImageView : UserControlRx
{
public ImageSource Image
{
@@ -51,30 +56,36 @@ namespace Wabbajack
public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register(nameof(Description), typeof(string), typeof(DetailImageView),
new FrameworkPropertyMetadata(default(string), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, WireNotifyPropertyChanged));
- private readonly ObservableAsPropertyHelper _showAuthor;
- public bool ShowAuthor => _showAuthor.Value;
+ [Reactive]
+ public bool ShowAuthor { get; private set; }
- private readonly ObservableAsPropertyHelper _showDescription;
- public bool ShowDescription => _showDescription.Value;
+ [Reactive]
+ public bool ShowDescription { get; private set; }
- private readonly ObservableAsPropertyHelper _showTitle;
- public bool ShowTitle => _showTitle.Value;
+ [Reactive]
+ public bool ShowTitle { get; private set; }
public DetailImageView()
{
InitializeComponent();
- _showAuthor = this.WhenAny(x => x.Author)
- .Select(x => !string.IsNullOrWhiteSpace(x))
- .ToProperty(this, nameof(ShowAuthor));
+ this.WhenActivated(dispose =>
+ {
+ this.WhenAny(x => x.Author)
+ .Select(x => !string.IsNullOrWhiteSpace(x))
+ .Subscribe(x => ShowAuthor = x)
+ .DisposeWith(dispose);
- _showDescription = this.WhenAny(x => x.Description)
- .Select(x => !string.IsNullOrWhiteSpace(x))
- .ToProperty(this, nameof(ShowDescription));
+ this.WhenAny(x => x.Description)
+ .Select(x => !string.IsNullOrWhiteSpace(x))
+ .Subscribe(x => ShowDescription = x)
+ .DisposeWith(dispose);
- _showTitle = this.WhenAny(x => x.Title)
- .Select(x => !string.IsNullOrWhiteSpace(x))
- .ToProperty(this, nameof(ShowTitle));
+ this.WhenAny(x => x.Title)
+ .Select(x => !string.IsNullOrWhiteSpace(x))
+ .Subscribe(x => ShowTitle = x)
+ .DisposeWith(dispose);
+ });
}
}
}
diff --git a/Wabbajack/Views/Common/TopProgressView.xaml b/Wabbajack/Views/Common/TopProgressView.xaml
index 6f362405..a47aa8d3 100644
--- a/Wabbajack/Views/Common/TopProgressView.xaml
+++ b/Wabbajack/Views/Common/TopProgressView.xaml
@@ -3,11 +3,13 @@
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:lib="clr-namespace:Wabbajack.Lib;assembly=Wabbajack.Lib"
xmlns:local="clr-namespace:Wabbajack"
xmlns:mahapps="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="450"
d:DesignWidth="800"
+ x:TypeArguments="lib:ViewModel"
BorderThickness="0"
mc:Ignorable="d">
diff --git a/Wabbajack/Views/Common/TopProgressView.xaml.cs b/Wabbajack/Views/Common/TopProgressView.xaml.cs
index 79cc9640..8a150a5f 100644
--- a/Wabbajack/Views/Common/TopProgressView.xaml.cs
+++ b/Wabbajack/Views/Common/TopProgressView.xaml.cs
@@ -2,13 +2,17 @@
using System.Windows;
using System.Windows.Controls;
using ReactiveUI;
+using System;
+using ReactiveUI.Fody.Helpers;
+using Wabbajack.Lib;
+using System.Reactive.Disposables;
namespace Wabbajack
{
///
/// Interaction logic for TopProgressView.xaml
///
- public partial class TopProgressView : UserControlRx
+ public partial class TopProgressView : UserControlRx
{
public double ProgressPercent
{
@@ -50,18 +54,22 @@ namespace Wabbajack
public static readonly DependencyProperty ShadowMarginProperty = DependencyProperty.Register(nameof(ShadowMargin), typeof(bool), typeof(TopProgressView),
new FrameworkPropertyMetadata(true));
- private readonly ObservableAsPropertyHelper _ProgressOpacityPercent;
- public double ProgressOpacityPercent => _ProgressOpacityPercent.Value;
+ [Reactive]
+ public double ProgressOpacityPercent { get; private set; }
public TopProgressView()
{
InitializeComponent();
- _ProgressOpacityPercent = this.WhenAny(x => x.ProgressPercent)
- .Select(x =>
- {
- return 0.3 + x * 0.7;
- })
- .ToProperty(this, nameof(ProgressOpacityPercent));
+ this.WhenActivated(dispose =>
+ {
+ this.WhenAny(x => x.ProgressPercent)
+ .Select(x =>
+ {
+ return 0.3 + x * 0.7;
+ })
+ .Subscribe(x => ProgressOpacityPercent = x)
+ .DisposeWith(dispose);
+ });
}
}
}
diff --git a/Wabbajack/Views/Installers/InstallationView.xaml b/Wabbajack/Views/Installers/InstallationView.xaml
index cd4b479e..f3f11ee4 100644
--- a/Wabbajack/Views/Installers/InstallationView.xaml
+++ b/Wabbajack/Views/Installers/InstallationView.xaml
@@ -7,6 +7,7 @@
xmlns:icon="http://metro.mahapps.com/winfx/xaml/iconpacks"
xmlns:lib="clr-namespace:Wabbajack.Lib;assembly=Wabbajack.Lib"
xmlns:local="clr-namespace:Wabbajack"
+ xmlns:mahapps="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DataContext="{d:DesignInstance local:InstallerVM}"
d:DesignHeight="500"
@@ -48,11 +49,14 @@
Grid.Row="1"
Margin="5,0,5,5">
-
+
+
+
+
diff --git a/Wabbajack/Views/ModListTileView.xaml b/Wabbajack/Views/ModListTileView.xaml
index dcee13dc..cb642064 100644
--- a/Wabbajack/Views/ModListTileView.xaml
+++ b/Wabbajack/Views/ModListTileView.xaml
@@ -86,6 +86,7 @@
BorderBrush="{StaticResource ButtonNormalBorder}"
BorderThickness="0,0,0,1">
+