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

Οκτώβριος 2010 - Δημοσιεύσεις

Silverlight Αttached Βehavior: Φτιάχνοντας ένα Marquee TextBox Control

Έχω ένα Grid στο Silverlight Application και θέλω να κάνω ένα marquee textbox (autoscroll) από δεξιά προς τα αριστερά, μέσα στο κελί του Grid. Τι χρειάζεται να κάνω για να προσθέσω σε ένα control αυτή τη λειτουργία;

Για αρχή έχουμε ένα textbox και στα γρήγορα κάνουμε ένα Translate RenderTransform αλλάζοντας την τιμή, στον άξονα των x από θετικές σε αρνητικές τιμές.

<TextBlock Grid.Row="0" Grid.Column="0" Foreground="#FF2755AF"            FontSize="13.333" x:Name="ScrollText" Text="Really Really Really Really Really Really Large Text" >     <TextBlock.RenderTransform>         <TranslateTransform x:Name="translate" />     </TextBlock.RenderTransform>     <TextBlock.Triggers>         <EventTrigger RoutedEvent="Grid.Loaded">             <BeginStoryboard>                 <Storyboard RepeatBehavior="Forever">                     <DoubleAnimation                         From="400" To="-400"                         Storyboard.TargetName="translate"                         Storyboard.TargetProperty="X"                         Duration="0:0:20" />                 </Storyboard>             </BeginStoryboard>         </EventTrigger>     </TextBlock.Triggers> </TextBlock>

Δυστυχώς όμως έχουμε πολλά προβλήματα όπως φαίνεται. Το κείμενο εκτός του ότι φεύγει τελείως εκτός των bounds του Grid, γιατί έγινε βίαιο translate χωρίς φόβο και πάθος, είναι και clipped ως προς το content. Αν παρατηρείτε μάλιστα, είναι clipped στο μέγεθος του κελιού (0,0). Αυτό γίνεται γιατί το Grid, πριν απεικονίσει τα αντικείμενα που έχει στο visual tree, τα κάνει clip, αυτόματα.

image

Το πρώτο πρόβλημα μπορούμε να το χειριστούμε με clipping στο Grid. Το δευτερο όμως όχι.

<Grid.Clip>     <RectangleGeometry Rect="0,0,100,20"/> </Grid.Clip>

Οπότε θέλουμε να κάνουμε override το clipping του Grid, όπως και τo clipping του textbox. Ο Canvas μας βοηθάει και στις δύο περιπτώσεις, γιατί αφενός αφήνει ελεύθερο το Actual Size του textbox και αφετέρου έχει και αυτό Clipping dependency property ως UIElement.

<Canvas >     <Canvas.Clip>         <RectangleGeometry Rect="0,0,100,20"/>     </Canvas.Clip>     <TextBlock Grid.Row="0" Grid.Column="0" Foreground="#FF2755AF"                 FontSize="13.333" x:Name="ScrollText" Text="Really Really Really Really Really Really Large Text" >     <TextBlock.RenderTransform>         <TranslateTransform x:Name="translate" />     </TextBlock.RenderTransform>     <TextBlock.Triggers>         <EventTrigger RoutedEvent="Grid.Loaded">             <BeginStoryboard>                 <Storyboard RepeatBehavior="Forever">                     <DoubleAnimation                         From="300" To="-300"                         Storyboard.TargetName="translate"                         Storyboard.TargetProperty="X"                         Duration="0:0:20" />                 </Storyboard>             </BeginStoryboard>         </EventTrigger>     </TextBlock.Triggers>     </TextBlock> </Canvas>

Δυστυχώς όμως έχουμε και εδώ μερικά προβλήματα. Καρφωτές τιμές στο Rect και στο From / To του δεν μπορούν να συνδυαστούν με ένα δυναμικό μέγεθος του Text, οπότε για να γίνει το control λίγο reusable χρειάζεται λίγο refactoring.

Η βασική ιδέα είναι να υπολογίζουμε δυναμικά κάποια πράγματα, για να μην χρειάζεται να βάλουμε καρφωτές τιμές. Μπορούμε να το κάνουμε αυτό με ένα πολύ απλό pattern που λέγεται attached behavior. Για να μπορούμε από ένα κεντρικό σημείο να αλλάξουμε ιδιότητες του TextBlock χωρίς να το κάνουμε extend, χρησιμοποιούμε attached property. Ορίζουμε λοιπόν ένα attached property κάπου (στη main page σε εμάς, σε ένα behavior σε άλλες περιπτώσεις), δεν έχει σημασία, για το demo μας, το οποίο θα το δηλώσουμε στο textblock που μας ενδιαφέρει. Θυμίζουμε ότι attached properties είναι properties που ορίζονται σε κάποια κλάση και μπορούν να χρησιμοποιηθούν από άλλες. Τυπικό παράδειγμα το Grid.Row και Grid.Column που βάζουμε στα elements, που περιέχονται σε Grid Panel.

