WPF baml bug: EventSetter in static resource being set twice, second time to null

637 Views Asked by At

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...

enter image description here

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...

enter image description here

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.

0

There are 0 best solutions below