Silverlight中无法使用Delegate.CreateDelegate()来创建对无法访问的方法的委托

这个标题很绕口,先说说我是怎么遇到这个问题的。 如果对问题解决过程不感兴趣,可以直接跳到最后看应对方法。

现在在做的一个项目使用了微软patterns & practices小组的Prism(Composite Application Guidance)框架,其提供了一个事件框架,使用IEventAggregator可以轻松创建低耦合的事件驱动应用程序。由于以前在WPF下使用过Prism,想来这次应该会比较轻松,不料刚开始用就遇到了问题。在启动Silverlight应用程序,加载Module时发生了以下异常:

Exception

单步调试发现是在一个Event的Subscribe方法中发生异常的,其最内层的异常(InnerException)是一个MethodAccessException。出错处代码如下:

eventAggregator.GetEvent<LoginCompletedEvent> ().Subscribe (p =>
{
    _regionManager.Regions[RegionNames.NavigateBarRegion].Clear ();
    _regionManager.RegisterViewWithRegion (RegionNames.NavigateBarRegion,
        () => _container.Resolve<INavigateBarPresenter> ().View);
}, ThreadOption.UIThread);

不解,上网广搜原因,发现两处直接相关内容,一说Prism的源码有bug,但我试了他的解决方法无效,而且他提到的bug检查后也不存在,当然我下载了十月份的最新版,可能问题已经修复了;二说SL目前不支持创建到lambda表达式或者匿名委托的弱引用,原话如下

image

这虽然是微软MSDN上说的,还是十月最新的,但也是扯.淡,因为我看了Prism代码根本没有创建对委托的弱引用。而且,当我把上面源码中的lambda表达式抽出为单独的方法时,依然无效。直到我将这个方法的签名设置为public,代码终于跑通了。同时,还发现下面的代码是不会抛异常的:

_eventAggregator.GetEvent<LoginCompletedEvent> ().Subscribe (p =>
{
    requestCurrentUserInfo (p);

    //Remove the login view when login completed. _regionManager.Regions[RegionNames.MainRegion].Clear ();
}, ThreadOption.UIThread, true);

 

同样的问题,还存在在Subscribe方法可选的第四个参数filter上(类型为Predicate<T>)。

很无奈,只能自己来探求这个问题的原因。我把Prism的原项目加入解决方案,把对dll的引用改为对项目的引用,单步跟踪Prism内部的Event机制调用。为了调试,我编写了一段代码来Subscribe一个Event,提供的action参数是具名public方法,filter参数是一个lambda表达式。单步进入Subscribe方法至一处要创建Subscription:

image

注意此时actionReference的Target属性是一个Action<UserInfo>委托,这是正确的,而下面的filterReference.Target则已经抛出了MethodAccessException(附带一提,VS2010 debug时可以随便pin变量提示,比watch更清晰,确实很好用):

image

看来异常就是这里发生的,那Target属性又是怎么定义的呢,如下:

 

image

最终,我们跟踪到TryGetDelegate()方法内部,第一次对action具名方法调用此方法成功通过,而对于lambda方法的filter:

 

image

此时根据lambda是否使用了所在类实例(或其实例成员),两行设断点的代码都有可能执行,但都会抛出MethodAccessException,提示不能访问该方法。于是打开MSDN查,结果本地MSDN中Delegate.CreateDelegate()方法的说明没有提到半点Silverlight...倒是提到了一个安全限制的可能:

Security about CreateDelegate()

把反射的安全权限了解了一遍,感觉不是这个问题,而且Silverlight根本不能引用System.Reflection程序集。又上网搜了一通,终于在在线的尚处于开发中的Silverlight版MSDN中找到了一行说明:

In Silverlight, method must specify an accessible method.

这下就都清楚了,lambda表达式在编译时被编译器实现成一个private方法,如果没有引用实例成员,则同时会被实现为static方法以提高性能。Prism的事件Subscribe中,在参数指定不需要保存对象引用时,会生成一个对对象的弱引用(WeakReference),需要调用注册方法时,才使用System.Delegate.CreateDelegate()方法来创建一个委托,而正如MSDN所属,Silverlight只能创建对可以访问的方法的委托,所以这里就出错了。

而如果指定了keepReferenceAlive参数为true,则直接保存方法委托,就会保持一个对对象的强引用。因此,上面的第二段代码没有出错。

对此问题的解决办法:

既然MSDN上都说了不行,那基本就只能照着办了。有以下两种种方案可以采取:

  1. 注册所有事件时都设置keepReferenceAlive为true,然后每个订阅者都手动Unsubscribe(),或者写到析构方法里面。不过很容易忘记,会导致潜在的内存泄漏危险。
  2. 将所有的事件处理方法设为单独的具名public方法。不过这样会暴露其内在实现,破坏了封装性。

我采取了方案2,为了代码一致性,我把那些keepReferenceAlive为true的也一并改写了;同时,我把所有事件处理方法命名为onSomeEvent(),即将开头第一个字母小写,以便类的使用者可以和真正的public方法区分。毕竟,假如哪一天Silverlight支持那么做了,把public改成private要比去掉每一处Unsubscribe()并且修改keepReferenceAlive参数要方便。至于lambda表达式以及匿名方法,只能说在此处是个杯具(不是它的问题,却不能用了)。其他地方也需要小心使用。

附带一提,这哥们遇到了同样的问题,他的解决方法是对的,不过他以为是自己傻了……(正如上面某附图里MSDN所述,从.NET 2.0起,已经可以创建对私有方法的委托。这里的问题是Silverlight的特殊情况。)

转载于:https://www.cnblogs.com/Gildor/archive/2009/12/20/1628051.html