Καλώς ορίσατε στο dotNETZone.gr - Σύνδεση | Εγγραφή | Βοήθεια

Σεπτέμβριος 2010 - Δημοσιεύσεις

Silverlight animatable custom controls: quick ‘n’ dirty!

Παρακάτω θα φτιάξουμε βήμα-βήμα ένα Notification box για το silverlight, χρησιμοποιώντας μία ελαφρώς παραλλαγμένη έκδοση του style που βρίσκεται ήδη στη νέα έκδοση Silverlight 4, που εισήγαγε το συγκεκριμένο control. Η λειτουργικότητα θα είναι επίσης ελαφρώς διαφορετική, αφήνοντας στην ευθύνη του silverlight control την λειτουργία του αυτόματου ελέγχου, για το αν υπάρχουν ή όχι notifications για τον χρήστη. Σκοπός είναι, καθώς θα καταγράφουμε βήμα βήμα την πορεία κατασκευής του control, θα καταλαβαίνουμε παράλληλα και τον κύκλο ζωής του. Θα κατασκευάσουμε λοιπόν, μία κλάση που θα κληρονομεί το Control, καθώς θα χρησιμοποιήσουμε Custom Control με Template. Πριν ξεκινήσουμε, αξίζει να σημείωσουμε ότι θα χρησιμοποιηθεί το live template για τον resharper, d(ependency) p(roperty), για γρηγορότερη κατασκευή των dependency properties που δίνουν αυτή τη τρελή ελευθερία στη διαδικασία ανάπτυξης rich εφαρμογών Silverlight.

Live Template

#region $NAME$ Property public $DECLARINGTYPE$ $NAME$ {     get { return ($DECLARINGTYPE$) GetValue($NAME$Property); }     set { SetValue($NAME$Property, value); } } public static readonly DependencyProperty $NAME$Property =     DependencyProperty.Register(         "$NAME$", typeof ($DECLARINGTYPE$), typeof ($DEPENDENCYOBJECT$),         new PropertyMetadata(On$NAME$Changed)); private static void On$NAME$Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) {     $DEPENDENCYOBJECT$ me = d as $DEPENDENCYOBJECT$; } #endregion

Control Structure

Ξεκινώντας την κατασκευή του control, θα αποφασίσουμε τι θέλουμε να κάνουμε από πλευρά λειτουργικότητας. Θέλουμε να φτιάξουμε ένα Notification (toast) menu το οποίο θα κάνει pop up στο silverlight application μας, θα μας δείχνει μηνύματα και στο τελευταίο μήνυμα θα σβήνει. Ανά τακτά χρονικά διαστήματα θα πρέπει το control να τσεκάρει με συγκεκριμένο τρόπο για το αν υπάρχουν μηνύματα και αν ναι, να εμφανίζει το pop-up. Σχεδιαστικά το control θα πρέπει να υποστηρίζει δύο καταστάσεις, μία ενεργή και μία κρυμμένη (normal και hidden).

Θέλουμε να φτιάξουμε ένα δικό μας control λοιπόν. Έχουμε δύο μέρη. Ένα σχεδιαστικό και ένα προγραμματιστικό. Πρώτα το προγραμματιστικό. Κοιτάμε να δούμε τι θέλουμε πρώτα από το control μας, πράγμα που θα επηρρεάσει ποια κλάση κάνουμε inherit από τις παρακάτω:

System.Object
  System.Windows.DependencyObject
    System.Windows.UIElement
      System.Windows.FrameworkElement
        System.Windows.Controls.Control
          System.Windows.Controls.UserControl

Όλα τα controls κάνουν inherit από το DependencyObject που είναι η βασική κλάση που επεκτείνει τα CLR properties ενός object και τα κάνει να μπορούν να συμμετέχουν στο μηχανισμό διαχείρισης του silverlight (να τίθενται παραπάνω από μία φορά μέσω data binding). Το control αρχίζει και αποκτά visual υπόσταση, υπάρχουν δομές για διαχείριση input, καθώς και κάποια visual properties. To Framework Element μπορεί να συμμετέχει σε DataBinding μέσω του DataContext property που έχει, να μπορεί να ρυθμίζει το layout του, καθώς και παρέχει βασικά events για τον έλεγχο ζωής του control, όπως το LayoutUpdated, το Loaded, το Unloaded. Εμείς θα χρησιμοποιήσουμε το Control type, μιας και επιθυμούμε όχι μόνο να φτιάξουμε ένα control, αλλά να αφήσουμε τη δυνατότητα να μπορεί κάποιος να του αλλάξει τελείως το visual κομμάτι με ένα template. Το Control, δίνει αυτή τη δυνατότητα.

