(correction : quel thread créé effectivement l'élément, 2ème workaround)

Cette exception est déroutante à plusieurs titres: le code en cause semble parfois marcher, le ou les storyboard(s) référençant le nom sont valides, le nom de l'élément existe et est déclaré avant sa référence dans le fichier XAML. Ce post décrit une des causes fréquentes de cette exception.

Ci-dessous du code démontrant le problème (disponible dans le projet accompagnant cet article)

<EventTrigger RoutedEvent="ButtonBase.Click">

    <EventTrigger.Actions>

        <BeginStoryboard>

            <Storyboard>

                <!-- La référence à myRectangle peut échouer! -->

                <ColorAnimation Storyboard.TargetName="myRectangle"

                                Storyboard.TargetProperty="Fill.Color"

                                To="Blue" AutoReverse="True"/>

            </Storyboard>

        </BeginStoryboard>

    </EventTrigger.Actions>

</EventTrigger>

et

Button btn = new Button();

btn.Style = this.FindResource("myFailingStyle") as Style;

gbxNewParent.Content = btn;

btn.RaiseEvent(new RoutedEventArgs(ButtonBase.ClickEvent)); // Crash

Au premier abord, tout devrait marcher :
- le bouton est créé et ajouté au GroupBox
- on y déclenche l'évènement Click
- qui déclenche à son tour l'EventTrigger qui animera l'élément myRectangle du ControlTemplate du bouton

Cette exécution provoque cependant une InvalidOperationException. La raison? Les threads du système de rendu de WPF. En effet - et contrairement à Windows Forms - ce n'est pas parceque la main nous est rendue après gbxNewParent.Content = btn, que les éléments visuels qui composent btn ont été créés. En effet, et comme l'explique très bien Nick Kramer, le thread UI pose des messages qui seront traités de manière asynchrone plus tard par le thread UI (lors de la passe de layout). Dans notre cas l'élément myRectangle du ControlTemplate n'a pas été créé, et une référence à celui-ci lève logiquement une exception. (Notons qu'il n'y a pas d'erreur lorsque le bouton change de parent car il n'y pas dans ce cas une re-création complète)

20081201 WPF Render thread

Il n'y a pour le moment (malheureusement) pas de moyen de connaître exactement le moment auquel un élément a été créé par le thread rendering. La solution que j'utilise est donc simple : différer l'appel de méthode déclenchant le référencement risqué au Dispatcher, et ce avec une faible priorité.

this.Dispatcher.BeginInvoke( 
    new Action(delegate { btn.RaiseEvent(new RoutedEventArgs(ButtonBase.ClickEvent)); }), 
    System.Windows.Threading.DispatcherPriority.ContextIdle);


Nick propose également une autre manière de procéder (merci à lui!), en appelant explicitement UpdateLayout() avant de déclencher l'évènement. Attention cependant aux performances car UpdateLayout() est une opération potentiellement gourmande.

btn.Style = this.FindResource("myFailingStyle") as Style;

gbxNewParent.Content = btn;

UpdateLayout();

btn.RaiseEvent(new RoutedEventArgs(ButtonBase.ClickEvent)); // No crash