#region MakeScrollable Property public static void SetMakeScrollable(UIElement element, bool value) {     element.SetValue(MakeScrollableProperty, value); } public static bool GetMakeScrollable(UIElement element) {     return (Boolean)element.GetValue(MakeScrollableProperty); } public static readonly DependencyProperty MakeScrollableProperty =     DependencyProperty.RegisterAttached(         "MakeScrollable", typeof (bool), typeof (MainPage),         new PropertyMetadata(false, OnMakeScrollableChanged)); private static void OnMakeScrollableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {     FrameworkElement me = d as FrameworkElement;     me.Loaded += me_Loaded; } static void me_Loaded(object sender, RoutedEventArgs e) {     Clip(sender); } private static void Clip(object sender) {     FrameworkElement me = (FrameworkElement)sender; //Textblock size changed     FrameworkElement parent = (FrameworkElement)me.Parent;     if (GetMakeScrollable(me))     {         parent.Dispatcher.BeginInvoke(()=>         {             parent.Clip = new RectangleGeometry { Rect = new Rect(0, 0, parent.ActualWidth, parent.ActualHeight) };             var translateTransform = new TranslateTransform();             translateTransform.SetValue(NameProperty, "translate");             me.RenderTransform = translateTransform;             Storyboard storyboard = new Storyboard() { RepeatBehavior = RepeatBehavior.Forever};             DoubleAnimation doubleAnimation = new DoubleAnimation()             {                 From = parent.ActualWidth, To = -(me.ActualWidth), Duration = new Duration(new TimeSpan(0, 0, 20))             };             doubleAnimation.SetValue(Storyboard.TargetNameProperty, "translate");             doubleAnimation.SetValue(Storyboard.TargetPropertyProperty,  new PropertyPath("(TranslateTransform.X)"));             storyboard.Children.Add(doubleAnimation);             me.Resources.Add("StoryBoard", storyboard);             storyboard.Begin();         });     } } #endregion

Στη δική μας περίπτωση, ορίζουμε το MakeScrollableProperty. Όταν τεθεί από το textblock, αυτό καλεί την αντίστοιχα ορισμένη OnMakeScrollableChanged που στην ουσία κάνει το clipping.

<Canvas Grid.Row="0" Grid.Column="0" >     <TextBlock SilverlightApplication2:MainPage.MakeScrollable="true"                Text="Really Really Really Really Really Really Large Text "/> </Canvas>

image

Σίγουρα μπορούμε να κάνουμε ακόμα πιο κομψό τo “behavior”, διαχειριζόμενοι και τον canva, αλλά νομίζω ότι ήδη είναι αρκετά reusable για να χρησιμοποιηθεί στα πλαίσια ενός μικρού project. Τέλος, κρατήστε τη βασική ιδέα πίσω από το attached behavior. Αποκτούμε reference στο control που μας ενδιαφέρει μέσα από attached property, κάνοντας hooks και οτιδήποτε, μέσα από PropertyMetadata, αφήνοντας την προσθήκη λειτουργικότητας να γίνει δηλωτικά.

Smile

Posted: Τετάρτη, 20 Οκτωβρίου 2010 4:23 πμ από George J. Capnias | 0 σχόλια
Δημοσίευση στην κατηγορία: , ,

A note to self: Silverlight DataBinding awesomeness μέσα από Expression Blend, XAML και κώδικα

 Post-it notes for la patilla

 

Το πιο δυνατό στοιχείο του Silverlight είναι η εκφραστικότητα των data binding μηχανισμών του με όλες τις μορφές που μας το παρέχει. Separation of concerns, animations, visual states, MVVM, αλληλένδετα controls και πολλά ακόμα “βαριά” ή ελαφρά features οφείλουν την ύπαρξή τους στο Data Binding.

Τι είναι Data Binding;

Data Binding είναι η σύνδεση μεταξύ μίας πηγής δεδομένων και ενός προορισμού.

Στο silverlight η πηγή είναι ένα CLR αντικείμενο που διατηρεί, ανανεώνει, κτλ πληροφορία (ένα object συγκεκριμένου τύπου) και ο προορισμός είναι ένα SL control. Όταν όλα είναι δηλωμένα σωστά, τότε όταν το CLR αντικείμενο αλλάζει με κάποιο τρόπο, μπορει να παρέχει ειδοποιήσεις για το τι άλλαξε, ποιό CLR property δηλαδή και όσα SL controls (elements) έχουν γίνει databound εκεί, μπορούν να ανανεώσουν τις τιμές τους. Η πρώτη επαφή με το DataBinding συνήθως έρχεται μέσα από τη XAML.