Φτιάχνουμε λοιπόν τη κλάση, η οποία θα έχει τα εξής Dependency Properties: Item source, για τη λίστα με τα αντικείμενα, ένα interval, ένα refreshcommand τύπου ICommand για τη συνάρτηση που εκτελείται ανα intervals, για να τσεκάρει αν υπάρχουν alerts. Χρησιμοποιούμε ICommand, το οποίο μας δίνει τη δυνατότητα να κάνουμε Databind, κάτι που να ελέγχει πότε μπορεί να εκτελεστεί (CanExecute()). Ένα message, ένα currentindex για να δείχνουμε πιο message διαβάζεται αυτή τη στιγμή, ένα IsOpen, για να καταλαβαίνουμε πότε είναι ανοικτό το notification και ένα total, για όλα τα μηνύματα. Αξίζει να δούμε ένα Dependency Propery πριν δούμε όλο το control. Ας παραθέσουμε το ItemSource. To Itemsource, είναι η εσωτερική λίστα με τα αντικείμενα που το κάθε ένα έχει μήνυμα που ενδιαφέρει το χρήστη. Στην παρούσα υλοποίηση έστω ότι το itemsource είναι object γενικότερα, το οποίο εμείς θα το χειριστούμε ως λίστα.

To Property που αφήνουμε το silverlight να μας το διαχειριστεί, φέρνοντάς μας τιμή όποτε αυτό είναι έτοιμο, παρομοίως και θέτει τιμή όποτε αυτό κρίνει. (Ο μηχανισμός των dp είναι σχεδιασμένος έτσι, ώστε αν ένα dp είναι data bound, συμμετέχει σε animation, κτλ)

public Object ItemsSource {     get { return (Object)GetValue(ItemsSourceProperty); }     set { SetValue(ItemsSourceProperty, value); } }

Εδώ είναι το πραγματικό μας dp. Έτσι δηλώνουμε, ότι το εν λόγω control έχει ένα dp ItemsSource, τύπου object, ο κάτοχος είναι το Alert Control καθώς και αν αλλάξει η τιμή του, κάνε κάποια πράγματα. Προσέξτε κάτι, το dependency property είναι static. Αυτό μας δείχνει ότι η δήλωση είναι ανα τύπο και όχι ανά στιγμιότυπο. Έτσι όταν κάνουμε register ένα dependency property δίνουμε όλα τα απαραίτητα στοιχεία, αλλά κατά το instantiation ενός control, δεν δεσμεύεται παρά μόνο ο απαραίτητος χώρος για όσα dependency properties ορίσουμε στη XAML. Επίσης ένα dp μη ξεχνάτε ότι μπορεί να γίνει και άλλα πράγματα όπως Data Binding, γράφοντας πχ. ItemSource = {Binding MyCollectionOnMyMVVMViewModel}.

public static readonly DependencyProperty ItemsSourceProperty =     DependencyProperty.Register(         "ItemsSource", typeof(Object), typeof(AlertControl),         new PropertyMetadata(OnItemsSourceChanged));

Όταν κληθεί το callback πρέπει πάντα να παίρνουμε το Dependency Object που έκανε raise to event (είπαμε είναι static η δήλωση).

private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {     AlertControl me = (AlertControl)d;     me.OnItemsSourceChanged((Object)e.OldValue, (Object)e.NewValue); }

Και να κάνουμε ότι θέλουμε βέβαια, όπως εγώ που τσεκάρω μία μεταβλητή και αλλάζω το Visual State, όπως θα δούμε παρακάτω.

private void OnItemsSourceChanged(Object oldValue, Object newValue) {     var enumerable = ItemsSource as IList;     if (enumerable == null) return;     Total = enumerable.OfType<Object>().Count();     if (Total > 0)     {         IsOpen = true;     }     ChangeVisualState(); }

Σε αυτό το σημείο είναι καλό να δούμε τον κώδικα των Dependency Properties μέσω reflector. Η DependencyProperty.Register κάνει κάποιους ελέγχους και στο τέλος καταλήγει απλά να αποθηκεύει το dependency property σε ένα dictionary, για το owner type:

internal static void RememberRegisteredProperty(string name, Type ownerType, DependencyProperty dp) {     Dictionary<string, DependencyProperty> dictionary;     if (!_registeredProperties.TryGetValue(ownerType, out dictionary))     {         dictionary = new Dictionary<string, DependencyProperty>();         _registeredProperties[ownerType] = dictionary;     }     dictionary[name] = dp;     if (dp.m_nKnownId == 0)     {         if (_customProperties.Count == 0)         {             _customProperties.Add(null);         }         dp.m_nKnownId = (uint) _customProperties.Count;         _customProperties.Add(dp);     } }

