I'm creating a fadeIn/fadeOut animation from code behind. I was trying to stop animation at a particular blink time and it is working fine. But I want to make sure that when my animation stops it should stop in fadeOut state. Below is my code:
public void AddAnimation(long blinkDuration = 0)
{
var fadeIn = new DoubleAnimation(0.3, 1, TimeSpan.FromSeconds(1), FillBehavior.HoldEnd)
{
BeginTime = TimeSpan.FromSeconds(0)
};
var fadeOut = new DoubleAnimation(1.0, 0.3, TimeSpan.FromSeconds(1), FillBehavior.HoldEnd)
{
BeginTime = TimeSpan.FromSeconds(0.5)
};
storyboard = new Storyboard();
Storyboard.SetTarget(fadeIn, this);
Storyboard.SetTarget(fadeOut, this);
Storyboard.SetTargetProperty(fadeIn, new PropertyPath("(Opacity)"));
Storyboard.SetTargetProperty(fadeOut, new PropertyPath("(Opacity)"));
storyboard.RepeatBehavior = blinkDuration == 0
? RepeatBehavior.Forever
: new RepeatBehavior(new TimeSpan(0, 0, Convert.ToInt32(blinkDuration)));
storyboard.Children.Add(fadeIn);
storyboard.Children.Add(fadeOut);
storyboard.Begin();
}
My question is how I will make my icon state fadeOut on Storyboard Stop after blink interval.
I have fixed it. I changed my Storyboard.Begin call and registered storyboard.Completed event, on which I'm calling storyboard.Stop().
Below is my changed code:
storyboard.Completed += StoryboardCompleted;
storyboard.Begin(this, true);
void StoryboardCompleted(object sender, EventArgs e)
{
storyboard.Stop(this);
}
You can use a FillBehaviour property value of HoldEnd which will keep the last value (or the To value) that you set in your Storyboard. This way, you can ensure what the end value will be. The only problem that you may have using this property is that you may need to run your animation several times.
To do that, you can attach a handler to the Completed Event and re-run the Storyboard after it finishes if a certain condition is met (number of times run, or seconds passed, or data has arrived, etc.):
private void OnStoryboardCompleted(object sender, EventArgs e)
{
if (someConditionIsTrue) storyboard.Start();
}
Related
I was trying to animate some stuff in WPF and run some other operations when animation finishes.
Also, wanted to avoid animation finish callback mechanism, so, I came up with a solution illustrated in the code below:
// Start one second of animation
...
// Pause for one second
Wait(this.Dispatcher, 1000);
// Continue and do some other stuff
...
Now, the interesting part is Wait method which magically makes blocking pause in my code but the animation and UI stays normal, responsive:
public static void Wait(Dispatcher Dispatcher, int Milliseconds)
{
var Frame = new DispatcherFrame();
ThreadPool.QueueUserWorkItem(State =>
{
Thread.Sleep(Milliseconds);
Frame.Continue = false;
});
Dispatcher.PushFrame(Frame);
}
I have read a documentation and a few articles about DispatcherFrame but I am still unable to figure out what is really happening under the hood, and I need some clarification about how this construction with PushFrame really works.
From MSDN:
PushFrame
Enters an execute loop.
That loop (i.e. the DispatcherFrame) executes as long as its Continue property is true.
As soon as the Continue property goes to false, the loop/frame is exited and the Dispatcher returns to the loop/frame that it executed before you called PushFrame.
If you want take a pause, why not anything like this?
private async void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
var btn = sender as Button;
btn.Content = "Before pause";
var animation = new DoubleAnimation();
animation.From = btn.ActualWidth;
animation.To = 100;
animation.Duration = TimeSpan.FromSeconds(2);
btn.BeginAnimation(Button.WidthProperty, animation);
await Task.Delay(2000);
btn.Content = "After pause";
}
I am trying to make a fading effect on my splash screen, on a WPF application.
The Opacity of the image object is initially 0. This code would modify the Opacity from 0 (min) to 1 (max), but the line img_waves.Opacity just doesn't work. The image opacity remains 0.
private void Splash_ContentRendered(object sender, EventArgs e)
{
System.Threading.Thread.Sleep(3000);
for (double x = 0; x<=1; x+=0.01d)
{
System.Threading.Thread.Sleep(15);
//MessageBox.Show(x.ToString());
img_waves.Opacity = x;
}
this.Close();
}
But, if I activate the line ´MessageBox.Show(x.ToString());´
as you can see on this image:
The code works, but I have to keep clicking on the message boxes.
My ask is: Why? Why doesn't work without the MessageBox.Show?
Because you're blocking the GUI thread. It never gets a chance to redraw the form. When you add the message box, the message queue is pumped, which allows the drawing.
The simplest way to deal with this would be like this:
private async void Splash_ContentRendered(object sender, EventArgs e)
{
await Task.Delay(3000);
for (double x = 0; x<=1; x+=0.01d)
{
await Task.Delay(15);
img_waves.Opacity = x;
}
this.Close();
}
Do note that this means the form can still be interacted with during the animation. This shouldn't be a problem for a splashscreen, but it could cause you trouble in a "real" form. Still, make sure the form can't be closed during the animation - that could cause exceptions :)
There's also other ways to force the message queue to be pumped, but it's usually frowned upon.
All that said, you're using WPF - why are you doing the animation manually like this? Can't you just handle it as an animation effect in WPF, natively? There's a sample on MSDN.
Whilst I agree with #Luaan explanation as to why as an alternative solution to your loop you can use Storyboard with DoubleAnimation on Opacity property
private void Splash_ContentRendered(object sender, EventArgs e)
{
var sb = new Storyboard();
var da = new DoubleAnimation(0, 1, new Duration(TimeSpan.FromSeconds(1.5)));
da.BeginTime = TimeSpan.FromSeconds(3);
Storyboard.SetTargetProperty(da, new PropertyPath("Opacity"));
Storyboard.SetTarget(da, img_waves);
sb.Children.Add(da);
sb.Completed += (s1, e1) => this.Close();
sb.Begin();
}
I refactored my WPF code recently and now my DispatcherTimer stopped firing. I checked other similar posts here, but they all seemed to be problems with having the wrong dispatcher thread set, which I tried...
My code looks like this:
class MainWindow : Window
{
private async void GoButton_Click(object sender, RoutedEventArgs e)
{
Hide();
m_files = new CopyFilesWindow();
m_files.Show();
m_dispatcherTimer = new DispatcherTimer();
m_dispatcherTimer.Tick += dispatcherTimer_Tick;
m_dispatcherTimer.Interval = new TimeSpan(0, 0, 0, 0, 250);
m_dispatcherTimer.Start();
await SomeLongRunningTask();
m_files.Hide();
Show();
}
(The current class is my main Window object, which I hide for the duration of file copying. CopyFilesWindow is a simple Xaml window that contains controls I modify...CopyFilesWindow does absolutely nothing itself.)
Basically, I await a long running task (copying a bunch of large files), and my DispatcherTimer is supposed to update the progress in dispatcherTimer_Tick. However, I set a breakpoint on that function and it doesn't get hit.
I have also tried setting the Dispatcher with the constructor like so:
m_dispatcherTimer = new DispatcherTimer(DispatcherPriority.Normal, m_files.Dispatcher);
m_dispatcherTimer = new DispatcherTimer(DispatcherPriority.Normal, this.Dispatcher);
But neither of these things change the behavior...it still doesn't fire.
What am I doing wrong here?
The DispatcherTime runs on the ... Dispatcher thread. Which is stuck waiting SomeLongRunningTask() to finish.
Indeed, when you press the button Go, it is the dispatcher thread which executes GoButton_Click. Thus, you should never make a method called by UI (the dispatcher thread) async.
private void GoButton_Click(object sender, RoutedEventArgs e)
{
Hide();
m_files = new CopyFilesWindow();
m_files.Show();
m_dispatcherTimer = new DispatcherTimer();
m_dispatcherTimer.Tick += dispatcherTimer_Tick;
m_dispatcherTimer.Interval = new TimeSpan(0, 0, 0, 0, 250);
m_dispatcherTimer.Start();
SomeLongRunningTask.ContinueWith(() =>
{
// Executes this once SomeLongRunningTask is done (even if it raised an exception)
m_files.Hide();
Show();
}, TaskScheduler.FromCurrentSynchronizationContext()); // This paramater is used to specify to run the lambda expression on the UI thread.
}
I've spent the last few days looking at the DispatcherTimer and I still can't wrap my head around some stuff. Here's what I understand so far,
the tick event will not occur twice at the same time link
there is no need to worry about the owner thread(s) of the objects
since the dispatcher timer automatically performs all the work in
the UI thread
the timing of the ticks may not be very accurate since the ticks are essentially executed from a queue
Now what I'm not clear about is the order of the code being executed if there is another event which runs in between a tick event. I've a test WPF application which uses a DispatcherTimer whose tick event performs 2 functions. firstStep() and secondStep() in sequence.
The firstStep()sets a variable to null while secondStep() sets it to a value that is not null. After setting the value, secondStep() will begin a storyboard which has a Completed event, which attempts to access this variable.
So my question is, is it possible for the Completed event to come in between the firstStep() and secondStep() function if we keep the timer running? I've written a test application and it seems to be that case, eventually we will reach a state where the variable is null when the Completed event gets executed. But I don't understand how that can happen, since firstStep() and secondStep() get executed in sequence, there should be no way the Completed event can be executed between the 2 functions (or I am wrong here). Does the UI thread execute the tick and the Completed event in parallel?
Can someone explain to me in detail how the UI thread executes events such as the example's storyboard completed event and dispatcherTimer's ticks in sequence? Thanks for reading, your comments are very much appreciated I'm trying very hard to get my head around this. The following is the test code I used, it will eventually throw an error after running for a while.
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
storyBoardTest = new Storyboard();
storyBoardTest.Completed += new EventHandler(storyBoardTest_Completed);
DoubleAnimation animation = new DoubleAnimation(1, 0.9, new Duration(TimeSpan.FromSeconds(1)));
Storyboard.SetTarget(animation, this);
Storyboard.SetTargetProperty(animation, new PropertyPath(UIElement.OpacityProperty));
storyBoardTest.Children.Add(animation);
DispatcherTimer dt = new DispatcherTimer();
dt.Interval = TimeSpan.FromMilliseconds(500);
dt.Tick += new EventHandler(dt_Tick);
dt.Start();
}
private Window windowTest = null;
private Storyboard storyBoardTest = null;
void dt_Tick(object sender, EventArgs e)
{
firstStep();
secondStep();
}
private void firstStep()
{
windowTest = null;
}
private void secondStep()
{
windowTest = this;
storyBoardTest.Stop();
storyBoardTest.Begin(this);
}
void storyBoardTest_Completed(object sender, EventArgs e)
{
//Attempt to access object throws null error. Why?
windowTest.Title = "test";
windowTest = null;
}
}
CallStack:
WpfApplication1.exe!WpfApplication1.Window1.storyBoardTest_Completed(object sender = {System.Windows.Media.Animation.ClockGroup}, System.EventArgs e = null) Line 63 C#
PresentationCore.dll!System.Windows.Media.Animation.Clock.FireEvent(System.Windows.EventPrivateKey key) + 0x5b bytes
PresentationCore.dll!System.Windows.Media.Animation.Clock.RaiseAccumulatedEvents() + 0x160 bytes
PresentationCore.dll!System.Windows.Media.Animation.TimeManager.RaiseEnqueuedEvents() + 0x60 bytes
PresentationCore.dll!System.Windows.Media.Animation.TimeManager.Tick() + 0x28a bytes
PresentationCore.dll!System.Windows.Media.MediaContext.RenderMessageHandlerCore(object resizedCompositionTarget) + 0xbc bytes
PresentationCore.dll!System.Windows.Media.MediaContext.AnimatedRenderMessageHandler(object resizedCompositionTarget) + 0x9d bytes
Every 500 milliseconds you are starting a Storyboard that runs for one second. This will inevitably lead to two consecutive Completed events without an intermediate Tick event.
Therefore you have to check if windowTest is already null in your Completed handler :
void storyBoardTest_Completed(object sender, EventArgs e)
{
if (windowTest != null)
{
windowTest.Title = "test";
windowTest = null;
}
}
Even if the Storyboard would run for less than 500 milliseconds there would be problem. As Storyboard.Completed events are appended to the Dispatcher queue in the same way as DispatcherTimer.Tick events and the timings of both DispatcherTimer and Storyboard are not exact, the execution order of the two event handlers is not reliable. Hence two Completed events may occur without an intermediate Tick event.
You may add some trace output to see that both handlers run in the same thread.
void dt_Tick(object sender, EventArgs e)
{
Trace.TraceInformation("Tick: {0}", Thread.CurrentThread.ManagedThreadId);
...
}
void storyBoardTest_Completed(object sender, EventArgs e)
{
Trace.TraceInformation("Completed: {0}", Thread.CurrentThread.ManagedThreadId);
...
}
While scaling a panel using a scale transform the application needs to reset the panel back to its original size. For this purpose a reset button starts a double animation that animates the scale transform from it's start value to 1 which means the panel will have it original value.
Visually the panel is scaled back to orignal size, but after the animation finishes the storyboard's complete event is raised twice, and once both of those events has been raised the value of the scale transform is set back to the value that it had before the animation.
private void ResetButton_Click(object sender, RoutedEventArgs e)
{
if (!isReseting)
{
isReseting = true;
this.doubleAnimation = new DoubleAnimation(1, new Duration(new TimeSpan(0,0,0, 1)), FillBehavior.Stop);
this.resetStoryboard = new Storyboard();
resetStoryboard.Children.Add(doubleAnimation);
Storyboard.SetTarget(doubleAnimation, zoomSliderControl);
Storyboard.SetTargetProperty(doubleAnimation, new PropertyPath(RangeBase.ValueProperty));
resetStoryboard.RepeatBehavior = new RepeatBehavior(1);
resetStoryboard.Completed += new EventHandler(ResetStoryboardCompleted);
resetStoryboard.Begin();
}
}
private void ResetStoryboardCompleted(object sender, EventArgs e)
{
if (resetStoryboard != null)
{
//resetStoryboard.Stop(zoomSliderControl);
//resetStoryboard.Remove(zoomSliderControl);
}
resetStoryboard = null;
doubleAnimation = null;
isReseting = false;
}
For example, if the value of the Slider control (named zoomSliderControl) is 1.5 before the animation, then it animates back to 1 as expected, but once the completed event of resetStoryBoard has been raised twice it is set back to 1.5 again.
I've tried debugging the application, and it's right after the second ResetStoryboardCompleted method has termined that the value is set to its original value so I'm guessing that I haven't configured the storyboard or animation correctly.
Appearently the default behaviour of storyboards is to revert to the original value once they have finished or(?) stopped. So the fix to this issue was to set the value of zoomSliderControl to the desired value upon storyboard completion.