Skip to content

Commit 46ebe09

Browse files
Merge pull request #27 from TheEightBot/feature/vm-disposal
2 parents 42a6579 + b06689a commit 46ebe09

File tree

5 files changed

+119
-23
lines changed

5 files changed

+119
-23
lines changed

Stellar/Diagnostics/MemoryDiagnostics.cs

Whitespace-only changes.

Stellar/Disposables/WeakCompositeDisposable.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -333,14 +333,19 @@ public bool Contains(IDisposable item)
333333
return _disposables.Contains(item);
334334
}
335335

336+
public void Clear()
337+
{
338+
_disposables.Clear();
339+
}
340+
336341
public void CopyTo(IDisposable[] array, int arrayIndex)
337342
{
338343
_disposables.CopyTo(array, arrayIndex);
339344
}
340345

341-
public void Clear()
346+
public List<IDisposable> ToList()
342347
{
343-
_disposables.Clear();
348+
return new List<IDisposable>(_disposables);
344349
}
345350

346351
public IEnumerator<IDisposable> GetEnumerator()

Stellar/Extensions/AttributeCache.cs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ namespace Stellar.Extensions;
1010
public static class AttributeCache
1111
{
1212
private static readonly ConcurrentDictionary<(Type TypeKey, Type AttributeType), Attribute?> TypeAttributeCache = new();
13+
private static readonly ConcurrentDictionary<(MemberInfo MemberKey, Type AttributeType), Attribute?> MemberAttributeCache = new();
1314

1415
/// <summary>
1516
/// Gets a custom attribute for a type with high performance caching.
@@ -34,8 +35,9 @@ public static class AttributeCache
3435
public static TAttribute? GetAttribute<TAttribute>(MemberInfo memberInfo)
3536
where TAttribute : Attribute
3637
{
37-
// For member-level attributes, we'd need a separate cache with appropriate key structure
38-
return Attribute.GetCustomAttribute(memberInfo, typeof(TAttribute)) as TAttribute;
38+
return (TAttribute?)MemberAttributeCache.GetOrAdd(
39+
(memberInfo, typeof(TAttribute)),
40+
key => Attribute.GetCustomAttribute(key.MemberKey, key.AttributeType));
3941
}
4042

4143
/// <summary>
@@ -49,4 +51,22 @@ public static bool HasAttribute<TAttribute>(Type type)
4951
{
5052
return GetAttribute<TAttribute>(type) != null;
5153
}
54+
55+
/// <summary>
56+
/// Clears the cache to prevent memory leaks in long-running applications.
57+
/// Call this periodically or when you know types are being unloaded.
58+
/// </summary>
59+
public static void ClearCache()
60+
{
61+
TypeAttributeCache.Clear();
62+
MemberAttributeCache.Clear();
63+
}
64+
65+
/// <summary>
66+
/// Gets the current cache statistics for monitoring memory usage.
67+
/// </summary>
68+
public static (int TypeCacheCount, int MemberCacheCount) GetCacheStatistics()
69+
{
70+
return (TypeAttributeCache.Count, MemberAttributeCache.Count);
71+
}
5272
}

Stellar/ViewManager.cs

Lines changed: 88 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
using System.Reactive.Subjects;
2-
using ReactiveUI;
32

43
namespace Stellar;
54

