【发布时间】:2020-04-26 15:24:29
【问题描述】:
作为创建我的 SimpleInjector 容器的一部分,我遵循了推荐的做法并致电 container.Verify() 以检查我的类型注册是否有意义。
这很好用,并且发现了我犯的一些错误 - 但它也会产生我想清理的碎片。
我的一个类是一个单例事件中心,用于在其他瞬态组件之间路由消息;这些其他组件在其构造函数中接受事件中心,创建订阅以接收他们有兴趣接收的消息,然后在完成后Dispose() 订阅。
对container.Verify() 的调用会创建每种对象中的一个,从而导致许多这些原本短暂的实例徘徊,因为事件中心仍然知道它们的订阅。
目前,我已通过在应用程序启动之前在Verify() 调用之后立即手动终止所有订阅来解决此问题。然而,这感觉像是一个必须已经解决的问题,尽管我无法在文档中、在 Stack Overflow 上或通过搜索找到答案。
也许使用范围生活方式是解决方案?它们似乎无关紧要,因为我正在构建 WPF 应用程序,但如果我知道答案,我就不会在这里问了!
更新,1 月 12 日 - 按照 @steven 的要求,这里有一些代码来演示我的问题。
我尝试(但失败了)用一些既可编译又短到可以内联共享的东西来证明这个问题;相反,我展示了实际项目的一些代码摘录。如果你想看到整个事情,WordTutor project is on GitHub。
在我的应用程序的核心,我有一个单例IReduxStore<T>,它既封装了应用程序状态又充当了一种事件中心。其他类订阅存储,以便在应用程序状态更改时主动通知。
这里是IReduxStore<T>,精简到了基本要素:
// IReduxStore.cs
public interface IReduxStore<T>
{
T State { get; }
void Dispatch(IReduxMessage message);
IDisposable SubscribeToReference<V>(
Func<T, V?> referenceReader,
Action<V?> whenChanged)
where V : class, IEquatable<V>?;
}
订阅将IDisposable 实现为在不再需要订阅时进行确定性清理的一种方便且惯用的方式。
商店注册为单例,绑定到特定类型的状态:
// Program.cs
container.RegisterSingleton<
IReduxStore<WordTutorApplication>,
ReduxStore<WordTutorApplication>>();
IReduxStore<T> 的实现存储了所有订阅:
private readonly HashSet<ReduxSubscription<T>> _subscriptions
= new HashSet<ReduxSubscription<T>>();
它们会在处理后从 HashSet 中删除。
我的许多 ViewModel 接受 IReduxStore<WordTutorApplication> 到它们的构造函数,以便他们可以订阅更新:
// VocabularyBrowserViewModel.cs
public sealed class VocabularyBrowserViewModel : ViewModelBase
{
private readonly IReduxStore<WordTutorApplication> _store;
private readonly IDisposable _screenSubscription;
private readonly IDisposable _vocabularySubscription;
public VocabularyBrowserViewModel(IReduxStore<WordTutorApplication> store)
{
_store = store ?? throw new ArgumentNullException(nameof(store));
// ... elided ...
_screenSubscription = _store.SubscribeToReference(
app => app.CurrentScreen as VocabularyBrowserScreen,
RefreshFromScreen);
_vocabularySubscription = _store.SubscribeToReference(
app => app.VocabularySet,
RefreshFromVocabularySet);
// ... elided ...
}
// ... elided ...
}
ViewModel 被注册为瞬态,因为每个窗口都需要一个唯一的实例:
// Program.cs
var desktopAssembly = typeof(WordTutorWindow).Assembly;
container.Collection.Register<ViewModelBase>(desktopAssembly);
ViewModel 在不再需要时主动释放订阅:
// VocabularyBrowserViewModel.cs
private void RefreshFromScreen(VocabularyBrowserScreen? screen)
{
if (screen == null)
{
_screenSubscription.Dispose();
_vocabularySubscription.Dispose();
return;
}
Selection = screen.Selection;
Modified = screen.Modified;
}
当在 SimpleInjector 容器上调用 Verify() 时,会创建每个对象的示例,包括单例 IReduxStore<T>。瞬态视图模型(例如上面显示的VocabularyBrowserViewModel)也会被创建,但这些实例仍然活动,因为它们的订阅仍由商店持有。
我尝试在 ViewModel 上实现 IDisposable,但由于它们的生活方式瞬态,唯一的效果是在调用 Verify() 时生成额外的警告。
更新 II,1 月 12 日:
我目前的解决方法是在容器成功初始化后手动清除所有订阅作为应用程序启动的一部分:
var store = (ReduxStore<WordTutorApplication>)
container.GetInstance<IReduxStore<WordTutorApplication>>();
store.ClearSubscriptions();
这感觉就像一个讨厌的黑客。首先它需要显式转换为实现类型,然后调用一个根本不需要存在的方法。
【问题讨论】:
-
一些代码显示了您在瞬态组件中执行的操作以连接到事件中心,以及一些显示事件中心如何保持订阅的代码将对您的问题有益。跨度>
-
@Bevan 您的解决方法似乎很好,因为它是在组合根目录中执行的。如果您不想针对这种情况扩展 API,您可以使用单元测试以一石二鸟。引用docs:“考虑将验证调用包装在#IF DEBUG 编译器指令中,或者仅将验证作为单元/集成测试的一部分运行。这样可以缩短启动时间,并将性能损失转移到每次首次解析个人注册。”。
-
如果您使用WeakEventManager,订阅不会使订阅者留在内存中。除此之外,我认为使用 IDisposable 接口和 View 的 RefreshFromScreen 回调来影响 View 和 ViewModel 的生命周期并不是一个好方法。 ViewModel 不应该知道 View。你不能只保留对活动 ViewModel 的引用并让其他人收集垃圾吗?
-
也许您可以考虑将 container.Verify() 调用移至单元测试,因为实际上它所做的是测试您的注册是否正确。它不回答你的问题,但解决了你的问题。
-
你有一个设计问题就是你说View Models在不需要的时候主动释放订阅,但是调用RefreshFromScreen的情况并不包括所有没有订阅的情况不再需要,但在这种情况下您没有调用 RefreshFromScreen。证据,不再需要由验证触发的订阅。我订阅了 Victor Ortundo 的评论,或者您可以在视图模型订阅的中心事件中使用弱引用。
标签: c# wpf .net-core simple-injector