If I try to store a collection of SetterBase objects in xaml, that includes and EventSetter, The xaml loader throws an error.
The root cause is that the xaml loader tries to set PresentationFramework.dll!System.Windows.EventSetters.Event twice: the first time to the correct value (ButtonBase.Click RoutedEvent) but the second time to null, and this throws an exception. My attached property callback is not involved.
Why does it try to add the event to the EventSetter twice and why is it null the second time? I checked that the ctor being used is the default one so, EventSeetter is not interacting with the collection in any unusual way, so that's not it. The actual reason is a bug in wpf that fluffs the challenge of parsing the two-part structure of an event (Event and 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}" />
The code behind is only InitializeComponent and a stub for the event handler. The error occurs during InitializeComponent.
Behaviour
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
Debugging
I set a function breakpoint at System.Windows.EventSetter.Event with an action to log the value passed to the setter...
Then I run the app and check the output window and can see that the setter was hit twice, first time with the correct value, second time with value null...
The working example can be found in the solution in this GITHub Repo in the project called EventSetterNull-SO-41604891-2670182
baml
By setting a BP in the Index member of XamlNodeList I could catch the xaml symbols associated with the SetterBaseCollection xaml object...
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?
The baml nodeList looks strange, first of all there is an extra Event member starting at idx [20] and this member is actually a System.NullReferenceException.
This is being passed to the XamlObjectWriter which is in turn passed to the EventSetter property and this is the cause of the error.
The baml then carries on as expected, showing the handler member and properly terminating the members and objects.
Conclusion
The problem is in the conversion from xaml to baml so I would say it's a bug. Albeit an avoidable edge case.
Work-around
Instead of trying to set the event in the style, use an attached property in a parent object. For example ButtonBase.Click="StyleClick" in a StackPanel will deliver the behaviour to everything clicky which is what I was originally trying to do. Collections of Property Setters can still be set in a static resource and consumed by attached property-based behaviours.
Further research on root cause
The problem is that an event property has two elements: the event and the handler. When the Baml2006Reader parses an object in the baml, it needs to allow for it's structure to ensure it is in the correct state to faithfully interpret the object members. To do this, it has a state machine, driven from a while loop in ReadObject, called Process_OneBamlRecord. This method decodes the next xamlNodeType and calls the appropriate method to parse it and write it as an object. One of these methods is called Process_Property and it has special logic hard-wired into it to handle the event complex in the baml.
The problem is that, if the event is recorded in the baml as a Process_PropertyWithConverter, this method is not aware of the special requirements for an event and stuffs everything up. The event handler is prefixed with a property tag (most likely the event parser was meant to recurse and use the same syntax for this sub-structure) and because there has been no EndMember, StartMember state change, the handler sub-property is interpreted by ReadObject as an Event property. And the event setter object that's being created throws an error because it's Event property is already set.
var ui = d as UIElement;
ui isnt null and loaded? Sounds like yourUIElement
hasnt loaded at the point you try to work with it – LaresaPresentationFramework.dll!System.Windows.Markup.WpfXamlLoader.TransformNodes
later and skipsSystem.Xaml.dll!System.Xaml.XamlObjectWriter.Logic_AssignProvidedValue
. Apart from that, the code paths are identical. – ShavonneshawEventSetter
which is always null not just the second time. I dont know why this is happening but as it seems to me, theEventSetter
does only work on typed elements such as<Style>
– Laresa