Through Expression Blend

Ας δούμε ένα μικρό παράδειγμα πριν πούμε τα βασικά του DataBinding και μάλιστα μέσα από το Blend, , ώστε να το γνωρίσουμε και αυτό το πολύτιμο εργαλείο καλύτερα. Έστω ότι θέλω να φτιάξω μία εφαρμογή για rating ταινιών (SL-IMDB δηλαδή), γρήγορα, τακτοποιημένα, μέσα από το Blend. Θέλω τα δεδομένα μου στην οθόνη να απεικονίζονται με βάση κάποιο συγκεκριμένο CLR Object, το οποίο μου έρχεται από τη βάση δεδομένων. Φτιάχνω ένα νέο silverlight project και για αρχή το object μου. Add New Item και φτιάχνω μία κλάση movie, που την γράφω κατευθείαν μέσα από το Blend έχοντας κανονικά Intellisense. Όχι ότι θα αναπτύξω κώδικα για το SL project στο Blend, αλλά μας δίνεται αυτή η ευκολία, χωρίς να χρειαζόμαστε πάντα και VS Studio και Expression Blend αναγκαστικά για κάποια μικροαλλαγή. Στη συνέχεια φτιάχνω μία καινούρια Page, ας χρησιμοποιήσουμε την ήδη υπάρχουσα MainPage.xaml. Ας δημιουργήσουμε και Sample Data στα γρήγορα για να βλέπουμε τη φτιάχνουμε.

Πηγαίνουμε δεξιά, πατάμε τη καρτέλα Data και Create διαλέγοντας την κλάση που θα κάνουμε populate με τυχαία (τα κλασσικά λατινικά) δεδομένα.

image

imageimageΜετά κάνοντας Drag το Movie στο Layout Root, έχω στο DataContext μου τυχαία δεδομένα, να κοιτάζω. Έχω τρεις περιοχές στην παραπάνω εικόνα. Έναν τίτλο (πάνω αριστερά), rating, περιγραφή και μία εικόνα, κάτω από τον τίτλο, που δεν έγινε populate με sample data, καθώς ο τύπος είναι Uri και το Sample Data από μόνο του δεν παρήγαγε κάποιο Url για να δείξει. Έχοντας το DataContext μου για τη σελίδα ορισμένο σε ένα Movie CLR object, το Expression Blend μπορεί να μου δώσει την δυνατότητα να επιλέξω γραφικά ποιό control θα παίρνει από που, δεδομένα. Επιλέγω το Textbox λοιπόν και πατάω δεξιά στα common properties το Text. Τι θέλω; Να γεμίζει με δεδομένα από την κλάση μου, οπότε πηγαίνω στα advanced properties και επιλέγω το DataContext tab. Εκεί θα βρω τα properties από τη Movie Class μου, και επιλέγω το Title. Κάνω το ίδιο και για τα υπόλοιπα. Στο Rating όμως θέλω και κάτι ακόμα. Όπως το Imdb. Να δείχνει μεν το μέσο όρο αλλά να μπορώ και εγώ να επιλέξω τιμή που αργότερα (θα συνυπολογιστεί στο ολικό μέσο όρο). Οπότε έχουμε μία διαφοροποίηση. Δεν θέλω η τιμή του να έρχεται στο UI μόνο, αλλά να επιλέξω κάτι εγώ και να κληθεί ο setter στο αντίστοιχο property επίσης. Επιλέγουμε λοιπόν το mode και το θέτουμε σε Two Way, όπως φαίνεται στο screenshot. Το μόνο που μένει για το demo μας είναι να θέσουμε με κάποιο τρόπο το DataContext της σελίδας μας σε ένα νέο Movie instance. Αυτό συνήθως το διαχειρίζεται το MVVM καλά, αλλά για την περίσταση μας αρκεί στο construction της σελίδας να γράψουμε ένα

 

