WPF “Adorner(s)” rulez: #1
- Supporto per la navigazione/gestione comandi con la tastiera;
- Supporto per la gestione dei comandi tramite shortcut;
- Comunicazione con l’utente;
- Async, Async, Async (more to come…);
- Disposizione, aspetto e layout dei controlli consistente con quello del sistema;
- etc..
Un esempio su tutti:
Adesso che lo switch tecnologico verso WPF è ormai definitivo mi sono posto lo stesso problema. La soluzione P/Invoke è comunque utilizzabile anche se con qualche “giro” in più, ma WPF offre una soluzione decisamente più figosa.
Adorner e AdornerLayer
Rimando alla spiegazione ufficiale per i dettagli: Adorners Overview
CueBanner Adorner
Partiamo dal risultato:
Nulla di speciale, semplicemente abbiamo quel testo, a mo da hint per l’utente, che nel momento in cui il focus entra nella TextBox scompare e quando la TextBox perde il focus, se è ancora vuota, ricompare. Il tutto è un ottimo supporto per l’utente durante l’uso dell’applicazione. Garantito.
Abbiamo già parlato di attached properties e scopriamo che anche in questo caso sono l’entry point per la soluzione al nostro problema:
Sfortunatamente, si fa per dire, in questo caso sono solo l’entry point, quello di cui abbiamo bisogno è poter disegnare in overlay su un controllo del testo (e volendo qualsiasi cosa ci venga mente… questo è WPF!), la soluzione sono gli Adorner: un adorner è una “sorta” di layer (come se fosse un foglio di acetato) che viene renderizzato sopra l’elemento a cui è associato, l’AdornedElement. Un adorner per poter essere visibile ha bisogno di un AdornerLayer in cui essere “ospitato”, ogni Window (non proprio…, ho un caso che sto cercando di debuggare in cui questo non succede…) ha implicitamente un AdornerLayer anche se non lo inseriamo noi nello xaml. Ogni Window può contenere n AdornerLayer. L’inghippo fastidioso è che non è possibile dichiarare una Adorner via xaml perchè l’unica possibilità è scrivere questo:<TextBox local:CueBannerService.CueBanner="Questo è il testo del CueBanner" ... />
Ecco perchè una attached property ci viene in aiuto permettendoci di continuare ad utilizzare la potenza espressiva di xaml senza scrivere una “,” di codice nel code-behind, vediamo come:AdornerLayer layer = AdornerLayer.GetAdornerLayer( control ); layer.Add( ... );
Partiamo con la solita definizione della proprietà, tralascio la parte Get/Set perchè triviale, e passo direttamente all’handler agganciato all’evento che ci notifica della variazione del valore della proprietà:public static readonly DependencyProperty CueBannerProperty = DependencyProperty.RegisterAttached( "CueBanner", typeof( String ), typeof( CueBannerService ), new FrameworkPropertyMetadata( String.Empty, OnCueBannerPropertyChanged ) );
Cominciamo con il prepararci, a monte nel costruttore statico, gli handler che ci serviranno.static RoutedEventHandler onShowCueBanner; static RoutedEventHandler onHideCueBanner; static RoutedEventHandler onUnloaded; static CueBannerService() { onShowCueBanner = ( s, e ) => { var control = ( Control )s; if( ShouldShowCueBanner( control ) ) { ShowCueBanner( control ); } }; onHideCueBanner = ( s, e ) => { var c = ( Control )s; if( ShouldShowCueBanner( c ) ) { RemoveCueBanner( c ); } }; onUnloaded = ( s, e ) => { var control = ( Control )s; control.Loaded -= onShowCueBanner; control.LostFocus -= onShowCueBanner; control.GotFocus -= onHideCueBanner; }; }
Che cosa facciamo, non molto a dire il vero:static void OnCueBannerPropertyChanged( DependencyObject sender, DependencyPropertyChangedEventArgs e ) { Ensure.That( sender.GetType() ) .IsTrue( t => t.Is<ComboBox>() || t.Is<TextBox>() ); var control = ( Control )sender; control.Loaded += onShowCueBanner;
control.Unloaded += onUnloaded; control.LostFocus += onShowCueBanner; control.GotFocus += onHideCueBanner; }
- abbiamo bisogno di sapere quando il controllo viene caricato/scaricato: Loaded e Unloaded;
- Ci interessano poi gli eventi GotFocus e LostFocus per sapere quando nascondere e visualizzare il CueBanner;
e nel caso semplicemente visualizzarlo o nasconderlo:static Boolean ShouldShowCueBanner( Control c ) { var dp = GetTextProperty( c ); var value = c.GetValue( dp ); return ( value is String && String.IsNullOrEmpty( ( String )value ) ); } static DependencyProperty GetTextProperty( Control control ) { if( control is ComboBox ) { return ComboBox.TextProperty; } else if( control is TextBoxBase ) { return TextBox.TextProperty; } else { throw new NotSupportedException(); } }
Il CueBannerAdorner è l’elemento responsabile di renderizzare il vero e proprio banner:static void RemoveCueBanner( Control control ) { AdornerLayer layer = AdornerLayer.GetAdornerLayer( control ); Debug.WriteIf( layer == null, "CueBannerService: cannot find any AdornerLayer" ); if( layer != null ) { var adorners = layer.GetAdorners( control ); if( adorners != null ) { adorners.OfType<CueBannerAdorner>() .ForEach( adorner => { adorner.Visibility = Visibility.Hidden; layer.Remove( adorner ); } ); } } } static void ShowCueBanner( Control control ) { var layer = AdornerLayer.GetAdornerLayer( control ); Debug.WriteIf( layer == null, "CueBannerService: cannot find any AdornerLayer" ); if( layer != null ) { layer.Add( new CueBannerAdorner( control, control.GetValue( CueBannerProperty ) ) ); } }
Per prima cosa rendiamo l’adorner invisibile all’HitTest in questo modo il click del mouse non verrà intercettato dai controlli presenti nell’adorner ma solo da quelli che stanno sotto; poi creiamo il contenuto vero e proprio facendo una sola distinzione: se è testo lo wrappiamo noi in un TextBlock che abbia un aspetto decente altrimenti ci limitiamo a prendere quello che arriva…e questo apre a scenari interessanti:sealed class CueBannerAdorner : OverlayAdorner { readonly ContentPresenter userContent; public CueBannerAdorner( UIElement adornedElement, Object content ) : base( adornedElement ) { this.IsHitTestVisible = false; this.userContent = new ContentPresenter(); var cueBannerText = content as String; if( cueBannerText != null ) { this.userContent.Content = new TextBlock() { FontStyle = FontStyles.Italic, Text = cueBannerText, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness( 4, 0, 0, 0 ), TextTrimming = TextTrimming.CharacterEllipsis }; } else { this.userContent.Content = content; } this.userContent.Opacity = 0.7; } protected override UIElement Content { get { return this.userContent; } } }
Che produce questo:<TextBox> <local:CueBannerService.CueBanner> <StackPanel Orientation="Horizontal"> <Image Margin="2,0,0,0" Source="/Samples;component/BalloonTip.gif" Stretch="None" /> <TextBlock Text="Try setting focus here..." FontStyle="Italic" VerticalAlignment="Center" Margin="2,0,0,0" /> StackPanel> local:CueBannerService.CueBanner> TextBox>
Io adoro WPF!
Manca solo un dettaglio: OverlayAdorner, la classe base da cui deriviamo il nostro adorner per il CueBanner:
Facciamo un paio di cose interessanti:abstract class OverlayAdorner : Adorner { public OverlayAdorner( UIElement adornedElement ) : base( adornedElement ) { } protected abstract UIElement Content { get; } protected override Visual GetVisualChild( int index ) { return this.Content; } protected override int VisualChildrenCount { get { return 1; } } protected override Size MeasureOverride( Size constraint ) { this.Content.Measure( this.AdornedElement.RenderSize ); return this.AdornedElement.RenderSize; } protected override Size ArrangeOverride( Size finalSize ) { this.Content.Arrange( new Rect( finalSize ) ); return finalSize; } }
- L’override di VisualChildrenCount informando il sistema che abbiamo 1 child, e l’override di GetVisualChild ritornando il nostro content;
- Facciamo poi l’override di MeasureOverride e ArrangeOverride perchè ci interessa partecipare nel processo di Rendering al fine di pemettere al nostro contenuto di disegnarsi correttamente, in questo modo supportiamo anche il resize del controllo su cui siamo “adorned”;
.m