Style

To style είναι ένας τρόπος να ορίσουμε τις μεταβλητές διάφορων dependency properties κάποιου element πακεταρισμένα. Με ένα style υπάρχει η δυνατότητα να πακετάρουμε τιμές που αλλάζουν το visual feeling πολλών elements. Να θυμάστε ότι ορίζοντας τιμές σε containers οι τιμές θα οριστούν και στα παιδιά.

Έστω ότι έχουμε ένα style για το control μας, λοιπόν και θέλουμε οι default τιμές να ορίζονται στο generic.xaml. Θα γράψουμε λοιπόν ένα πακετάκι που θα λέει:

<Style TargetType="local:AlertControl">   <Setter Property="Height" Value="100"/>   <Setter Property="Width" Value="350"/>   <Setter Property="HorizontalAlignment" Value="Right"/>   <Setter Property="Margin" Value="0,0,30,0"/>   <Setter Property="VerticalAlignment" Value="Bottom"/> </Style>

Μία απλή λεπτομέρεια είναι ότι για να δεθεί το control με το default style χρειάζονται δύο πράγματα. Ένα το targetType και δύο το property του control μας (ως Control) DefaultStyleKey που λέει το αντίστοιχο πράγμα: DefaultStyleKey = typeof(AlertControl); Υπάρχουν πάρα πολύ ωραία πράγματα που μπορούμε να κάνουμε με τα styles, όπως style inheritance κτλ, αλλά ας προχωρήσουμε

Template

Το silverlight μας παρέχει τη δυνατότητα να αλλάξουμε τελείως το visual “face” ενός control, η να ορίσουμε ένα default με τον ίδιο τρόπο. Όταν δηλαδή, καταλάβετε ότι δεν μπορείτε να φέρετε στα μέτρα σας, ένα control, τότε φτιάξτε το δικό σας template (χωρίς να δημιουργήσετε ένα ολοκαίνουριο control). Το Template είναι απλά ένα ακόμα DependencyProperty, που το χρησιμοποιεί το silverlight για να χτίσει το visual tree, και να το παρουσιάσει. Μπορεί να οριστεί είτε μέσα στον κώδικα

<Button Content=”Yo, I am a Custom Control”>     <Button.Template>         <ControlTemplate TargetType=”Button”>             <Border…>             <Textbox…>             <AnotherCustomControl…>         </ControlTemplate >     </Button.Template> </Button>

είτε μέσα από ένα style, ορίζοντας ένα ακόμα dependency property όπως όλα τα υπόλοιπα μέσα από το προκαθορισμένο syntax του style tag.

<Setter Property="Template">     <Setter.Value>         <ControlTemplate  TargetType="local:AlertControl">             <Grid HorizontalAlignment="Stretch" x:Name="LayoutRoot" VerticalAlignment="Stretch">                 <Grid.Resources>                     <converters:ObjectToStringConverter x:Key="ObjectToStringConverter"/>                 </Grid.Resources>                     <Border x:Name="PopUp" Opacity="0" Visibility="Collapsed" >                                                <StackPanel Orientation="Vertical">                         <local:SynchedSoundPlayer x:Name="NotificationSound" HorizontalAlignment="Left" VerticalAlignment="Top" Source="/NameSpace;component/Resources/Sounds/notification.mp3" Visibility="Collapsed" AnimatablePlay="0"/>                         <Border Height="24" CornerRadius="4" >                             <Border.Background>                                <LinearGradientBrush StartPoint="0.5,0.0" EndPoint="0.5,1.0">                                     <GradientStop Offset="0.2" Color="#FF1C68A0" />                                     <GradientStop Offset="1.0" Color="#FF54A7E2" />                                </LinearGradientBrush>                             </Border.Background>                             <Border.Effect>                                 <DropShadowEffect BlurRadius="4" ShadowDepth="4" Opacity="0.4" />                             </Border.Effect>                             <TextBlock Text="Notification" FontSize="12" FontWeight="Bold" Foreground="White" Margin="4" />                         </Border>                         <Grid Background="LightYellow" Height="{TemplateBinding Height}" MinHeight="70" >                             <Grid.Effect>                                 <DropShadowEffect BlurRadius="4" ShadowDepth="4" Opacity="0.4" />                             </Grid.Effect>                             <Grid.ColumnDefinitions>                                 <ColumnDefinition Width="Auto"/>                                 <ColumnDefinition Width="*"/>                                </Grid.ColumnDefinitions>                             <Grid.RowDefinitions>                                 <RowDefinition Height="Auto"/>                                 <RowDefinition Height="Auto"/>                             </Grid.RowDefinitions>                             <Image Grid.Column="0" Grid.Row="0" Source="/NameSpace;component/Resources/Images/refresh.png" Width="32" Height="34" Stretch="Fill" Margin="4" VerticalAlignment="Top" />                             <TextBlock Grid.Column="1" Grid.Row="0" x:Name="Message" Text="{TemplateBinding Message}" TextWrapping="Wrap"/>                             <StackPanel Grid.Column="1" Orientation="Horizontal" Grid.Row="1" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="4">                                 <TextBlock x:Name="CurrentIndex" DataContext="{TemplateBinding CurrentIndex}" Text="{Binding Converter={StaticResource ObjectToStringConverter}}" />                                 <TextBlock Text=" / "/>                                 <TextBlock DataContext="{TemplateBinding Total}" Text="{Binding Converter={StaticResource ObjectToStringConverter}}" />                             </StackPanel>                         </Grid>                     </StackPanel>                 </Border>             </Grid>         </ControlTemplate>     </Setter.Value> </Setter>

