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

0

1

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.

Shavonneshaw answered 12/1, 2017 at 3:59 Comment(8)
can you ensure that after this line var ui = d as UIElement; ui isnt null and loaded? Sounds like your UIElement hasnt loaded at the point you try to work with itLaresa
@lokusking, Sorry, I should get rid of that code from the question when I simplified the example, it's not involved.Shavonneshaw
Let me reformulate my comment. The issue you're describing seems to come from a racecondition when UIElements are first loaded from XAML bur behavior isnt. Also the line from my first comment still exists in your editLaresa
@lokusking, sorry about the confusion, there are a lot of permutations and I was trying to find the minimal one. Hopefully is clearer now. If there is a race then it is purely in the xaml loader. The code path to the Event setter only has one slight difference: the second time, it leaves PresentationFramework.dll!System.Windows.Markup.WpfXamlLoader.TransformNodes later and skips System.Xaml.dll!System.Xaml.XamlObjectWriter.Logic_AssignProvidedValue. Apart from that, the code paths are identical.Shavonneshaw
Iv'e tried some things based on your code. The issue is the EventSetter which is always null not just the second time. I dont know why this is happening but as it seems to me, the EventSetter does only work on typed elements such as <Style>Laresa
@lokusking, thanks for your interest, that's really strange, I posted some trace details to show that I am only getting a null value the second time. I posted a link to a github repo so you can try it yourself. Regarding the context, if you look at the constructors for EventSetters, there is nothing to suggest that it needs to have any special context to work and plain Setters work fine in the static resource so I can't understand why the EventSetter needs to be in a style.Shavonneshaw
For even more confusion, i removed the Handler from the EventSetter. Result is.. lets say interesting..Laresa
Maximum confusion is exceeded when you read the code for WpfXamlLoader and XamlObjectWriter... After trying to trace through this, I think that the problem is in the baml. The code just continues through its read loop on the baml. I don't have the tools to read it but I set a bp in XamlNodeList processor and I could print the lines of xaml symbols which I will add to the question.Shavonneshaw

© 2022 - 2024 — McMap. All rights reserved.