WPF baml bug: EventSetter в статическом ресурсе устанавливается дважды, второй раз в ноль
Если я пытаюсь сохранить коллекцию объектов SetterBase в xaml, в том числе и EventSetter, загрузчик xaml выдает ошибку.
Основная причина в том, что загрузчик xaml пытается установить PresentationFramework.dll!System.Windows.EventSetters.Event дважды: первый раз на правильное значение (ButtonBase.Click RoutedEvent), но второй раз на ноль, и это вызывает исключение. Мой прикрепленный обратный вызов свойства не задействован.
Почему он пытается добавить событие в 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 с помощью действия, чтобы записать значение, переданное установщику...
Затем я запускаю приложение, проверяю окно вывода и вижу, что установщик получил два удара: первый раз с правильным значением, второй раз со значением null...
Рабочий пример можно найти в решении этого репозитория GITHub в проекте под названием EventSetterNull-SO-41604891-2670182.
BAML
Установив BP в элементе Index в XamlNodeList, я мог бы поймать символы xaml, связанные с объектом SetterBaseCollection 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 выглядит странно, во-первых, есть дополнительный член Event, начинающийся с idx [20], и этот член на самом деле является System.NullReferenceException.
Это передается XamlObjectWriter, который, в свою очередь, передается свойству EventSetter, и это является причиной ошибки.
Затем baml продолжает работу, как и ожидалось, показывая элемент-обработчик и правильно завершая элементы и объекты.
Заключение
Проблема в преобразовании из xaml в baml, поэтому я бы сказал, что это ошибка. Хотя это крайний случай, которого можно избежать.
Работа вокруг
Вместо того чтобы пытаться установить событие в стиле, используйте вложенное свойство в родительском объекте. Например, ButtonBase.Click="StyleClick" в StackPanel доставит поведение всем щелчкам, что я и пытался сделать изначально. Коллекции сеттеров свойств по-прежнему можно задавать в статическом ресурсе и использовать в привязанном поведении на основе свойств.
Дальнейшие исследования первопричины
Проблема в том, что свойство события имеет два элемента: событие и обработчик. Когда Baml2006Reader анализирует объект в baml, ему необходимо разрешить его структуру, чтобы гарантировать, что он находится в правильном состоянии, для точной интерпретации членов объекта. Для этого у него есть конечный автомат, управляемый из цикла while в ReadObject, который называется Process_OneBamlRecord. Этот метод декодирует следующий xamlNodeType и вызывает соответствующий метод, чтобы проанализировать его и записать как объект. Один из этих методов называется Process_Property, и в него встроена специальная логика для обработки комплекса событий в baml.
Проблема в том, что, если событие записывается в baml как Process_PropertyWithConverter, этот метод не знает о специальных требованиях для события и заполняет все. Обработчик события имеет префикс с тегом свойства (наиболее вероятно, что анализатор событий предназначался для повторения и использования одного и того же синтаксиса для этой подструктуры), и поскольку не было никакого изменения состояния EndMember, StartMember, подсвойство обработчика интерпретируется как ReadObject как свойство события. И созданный объект установки событий выдает ошибку, потому что его свойство Event уже установлено.