Θυμηθείτε ότι με το style ορίζουμε τιμές σε Dependency Properties πακεταρισμένα, με τα templates ορίζουμε visual tree, αλλά πώς περνάμε τιμές από το style μας, ή τιμές που έχουν περάσει από τον ορισμό του control, μέσα στα elements που ορίζουμε στο Control Template? Μέσω Template Binding φυσικά. Δείτε το παραπάνω και θα καταλάβετε.

Visual State Manager

H πηγή των animations στο silverlight είναι το storyboard. Τι γίνεται δηλαδή σε ένα control όταν αλληλεπιδρούμε πάνω του και πώς γίνεται τι. Εξωτερικά και συνοπτικά, ένα control έχει καταστάσεις και μεταβάσεις, δηλαδή ένα button μπορεί να είναι pressed, focused, normal, επίσης η μετάβαση από το normal στο focused περιλαμβάνει το highlight του περιγράμματος, ή όταν είναι pressed φαίνεται πιο σκοτεινό και ελαφρώς πιο μικρό ώστε να φαίνεται πατημένο. Στην περίπτωση μου θέλω να είναι σε Normal και Hidden. Η μετάβαση να γίνεται ομαλά αλλάζοντας κάποια τιμή όπως το Visibility και το Opacity σταδιακά για να κάνει ένα ωραίο fade in όταν εμφανίζεται. Αυτά ορίζονται στη XAML και το παρακάτω κομμάτι κώδικα μιας και αφορά το control μας μπαίνει κανονικά στο template που φτιάξαμε παραπάνω.

<VisualStateManager.VisualStateGroups>                             <VisualStateGroup x:Name="VisualStateGroup">                                 <VisualState x:Name="Normal">                                     <Storyboard x:Name="FaderStory">                                         <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="PopUp" Storyboard.TargetProperty="Visibility">                                             <DiscreteObjectKeyFrame KeyTime="00:00:00.5000000" Value="Visible"/>                                         </ObjectAnimationUsingKeyFrames>                                         <DoubleAnimation BeginTime="00:00:00.5000000" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="PopUp" From="0.0" To="1.0" Duration="0:0:1" />                                         <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="NotificationSound" Storyboard.TargetProperty="(SynchedSoundPlayer.AnimatablePlay)">                                             <DiscreteDoubleKeyFrame KeyTime="0" Value="1"/>                                         </DoubleAnimationUsingKeyFrames>                                     </Storyboard>                                 </VisualState>                                 <VisualState x:Name="Hidden">                                     <Storyboard x:Name="HideFaderStory">                                         <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="PopUp">                                             <EasingDoubleKeyFrame KeyTime="0" Value="0"/>                                         </DoubleAnimationUsingKeyFrames>                                         <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="PopUp" Storyboard.TargetProperty="Visibility">                                             <DiscreteObjectKeyFrame KeyTime="00:00:00.5000000" Value="Collapsed"/>                                         </ObjectAnimationUsingKeyFrames>                                     </Storyboard>                                 </VisualState>                             </VisualStateGroup>                         </VisualStateManager.VisualStateGroups>

Προφανώς υπάρχουν πάρα πολλά είδη μεταβάσεων από μία τιμή στην άλλη. Έστω ότι θέλουμε να αλλάξουμε το height δηλαδή. Δεν χρειάζεται να το κάνουμε μέσα σε 5 δευτερόλεπτα να αλλάζει ισομοιρασμένα τιμές (γραμμικά, δηλαδή), αλλά να κάνει σαν να αναπηδά. Να κάνει bounceEase όπως λέμε (φανταστείτε το σαν συνάρτηση t, f(t) )

