Débogage WPF/Silverlight : Déterminer l’élément ayant le focus clavier
Lors du développement d’applications complexes/composites en WPF ou Silverlight, il peut arriver de recontrer le problème du “mais où est-ce que je tape?!”. Ce post vous présente une manière de déboguer ce cas de figure.
Silverlight
Au sein d’une application Silverlight, déterminer quel control dispose du focus clavier revient à utiliser la propriété FocusManager.FocusedElement. Cette propriété n’est malheureusement pas bindable directement, et ne fournit pas de système de notification. Lors du débogage, j’utilise donc un timer pour obtenir régulièrement sa valeur, mettant à jour une propriété qui est bindée à un élement de l’interface utilisateur. Pour implémenter ce mécanisme, j’utilise le pattern de l’objet proxy comme suit :
<local:DebugFocusedElementProxy x:Name="debugFocusedElement"/>
<TextBlock TextWrapping="Wrap" Text="{Binding FocusedElementDescription,ElementName=debugFocusedElement}"/>
L’implémentation du proxy est la suivante
private string _FocusedElementDescription;
public string FocusedElementDescription
{
get { return _FocusedElementDescription; }
set
{
_FocusedElementDescription = value;
OnNotifyPropertyChanged("FocusedElementDescription");
}
}
public DebugFocusedElementProxy()
{
var timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromMilliseconds(100);
timer.Tick += (o, ea) =>
{
var fe = FocusManager.GetFocusedElement();
if (fe == null)
this.FocusedElementDescription = "None";
else
{
var element = fe as FrameworkElement;
if (string.IsNullOrEmpty(element.Name))
this.FocusedElementDescription = fe.GetType().Name;
else
this.FocusedElementDescription = element.Name;
}
};
timer.Start();
}
WPF
Le système de focus de WPF est plus complexe que celui de Silverlight, notamment dû au fait que WPF supporte les portées de focus (focus scopes). Ces portées multiples permettent de faciliter les scénarios comme les menus ou les barres d’outil, où le focus peut être momentanément perdu, puis re-récupéré par élément (mécanisme “push/pop”). En terminologie WPF, on dit qu’un objet perd le focus clavier, tout en préservant le focus logique dans sa portée. Pour cette raison, le FocusManager WPF requiert qu’on lui fournisse une portée pour déterminer l’élément ayant le focus.
Fort heureusement Windows, tout comme les OS les plus répandus, ne permet de donner le focus clavier qu’à un seul élément à la fois. La propriété Keyboard.FocusedElement de WPF (qui n’existe pas en Silverlight, mais dont l’équivalent est expliqué dans la section précédente) de permet ainsi d’obtenir l’élément ayant le focus clavier. L’implémentation présentée ici est similaire à celle de Silverlight.
<local:DebugFocusedElementProxy x:Name="debugFocusedElement"/>
<TextBlock TextWrapping="Wrap" Text="{Binding FocusedElementDescription, ElementName=debugFocusedElement}"/>
<Rectangle Height="{Binding ActualHeight, ElementName=debugFocusedElement}"
Width="{Binding ActualWidth, ElementName=debugFocusedElement}" MaxHeight="120">
<Rectangle.Fill>
<VisualBrush Visual="{Binding FocusedElement, ElementName=debugFocusedElement}" Stretch="Uniform"/>
</Rectangle.Fill>
</Rectangle>
Une évolution rendue possible par le VisualBrush WPF permet très facilement d’afficher, en plus, le rendu visuel du control ayant le focus clavier.
Une autre manière de procéder, ne nécessitant pas de timer, consiste à se binder à la propriété attachée FocusManager.FocusedElement. Cette propriété de part sa nature attachée, est notifiante et met à jour ses consommateurs automatiquement. Un exemple de binding est le suivant :
<TextBlock Text="{Binding (FocusManager.FocusedElement), Converter={StaticResource GetTypeConv}, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"/>
L’idée est simplement de remonter l’arbre visuel jusqu’à rencontrer un control disposant d’un focus scope (ex: Window), et de se binder à sa propriété FocusManager.FocusedElement. Notons que cette technique ne fonctionne pas toujours lorsque de multiples portées sont présentes simultanément.
Projet exemple
Comme à l’habitude, je joins à ce post un exemple et pour les plus curieux, essayez de l’ouvrir dans Expression Blend !
Merci à mon ami et collègue João de m’avoir motivé à publier ce post !