I'm experiencing a problem where the I am unable to issue a Stop() command to a Storyboard in the Completed event.
For example
private void ShowStandbyGrid()
{
StandbyGrid.Visibility = Visibility.Visible;
Storyboard sbFadeIn = this.FindResource("StandbyGridFadeIn") as Storyboard;
sbFadeIn.Completed += (s, a) =>
{
Console.WriteLine("Completed Event Fired");
sbFadeIn.Stop(this); //<-- This does not cause the storyboard animation to stop
};
sbFadeIn.Begin(this, true);
}
I can see the event is firing, however it doesn't seems want to stop the Storyboard Animation.
Because of this problem, every time when doing something to the UI, this animation will run continuously. I believe it is because the animation never got stopped.
Am I missing something?
UPDATE 1
Here's the xaml for the storyboard
<Storyboard x:Key="StandbyGridFadeIn">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="StandbyGrid">
<EasingDoubleKeyFrame KeyTime="0" Value="0"/>
<EasingDoubleKeyFrame KeyTime="0:0:1" Value="1"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
UPDATE 2
OK, after 8+ hours of testing and trying, I still have no ideas why it is not working. However, here are several facts (or problem/discovery)
I created a button to run the storyboard in reverse direction. The storyboard executed as expected (the inverse of StandbyGridFadeIn, so more like FadeOut). However, after it is performed, the StandByGridFadeIn automatically executed again without any function calls. When it is done, I saw the console output fire 2 x "Completed Event Fired".
I output GetCurrentState(this); before the sbFadeIn.Stop(this); The output shows "Filling". I then output GetCurrentState(this); after the sbFadeIn.Stop(this);, the output still shows "Filling".
I did another test, I put sbFadeInStop(this); immediately after sbFadeIn.Begin(this, true); The animation stopped successfully.
I want to specifically discuss observation #2, considered that the event is fired if and only if the animation completed, the first GetcurrentState(this) should already output "Stopped". Is that correct?
Judging from the code at
https://stackoverflow.com/a/5645796/903783
you may need to use
Dispatcher.Invoke(DispatcherPriority.Normal,
(Action)delegate() { sbFadeIn.Stop(); });
in recent C# you probably can just write
Dispatcher.Invoke(DispatcherPriority.Normal,
()=> sbFadeIn.Stop() );
however, as others have commented below, it is unlikely to be the issue since you should be already executing on the UI thread
Update:
The comment by Clemens above is also a probable path to try, that is you may need to change the FillBehavior to Stop instead of the default HoldEnd
https://msdn.microsoft.com/en-us/library/system.windows.media.animation.timeline.fillbehavior(v=vs.110).aspx
Alternative is to Remove the StoryBoard (e.g. MyStoryboard.Remove(MyControl)) as shown at:
https://learn.microsoft.com/en-us/dotnet/framework/wpf/graphics-multimedia/how-to-set-a-property-after-animating-it-with-a-storyboard
There is a sample there that even shows how to remove an animation from a particular property using BeginAnimation and passing a null 2nd parameter
Related
I have a Storyboard in XAML with ReapeatBehaviour="Forever". I run the storyboard when uploading files, which can vary quite a bit in size. I can stop the storyboard no problem when the file upload is complete, but what I'd like to do is run the storyboard just one more time, rather than stopping it midflow by calling Storyboard.Stop().
How do I accomplish this?
Ah sussed it - instead of calling Storyboard.Stop(), change the RepeatBehaviour property instead:
Storyboard11.RepeatBehavior = new RepeatBehavior(1.0);
I have a storyboard that I would like to run multiple times. Unfortunately, after it runs the first time, even when this code is hit, it doesn't animate again. It only animates the first time. I'm setting an initial value....even when I set the value before I run it again.
DoubleAnimationUsingKeyFrames animateTransformX = new DoubleAnimationUsingKeyFrames();
EasingDoubleKeyFrame initialTransformXKeyframe = new EasingDoubleKeyFrame();
initialTransformXKeyframe.KeyTime = TimeSpan.FromSeconds(0);
//Resetting the value before running it again.
initialTransformXKeyframe.Value = 0;
animateTransformX.KeyFrames.Add(initialTransformXKeyframe);
EasingDoubleKeyFrame animateTransformXKeyframe = new EasingDoubleKeyFrame();
animateTransformXKeyframe.KeyTime = TimeSpan.FromSeconds(0.5);
animateTransformXKeyframe.Value = aDynamicValueGoesHere;
animateTransformX.KeyFrames.Add(animateTransformXKeyframe);
Storyboard.SetTarget(animateTransformX, image);
Storyboard.SetTargetProperty(animateTransformX, new PropertyPath("(UIElement.RenderTransform).(TransformGroup.Children)[0].(TranslateTransform.X)"));
Storyboard myStoryboard = new Storyboard();
myStoryboard.Children.Add(animateTransformX);
myStoryboard.Begin(this);
I feel like it is very simple but for the life of me I can't understand why. Any help would be appreciated.
EDIT
The reason I was doing it from codebehind is because storyboards are freezable and I have had trouble in the past with dynamic values in them. For simplicity I didn't put that I was using a dynamic value in the code; I updated the code above to show I'm using a dynamic value.
I tried adding the storyboard as XAML inside a resource with the use of a dynamic value. It works fine (and replays) if a non-dynamic value is used but it only plays once (the first time) when a dynamic value is used.
So, if {Binding OffsetX} were replaced with 50, for example, this works fine, and repeatedly.OffsetX gets updated through a property that implements INotifyPropertyChanged. Even if it were the same, if it were never updated, I would think it should still animate, just to the same value it animated to before (like it does with 50). But that isn't the case; it just doesn't animate again. So now I'm really lost.
<Storyboard x:Key="InitialAnimate">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(TranslateTransform.X)" Storyboard.TargetName="StoryImage">
<EasingDoubleKeyFrame KeyTime="0:0:0.6" Value="{Binding OffsetX}"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
EDIT 2
I didn't fix the problem but I found a workaround that works for my particular situation. I was getting this error in my output window:
System.Windows.Media.Animation Warning: 6 : Unable to perform action because the specified Storyboard was never applied to this object for interactive control.; Action='Stop'; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='28715394'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='6255521'; TargetElement.Type='System.Windows.Controls.Grid'
I found a few StackOverflow answers regarding it, including how to find what Storyboard caused the issue and another unanswered question.
I am still not sure how to fix my original issue, but here's my workaround. Since I only needed the animation to run when you restarted (an event that happens in my viewmodel), I use an event raised in the viewmodel and consumed in the parent view to remove and then re-add the UserControl that contains the animation.
In that UserControl, I trigger the storyboard through a ControlStoryboardAction tied to a Loaded EventTrigger. This makes the animation run when it loads, and it only ever runs the animation once during the life of the UserControl. I am able to use the dynamic values in XAML. Here's how I'm removing and readding the view from it's parent:
Parent View:
<grid x:Name="imageHolder></grid>
Parent Codebehind:
private void CreateAnimations(object sender, EventArgs e)
{
imageHolder.Children.RemoveAt(0);
Views.Image image = new Image();
image.Name = "image";
imageHolder.Children.Add(image);
}
You can't change the Value property at runtime the way you're doing after the edit.
This is because Animations are freezable objects. There is more
information in the MSDN Documentation, but basically it means you
can't use binding because properties in the frozen object (i.e. the
animation) cannot change.
To get around this limitation, you will need to do some or all of the
work in code-behind.
See Why Storyboard value wouldn't get from a resource or binding?
So You probably want to go back to the codebehind method.
So as a final answer, what I would do is when I want to run it again:
Remove the previous storyboard
create a whole brand new Storyboard(or just the animation) and run that.
In my application I use two sliders to control the brightness and contrast of certain images and the image has to be completely recalculated pixel by pixel every single time when either one of the two sliders changes its value-property.
The recalculation of smaller images goes completely fine and doesn't cause any problems, however, larger images need longer to be recalculated and the slider thumb moves with a slight delay compared to the mouse pointer. I do need the image to be recalculated more or less in real time so simply having an event on DragCompleted or similarly is not acceptable.
The recalculation is initialized using the ValueChanged-event of the slider.
I think a good solution to this problem would be if the event is not fired as quickly as possible but will at least wait say 50ms before it is fired again, but is there a property of a slider that can control that?
Another solution I was thinking of, is removing the event from the slider right at the start when the event gets handled and adding it again some short time later, but that might cause some delay itself which is also not preferred here.
I couldn't really find anything on this topic anywhere, so if somebody has any good suggestions or directions I could use, I would be very greatful.
You can also make use of BindingBase.Delay property introduced in WPF 4.5.
Simply bind Slider's value to a dependency property setting Delay on the binding. This will cause value updates only after certain time (e.g. 500 ms) and this can make your app smoother.
If you think your application don't need to do the calculations every time the ValueChanged event is triggered,You can use the DragCompleted Event in Thumb control to determine the position after the user finished dragging the control.
<Slider Thumb.DragCompleted="Slider_DragCompleted_1"
Height="27" Margin="132,162,0,0" VerticalAlignment="Top" Width="303"/>
When the user stopped dragging,
private void Slider_DragCompleted_1(object sender, DragCompletedEventArgs e)
{
Slider s = sender as Slider;
// Your code
MessageBox.Show(s.Value.ToString());
}
But beware that this works only when user drags the slider.This doesn't get triggered when user clicks on the slider.
Refer this for handling other events like mouse click etc..
If you want to calculate with some time delay then you can use a timer .
EDIT:
Based on your request you can do like this.
In the 'ValueChanged' event.
// Start a new thread only if the thread is stopped
// or the thread has not been created yet.
if (threadPopular == null || threadPopular.ThreadState == ThreadState.Stopped)
{
threadPopular = new Thread(new ThreadStart(Your function));
threadPopular.Start();
}
I might implement this using the Backgroundworker where image processing will be done on Backgroundworker asynchronously.
Also what I will suggest is you can use Timer here and set its tick time to the comfortable value. On every sliderchanged event, you start the timer if it is not enabled. In timer tick event handler you can check if the background worker is working then you can cancel the previous operation and put the new operation on it. In bacgroundworkerdone event handler, just stop the timer.
Thanks
While you could use BindingBase.Delay, this causes a delay even when a single change is required. another approach might be to use a OneWay binding in the Slider Value and use an asynchronous command like so:
XAML code:
<Slider Value="{Binding MyValue, Mode=OneWay}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="ValueChanged">
<mvvmlight:EventToCommand
Command="{Binding SetValueCommand, Mode=OneWay}"
EventArgsConverter="{StaticResource
RoutedPropertyChangedEventArgsToDoubleConverter}"
PassEventArgsToCommand="True" />
</i:EventTrigger>
</i:Interaction.Triggers>
Value Converter:
using GalaSoft.MvvmLight.Command;
public class RoutedPropertyChangedEventArgsToDoubleConverter : IEventArgsConverter
{
public object Convert(object value, object parameter)
{
var args = (RoutedPropertyChangedEventArgs<double>)value;
var element = (FrameworkElement)parameter;
return args.NewValue;
}
}
And the callback for the command:
double _updateVal;
Task _delay;
private async void SetValue(double val)
{
if (_delay != null)
{
// in case of high frequency updates, most updates will return here
_updateVal = val;
return;
}
// only the first update reaches here
// caluclate the image here
MyValue = val; // update slider
_delay = Task.Delay(500);
await _delay;
// in case there are pending updates:
while (_updateVal.HasValue)
{
// caluclate the image here
MyValue = _updateVal.Value; // update slider
_updateVal = null;
_delay = Task.Delay(500);
await _delay;
}
_delay = null;
}
This way you only get to reduce the frequency of the image calculations without a significant delay on the first value change.
I am trying to get the current state of a storybaord.
I wanted something like if the storyboard have stopped play it again.
How should i go about getting the current state??
Below is my code to play teh storyboard :
void loadtime()
{
ringingAlarm.Begin();
}
Storyboard does not have any properties or methods that indicate its current state. However it does fire a Completed event when it has finished. You could create a simple wrapper around your Storyboard to track state, see this forum post for an example.
You can use Storyboard.GetCurrentState to get the ClockState. This ClockState is an enumeration that has a Stopped property, which is returned if your animation is stopped.
Or, you can create a wrapper as ColinE suggested.
I am working on a text editor that is based on RichEditBox. I have implemented functionality "Go to line" which eventually resolves to
TextPointer.Paragraph.BringIntoView();
Along with this I also set the caret position.
What I found out is that BringIntoView only works when I click on the RichEditBox first (focus it). Otherwise it seems to get ignored. I can see that the caret position has been adjusted by the code around BringIntoView though.
Does anybody know what is the reason/nature of that problem? How can I overcome it?
Found a workaround for this, not sure if it will work in a pure WPF environment, in my case I'm running WPF inside a mainly Windows Forms solution using WPF UserControls where needed.
Instead of invoking BringIntoFocus() immediately, defer it to a later moment by adding it to a queue that gets handled by a timer. For example:
System.Windows.Forms.Timer DeferredActionTimer = new System.Windows.Forms.Timer() { Interval = 200 };
Queue<Action> DeferredActions = new Queue<Action>();
void DeferredActionTimer_Tick(object sender, EventArgs e) {
while(DeferredActions.Count > 0) {
Action act = DeferredActions.Dequeue();
act();
}
}
In your forms constructor, or in the OnLoad event add:
DeferredActionTimer.Tick += new EventHandler(DeferredActionTimer_Tick);
DeferredActionTimer.Enabled = true;
Finally, instead of calling TextPointer.Paragraph.BringIntoView(); directly, call it like this:
DeferredActions.Enqueue(() => TextPointer.Paragraph.BringIntoView());
Note that the Windows Forms timer kicks events off in the main thread (via the message pump loop). If you have to use another timer you need a bit of extra code. I'd recommend you to use System.Timers.Timer rather than the System.Threading.Timer (it's a little more thread-safe). You would also have to wrap the action in a Dispatcher.Invoke structure. In my case, the WinForms timer works like a charm.
Can't you just give the RichTextBox(?) focus first then, using Keyboard.Focus(richTextBox) or richTextBox.Focus()?