BounceEase

IC270154

To πότε αλλάζει state το control μας έχει να κάνει με τη λογική του control μας. Μέσα στον κώδικα λοιπόν, έχω τη λογική μου να τσεκάρω κάθε 15 δευτερόλεπτα αν υπάρχουν καινούρια alerts για τον χρήστη μου και αν βρω ότι υπάρχουν, γράφω ένα

VisualStateManager.GoToState(this, "Normal", true);

image

και παρατηρώ τη μαγεία να ενεργοποιείται. Φανταστείτε να έχετε έναν designer που να σας έχει φτιάξει το υπερτέλειο animation και να το χειρίζεστε εσείς, από κώδικα (σε μία line of business application, ή σε ένα παιχνιδάκι σε silverlight).

Τελικά έχουμε κώδικα που κάνει compile και η xaml μας ερμηνεύει όμορφα ότι γράψαμε και όλα παίζουν. Τι συμβαίνει όμως κατά σειρά;

Lifecycle

Ας πούμε σε γενικές γραμμές τι γίνεται όταν parsarεται το XAML αρχείο. Όταν ανιχνευθεί το opening tag, καλείται ο constructor του control, γίνονται wire κάποιοι handlers, τίθεται το DefaultStyleKey αν υπάρχει και συνεχίζουμε. Αν υπάρχει Style, που τίθεται πχ με StaticResource, τότε καλούνται όσοι setters ορίζονται στο style και τίθενται τα values. Στη συνέχεια, γίνεται apply το style στο generic.xaml (χωρίς να κάνει override τιμές). Συνεχίζει ο parser και συναντά properties, που είναι explicitely setted. Σε αυτό το σημείο το element (control), προστίθεται στο Visual Tree και γίνεται raise το Loaded event. Αν το control δεν έχει visual tree, αναλαμβάνει δράση το Template, που ορίζει πώς θα σχεδιαστεί το control. Αυτό γίνεται με κλήση στην ApplyTemplate function. Όταν γίνει apply το template, μπορούμε να παρέμβουμε εμείς κάνοντας override την OnApplyTemplate. Τι μπορούμε να κάνουμε, εξαρτάται πραγματικά από το τι θέλουμε. Σε αυτό το σημείο γνωρίζουμε τα controls τα οποία απαρτίζουν την υπόσταση του control στο Visual Tree. Μπορούμε λοιπόν, να παρέμβουμε σε αυτό το Visual Tree, πριν αποσταλλεί για παρουσίαση στο χρήστη και να κάνουμε fine tuning κάποια πράγματα (όπως να κάνουμε wire up κάποιους handlers για interraction). Επίσης γνωρίζοντας τα control, μπορούμε να αποκτήσουμε references σε αυτά (GetTemplateChild) και να κάνουμε διάφορα πράγματα. Αν για παράδειγμα υπάρχει ένα drop down, μπορούμε να πάρουμε reference σε αυτό και να του κάνουμε hook κάποιους handlers. Στη συνέχεια καλούνται οι MeasureOverride και ArrangeOverride, που ορίζουν το μέγεθος και τη θέση των controls.

 

Σε αυτό το post:

  1. Κατέγραψα εν συντομία μία περιγραφή των custom controls
  2. Κατέγραψα μερικά πράγματα στα γρήγορα σχετικά με το lifecycle ενός control
  3. Δημιουργήσαμε μία custom έκδοση του Notification Control που έχει το silverlight 4 με δικά μας χαρακτηριστικά αλλά με παρόμοιο visual feeling, εκτός του ότι μπορεί να παίξει ήχο μέσα στο storyboard (an idea from http://forums.silverlight.net/forums/p/127010/285086.aspx ), παίρνει μία custom λογική από dependency property (ένα object τύπου ICommand που μπορεί να γίνει bind στο ViewModel (Presentation Model) μας και να το ορίσουμε στο View) και έχει κάτι σαν paging.

Το παραπάνω control μπορεί να χρησιμοποιηθεί δηλαδή ως:

<myControls:AlertControl x:Name="Notifications" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Grid.RowSpan="2"                             RefreshCommand="{Binding RefreshAlertsCommand}"                             ItemsSource="{Binding AlertInstances}"/>

Play with some code: http://cid-c6b2e583e302124e.office.live.com/self.aspx/Public/AlertControl.zip

Posted: Παρασκευή, 17 Σεπτεμβρίου 2010 11:00 μμ από George J. Capnias | 0 σχόλια
Δημοσίευση στην κατηγορία: ,