I'm finding that in Silverlight 2, I quite often want to write code which does something, waits for it to finish and then does something else.
Say I'm building a video player. I might have a UI like this;
<UserControl
x:Class="SilverlightApplication5.Page"
xmlns="http://schemas.microsoft.com/client/2007"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="400"
Height="300">
<Grid
x:Name="LayoutRoot"
Background="Black">
<Grid.Resources>
<Storyboard
x:Name="fadeIn"
Storyboard.TargetProperty="Opacity"
Duration="00:00:02"
FillBehavior="HoldEnd">
<DoubleAnimation
To="1" />
Storyboard>
<Storyboard
x:Name="fadeOut"
Storyboard.TargetProperty="Opacity"
Duration="00:00:02"
FillBehavior="HoldEnd">
<DoubleAnimation
To="0" />
Storyboard>
Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition
Height="0.8*" />
<RowDefinition
Height="0.2*" />
Grid.RowDefinitions>
<MediaElement
x:Name="media"
Margin="10"
Source="dummyVideo.wmv"
AutoPlay="false"
Stretch="Uniform"
Opacity="0"/>
<Image
x:Name="image"
Source="SL.png"
Margin="10"
Stretch="Uniform" />
<Button
Grid.Row="1"
Margin="10"
Content="Play"
Click="OnPlay" />
Grid>
UserControl>
So...all in all, I just have an Image, MediaElement and a Button and the MediaElement is on top of the Image and transparent. Say that when I click the button, I want to do the following;
- Fade out the image.
- Then...start playing the media.
- Then...fade in the media.
I'm probably missing something about Storyboards. I know that I can play around with their BeginTimes to simulate sequential execution but I want to have a pattern of execution that goes Begin->Wait->Begin->Wait and I want it to potentially span more than just Storyboards. Also, it seems to me that reuse of Storyboards is enhanced by having them small and dedicated to one task rather than trying to build bigger Storyboards that do multiple things.
Regardless...I'm halfway through the post now so I may as well continue :-)
I might write code like;
public partial class Page : UserControl
{
public Page()
{
InitializeComponent();
}
void OnPlay(object sender, EventArgs args)
{
Storyboard.SetTarget(fadeOut, image);
fadeOut.Completed += OnFadeOutImageCompleted;
fadeOut.Begin();
}
void OnFadeOutImageCompleted(object sender, EventArgs args)
{
image.Visibility = Visibility.Collapsed;
fadeOut.Stop();
media.Play();
Storyboard.SetTarget(fadeIn, media);
fadeIn.Completed += OnFadeInMediaCompleted;
fadeIn.Begin();
}
void OnFadeInMediaCompleted(object sender, EventArgs e)
{
fadeIn.Stop();
media.Opacity = 1.0;
fadeIn.Completed -= OnFadeInMediaCompleted;
}
}
and it's all fine and easy enough but I've been writing quite a bit of this and it starts to wind me up. Basically, it's just a list of;
- Do this.
- Wait for that.
- Do something else.
- Wait for it.
So, I've started wrapping some of this up. I wrote an interface;
public interface IAsyncAction
{
event EventHandler Completed;
void Execute();
}
Then I implemented it for a Storyboard;
public class StoryboardAction : IAsyncAction
{
public event EventHandler Completed;
public StoryboardAction(Storyboard storyBoard)
{
this.storyBoard = storyBoard;
}
public StoryboardAction(Storyboard storyBoard,
DependencyObject targetObject) : this(storyBoard)
{
this.storyBoardTarget = targetObject;
}
public void Execute()
{
if (storyBoardTarget != null)
{
Storyboard.SetTarget(storyBoard, storyBoardTarget);
}
storyBoard.Completed += OnStoryboardCompleted;
storyBoard.Begin();
}
void OnStoryboardCompleted(object sender, EventArgs e)
{
storyBoard.Completed -= OnStoryboardCompleted;
if (Completed != null)
{
Completed(this, null);
}
}
DependencyObject storyBoardTarget;
Storyboard storyBoard;
}
And then I wanted a way to chain this together so I wrote a list;
public class ActionList : IAsyncAction
{
public event EventHandler Completed;
public ActionList()
{
}
public ActionList(List actions)
{
this.actions = new List(actions);
}
public ActionList(params IAsyncAction[] actions)
{
this.actions = new List(actions);
}
public void AddAction(IAsyncAction action)
{
Actions.Add(action);
}
List Actions
{
get
{
if (actions == null)
{
actions = new List();
}
return (actions);
}
}
public void Execute()
{
index = 0;
ExecuteActions();
}
public int Count
{
get
{
return (Actions.Count);
}
}
void ExecuteActions()
{
if (index < Actions.Count)
{
Actions[index].Completed += OnActionCompleted;
Actions[index].Execute();
}
else if (Completed != null)
{
Completed(this, null);
}
}
void OnActionCompleted(object sender, EventArgs args)
{
IAsyncAction action = sender as IAsyncAction;
action.Completed -= OnActionCompleted;
index++;
ExecuteActions();
}
List actions;
int index;
}
and that allows me to cut my code down to something like;
public partial class Page : UserControl
{
public Page()
{
InitializeComponent();
}
void OnPlay(object sender, EventArgs args)
{
StoryboardAction fadeOutAction = new StoryboardAction(fadeOut, image);
fadeOut.Completed += (s, a) =>
{
image.Visibility = Visibility.Collapsed;
media.Play();
};
ActionList actions = new ActionList(
fadeOutAction,
new StoryboardAction(fadeIn, media));
actions.Execute();
}
}
and then I figured I might go a little over the top with it and write a DelegateAction class;
public class DelegateAction : IAsyncAction
{
public event EventHandler Completed;
public DelegateAction(Action action)
{
this.action = action;
}
public void Execute()
{
action();
if (Completed != null)
{
Completed(this, null);
}
}
private Action action;
}
and then that drops my original code down to;
public partial class Page : UserControl
{
public Page()
{
InitializeComponent();
}
void OnPlay(object sender, EventArgs args)
{
ActionList actions = new ActionList(
new StoryboardAction(fadeOut, image),
new DelegateAction(
(Action)(
() =>
{
image.Visibility = Visibility.Collapsed;
media.Play();
}
)),
new StoryboardAction(fadeIn, media));
actions.Execute();
}
}
Now, arguably the Lambda Statement in the middle of there isn't doing much to aid readability so perhaps it's better at;
public partial class Page : UserControl
{
public Page()
{
InitializeComponent();
}
void OnPlay(object sender, EventArgs args)
{
ActionList actions = new ActionList(
new StoryboardAction(fadeOut, image),
new DelegateAction(HideVideoPlayMedia),
new StoryboardAction(fadeIn, media));
actions.Execute();
}
void HideVideoPlayMedia()
{
image.Visibility = Visibility.Collapsed;
media.Play();
}
}
Which I prefer. I could see taking this further in terms of writing some easier ways to create particular Action types like the DelegateAction above and also maybe to include things like;
- WebClientAction ( i.e. download some stuff and then run this animation )
- MediaPlayerAction ( i.e. wait for the player to enter state X, Y, Z and then go and do something ).
Feels like an easier way to write this kind of code to me.