Confessions of a Dangerous Mind

Brain.FlushBuffer()
posts - 176, comments - 234, trackbacks - 93

MVVM Applied: WPF Analog Clock

Lo abbiamo visto. Lo abbiamo desiderato. Abbiamo usato le GDI32 per disegnare le “lancette” e creato fantastici quadranti con i più vari programmi di grafica. Ammettiamolo, l’orologio analogico mantiene immutato il suo fascino, anche sulle nostre postazioni di lavoro iper-tecnologiche. E’ per questo che, per crearne una nuova e moderna versione, andremo a scomodare nientemeno che sua signoria Model-View-ViewModel.

Cosa vogliamo vedere in questo articolo? E’ presto detto: MVVM applicato non a problematiche di DataBinding classico (Textbox con codice cliente tanto per capirci), bensì al disegno di un orologio analogico, con lancette delle ore, minuti e secondi che si muovono in modo realistico. In ultima analisi, questo ci consentirà di mantenere assolutamente distinte la logica di gestione del tempo da quella di rappresentazione dello stesso.

MVVM: To the Max!

La prima cosa da fare è realizzare un “engine” per il nostro orologio. Engine è in effetti una parola un pò grossa, ma in pratica ciò che faremo è creare un ViewModel che contenga al suo interno un Timer ed una proprietà CurrentDateTime che esporrà il valore corrente della data. Fate attenzione, ho detto una sola proprietà, non ore, minuti, secondi: vedremo poi, nella view, come gestire la rappresentazione del tempo. In questa fase, l’unica cosa che ci interessa è come realizzare il motore del nostro orologio. Nello snippet seguente è possibile vedere il codice del ClockViewModel:

   1: class ClockViewModel : ViewModelBase
   2:  {
   3:      private System.Windows.Threading.DispatcherTimer _timerClock;
   4:  
   5:      public ClockViewModel()
   6:      {
   7:          CurrentDateTime = DateTime.Now;
   8:          _timerClock = new System.Windows.Threading.DispatcherTimer();
   9:          //Crea un timer e lo abilita
  10:          _timerClock.Interval = new TimeSpan(0, 0, 1);
  11:          _timerClock.IsEnabled = true;
  12:          _timerClock.Tick += new EventHandler(TimerClock_Tick);
  13:      }
  14:  
  15:      private void TimerClock_Tick(object sender, EventArgs e)
  16:      {
  17:          CurrentDateTime = DateTime.Now;
  18:      }
  19:  
  20:      private DateTime _currentDateTime;
  21:      public DateTime CurrentDateTime
  22:      {
  23:          get
  24:          {
  25:              return _currentDateTime;
  26:          }
  27:          set
  28:          {
  29:              _currentDateTime = value;
  30:              OnPropertyChanged("CurrentDateTime");
  31:          }
  32:      }      
  33:  }

L’implementazione è piuttosto semplice. Alla creazione imposto un timer con intevallo di un secondo, lo abilito ed imposto la callback dell’evento Tick. Al verificarsi dell’evento Tick, vado ad impostare la proprietà CurrentDateTime, ed avendo gestito all’interno di questa l’evento OnPropertyChanged, potrò scatenare un aggiornamento di tutte le entità della view che sono bindate (collegate) a questa proprietà. A proposito: andiamo a vedere la parte più interessante, ovvero la view!

L’abito (non) fa il monaco

La view del nostro orologio analogico è piuttosto interessante. Non dal punto di vista grafico/stilistico, bensì per l’uso che fa della proprietà CurrentDateTime associata al Converter AngleConverter che permette di far ruotare le lancette di ore, minuti e secondi seguendo lo schema corretto.

   1: class AngleConverter : IValueConverter
   2: {
   3:     public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
   4:     {
   5:         DateTime _currentDateTime = (DateTime)value;            
   6:         switch (parameter.ToString())
   7:         {
   8:             case "S":
   9:                 return _currentDateTime.Second * 6 % 360;                    
  10:             case "M":
  11:                 return _currentDateTime.Minute * 6 % 360;                    
  12:             case "H":
  13:                 return (_currentDateTime.Hour * 30 +  _currentDateTime.Minute/2) % 360;
  14:                 break;
  15:             default:
  16:                 return 0;                    
  17:         }            
  18:     }
  19:  
  20:     public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
  21:     {
  22:         throw new NotSupportedException();
  23:     }
  24: }

Lo scopo del converter è quello di convertire un valore di tempo, inteso come ore, minuti e secondi, in un valore angolare, a seconda del parametro passato al convertitore. E’ quindi piuttosto interessante come un unico convertitore possa restituire, a seconda del parametro passato, valori angolari diversi che andrano applicati alle varie lancette del nostro orologio.

