Loading… Adorner #2
In effetti se dato uno sguardo al task manager scoprite cose interessanti:
Un mondo multithreaded è sicuramente più difficile da dominare ma è anche vero che una segretaria incazz*ta è forse peggio di una suocera logorroica, inoltre se cerchiamo scopriamo che il framework .net ci mette a disposizion, da molte versioni, validi strumenti per domare la situazione. Uno su tutti il BackgroundWorker ma anche, e perchè no, strumenti di più basso livello come AsyncOperationManager.
Se vogliamo possiamo poi renderci la vita facile, e avere in mano qualcosa di fortemente tipizzato (il background worker conosce solo Object), possiamo wrappare il tutto e, sfruttando un po’ di fluent interface design, giungere a qualcosa del genere:
Cosa abbiamo:this.Browse = DelegateCommand.Create( "Cerca..." ) .OnCanExecute( o => { return !this.IsBusy && this.Query.AsKeywords( ';' ).Any(); } ) .OnExecute( o => { var keywords = this.Query.AsKeywords( ';' ); Worker.UsingAsArgument( keywords ) .AndExpectingAsResult<IEnumerable<ISubject>>() .WhenExecutedDo( arg => { var query = new SubjectsByKeywordsQuery( arg.Argument ); arg.Result = this.subjectsRepository.GetByQuery( query ); } ) .ButBeforeDo( arg => { this.IsBusy = true; this.SelectedItems.DataSource.Clear(); } ) .AndAfterDo( arg => { this.AvailableItems.DataSource .CastTo<IEntityCollection<ISubject>>() .BulkLoad( arg.Result ); this.IsBusy = false; } ) .Execute(); } );
- La definizione di un ICommand, e la gestione dei relativi delegate per la CanExecute e la Execute;
- La definizione di workflow asincrono per l’esecuzione della richiesta dell’utente, la cosa interessante qui è:
- ButBeforeDo() e AndAfterDo() sono eseguiti nel thread della UI, quindi niente plumbing code per la sincronizzazione;
- WhenExecutedDo() viene eseguito in un background thread;
- Un po’ di pattern vari applicati…
Grazie, “semplicemente” a questo:
Ci sono 2 cose che non compaiono sulla ListView normale:<ListView local:BusyStatusManager.Status="{Binding Path=IsBusy, Converter={StaticResource boolBusyStatusConverter}}"> <local:BusyStatusManager.Content> <dropShadow:SystemDropShadowChrome HorizontalAlignment="Center" VerticalAlignment="Center"> <Border BorderBrush="Black" Background="LightGray" BorderThickness="1" > <StackPanel Margin="5"> <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="Attendere..." FontStyle="Italic" /> <ProgressBar Margin="0,2,0,0" IsIndeterminate="True" HorizontalAlignment="Stretch" Height="5" /> StackPanel> Border> dropShadow:SystemDropShadowChrome> local:BusyStatusManager.Content> ListView>
- BusyStatusManager.Status: è un’enumerazione che può assumere i valori Idle o Busy ed è l’entry point del sistema;
- BusyStatusManager.Content: in perfetto stile WPF è di tipo System.Object ed è il vero e proprio contenuto che deve essere visualizzato durante l’operazione asincrona, la visualizzazione è triggherata dalla proprietà Status, come è facile immaginare quando è Idle non viene visualizzato nulla mentre è Busy viene visualizzato il contenuto della proprietà Content;
Inoltre sottolineo che quel blocco di xaml è applicabile a qualsiasi controllo, del resto dipende solo dal VM con cui è in binding e non dal controllo su cui è applicato. Il controllo è importante solo in termini di Rendering, per l’esattezza di Measure e Arrange.
Naturalmente il tutto per funzionare sfrutta un immancabile, e tra un po’ inflazionato, Adorner; ma iniziamo come al solito con il behavior, la classe statica che fa da ponte ed espone le attached properties:
Dichiariamo le attached property:public static readonly DependencyProperty ContentProperty = DependencyProperty.RegisterAttached( "Content", typeof( Object ), typeof( BusyStatusManager ), new FrameworkPropertyMetadata( null, OnPropertyChanged ) ); public static readonly DependencyProperty StatusProperty = DependencyProperty.RegisterAttached( "Status", typeof( BusyStatus ), typeof( BusyStatusManager ), new FrameworkPropertyMetadata( BusyStatus.Idle, OnPropertyChanged ) ); static readonly DependencyProperty handledProperty = DependencyProperty.RegisterAttached( "handled", typeof( Boolean ), typeof( BusyStatusManager ), new FrameworkPropertyMetadata( false ) );
- Content: semplicemente il contenuto da visualizzare;
- Status: l’enumerazione (Idle o Busy) che trigghera la visualizzazione di quello che c’è in Content, o se non c’è nulla si limita a disabilitare/abilitare il controllo;
- handled: una proprietà privata che ci serve per capire a che punto siamo: il problema è che abbiamo più di una attached property e la notifica della variazione del valore della proprietà ci arriva in 2 casi:
- Quando effettivamente il valore cambia;
- Quando il valore viene settato la prima volta durante l’initialize del controllo;
Quando il controllo termina la fase di inizializzazione impostiamo il nostro mondo, stessa cosa facciamo se il controllo è completamente caricato e cambia una delle 2 proprietà (in realtà solo una perchè per ora non mi interessa supportare la variazione di content a runtime):static void OnPropertyChanged( DependencyObject d, DependencyPropertyChangedEventArgs e ) { var control = ( FrameworkElement )d; if( !control.IsLoaded && !Gethandled( control ) ) { control.Loaded += ( s, rea ) => HandleStatusChanged( control ); Sethandled( control, true ); } else if( control.IsLoaded && e.Property == StatusProperty ) { HandleStatusChanged( control ); } else if( control.IsLoaded && e.Property == ContentProperty ) { HandleContentChanged( control ); } }
Cosa facciamo:static void HandleContentChanged( FrameworkElement element ) { throw new NotSupportedException( "BusyStatusManager: Content property cannot be changed at runtime." ); }static void HandleStatusChanged( FrameworkElement element ) { var layer = AdornerLayer.GetAdornerLayer( element ); Debug.WriteLineIf( layer == null, "BusyStatusManager: cannot find any AdornerLayer on the given element." ); if( layer != null ) { var content = GetContent( element ); var status = GetStatus( element ); switch( status ) { case BusyStatus.Idle: element.IsEnabled = true; if( content != null ) { var adorners = layer.GetAdorners( element ); Debug.WriteLineIf( adorners == null, "BusyStatusManager: cannot find any Adorner on the given element." ); if( adorners != null ) { var la = adorners.Where( a => a is BusyAdorner ).SingleOrDefault(); if( la != null ) { layer.Remove( la ); } } } break; case BusyStatus.Busy: element.IsEnabled = false; if( content != null ) { layer.Add( new BusyAdorner( element, GetContent( element ) ) ); } break; default: throw new NotSupportedException(); } } }
- Recuperiamo un riferimento all’AdornerLayer;
- Recuperiamo un riferimento al contenuto da visualizzare e allo stato attuale;
- In base allo stato decidiamo il da farsi;
A: Perchè è il sistema più semplice per evitare che l’utente durante l’operazione asincrona possa ciclare sui controlli “coperti” dal nostro adorner spostando il focus con la tastiera. ndr: Non c’è nessun controllo sul fatto che il controllo target sia disabilitato prima dell’operazione e quindi non debba essere riabilitato, non sarebbe un problema aggiungerlo.
Infine vediamo le parti salienti del nostro BusyAdorner, che deriva da OverlayAdorner di cui abbiamo già parlato:
Unica cosa degna di nota è che senza chiedere nulla a nessuno facciamo l’override di OnRender e disegnamo un rettangolo grigio, con un canale alpha, come sfondo.sealed class BusyAdorner : OverlayAdorner { private readonly ContentPresenter userContent; public BusyAdorner( UIElement adornedElement, Object userContent ) : base( adornedElement ) { this.userContent = new ContentPresenter() { Content = userContent }; } protected override UIElement Content { get { return this.userContent; } } protected override void OnRender( DrawingContext drawingContext ) { var brush = new SolidColorBrush( Color.FromArgb( 100, 220, 220, 220 ) ); var rect = new Rect( new Point( 0, 0 ), this.DesiredSize ); drawingContext.DrawRectangle( brush, null, rect ); base.OnRender( drawingContext ); } }
Q: Perchè lo facciamo così e non usiamo un Border con Opacity?
A: Perchè l’Opacity ha lo spiacevole side effect che viene propagata, senza possibilità di modificare questo comportamento (o almeno io non l’ho trovato), anche ai child controls del controllo su cui è impostata… Una cosa che sto pensando è di esporre una nuova attached property “Options” per permettere di variare a design time sia il colore di sfondo che la percentuale di trasparenza.
.m