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, αυτόματα.

Το πρώτο πρόβλημα μπορούμε να το χειριστούμε με 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>

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