Vediamo assieme come utilizzare questo convertitore sullo XAML della nostra View:

   1: <Window x:Class="WPFClock.Views.Clock"
   2:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   3:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:     xmlns:Converters="clr-namespace:WPFClock.Converters"
   5:     Height="350" Width="350" Title="MVVM Analog Clock">
   6:     
   7:     <Window.Resources>
   8:         <Converters:AngleConverter x:Key="angleConverter"/>
   9:     </Window.Resources>
  10:     
  11:     <StackPanel>
  12:         <TextBlock Text="{Binding Path=CurrentDateTime}" HorizontalAlignment="Center"/>
  13:  
  14:         <Grid x:Name="LayoutRoot" Width="300" Height="300" Background="White">
  15:             <Ellipse Margin="10">
  16:                 <Ellipse.Fill>
  17:                     <RadialGradientBrush>
  18:                         <GradientStop Color="#FFB1B1B1"/>
  19:                         <GradientStop Color="#FF989898" Offset="0.953"/>
  20:                         <GradientStop Color="White" Offset="0.909"/>
  21:                         <GradientStop Color="#FF121212" Offset="1"/>
  22:                         <GradientStop Color="#FF9B9B9B" Offset="0.884"/>
  23:                     </RadialGradientBrush>
  24:                 </Ellipse.Fill>
  25:             </Ellipse>
  26:     
  27:             <TextBlock Margin="74.333,0,74.667,154" Text="12" TextWrapping="Wrap" FontSize="96" 
  28:                 FontWeight="Bold" TextAlignment="Center" FontFamily="Nightclub BTN Cn"/>
  29:             <TextBlock Margin="107,195.999,110.333,27.65" FontSize="64" 
  30:                 FontWeight="Bold" Text="6" TextAlignment="Center" TextWrapping="Wrap" Foreground="#FFC90000" FontFamily="Nightclub BTN Cn"/>
  31:             <TextBlock Margin="0,80,30,117.001" FontSize="96" 
  32:                 FontWeight="Bold" Text="3" TextAlignment="Center" TextWrapping="Wrap" HorizontalAlignment="Right" Width="72.667" Foreground="#FF000BFF" FontFamily="Nightclub BTN Cn"/>
  33:             <TextBlock Margin="-6.667,93.333,0,94.668" FontSize="96" 
  34:                 FontWeight="Bold" Text="9" TextAlignment="Center" TextWrapping="Wrap" HorizontalAlignment="Left" Width="128" Foreground="#FF004D10" FontFamily="Nightclub BTN Cn"/>
  35:  
  36:             <Rectangle Margin="148,0,148,150" x:Name="rectangleSecond" Height="120" VerticalAlignment="Bottom" RadiusX="1" RadiusY="1" Fill="#FF2E2E2E">
  37:                 <Rectangle.RenderTransform>
  38:                     <RotateTransform CenterX="2" CenterY="120" 
  39:                         Angle="{Binding Path=CurrentDateTime, Converter={StaticResource angleConverter}, ConverterParameter=S}" />
  40:                 </Rectangle.RenderTransform>
  41:             </Rectangle>
  42:                     
  43:             <Rectangle Margin="148,49,148,151" x:Name="rectangleMinute" RadiusX="1" RadiusY="1" Fill="#FF2E2E2E">
  44:                 <Rectangle.RenderTransform>
  45:                     <RotateTransform CenterX="2" CenterY="100" 
  46:                         Angle="{Binding Path=CurrentDateTime, Converter={StaticResource angleConverter}, ConverterParameter=M}" />
  47:                 </Rectangle.RenderTransform>
  48:             </Rectangle>
  49:  
  50:             <Rectangle Margin="148,80,148,150" x:Name="rectangleHour" RadiusX="1" RadiusY="1" Fill="#FF2E2E2E">
  51:                 <Rectangle.RenderTransform>
  52:                     <RotateTransform CenterX="2" CenterY="70" 
  53:                         Angle="{Binding Path=CurrentDateTime, Converter={StaticResource angleConverter}, ConverterParameter=H}" />
  54:                 </Rectangle.RenderTransform>
  55:             </Rectangle>
  56:             
  57:             <Ellipse Margin="145" Fill="#FF2E2E2E"></Ellipse>
  58:             <Ellipse Margin="149" Fill="White"></Ellipse>
  59:             
  60:             <Path Stretch="Fill" Margin="20,20,20,0" 
  61:                 Data="M260,130 C260,58.202983 201.79702,0 130,0 58.202983,0 0,58.202983 0,130 43.777778,130 72.635599,95.461382 
  62:                 130.66667,97.333343 192.66667,99.333333 217.11111,130 260,130 z" VerticalAlignment="Top" Height="130">
  63:                 <Path.Fill>
  64:                     <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
  65:                         <GradientStop Color="#2FFFFFFF" Offset="1"/>
  66:                         <GradientStop Color="White"/>
  67:                     </LinearGradientBrush>
  68:                 </Path.Fill>
  69:             </Path>
  70:         </Grid>
  71:     </StackPanel>
  72: </Window>

Togliendo un po’ di “rumore di fondo”, ciò che veramente ci interessa sono i tre rettangoli rectangleHour, rectangleMinute e rectangleSecond. Come è possibile vedere dallo snippet, su ciascuno è stata definita una rotate transformation, centrata sull’estremità inferiore del rettangolo e “bindata” con la proprietà CurrentDateTime facendo uso dell’ AngleConverter con il parametro corretto (S per i secondi, M per i minuti, H per le ore). Il risultato? E’ presto detto! il nostro orologio funziona perfettamente, senza strani calcoli trigonometrici per disegnare le lancette. In effetti è tutto molto semplice, ed è dovuto alla netta separazione tra codice e visualizzazione implementata da MVVM.

Potremmo a questo punto sbizzarrirci nel creare lancette fantasiose o sfondi a nostro piacimento, perchè il “motore” e la “rappresentazione” tramite i converter e le rotazioni non ne risentirebbe.

Se qualcuno vuole dare un’occhiata al tutto in movimento, il codice sorgente lo potete trovare qui.

Print | posted on lunedì 26 ottobre 2009 12:57 |

Comments have been closed on this topic.

Powered by:
Powered By Subtext Powered By ASP.NET