I’ve created a custom control I call “ControlFader”. It inherits from the Selector primitive. Items added to the control are placed into a single grid as overlapping controls. The goal is to have an animation where the selected item is faded in and the previously selected item is faded out.
To start, I supply a custom item container (of type ControlFaderItem) which has its opacity set to zero. The following code is used to create the storyboard (FadeTime is defined elsewhere):
private Storyboard CreateStoryboard(ControlFaderItem sourceItem, ControlFaderItem targetItem)
{
var fadeControlsStoryboard = new Storyboard();
if (sourceItem != null)
{
var sourceAnimation = new DoubleAnimation(1, 0, FadeTime);
sourceAnimation.SetValue(Storyboard.TargetProperty, sourceItem);
sourceAnimation.SetValue(Storyboard.TargetPropertyProperty, new PropertyPath(UIElement.OpacityProperty));
fadeControlsStoryboard.Children.Add(sourceAnimation);
}
if (targetItem != null)
{
var targetAnimation = new DoubleAnimation(0, 1, FadeTime);
targetAnimation.SetValue(Storyboard.TargetProperty, targetItem);
targetAnimation.SetValue(Storyboard.TargetPropertyProperty, new PropertyPath(UIElement.OpacityProperty));
fadeControlsStoryboard.Children.Add(targetAnimation);
}
return fadeControlsStoryboard;
}
I added a queue to indicate which item is to be faded next. Items are removed from the queue using this method:
private void ProcessQueue()
{
if (isFadeAnimating)
return;
// can't process if there are no queued items
if (FadeQueue.Count == 0)
return;
// get the next item to fade to
var nextItem = FadeQueue.Dequeue();
// locate the index of the item
var itemIndex = Items.IndexOf(nextItem);
if (itemIndex != -1)
SelectedIndex = itemIndex;
}
I override the OnSelectionChanged event where I:
- check if a fade is already happening and queue items if it is
- start a fade
It looks like this:
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
// code to simulate an OnSelectionChanging is here (removed for brevity)
// it's used to queue a new selection if isFadeAnimating is true, and reverse the selection
// code to obtain source and target container item also removed for brevity
if (isFadeAnimating)
return;
isFadeAnimating = true;
var storyboard = CreateStoryboard(sourceItem, targetItem);
storyboard.Completed += (s, arg) =>
{
if (sourceItem != null)
sourceItem.Opacity = 0d;
isFadeAnimating = false;
base.OnSelectionChanged(e);
ProcessQueue();
};
storyboard.Begin();
}
For some odd reason, the call to ProcessQueue from inside the storyboard completed event seems to break the storyboard. But only if the right sequence of fades is executed.
If I fade from index 0 to 1 then back to 0 quickly enough, the fades stop working. It has to be quick enough that the fade back to 0 is attempted while the first fade is occurring (therefore placing it on the queue).
In a Window where I’m using the ControlFader, to trigger the fades I bind to the SelectedIndex of the ControlFader. The binding stops updating after the fades from index 0 to 1 then back to 0.
When I remove the call to ProcessQueue, the animation and binding never break, but my fades are not in sync (they fall behind) with what should be the current SelectedIndex since the queue isn’t being processed.
I’m stumped. Any suggestions?
The last line in my ProcessQueue method was the problem:
Because I had setup a binding on SelectedIndex, by assigning it inside the controls code, my binding was being removed. Since this was inside the ProcessQueue method, the problem only started to showed up when there were queued items.
I fixed it by using selected item instead:
Since I never set any binding expressions on SelectedItem, the bug is never hit.