如果我尝试在 xaml 中存储 SetterBase 对象的集合(其中包括 EventSetter),则 xaml 加载器会引发错误。
根本原因是 xaml 加载器尝试两次设置PresentationFramework.dll!System.Windows.EventSetters.Event:第一次设置为正确值 (ButtonBase.Click RoutedEvent),但第二次设置为 null,这会引发异常。我的附加属性回调不涉及。
为什么它尝试将事件添加到 EventSetter 两次,为什么第二次它为空?我检查了正在使用的 ctor 是默认的,因此,EventSeetter 不会以任何不寻常的方式与集合交互,所以不是这样。真正的原因是 wpf 中的一个错误,它解决了解析事件的两部分结构(Event 和 EventHandler)的挑战。
View
<Window x:Class="Spec.Plain.MTCMinimal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ContentToggleButton;assembly=ContentToggleButton"
Title="MTCMinimal" Height="300" Width="300">
<Window.Resources>
<SetterBaseCollection x:Key="ButtonStyleSetters">
<Setter Property="FrameworkElement.Height" Value="30"></Setter>
<EventSetter Event="ButtonBase.Click" Handler="StyleClick" />
</SetterBaseCollection>
</Window.Resources>
<Button Name="Button1"
local:Behaviours.StyleSetters="{StaticResource ButtonStyleSetters}" />
后面的代码只是 InitializeComponent 和事件处理程序的存根。该错误发生在InitializeComponent 期间。
行为
public static readonly DependencyProperty StyleSettersProperty =
DependencyProperty.RegisterAttached(
"StyleSetters", typeof(MyStyleSetters),
typeof(Behaviours),
new PropertyMetadata(default(MyStyleSetters),
ButtonSettersChanged));
private static void ButtonSettersChanged (DependencyObject d,
DependencyPropertyChangedEventArgs args)
{
var fe = d as FrameworkElement;
if (fe == null) return;
var ui = d as UIElement;
var newValue = args.NewValue as MyStyleSetters;
if (newValue != null)
{
foreach (var member in newValue)
{
var setter = member as Setter;
if(setter != null)
{
fe.SetValue(setter.Property, setter.Value);
continue;
}
var eventSetter = member as EventSetter;
if (eventSetter == null) continue;
if (ui == null || eventSetter.Event == null) continue;
ui.AddHandler(eventSetter.Event, eventSetter.Handler);
}
}
}
public static void SetStyleSetters(DependencyObject element,
MyStyleSetters value)
{
element.SetValue(StyleSettersProperty, value);
}
public static MyStyleSetters GetStyleSetters (
DependencyObject element)
{
return (MyStyleSetters)element
.GetValue(StyleSettersProperty);
}
Error
System.Windows.Markup.XamlParseException occurred
_HResult=-2146233087
_message='Set property 'System.Windows.EventSetter.Event' threw an exception.' Line number '11' and line position '26'.
HResult=-2146233087
IsTransient=false
Message='Set property 'System.Windows.EventSetter.Event' threw an exception.' Line number '11' and line position '26'.
Source=PresentationFramework
LineNumber=11
LinePosition=26
StackTrace:
at System.Windows.Markup.XamlReader.RewrapException(Exception e, IXamlLineInfo lineInfo, Uri baseUri)
InnerException: System.ArgumentNullException
_HResult=-2147467261
_message=Value cannot be null.
HResult=-2147467261
IsTransient=false
Message=Value cannot be null.
Parameter name: value
Source=PresentationFramework
ParamName=value
StackTrace:
at System.Windows.EventSetter.set_Event(RoutedEvent value)
InnerException
调试
我在 System.Windows.EventSetter.Event 设置了一个函数断点,并执行了一个操作来记录传递给 setter 的值...
然后我运行应用程序并检查输出窗口,可以看到设置器被击中两次,第一次使用正确的值,第二次使用值 null...
工作示例可以在解决方案中找到GITHub 仓库在名为 EventSetterNull-SO-41604891-2670182 的项目中
baml
通过在 XamlNodeList 的 Index 成员中设置 BP,我可以捕获与 SetterBaseCollection xaml 对象关联的 xaml 符号...
XamlNode [0] "None: LineInfo: System.Xaml.LineInfo"
XamlNode [1] "StartObject: SetterBaseCollection"
XamlNode [2] "StartMember: _Items"
XamlNode [3] "None: LineInfo: System.Xaml.LineInfo"
XamlNode [4] "StartObject: Setter"
XamlNode [5] "StartMember: Property"
XamlNode [6] "Value: Height"
XamlNode [7] "EndMember: "
XamlNode [8] "StartMember: Value"
XamlNode [9] "Value: 30"
XamlNode [10] "EndMember: "
XamlNode [11] "None: LineInfo: System.Xaml.LineInfo"
XamlNode [12] "EndObject: "
XamlNode [13] "None: LineInfo: System.Xaml.LineInfo"
XamlNode [14] "None: LineInfo: System.Xaml.LineInfo"
XamlNode [15] "StartObject: EventSetter"
XamlNode [16] "StartMember: Event"
XamlNode [17] "Value: System.Windows.Baml2006.TypeConverterMarkupExtension"
XamlNode [18] "EndMember: "
-->EventSetter value: {System.Windows.RoutedEvent}
XamlNode [19] "None: LineInfo: System.Xaml.LineInfo"
XamlNode [20] "StartMember: Event"
XamlNode [21] {System.NullReferenceException: Object reference not set to an instance of an object.
at System.Xaml.XamlNode.ToString() in ...\AppData\Local\JetBrains\Shared\v06\DecompilerCache\...\XamlNode.cs:line 159
at <>x.<>m0(XamlNode& <>4__this)}
XamlNode [22] "EndMember: "
-->EventSetter value: null
!!!Then the null reference error throws
XamlNode [23] = "StartMember: Handler"
XamlNode [24] = "Value: StyleClick"
XamlNode [25] = "EndMember: "
XamlNode [26] = "None: LineInfo: System.Xaml.LineInfo"
XamlNode [27] = "EndObject: "
XamlNode [28] = "EndMember: "
XamlNode [29] = "EndObject: "
XamlNode [30] = "None: "
The remaining of the 41 nodes are all "None: "
The Bug?
baml nodeList看起来很奇怪,首先有一个从idx [20]开始的额外Event成员,而这个成员实际上是一个System.NullReferenceException。
它被传递到 XamlObjectWriter,而 XamlObjectWriter 又被传递到 EventSetter 属性,这就是错误的原因。
然后 baml 按预期继续运行,显示处理程序成员并正确终止成员和对象。
结论
问题出在从 xaml 到 baml 的转换中,所以我认为这是一个错误。尽管是可以避免的边缘情况。
解决方法
不要尝试在样式中设置事件,而是使用父对象中的附加属性。例如,StackPanel 中的 ButtonBase.Click="StyleClick" 会将行为传递给所有可点击的内容,这正是我最初想要做的。属性设置器的集合仍然可以在静态资源中设置并由附加的基于属性的行为使用。
进一步研究根本原因
问题是事件属性有两个元素:事件和处理程序。当。。。的时候Baml2006读者解析 baml 中的对象,需要考虑其结构以确保其处于正确的状态以忠实地解释对象成员。为此,它有一个状态机,由 while 循环驱动读取对象,称为Process_OneBamlRecord。此方法解码下一个 xamlNodeType 并调用适当的方法来解析它并将其写入为对象。其中一种方法称为进程属性并且它具有硬连线的特殊逻辑来处理 baml 中的事件复合体。
问题是,如果事件在 baml 中记录为Process_PropertyWithConverter,这种方法不知道事件的特殊要求,并且把所有东西都塞满了。事件处理程序以属性标记为前缀(很可能事件解析器打算递归并为此子结构使用相同的语法),并且因为没有 EndMember、StartMember 状态更改,所以处理程序子属性由ReadObject 作为事件属性。正在创建的事件设置器对象会抛出错误,因为它的 Event 属性已经设置。