Silverlight i behawiory cz. 2

Mam nadzieję, że poprzednim odcinku udało mi się przekonać Was do zainteresowania behawiorami od strony czysto konsumenckiej. Jak już wspominałem spora część użytkowników Expression Blend korzysta na co dzień z tej technologii nie mając o tym zielonego pojęcia – i bardzo dobrze! Tym razem razem postaram się pokazać, że wbrew pozorom tworzenie własnych behawiorów nie jest czynnością skomplikowaną, a potrafi skutecznie podnieść komfort naszej pracy.

W opisywanym poniżej, zupełnie podstawowym przykładzie skupimy na przygotowaniu akcji (action), która symulować ma często spotykany efekt rozmycia (fade out).

Szablony

W dokumentacji dystrybuowanej wraz z Blend SDK można znaleźć kilka podstawowych przykładów prezentujących sposoby tworzenia własnych behawiorów. Zamiast bazować na zawartym tam kodzie proponuję sięgnąć po szablony, które znaleźć można w samym Blendzie. Swoją drogą zdaje się, że kolejna wersja SDK zawierać ma również szablony dla Visual Studio - póki co możemy cieszyć się myślą, że Blend jest w tym aspekcie lepszym narzędziem programistycznym niż sztandarowe IDE Microsoftu ;)

Po utworzeniu w Expression Blend 3 pustego projektu aplikacji Silverlight (Silverlight 3 Application + Website) dodajmy do niego nowy składnik (File > New Item lub po prostu Ctrl+N):

image

Na liście dostępne są  3 podstawowe typy behawiorów – nas interesować będzie w tym wypadku akcja. Korzystając z okazji nazwijmy nowy obiekt FadeOutAction.cs. Po kliknięciu przycisku OK w naszym projekcie pojawi się nowy plik klasy, oparty o gotowy, predefiniowany szablon. Dzięki zawartym w nim komentarzom możemy łatwo zorientować się, że do zaimplementowania w podstawowym scenariuszu pozostają 2 metody: konstruktor klasy oraz metoda Invoke, która odpowiada za implementację obsługi zdarzenia. Zanim zajmiemy się tworzeniem logiki, warto na samym początku zmienić definicję klasy tak, aby móc łatwiej operować na właściwościach wizualnych obiektu. W tym celu zastępujemy linę:

 public class FadeOutAction : TriggerAction<DependencyObject> 

wpisem:

 public class FadeOutAction : TriggerAction<FrameworkElement> 

Ponieważ będziemy korzystać z animacji, zadbajmy również o właściwą widoczność przestrzeni nazw. Dodajemy jeszcze jedną klauzulę using:

 using System.Windows.Media.Animation;

 

Prosta animacja

Gwoli wyjaśnienia FrameworkElement jest potomkiem klasy DependencyObject rozszerzonym o szereg istotnych (jak się za chwilę okaże) dla nas atrybutów. Efekt rozmycia najprościej będzie uzyskać zwiększając stopniowo przezroczystość obiektu. Aby zachować płynność potrzebna nam będzie animacja, zmieniająca wartość zmiennej Opacity do zera (wtedy obiekt stanie się całkowicie niewidoczny). Realizacja tych czynności wymaga dosłownie kilku linii kodu, które umieszczamy w metodzie Invoke:

         protected override void Invoke(object o)
        {
            //tworzymy nowy storyboard
            Storyboard myStoryboard = new Storyboard();
            //definiujemy nową animację
            DoubleAnimation OpacityAnim = new DoubleAnimation();
            OpacityAnim.To = 0;
            OpacityAnim.Duration = TimeSpan.FromSeconds(5);
            //kojarzymy animację z właściwością obiektu
            myStoryboard.Children.Add(OpacityAnim);
            Storyboard.SetTarget(OpacityAnim, AssociatedObject);
            Storyboard.SetTargetProperty(OpacityAnim, new PropertyPath("(Opacity)"));
            //rozpoczynamy odtwarzanie animacji
            myStoryboard.Begin();
        }

Zbudujmy teraz nasz projekt (Project > Build Project lub Ctrl+Shift+B) i – jeśli wszystko udało się pomyślnie – w oknie Assets będziemy mogli znaleźć gotową do wykorzystania akcję o nazwie FadeOutAction:

image

Możemy szybko sprawdzić poprawność jej działania przypisując ją do dowolnego wizualnego elementu naszej aplikacji. W moim przypadku posłużyłem się po prostu standardową kontrolką typu TextBlock, na którą metodą drag’n’drop przeciągnąłem nasz behawior:

image

Czy to wszystko? Aby całość zadziałała brakuje jeszcze podstawowego elementu, czy zdarzenia, które wyzwalać ma naszą akcję. Wybierzmy w oknie Object and Timeline naszą akcję i przenieśmy się na chwilę do zakładki Properties. W tym miejscu określamy, jakie zdarzenie ma być wyzwalaczem. Może być nim na przykład kliknięcie lewym przyciskiem myszy:

image

Po uruchomieniu projektu (Project > Run Project lub klawisz F5) i kliknięciu na napis powinniśmy zobaczyć, jak w ciągu 5 sekund stopniowo zaniknie.

Obsługa własnych parametrów

Następnym krokiem, który warto rozważyć w naszym scenariuszu jest dostarczenie mechanizmu, który pozwoli na dostosowanie właściwości naszego efektu bez potrzeby modyfikowania kodu. Nie ma z tym większego problemu, behawiory da się parametryzować wykorzystując do tego mechanizm Dependency Properties (DP). DP to specyficzna właściwość obiektu, która (w dużym uproszczeniu) dzięki swojej specyficznej konstrukcji pozwoli na łatwą integrację ze środowiskiem Expression Blenda. Poniższy fragment kodu zdefiniuje prywatną właściwość duration “opakowując ją” w stosowne Dependency Property:

         private Duration duration;

        public double Duration
        {
            get { return (double)GetValue(DurationProperty); }
            set { SetValue(DurationProperty, value); }
        }
        public static readonly DependencyProperty DurationProperty =
            DependencyProperty.Register("Duration", typeof(double), typeof(FadeOutAction),
            new PropertyMetadata(0.5, new PropertyChangedCallback(OnDurationChanged)));

        private static void OnDurationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((FadeOutAction)d).OnDurationChanged(e);
        }

        protected virtual void OnDurationChanged(DependencyPropertyChangedEventArgs e)
        {
            duration = TimeSpan.FromSeconds((double)e.NewValue);
        }

Zanim złapiesz się za głowę pamiętaj – większość kodu obsługującego Dependency Properties można wygenerować automatycznie, wykorzystując do tego gotowe snippety. Mam nadzieję, że z grubsza zorientowałeś się, że zamierzamy udostępnić na zewnątrz właściwość o nazwie Duration i domyślnej wartości równej 0,5 (sekundy). Ostatnią zmianą, jaką musimy uwzględnić to korekta metody Invoke, gdzie linię:

 OpacityAnim.Duration = TimeSpan.FromSeconds(5);

zamieniamy na:

 OpacityAnim.Duration = duration;.csharpcode, .csharpcode pre
{
   font-size: small;
   color: black;
   font-family: consolas, "Courier New", courier, monospace;
   background-color: #ffffff;
  /*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt 
{
   background-color: #f4f4f4;
  width: 100%;
    margin: 0em;
}
.csharpcode .lnum { color: #606060; }

Voila! Zaglądając teraz do właściwości naszej akcji możemy zobaczyć i zmodyfikować czas trwania efektu rozmycia:

image

Końcowy efekt naszych prac zobaczyć można tutaj.