6-
#pragma warning disable CA1001
7-
public abstract class ViewManager<TViewModel>
5+
public abstract class ViewManager<TViewModel> : IDisposable
86
where TViewModel : class
9-
#pragma warning restore CA1001
107
{
118
private readonly Lazy<Subject<LifecycleEvent>> _lifecycleEvents;
129

@@ -18,33 +15,44 @@ public abstract class ViewManager<TViewModel>
1815

1916
private bool _controlsBound;
2017

21-
public IObservable<Unit> Initialized => _lifecycleEvents.Value.Where(x => x == LifecycleEvent.Initialized).SelectUnit().AsObservable();
18+
private bool _disposed = false;
2219

23-
public IObservable<Unit> Activated => _lifecycleEvents.Value.Where(x => x == LifecycleEvent.Activated).SelectUnit().AsObservable();
20+
public IObservable<Unit> Initialized => _disposed ? Observable.Empty<Unit>() : _lifecycleEvents.Value.Where(x => x == LifecycleEvent.Initialized).SelectUnit().AsObservable();
2421

25-
public IObservable<Unit> Attached => _lifecycleEvents.Value.Where(x => x == LifecycleEvent.Attached).SelectUnit().AsObservable();
22+
public IObservable<Unit> Activated => _disposed ? Observable.Empty<Unit>() : _lifecycleEvents.Value.Where(x => x == LifecycleEvent.Activated).SelectUnit().AsObservable();
2623

27-
public IObservable<Unit> IsAppearing => _lifecycleEvents.Value.Where(x => x == LifecycleEvent.IsAppearing).SelectUnit().AsObservable();
24+
public IObservable<Unit> Attached => _disposed ? Observable.Empty<Unit>() : _lifecycleEvents.Value.Where(x => x == LifecycleEvent.Attached).SelectUnit().AsObservable();
2825

29-
public IObservable<Unit> IsDisappearing => _lifecycleEvents.Value.Where(x => x == LifecycleEvent.IsDisappearing).SelectUnit().AsObservable();
26+
public IObservable<Unit> IsAppearing => _disposed ? Observable.Empty<Unit>() : _lifecycleEvents.Value.Where(x => x == LifecycleEvent.IsAppearing).SelectUnit().AsObservable();
3027

31-
public IObservable<Unit> Detached => _lifecycleEvents.Value.Where(x => x == LifecycleEvent.Detached).SelectUnit().AsObservable();
28+
public IObservable<Unit> IsDisappearing => _disposed ? Observable.Empty<Unit>() : _lifecycleEvents.Value.Where(x => x == LifecycleEvent.IsDisappearing).SelectUnit().AsObservable();
3229

33-
public IObservable<Unit> Deactivated => _lifecycleEvents.Value.Where(x => x == LifecycleEvent.Deactivated).SelectUnit().AsObservable();
30+
public IObservable<Unit> Detached => _disposed ? Observable.Empty<Unit>() : _lifecycleEvents.Value.Where(x => x == LifecycleEvent.Detached).SelectUnit().AsObservable();
3431

35-
public IObservable<Unit> Disposed => _lifecycleEvents.Value.Where(x => x == LifecycleEvent.Disposed).SelectUnit().AsObservable();
32+
public IObservable<Unit> Deactivated => _disposed ? Observable.Empty<Unit>() : _lifecycleEvents.Value.Where(x => x == LifecycleEvent.Deactivated).SelectUnit().AsObservable();
3633

37-
public IObservable<LifecycleEvent> LifecycleEvents => _lifecycleEvents.Value.AsObservable();
34+
public IObservable<Unit> Disposed => _disposed ? Observable.Empty<Unit>() : _lifecycleEvents.Value.Where(x => x == LifecycleEvent.Disposed).SelectUnit().AsObservable();
3835

39-
public IObservable<Unit> NavigatedTo => _navigationEvents.Value.Where(x => x == NavigationEvent.NavigatedTo).SelectUnit().AsObservable();
36+
public IObservable<LifecycleEvent> LifecycleEvents => _disposed ? Observable.Empty<LifecycleEvent>() : _lifecycleEvents.Value.AsObservable();
4037

41-
public IObservable<Unit> NavigatedFrom => _navigationEvents.Value.Where(x => x == NavigationEvent.NavigatedFrom).SelectUnit().AsObservable();
38+
public IObservable<Unit> NavigatedTo => _disposed ? Observable.Empty<Unit>() : _navigationEvents.Value.Where(x => x == NavigationEvent.NavigatedTo).SelectUnit().AsObservable();
4239

43-
public IObservable<NavigationEvent> NavigationEvents => _navigationEvents.Value.AsObservable();
40+
public IObservable<Unit> NavigatedFrom => _disposed ? Observable.Empty<Unit>() : _navigationEvents.Value.Where(x => x == NavigationEvent.NavigatedFrom).SelectUnit().AsObservable();
41+
42+
public IObservable<NavigationEvent> NavigationEvents => _disposed ? Observable.Empty<NavigationEvent>() : _navigationEvents.Value.AsObservable();
4443

4544
public bool Maintain { get; set; }
4645

47-
public bool ControlsBound => Volatile.Read(ref _controlsBound);
46+
public bool ControlsBound
47+
{
48+
get
49+
{
50+
lock (_bindingLock)
51+
{
52+
return _controlsBound;
53+
}
54+
}
55+
}
4856

4957
public ViewManager()
5058
{
@@ -54,8 +62,53 @@ public ViewManager()
5462
_navigationEvents = new(() => new Subject<NavigationEvent>().DisposeWith(_controlBindings), LazyThreadSafetyMode.ExecutionAndPublication);
5563
}
5664

65+
public void Dispose()
66+
{
67+
Dispose(true);
68+
GC.SuppressFinalize(this);
69+
}
70+
71+
protected virtual void Dispose(bool disposing)
72+
{
73+
if (_disposed)
74+
{
75+
return;
76+
}
77+
78+
if (disposing)
79+
{
80+
lock (_bindingLock)
81+
{
82+
_controlBindings.Dispose();
83+
84+
// Dispose lazy-created subjects if they were created
85+
if (_lifecycleEvents.IsValueCreated)
86+
{
87+
_lifecycleEvents.Value.Dispose();
88+
}
89+
90+
if (_navigationEvents.IsValueCreated)
91+
{
92+
_navigationEvents.Value.Dispose();
93+
}
94+
}
95+
}
96+
97+
_disposed = true;
98+
}
99+
100+
private void ThrowIfDisposed()
101+
{
102+
if (_disposed)
103+
{
104+
throw new ObjectDisposedException(nameof(ViewManager<TViewModel>));
105+
}
106+
}
107+
57108
public void RegisterBindings(IStellarView<TViewModel> view)
58109
{
110+
ThrowIfDisposed();
111+
59112
lock (_bindingLock)
60113
{
61114
if (_controlsBound)
@@ -76,6 +129,8 @@ public void RegisterBindings(IStellarView<TViewModel> view)
76129

77130
public void UnregisterBindings(IStellarView<TViewModel> view)
78131
{
132+
ThrowIfDisposed();
133+
79134
lock (_bindingLock)
80135
{
81136
if (Maintain || !_controlsBound)
@@ -93,6 +148,8 @@ public void UnregisterBindings(IStellarView<TViewModel> view)
93148

94149
public virtual void HandleActivated(IStellarView<TViewModel> view)
95150
{
151+
ThrowIfDisposed();
152+
96153
view.RegisterViewModelBindings();
97154

98155
RegisterBindings(view);
@@ -102,6 +159,8 @@ public virtual void HandleActivated(IStellarView<TViewModel> view)
102159

103160
public virtual void HandleDeactivated(IStellarView<TViewModel> view)
104161
{
162+
ThrowIfDisposed();
163+
105164
OnLifecycle(view, LifecycleEvent.Deactivated);
106165

107166
UnregisterBindings(view);
@@ -110,6 +169,8 @@ public virtual void HandleDeactivated(IStellarView<TViewModel> view)
110169
public virtual void PropertyChanged<TView>(TView view, string? propertyName = null)
111170
where TView : IViewFor<TViewModel>
112171
{
172+
ThrowIfDisposed();
173+
113174
if (propertyName == nameof(IViewFor<TViewModel>.ViewModel) && view.ViewModel is not null)
114175
{
115176
view.SetupViewModel(view.ViewModel);
@@ -118,6 +179,11 @@ public virtual void PropertyChanged<TView>(TView view, string? propertyName = nu
118179

119180
public void OnLifecycle(IStellarView<TViewModel> view, LifecycleEvent lifecycleEvent)
120181
{
182+
if (_disposed)
183+
{
184+
return; // Silently return if disposed to avoid exceptions during cleanup
185+
}
186+
121187
if (view.ViewModel is ILifecycleEventAware lea)
122188
{
123189
lea.OnLifecycleEvent(lifecycleEvent);
@@ -133,6 +199,11 @@ public void OnLifecycle(IStellarView<TViewModel> view, LifecycleEvent lifecycleE
133199

134200
public void OnNavigating(IStellarView<TViewModel> view, NavigationEvent navigationEvent)
135201
{
202+
if (_disposed)
203+
{
204+
return; // Silently return if disposed to avoid exceptions during cleanup
205+
}
206+
136207
if (view.ViewModel is INavigationEventAware nea)
137208
{
138209
nea.OnNavigationEvent(navigationEvent);

Stellar/ViewModel/ViewModelBase.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public void Register()
6666

6767
lock (_vmLock)
6868
{
69-
if (_bindingsRegistered)
69+
if (_bindingsRegistered || IsDisposed)
7070
{
7171
return;
7272
}
@@ -87,7 +87,7 @@ public void Unregister()
8787

8888
lock (_vmLock)
8989
{
90-
if (Maintain || !_bindingsRegistered)
90+
if (Maintain || !_bindingsRegistered || IsDisposed)
9191
{
9292
return;
9393
}

0 commit comments

Comments
 (0)