diff --git a/Wabbajack.Common/Extensions/RxExt.cs b/Wabbajack.Common/Extensions/RxExt.cs index bdefd987..0a1b643e 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() @@ -226,5 +226,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/View Models/Compilers/MO2CompilerVM.cs b/Wabbajack/View Models/Compilers/MO2CompilerVM.cs index 78128111..d993df53 100644 --- a/Wabbajack/View Models/Compilers/MO2CompilerVM.cs +++ b/Wabbajack/View Models/Compilers/MO2CompilerVM.cs @@ -143,7 +143,7 @@ namespace Wabbajack .DisposeWith(CompositeDisposable); // If Mo2 folder changes and download location is empty, set it for convenience - (this).WhenAny(x => x.Mo2Folder) + this.WhenAny(x => x.Mo2Folder) .DelayInitial(TimeSpan.FromMilliseconds(100)) .Where(x => Directory.Exists(x)) .FlowSwitch( diff --git a/Wabbajack/View Models/Installers/InstallerVM.cs b/Wabbajack/View Models/Installers/InstallerVM.cs index 29d262e4..6d229346 100644 --- a/Wabbajack/View Models/Installers/InstallerVM.cs +++ b/Wabbajack/View Models/Installers/InstallerVM.cs @@ -185,9 +185,19 @@ namespace Wabbajack if (!File.Exists(modListPath)) return default(ModListVM); return new ModListVM(modListPath); }) + .DisposeOld() .ObserveOnGuiThread() .StartWith(default(ModListVM)) .ToProperty(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)) + .Subscribe(x => + { + GC.Collect(); + }); + _LoadingModlist = Observable.Merge( // When active path changes, mark as loading activePath diff --git a/Wabbajack/View Models/ModListGalleryVM.cs b/Wabbajack/View Models/ModListGalleryVM.cs index 84ff0a96..6c95081f 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)) + .Subscribe(_ => + { + GC.Collect(); + }) + .DisposeWith(CompositeDisposable); } } } diff --git a/Wabbajack/View Models/ModListVM.cs b/Wabbajack/View Models/ModListVM.cs index 94962a4e..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; @@ -123,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/SlideShow.cs b/Wabbajack/View Models/SlideShow.cs index 625eb603..74bf9a80 100644 --- a/Wabbajack/View Models/SlideShow.cs +++ b/Wabbajack/View Models/SlideShow.cs @@ -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)