如果我尝试在 xaml 中存储 SetterBase 对象的集合,其中包括 EventSetter,则 xaml 加载程序会引发错误。
根本原因是 xaml 加载器尝试设置 PresentationFramework.dll!System.Windows.EventSetters.Event 两次:第一次设置为正确的值 (ButtonBase.Click RoutedEvent),但第二次设置为 null,这会引发异常。不涉及我的附加属性回调。
为什么它尝试将事件添加到 EventSetter 两次,为什么它第二次为空?我检查了正在使用的 ctor 是默认的,所以 EventSeetter 没有以任何不寻常的方式与集合交互,所以不是这样。实际原因是 wpf 中的一个错误,它使解析事件的两部分结构(Event 和 EventHandler)的挑战变得模糊。
看法
<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);
}
错误
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 的值...
然后我运行应用程序并检查输出窗口,可以看到 setter 被击中了两次,第一次使用正确的值,第二次使用 null ...
工作示例可以在名为 EventSetterNull-SO-41604891-2670182 的项目中的这个GITHub Repo的解决方案中找到
巴姆
通过在 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: "
错误?
baml nodeList 看起来很奇怪,首先从 idx[20] 开始多了一个 Event 成员,而这个成员实际上是一个 System.NullReferenceException。
这被传递给 XamlObjectWriter,而 XamlObjectWriter 又被传递给 EventSetter 属性,这就是错误的原因。
baml 然后按预期进行,显示处理程序成员并正确终止成员和对象。
结论
问题在于从 xaml 到 baml 的转换,所以我会说这是一个错误。尽管这是一个可以避免的边缘情况。
解决方法
与其尝试在样式中设置事件,不如在父对象中使用附加属性。例如,StackPanel 中的 ButtonBase.Click="StyleClick" 会将行为传递给我最初尝试做的所有 clicky。属性设置器的集合仍然可以在静态资源中设置并由附加的基于属性的行为使用。
进一步研究根本原因
问题是事件属性有两个元素:事件和处理程序。当Baml2006Reader解析 baml 中的对象时,它需要允许它的结构以确保它处于正确的状态以忠实地解释对象成员。为此,它有一个状态机,由ReadObject中的 while 循环驱动,称为Process_OneBamlRecord。此方法解码下一个 xamlNodeType 并调用适当的方法对其进行解析并将其写入为对象。其中一种方法称为Process_Property,它具有硬连接到其中的特殊逻辑来处理 baml 中的事件复合体。
问题是,如果事件作为 Process_PropertyWithConverter 记录在 baml 中,则此方法不知道事件的特殊要求并将所有内容都塞满。事件处理程序以属性标记为前缀(很可能事件解析器是为了递归并为此子结构使用相同的语法),并且由于没有 EndMember、StartMember 状态更改,处理程序子属性由ReadObject 作为事件属性。并且正在创建的事件设置器对象会引发错误,因为它的 Event 属性已设置。