public MainPage()         {             // Required to initialize variables             InitializeComponent();             this.DataContext = new Movie             {                 Title = "The Hustler",                 Rating = 0.85,                 Description = "The Hustler is a 1961 American drama film directed by Robert Rossen from the 1959 novel of the same name he and Sidney Carroll adapted for the screen. It tells the story of small-time pool hustler Fast Eddie. Felson and his desire to prove himself the best player in the country by beating legendary pool player Minnesota Fats. After initially losing to Fats and getting involved with unscrupulous manager Bert Gordon, Eddie returns to beat Fats, but only after paying a terrible personal price. The film was shot on location in New York City. It stars Paul Newman as Eddie Felson, Jackie Gleason as Minnesota Fats, Piper Laurie as Sarah, and George C. Scott as Bert.",                 Poster = new Uri("http://i23.photobucket.com/albums/b352/grnemo/the-hustler-paul-newman-jackie-gleason1.jpg")             };         }

Το αποτέλεσμα είναι το παρακάτω, έχοντας πάρει τις τιμές για το Silverlight application μας από κάποιο object του model μας. Ανοίγοντας το project από το Visual Studio, μας ζητάει να ενεργοποιήσουμε στο web config το debugging. Ανοίγουμε την κλάση μας, βαζουμε ένα breakpoint στον setter στο Rating και πατάμε τη δική μας βαθμολογία. Βλέπουμε ότι καλείται ο setter με τη νέα τιμή.

image  image

Έστω ότι θέλω να έχω μία λίστα από ταινίες, ή αντικείμενα και να βλέπω την περιγραφή τους σε παρόμοιο layout. Το DataBinding από την έκδοση 3 του silverlight έχει ένα feature με το οποίο μπορούμε να συνδέσουμε, controls μεταξύ τους, με καταγραφή μόνο των controls που έχουν κάποια τιμή που μας ενδιαφέρει. Θέλουμε λοιπόν, όποτε αλλάζουμε το SelectedItem από τη λίστα, να αλλάζουμε properties σε άλλα controls. Για τις ανάγκες του demo, έφτιαξα ένα demo datasource από το ίδιο menu όπως και πριν (ορίζοντας τον τύπο των dummy δεδομένων μου, ακόμα και της εικόνας), έφτιαξα ένα ListBox και έκανα drag and drop το property name, από το Data Context παράθυρο στο ListBox που ήθελα, όπως φαίνεται στην εικόνα. Στα επιμέρους controls, έβαλα bindings από το Expression Blend, με το ίδιο τρόπο. Επιλέγοντας την τελίτσα δεξιά από το property του κάθε control στο common properties, διαλέγοντας αυτή τη φορά, το tab, Element Property, επιλέγοντας το scene element που με ενδιαφέρει και ορίζοντας το property στο custom path expression. To αποτέλεσμα φαίνεται στην παρακάτω εικόνα.

imageimage

Revealing XAML

Αφού είχαμε το πρώτο μας Demo στο expression blend, ας δούμε τι γίνεται στη XAML που παράγεται από κάτω. Η αλήθεια είναι ότι αρκετές φορές όταν σχεδιάζεται μία σελίδα θα χρειαστεί να γράψετε αυτούσια XAML με το χέρι. Η XAML ορίζει τo λεγόμενo binding markup extension, όπου για το Silverlight (στην τρέχουσα version, 4)  είναι ένα από τα, Binding, StaticResource, TemplateBinding, RelativeSource. Εμάς μας ενδιαφέρει εκείνο το extension που λέγεται binding. Όπως κάθε extension έτσι και το Binding ορίζεται μέσα σε “{” “}” και προσδιορίζει το binding expression μεταξύ εκείνου του Dependency property και του property κάποιου CLR Object. Στην εφαρμογή μας ο τίτλος, η περιγραφή, το poster και το rating έχουν την παρακάτω απεικόνιση σε XAML:

      <TextBlock Text="{Binding Title}" …. />       <TextBlock Text="{Binding Description}" …. />       <Image>                 <Image.Source>                     <BitmapImage UriSource="{Binding Poster}"/>                 </Image.Source>       </Image>       <toolkit:Rating ItemCount="10" Value="{Binding Rating, Mode=TwoWay}"  />

Στo rating, που θέλαμε, όχι μόνο να διαβάζουμε τιμές από το object και να τις απεικονίζουμε, αλλά και να θέτουμε τιμές, πρέπει να δηλώσουμε ότι το DataBinding αφορά και data entry. Αυτό γίνεται με το Mode=TwoWay.

Στη συνέχεια μετασχηματίσαμε το projectaki ώστε να υποστηρίζει selection από listbox και τα αντίστοιχα controls να γεμίζουν πληροφορία, από το Data Context του selected item του Listbox. Το Binding expression είναι διαφορετικό ως εξής. Ας πάρουμε το Image Control για την απεικόνιση του Poster. Δηλώνουμε το element με ElementName και μετά το Path του property που μας ενδιαφέρει. Όμοια και για τα υπόλοιπα.

<Image Margin="236,110,258,178" Source="{Binding  ElementName=MovieList, Path=SelectedItem.Poster}"/>

Το ElementName, το Path και το Mode είναι τρία από τα features του Binding. Ακολουθεί μία λίστα και σύντομη περιγραφή από τα πιο συχνά χρησιμοποιούμενα. Μπορούν αν χρησιμοποιηθούν χωριζόμενα με κόμμα.

Property Περιγραφή
Path To property path που μας ενδιαφέρει από το object που γίνεται bound.
Converter Ένα object που αλλάζει την τιμή ενός object σε μία άλλη (συμβατή με το target dependency property). Υλοποιεί το IValueConverter. Για παράδειγμα. BooleanToVisibility, ή UrlToImage.
ElementName Για databinding μεταξύ elements.
FallbackValue Default τιμή για εμφάνιση αν αποτύχει το data binding.
RelativeSource Πραγματοποιείται bind, με object που βρίσκεται σε μία σχετική θέση (ιεραρχίας) με το target object.
StringFormat Για παραμετροποιήσιμη εμφάνιση των strings.
Source Χρήση όταν θέλουμε να κάνουμε bind με κάτι που δεν βρίσκεται στο DataContext. Για παράδειγμα,  {Binding Source={x:Static DateTime.Now}, Path=Day}

Αξίζει σε αυτό το σημείο να σημειώσουμε κάποια πράγματα. Για αρχή, εξ ορισμού το silverlight θα πετάξει exception αν χρησιμοποιηθούν δύο ή περισσότερα ταυτόχρονα, των Source, RelativeSource και ElementName, για ευνόητους λόγους.

Αξίζει να πούμε ένα παράδεγμα για τον πολύ χρήσιμο Converter.  O converter είναι ένα object που υλοποιεί δύο συναρτήσεις, μία για conversion του source object σε κάτι άλλο και μία την ανάποδη διαδικασία. Έστω ότι θέλουμε να χρησιμοποιήσουμε ένα string formatter έτσι ώστε, όταν έρχεται ένα string object από το source να μετασχηματίζεται σε κάτι άλλο.

<StackPanel> <HyperLink NavigateUri=”{Binding Uri, Converter={StaticResource StringToUriConverter}, ConverterParameter=’{0:d}’}“ /> </StackPanel>

Υλοποιούμε τον converter και τον κάνουμε reference ως static resource στο XAML μας, χρησιμοποιώντας το κανονικά.

public class StringToUriConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return new Uri(value as string); } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } }

Ένα πολύ ωραίο trick όταν βλέπετε τι σας έρχεται από τον binding μηχανισμό στο debug, είναι να κάνετε έναν DebugConverter, υλοποιώντας τον και κάνοντας ότι θέλετε μέσα του, ή απλά να τον έχετε για το πολύτιμo brakepoint που μπορείτε να βάλετε για να κάνετε inspect το value.

Code

Όταν γράφουμε στη XAML το binding, τότε στη πραγματικότητα το Silverlight δημιουργεί ένα instance της κλάσης Binding που ορίζεται στο System.Windows.Data και πραγματοποιεί αυτό ακριβώς το δέσιμο. Μας δίδεται και εμάς η δυνατότητα να πραγματοποιήσουμε δικά μας bindings in code .

<txtEcho Text="{Binding}" .../>

Binding myBinding = new Binding("Text"); myBinding.Source = txtRealBox;   txtEcho.SetBinding(System.Windows.Controls.TextBox.TextProperty, myBinding);

Η βασική προϋπόθεση για να παίξει το binding είναι η κλάση που μπαίνει στο datacontext να υλοποιεί το

public interface INotifyPropertyChanged {     event PropertyChangedEventHandler PropertyChanged; }

Και τα properties που γίνονται bound να κάνουν Raise όταν αυτά γίνονται set (o untyped τρόπος με magic-string στο propertyName).

public void NotifyPropertyChanged(string propertyName)     {         if (PropertyChanged != null)         {             PropertyChanged(this,                 new PropertyChangedEventArgs(propertyName));         }     }

To blog post για το MVVM (μία custom έκδοση δλδ, όπου το ViewModel γνώριζε το View - [Μαθαίνοντας Design Patterns] Model – View – ViewModel) κάνει χρήση αυτών των μηχανισμών.

Ας κλείσω με μία τυπική PIC από το msdn, που τα λέει όλα!

Posted: Δευτέρα, 18 Οκτωβρίου 2010 11:05 πμ από George J. Capnias | 0 σχόλια
Δημοσίευση στην κατηγορία: , ,