Instanciations multiples de contrôles dans Silverlight
Je vais exposer ci-après une situation pour laquelle je requière les lumières de quiconque voudra bien y 'perdre' un peu de temps.
Il me semble que la problèmatique exposée ci-après relève d'un niveau expert. Je m'efforce de l'exposer le plus clairement possible en espérant que cela ne sera pas trop confus.
L'exemple présenté ci après n'est pas tiré d'une situation réelle, mais à simplement été construit pour mettre en lumière un point qui me semble très étrange, et qui me pose problème. Je me dis que peut être la solution est évidente mais que je tourne trop en rond depuis un moment pour la voir.
Voici le XAML d'une page suivi de son code behind :
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
<UserControl x:Class="SilverlightApplication1.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="400" Height="300">
<Grid x:Name="LayoutRoot" Background="White">
<StackPanel>
<StackPanel x:Name="pnlTest">
</StackPanel>
<Button Click="Button_Click" Content="Test" />
</StackPanel>
</Grid>
</UserControl>
|
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
namespace SilverlightApplication1
{
publicpartialclassPage : UserControl
{
public Page()
{
InitializeComponent();
}
privatevoid Button_Click(object sender, RoutedEventArgs e)
{
//---> On vide le stackpanel. En effet, si ce n'est pas le
// premier click sur le bouton, le stackpanel contient d‚j…
// un UserControlTest
pnlTest.Children.Clear();
//---> Instanciation d'un UserControlTest
var uc = newUserControlTest();
//---> On le place dans le stackpanel
pnlTest.Children.Add(uc);
}
}
}
|
Voici le XAML du UserControl utilisé dans la page, suivi de son code behind :
Code:
1 2 3 4 5 6 7 8 9 10
|
<UserControl x:Class="SilverlightApplication1.UserControlTest"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="400" Height="300">
<Grid x:Name="LayoutRoot" Background="White">
<TextBlock Text="Text de test" />
</Grid>
</UserControl>
|
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
namespace SilverlightApplication1
{
publicpartialclassUserControlTest : UserControl
{
//---> Ce champ static sert juste de compteur afin
// d'identifier les diff‚rentes instance de UserControlTest
// En effet, a la cr‚ation, le num‚ro d'instance sera plac‚ dans
// le DataContext
privatestaticint Nb = 0;
public UserControlTest()
{
InitializeComponent();
//---> On affecte le num‚ro d'instance
this.DataContext = Nb;
Nb++;
//---> On s'abonne … l'‚vŠnement LayoutUpdated
this.LayoutUpdated += newEventHandler(UserControlTest_LayoutUpdated);
}
void UserControlTest_LayoutUpdated(object sender, EventArgs e)
{
//---> Placer un point d'arret ici, afin de suivre les appels … cet ‚vŠnement
//---> Ajouter un espion sur this.DataContext afin de suivre quelle instance est en cours
}
}
} |
Maintenant, passons aux explications :
Le code ci-dessus parrait on ne peut plus simple. Une Page avec un Button. On Click sur le Button et une instance d'un UserControlTest est crée puis placée dans un StackPanel.
Ce Click peut être effectuée autant de fois que voulu. C'est pour cette raison que le StackPanel est préalablement vidé via Clear() afin de garantir qu'il ne contienne qu'une seule instance de UserControlTest à la fois.
Après plusieurs Click sur le Button, on s'attend à ce qu'une seule instance du UserControlTest soit présente dans le StackPanel et participe à la 'vie' de la Page. En testant, c'est bel et bien ce qui se passe sur le plan visuel.
Et bien en fait, le code ci-dessus montre que ce n'est pas vraiment le cas. Pour vous aider à identifier les instances de UserControlTest en cause, un Id est placé dans le DataContext. Ainsi, via un simple espion dans le debogueur il est aisé d'identifier en permanence sur quelle instance le code s'applique.
Vous ne serez pas surpris de constater que même après le Clear(), les instances précédentes de UserControlTest ne sont pas dédruites mais qu'elles persistent toujours bel et bien en mémoire. En effet, le Clear() supprime le UserControlTest de la collection Children du StackPanel, le supprime donc à priori de l'arboressance visuel.
Pourtant, lorsque l'on suit les appels effectués à UserControlTest_LayoutUpdated, on constate que toutes les instances de UserControlTest (qu'elles soient encore dans le StackPanel ou non) continue de répondre à l'évènement LayoutUpdated et donc de participer à la vie de la Page.
Quand on y réfléchis, cela peut sembler logique. En effet, l'objet est abonné à l'évènement et toujours instancié. Mais comment se fait-il que un UpdateLayout ait-lieu sur un contrôle qui n'est plus dans l'arboressance visuelle ?
En tous les cas, il semblerait logique de pouvoir se désabonner à l'évènement. De plus, libérer l'instance serait utile. Mais, 1) Le destructeur n'est pas appelé 2) Même en implémentant IDisposable, la méthode Dipsose n'est pas appelée 3) Il n'y a pas d'évènement UnLoaded
Question : comment le contrôle peut-il se tenir informé de sa vie au sein de l'arboressance visuelle des contrôles ?
Problèmatique n°1
Je m'intérroge quand aux performances et à la mémoire. Après plusieurs centaines d'instanciations, le phénomène devient inquiétant. La moindre modification au niveau UI déclanche des centaines d'appels inutiles et suceptibles d'engendrer des bogues sur UpdatedLayout.
Problématique n°2
L'explication suivante semble faire apparaitre que ce problème dépasse le simple contexte lié aux évènements.
Imaginez mainteant que vous souhaitiez créer un contrôle du type PopupButton. C'est à dire un Button qui ouvre un Popup lors du click. Le contenu de ce Popup pouvant être défini selon les besoins. Par exemple :
Code:
1 2 3 4 5 6
|
<PopupButton Content="Cliquez ici">
<Button x:Name="btActionA" Content="Action A"/>
<Button x:Name="btActionB" Content="Action B"/>
<Button x:Name="btActionC" Content="Action C"/>
</PopupButton> |
Lorsque l'on Click sur le Button, on veut voir le Popup s'ouvrir et contenir les trois Button d'action précédents. Ces trois Button sont nommés car référencés dans le code (pour activation / désactivation en fonction du context par exemple). A vous d'imaginez ce qui vous voulez. Le principal ici c'est que 1) PoupButton est un ContentControl dont le Template est défini dans generic.xaml 2) Il contient des contrôles nommés.
On peut alors utiliser ce PopupButton dans des UserControl pour par exemple créer différents ecrans de saisie qui seront instanciés selon les besoins au cours de l'application. Ces différents UserControl pourront par exemple être instanciés exactement de la même façon que dans l'exemple de page ci-dessus, pour être ensuite placés par exemple dans un StackPanel.
Le problème, c'est que tout se passe bien lors de la première instanciation. Mais dès la seconde, une erreur se produit signalant que les noms utilisés sont déjà utilisés dans la page.
Je précise pour compliquer, que ce message ne survient que si les contrôles nommés sont placés dans un contrôl Popup. Si l'on remplace le Popup par un StackPanel, le phénomène des appels aux objets qui ne font plus parti de l'arboressance visuelle persiste, mais il n'y a pas de message d'erreur.
Merci d'avance d'avoir lu ce message jusqu'ici