Storyboardy zgodnie z duchem MVVM

Projektując programy w technologiach XAMLowych z wykorzystaniem MVVM Light Toolkit (w zasadzie liczy się wzorzec), zawsze napotykam problemy, na które trudno znaleźć jednoznaczną odpowiedź. Najczęściej kłopoty sprawia widok, który nie zawsze „chce” się zmieścić w kodzie xaml. Prostsze zadania można rozwiązać przy pomocy zachowań (?) czy też triggerów. Sprawy się komplikują w przypadku storyboardów. Co zrobić, jeżeli chcemy uruchomić animacje nie po zdarzeniu, a po wykonaniu konkretnego kroku we View-Model?

Czym się zajmiemy?

Odpowiedź na powyższe pytanie nie jest zbyt skomplikowana. Wystarczy wykorzystać gotowe rozwiązanie takie jak StoryboardManager. Kilka kroków i możemy zarządzać animacjami we View-Model.

Skoro można ten problem łatwo rozwiązać – skomplikujmy go trochę. Tym razem chcemy, aby po zakończeniu animacji wykonana została komenda we View-Model, a później kolejna animacja. Np. mamy tablicę, która się chowa „za ekran”, zawartość tablicy się zmienia, następnie triumfalnie wjeżdża na ekran wniebowziętego użytkownika. Zakładam, że nie chcemy złamać MVVM (może czasem warto?).

Co przyszło do głowy?

Tak jak w storyboard można ustawić kolejkę animacji, tak tu potrzebujemy kontenera, który będzie wykonywał kolejne akcje. W tym celu stwórzmy klasę ActionQueue, która zawierać będzie listę akcji oraz metodę start. Aby można było uzupełnić wszystko w kodzie xaml (o to przecież chodzi), dziedziczymy po DependencyObject. Lista akcji musi być uniwersalna, żeby można było podpiąć zarówno storyboardy, komendy itp. Dlatego utworzymy interfejs IQueuedAction. Musimy posiadać takie informacje jak: akcja zakończona i metoda startująca akcję.

public interface IQueuedAction
{
	event EventHandler Completed;

	void Start();
}

A oto kod głównej klasy:

public class ActionQueue : DependencyObject
{
	private int _timelineIndex = 0;
	private bool _isPlaying = false;

	public ObservableCollection<IQueuedAction> Actions
	{
		get;
		private set;
	}

	public ActionQueue()
	{
		Actions = new ObservableCollection<IQueuedAction>();
	}

	public void Start()
	{
		// Kolejka uzupełniona i nie w trakcie odtwarzania
		if (!_isPlaying && Actions.Count > 0)
		{
			_isPlaying = true;
			_timelineIndex = 1;

			Actions[0].Completed += NextAction;
			Actions[0].Start();
		}
	}

	// Po zakończeniu każdej akcji odpinamuy się i startujemy kolejną
	private void NextAction(object sender, EventArgs e)
	{
		Actions[_timelineIndex - 1].Completed -= NextAction;
		if (_timelineIndex == Actions.Count)
		{
			_timelineIndex = 0;
			_isPlaying = false;
		}
		else
		{
			Actions[_timelineIndex].Completed += NextAction;

			_timelineIndex++;
			Actions[_timelineIndex - 1].Start();
		}
	}
}

Kolekcje można zastąpić zwykłą listą, ale wykorzystując ObservableCollection<> pozostawiamy sobie możliwość zbindowania danych.

Implementujemy IQueuedAction

Potrzebujemy jeszcze obiektów, które możemy włożyć do kolekcji akcji. Zacznijmy od storyboardów:

public class QueuedStoryboardAction : DependencyObject, IQueuedAction
{
	public event EventHandler Completed;

	public const string StoryboardPropertyName = "Storyboard";
	public Storyboard Storyboard
	{
		get
		{
			return (Storyboard)GetValue(StoryboardProperty);
		}
		set
		{
			SetValue(StoryboardProperty, value);
		}
	}
	public static readonly DependencyProperty StoryboardProperty
	= DependencyProperty.Register(
		StoryboardPropertyName,
		typeof(Storyboard),
		typeof(QueuedStoryboardAction),
		new PropertyMetadata(null));

	public void Start()
	{
		if (Storyboard != null)
		{
			Storyboard.Completed += StoryboardCompleted;
			Storyboard.Begin();
		}
	}

	private void StoryboardCompleted(object sender, EventArgs e)
	{
		Storyboard.Completed -= StoryboardCompleted;

		if (Completed != null)
			Completed(null, EventArgs.Empty);
	}
}

Powyższy kod raczej nie wymaga komentarza. Umożliwiamy bindowanie storyboardów.

Kod dla ICommand jest równie prosty:

public class QueuedCommandAction : DependencyObject, IQueuedAction
{
	public event EventHandler Completed;

	public const string CommandPropertyName = "Command";
	public ICommand Command
	{
		get
		{
			return (ICommand)GetValue(CommandProperty);
		}
		set
		{
			SetValue(CommandProperty, value);
		}
	}

	public static readonly DependencyProperty CommandProperty 
	= DependencyProperty.Register(
		CommandPropertyName,
		typeof(ICommand),
		typeof(QueuedCommandAction),
		new PropertyMetadata(null));

	public const string CommandParameterPropertyName = "CommandParameter";
	public object CommandParameter
	{
		get
		{
			return (object)GetValue(CommandParameterProperty);
		}
		set
		{
			SetValue(CommandParameterProperty, value);
		}
	}

	public static readonly DependencyProperty CommandParameterProperty 
	= DependencyProperty.Register(
		CommandParameterPropertyName,
		typeof(object),
		typeof(QueuedCommandAction),
		new PropertyMetadata(null));

	public void Start()
	{
		if (Command.CanExecute(CommandParameter))
			Command.Execute(CommandParameter);

		if (Completed != null)
			Completed(null, EventArgs.Empty);
	}
}

ActionQueue w kodzie XAML

Teraz możemy w łatwy sposób skorzystać z nowej klasy w kodzie xaml:

<helpers:ActionQueue x:Name="ActionQueue1">
	<helpers:ActionQueue.Actions>
		<helpers:QueuedStoryboardAction 
		         Storyboard="{StaticResource Storyboard1}" />
		<helpers:QueuedCommandAction 
		         Command="{Binding SomeCommand, 
		         Source={StaticResource Locator}}" />
		<helpers:QueuedStoryboardAction 
		         Storyboard="{StaticResource Storyboard2}" />
	</helpers:ActionQueue.Actions>
</helpers:ActionQueue>

Jak to uruchomić? Można dopisać własny behavior lub trigger, który to uruchamia. Gdy już tyle się namęczyliśmy, głupio byłoby wywołać metodę Start z code-behind…

Share

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

Spam protection by WP Captcha-Free