Baml bug di WPF: EventSetter nella risorsa statica impostata due volte, seconda volta su null



visual-studio-2013 staticresource (0)

Se provo a memorizzare una raccolta di oggetti SetterBase in xaml, che include e EventSetter, il loader xaml genera un errore.

La causa principale è che il loader xaml tenta di impostare PresentationFramework.dll! System.Windows.EventSetters.Event due volte: la prima volta sul valore corretto (ButtonBase.Click RoutedEvent) ma la seconda volta su null, e questa genera un'eccezione. La mia callback proprietà allegata non è coinvolta.

Perché tenta di aggiungere l'evento a EventSetter due volte e perché è nullo la seconda volta? Ho verificato che il Ctor utilizzato sia quello predefinito, quindi EventSeetter non interagisce con la raccolta in alcun modo insolito, quindi non è così. Il vero motivo è un bug in wpf che lancia la sfida di analizzare la struttura in due parti di un evento (Event ed EventHandler).

vista

<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}" />

Il codice sottostante è solo InitializeComponent e uno stub per il gestore eventi. L'errore si verifica durante InitializeComponent.

Comportamento

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);
}

Errore

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

Debug

Ho impostato un breakpoint di funzione su System.Windows.EventSetter.Event con un'azione per registrare il valore passato al setter ...

Quindi eseguo l'app e controllo la finestra di output e posso vedere che il setter è stato colpito due volte, la prima volta con il valore corretto, la seconda volta con il valore null ...

L'esempio di lavoro può essere trovato nella soluzione in questo GITHub Repo nel progetto chiamato EventSetterNull-SO-41604891-2670182

BAML

Impostando un BP nel membro Index di XamlNodeList, è possibile rilevare i simboli xaml associati all'oggetto xaml SetterBaseCollection ...

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: "

Il bug?

Baml nodeList sembra strano, prima di tutto c'è un membro evento extra che inizia con idx [20] e questo membro è in realtà un System.NullReferenceException.
Questo viene passato a XamlObjectWriter, che a sua volta viene passato alla proprietà EventSetter e questa è la causa dell'errore.
Il baml procede quindi come previsto, mostrando il membro del conduttore e terminando correttamente i membri e gli oggetti.

Conclusione

Il problema è nella conversione da xaml a baml quindi direi che è un bug. Anche se un caso limite evitabile.

Work-around

Invece di provare a impostare l'evento nello stile, utilizzare una proprietà associata in un oggetto padre. Ad esempio ButtonBase.Click = "StyleClick" in uno StackPanel fornirà il comportamento a tutto ciò che è clicky, che è ciò che cercavo in origine di fare. Le raccolte di Coloni di proprietà possono ancora essere impostate in una risorsa statica e consumate da comportamenti basati su proprietà collegate.

Ulteriori ricerche sulla causa principale

Il problema è che una proprietà evento ha due elementi: l'evento e il gestore. Quando Baml2006Reader analizza un oggetto in baml, deve consentire che la sua struttura garantisca che sia nello stato corretto per interpretare fedelmente i membri dell'oggetto. Per fare ciò, ha una macchina a stati, guidata da un ciclo while in ReadObject , chiamata Process_OneBamlRecord . Questo metodo decodifica il prossimo xamlNodeType e chiama il metodo appropriato per analizzarlo e scriverlo come oggetto. Uno di questi metodi è chiamato Process_Property e dispone di una speciale logica cablata per gestire il complesso di eventi in baml.

Il problema è che, se l'evento è registrato in baml come Process_PropertyWithConverter , questo metodo non è a conoscenza dei requisiti speciali per un evento e riempie tutto. Il gestore di eventi ha come prefisso un tag di proprietà (molto probabilmente il parser di eventi intendeva recurse e utilizza la stessa sintassi per questa sottostruttura) e poiché non c'è stata alcuna modifica di stato di EndMember, StartMember, la proprietà secondaria del gestore è interpretata da ReadObject come proprietà Event. E l'oggetto event setter che viene creato genera un errore perché la proprietà Event è già impostata.