Browse Source

Merge pull request #6018 from AvaloniaUI/cancellable-animations-expander-fix

Cancellable animations (for async methods) + Expander fix
# Conflicts:
#	src/Avalonia.Visuals/ApiCompatBaseline.txt
release/0.10.11-rc.1
Max Katz 5 years ago
committed by Dan Walmsley
parent
commit
85e7643caa
  1. 30
      src/Avalonia.Animation/Animation.cs
  2. 6
      src/Avalonia.Animation/ApiCompatBaseline.txt
  3. 3
      src/Avalonia.Animation/IAnimation.cs
  4. 21
      src/Avalonia.Controls/Expander.cs
  5. 2
      src/Avalonia.Controls/Presenters/CarouselPresenter.cs
  6. 13
      src/Avalonia.ReactiveUI/TransitioningContentControl.cs
  7. 21
      src/Avalonia.Visuals/Animation/CompositePageTransition.cs
  8. 64
      src/Avalonia.Visuals/Animation/CrossFade.cs
  9. 6
      src/Avalonia.Visuals/Animation/IPageTransition.cs
  10. 30
      src/Avalonia.Visuals/Animation/PageSlide.cs
  11. 11
      src/Avalonia.Visuals/ApiCompatBaseline.txt
  12. 268
      tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs

30
src/Avalonia.Animation/Animation.cs

@ -3,10 +3,11 @@ using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Animation.Animators;
using Avalonia.Animation.Easings;
using Avalonia.Collections;
using Avalonia.Data;
using Avalonia.Metadata;
@ -319,7 +320,7 @@ namespace Avalonia.Animation
return (newAnimatorInstances, subscriptions);
}
/// <inheritdocs/>
/// <inheritdoc/>
public IDisposable Apply(Animatable control, IClock clock, IObservable<bool> match, Action onComplete)
{
var (animators, subscriptions) = InterpretKeyframes(control);
@ -344,25 +345,40 @@ namespace Avalonia.Animation
if (onComplete != null)
{
Task.WhenAll(completionTasks).ContinueWith(_ => onComplete());
Task.WhenAll(completionTasks).ContinueWith(
(_, state) => ((Action)state).Invoke(),
onComplete);
}
}
return new CompositeDisposable(subscriptions);
}
/// <inheritdocs/>
public Task RunAsync(Animatable control, IClock clock = null)
/// <inheritdoc/>
public Task RunAsync(Animatable control, IClock clock = null, CancellationToken cancellationToken = default)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.CompletedTask;
}
var run = new TaskCompletionSource<object>();
if (this.IterationCount == IterationCount.Infinite)
run.SetException(new InvalidOperationException("Looping animations must not use the Run method."));
IDisposable subscriptions = null;
IDisposable subscriptions = null, cancellation = null;
subscriptions = this.Apply(control, clock, Observable.Return(true), () =>
{
run.SetResult(null);
run.TrySetResult(null);
subscriptions?.Dispose();
cancellation?.Dispose();
});
cancellation = cancellationToken.Register(() =>
{
run.TrySetResult(null);
subscriptions?.Dispose();
cancellation?.Dispose();
});
return run.Task;

6
src/Avalonia.Animation/ApiCompatBaseline.txt

@ -0,0 +1,6 @@
Compat issues with assembly Avalonia.Animation:
MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.Animation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock, System.Threading.CancellationToken)' is present in the implementation but not in the contract.
Total Issues: 4

3
src/Avalonia.Animation/IAnimation.cs

@ -1,4 +1,5 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Avalonia.Animation
@ -16,6 +17,6 @@ namespace Avalonia.Animation
/// <summary>
/// Run the animation on the specified control.
/// </summary>
Task RunAsync(Animatable control, IClock clock);
Task RunAsync(Animatable control, IClock clock, CancellationToken cancellationToken = default);
}
}

21
src/Avalonia.Controls/Expander.cs

@ -1,7 +1,11 @@
using System.Threading;
using Avalonia.Animation;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
@ -36,8 +40,8 @@ namespace Avalonia.Controls
[PseudoClasses(":expanded", ":up", ":down", ":left", ":right")]
public class Expander : HeaderedContentControl
{
public static readonly StyledProperty<IPageTransition> ContentTransitionProperty =
AvaloniaProperty.Register<Expander, IPageTransition>(nameof(ContentTransition));
public static readonly StyledProperty<IPageTransition?> ContentTransitionProperty =
AvaloniaProperty.Register<Expander, IPageTransition?>(nameof(ContentTransition));
public static readonly StyledProperty<ExpandDirection> ExpandDirectionProperty =
AvaloniaProperty.Register<Expander, ExpandDirection>(nameof(ExpandDirection), ExpandDirection.Down);
@ -50,6 +54,7 @@ namespace Avalonia.Controls
defaultBindingMode: Data.BindingMode.TwoWay);
private bool _isExpanded;
private CancellationTokenSource? _lastTransitionCts;
static Expander()
{
@ -61,7 +66,7 @@ namespace Avalonia.Controls
UpdatePseudoClasses(ExpandDirection);
}
public IPageTransition ContentTransition
public IPageTransition? ContentTransition
{
get => GetValue(ContentTransitionProperty);
set => SetValue(ContentTransitionProperty, value);
@ -83,19 +88,23 @@ namespace Avalonia.Controls
}
}
protected virtual void OnIsExpandedChanged(AvaloniaPropertyChangedEventArgs e)
protected virtual async void OnIsExpandedChanged(AvaloniaPropertyChangedEventArgs e)
{
if (Content != null && ContentTransition != null && Presenter is Visual visualContent)
{
bool forward = ExpandDirection == ExpandDirection.Left ||
ExpandDirection == ExpandDirection.Up;
_lastTransitionCts?.Cancel();
_lastTransitionCts = new CancellationTokenSource();
if (IsExpanded)
{
ContentTransition.Start(null, visualContent, forward);
await ContentTransition.Start(null, visualContent, forward, _lastTransitionCts.Token);
}
else
{
ContentTransition.Start(visualContent, null, !forward);
await ContentTransition.Start(visualContent, null, forward, _lastTransitionCts.Token);
}
}
}

2
src/Avalonia.Controls/Presenters/CarouselPresenter.cs

@ -186,7 +186,7 @@ namespace Avalonia.Controls.Presenters
if (PageTransition != null && (from != null || to != null))
{
await PageTransition.Start((Visual)from, (Visual)to, fromIndex < toIndex);
await PageTransition.Start((Visual)from, (Visual)to, fromIndex < toIndex, default);
}
else if (to != null)
{

13
src/Avalonia.ReactiveUI/TransitioningContentControl.cs

@ -1,4 +1,6 @@
using System;
using System.Threading;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Styling;
@ -22,7 +24,9 @@ namespace Avalonia.ReactiveUI
/// </summary>
public static readonly StyledProperty<object?> DefaultContentProperty =
AvaloniaProperty.Register<TransitioningContentControl, object?>(nameof(DefaultContent));
private CancellationTokenSource? _lastTransitionCts;
/// <summary>
/// Gets or sets the animation played when content appears and disappears.
/// </summary>
@ -62,11 +66,14 @@ namespace Avalonia.ReactiveUI
/// <param name="content">New content to set.</param>
private async void UpdateContentWithTransition(object? content)
{
_lastTransitionCts?.Cancel();
_lastTransitionCts = new CancellationTokenSource();
if (PageTransition != null)
await PageTransition.Start(this, null, true);
await PageTransition.Start(this, null, true, _lastTransitionCts.Token);
base.Content = content;
if (PageTransition != null)
await PageTransition.Start(null, this, true);
await PageTransition.Start(null, this, true, _lastTransitionCts.Token);
}
}
}

21
src/Avalonia.Visuals/Animation/CompositePageTransition.cs

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Metadata;
@ -35,25 +36,11 @@ namespace Avalonia.Animation
[Content]
public List<IPageTransition> PageTransitions { get; set; } = new List<IPageTransition>();
/// <summary>
/// Starts the animation.
/// </summary>
/// <param name="from">
/// The control that is being transitioned away from. May be null.
/// </param>
/// <param name="to">
/// The control that is being transitioned to. May be null.
/// </param>
/// <param name="forward">
/// Defines the direction of the transition.
/// </param>
/// <returns>
/// A <see cref="Task"/> that tracks the progress of the animation.
/// </returns>
public Task Start(Visual from, Visual to, bool forward)
/// <inheritdoc />
public Task Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken)
{
var transitionTasks = PageTransitions
.Select(transition => transition.Start(from, to, forward))
.Select(transition => transition.Start(from, to, forward, cancellationToken))
.ToList();
return Task.WhenAll(transitionTasks);
}

64
src/Avalonia.Visuals/Animation/CrossFade.cs

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Animation.Easings;
using Avalonia.Styling;
@ -97,49 +99,39 @@ namespace Avalonia.Animation
set => _fadeOutAnimation.Easing = value;
}
/// <summary>
/// Starts the animation.
/// </summary>
/// <param name="from">
/// The control that is being transitioned away from. May be null.
/// </param>
/// <param name="to">
/// The control that is being transitioned to. May be null.
/// </param>
/// <returns>
/// A <see cref="Task"/> that tracks the progress of the animation.
/// </returns>
public async Task Start(Visual from, Visual to)
/// <inheritdoc cref="Start(Visual, Visual, CancellationToken)" />
public async Task Start(Visual from, Visual to, CancellationToken cancellationToken)
{
var tasks = new List<Task>();
if (to != null)
{
to.Opacity = 0;
}
if (from != null)
if (cancellationToken.IsCancellationRequested)
{
tasks.Add(_fadeOutAnimation.RunAsync(from));
return;
}
if (to != null)
var tasks = new List<Task>();
using (var disposables = new CompositeDisposable())
{
to.IsVisible = true;
tasks.Add(_fadeInAnimation.RunAsync(to));
if (to != null)
{
disposables.Add(to.SetValue(Visual.OpacityProperty, 0, Data.BindingPriority.Animation));
}
}
if (from != null)
{
tasks.Add(_fadeOutAnimation.RunAsync(from, null, cancellationToken));
}
await Task.WhenAll(tasks);
if (to != null)
{
to.IsVisible = true;
tasks.Add(_fadeInAnimation.RunAsync(to, null, cancellationToken));
}
if (from != null)
{
from.IsVisible = false;
}
await Task.WhenAll(tasks);
if (to != null)
{
to.Opacity = 1;
if (from != null && !cancellationToken.IsCancellationRequested)
{
from.IsVisible = false;
}
}
}
@ -159,9 +151,9 @@ namespace Avalonia.Animation
/// <returns>
/// A <see cref="Task"/> that tracks the progress of the animation.
/// </returns>
Task IPageTransition.Start(Visual from, Visual to, bool forward)
Task IPageTransition.Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken)
{
return Start(from, to);
return Start(from, to, cancellationToken);
}
}
}

6
src/Avalonia.Visuals/Animation/IPageTransition.cs

@ -1,3 +1,4 @@
using System.Threading;
using System.Threading.Tasks;
namespace Avalonia.Animation
@ -19,9 +20,12 @@ namespace Avalonia.Animation
/// <param name="forward">
/// If the animation is bidirectional, controls the direction of the animation.
/// </param>
/// <param name="cancellationToken">
/// Animation cancellation.
/// </param>
/// <returns>
/// A <see cref="Task"/> that tracks the progress of the animation.
/// </returns>
Task Start(Visual from, Visual to, bool forward);
Task Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken);
}
}

30
src/Avalonia.Visuals/Animation/PageSlide.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Animation.Easings;
using Avalonia.Media;
@ -60,23 +61,14 @@ namespace Avalonia.Animation
/// </summary>
public Easing SlideOutEasing { get; set; } = new LinearEasing();
/// <summary>
/// Starts the animation.
/// </summary>
/// <param name="from">
/// The control that is being transitioned away from. May be null.
/// </param>
/// <param name="to">
/// The control that is being transitioned to. May be null.
/// </param>
/// <param name="forward">
/// If true, the new page is slid in from the right, or if false from the left.
/// </param>
/// <returns>
/// A <see cref="Task"/> that tracks the progress of the animation.
/// </returns>
public async Task Start(Visual from, Visual to, bool forward)
/// <inheritdoc />
public async Task Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return;
}
var tasks = new List<Task>();
var parent = GetVisualParent(from, to);
var distance = Orientation == SlideAxis.Horizontal ? parent.Bounds.Width : parent.Bounds.Height;
@ -109,7 +101,7 @@ namespace Avalonia.Animation
},
Duration = Duration
};
tasks.Add(animation.RunAsync(from));
tasks.Add(animation.RunAsync(from, null, cancellationToken));
}
if (to != null)
@ -140,12 +132,12 @@ namespace Avalonia.Animation
},
Duration = Duration
};
tasks.Add(animation.RunAsync(to));
tasks.Add(animation.RunAsync(to, null, cancellationToken));
}
await Task.WhenAll(tasks);
if (from != null)
if (from != null && !cancellationToken.IsCancellationRequested)
{
from.IsVisible = false;
}

11
src/Avalonia.Visuals/ApiCompatBaseline.txt

@ -1,4 +1,10 @@
Compat issues with assembly Avalonia.Visuals:
MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.CompositePageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.CrossFade.Start(Avalonia.Visual, Avalonia.Visual)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean, System.Threading.CancellationToken)' is present in the implementation but not in the contract.
MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.PageSlide.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.PopBitmapBlendMode()' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.PushBitmapBlendMode(Avalonia.Visuals.Media.Imaging.BitmapBlendingMode)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Double Avalonia.Platform.IGeometryImpl.ContourLength' is present in the implementation but not in the contract.
@ -8,11 +14,8 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalon
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetSegment(System.Double, System.Double, System.Boolean, Avalonia.Platform.IGeometryImpl)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGeometryImpl Avalonia.Platform.IPlatformRenderInterface.CreateCombinedGeometry(Avalonia.Media.GeometryCombineMode, Avalonia.Media.Geometry, Avalonia.Media.Geometry)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGeometryImpl Avalonia.Platform.IPlatformRenderInterface.CreateGeometryGroup(Avalonia.Media.FillRule, System.Collections.Generic.IReadOnlyList<Avalonia.Media.Geometry>)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun, System.Double)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun, System.Double)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmap(System.IO.Stream)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmap(System.String)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToHeight(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToWidth(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract.
Total Issues: 17
Total Issues: 19

268
tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs

@ -9,6 +9,8 @@ using Avalonia.UnitTests;
using Avalonia.Data;
using Xunit;
using Avalonia.Animation.Easings;
using System.Threading;
using System.Reactive.Linq;
namespace Avalonia.Animation.UnitTests
{
@ -176,5 +178,271 @@ namespace Avalonia.Animation.UnitTests
clock.Step(TimeSpan.FromSeconds(0.100d));
Assert.Equal(border.Width, 300d);
}
[Fact(Skip = "See #6111")]
public void Dispose_Subscription_Should_Stop_Animation()
{
var keyframe1 = new KeyFrame()
{
Setters =
{
new Setter(Border.WidthProperty, 200d),
},
Cue = new Cue(1d)
};
var keyframe2 = new KeyFrame()
{
Setters =
{
new Setter(Border.WidthProperty, 100d),
},
Cue = new Cue(0d)
};
var animation = new Animation()
{
Duration = TimeSpan.FromSeconds(10),
Delay = TimeSpan.FromSeconds(0),
DelayBetweenIterations = TimeSpan.FromSeconds(0),
IterationCount = new IterationCount(1),
Children =
{
keyframe2,
keyframe1
}
};
var border = new Border()
{
Height = 100d,
Width = 50d
};
var propertyChangedCount = 0;
var animationCompletedCount = 0;
border.PropertyChanged += (sender, e) =>
{
if (e.Property == Control.WidthProperty)
{
propertyChangedCount++;
}
};
var clock = new TestClock();
var disposable = animation.Apply(border, clock, Observable.Return(true), () => animationCompletedCount++);
Assert.Equal(0, propertyChangedCount);
clock.Step(TimeSpan.FromSeconds(0));
Assert.Equal(0, animationCompletedCount);
Assert.Equal(1, propertyChangedCount);
disposable.Dispose();
// Clock ticks should be ignored after Dispose
clock.Step(TimeSpan.FromSeconds(5));
clock.Step(TimeSpan.FromSeconds(6));
clock.Step(TimeSpan.FromSeconds(7));
// On animation disposing (cancellation) on completed is not invoked (is it expected?)
Assert.Equal(0, animationCompletedCount);
// Initial property changed before cancellation + animation value removal.
Assert.Equal(2, propertyChangedCount);
}
[Fact]
public void Do_Not_Run_Cancelled_Animation()
{
var keyframe1 = new KeyFrame()
{
Setters =
{
new Setter(Border.WidthProperty, 200d),
},
Cue = new Cue(1d)
};
var keyframe2 = new KeyFrame()
{
Setters =
{
new Setter(Border.WidthProperty, 100d),
},
Cue = new Cue(0d)
};
var animation = new Animation()
{
Duration = TimeSpan.FromSeconds(10),
Delay = TimeSpan.FromSeconds(0),
DelayBetweenIterations = TimeSpan.FromSeconds(0),
IterationCount = new IterationCount(1),
Children =
{
keyframe2,
keyframe1
}
};
var border = new Border()
{
Height = 100d,
Width = 100d
};
var propertyChangedCount = 0;
border.PropertyChanged += (sender, e) =>
{
if (e.Property == Control.WidthProperty)
{
propertyChangedCount++;
}
};
var clock = new TestClock();
var cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.Cancel();
var animationRun = animation.RunAsync(border, clock, cancellationTokenSource.Token);
clock.Step(TimeSpan.FromSeconds(10));
Assert.Equal(0, propertyChangedCount);
Assert.True(animationRun.IsCompleted);
}
[Fact(Skip = "See #6111")]
public void Cancellation_Should_Stop_Animation()
{
var keyframe1 = new KeyFrame()
{
Setters =
{
new Setter(Border.WidthProperty, 200d),
},
Cue = new Cue(1d)
};
var keyframe2 = new KeyFrame()
{
Setters =
{
new Setter(Border.WidthProperty, 100d),
},
Cue = new Cue(0d)
};
var animation = new Animation()
{
Duration = TimeSpan.FromSeconds(10),
Delay = TimeSpan.FromSeconds(0),
DelayBetweenIterations = TimeSpan.FromSeconds(0),
IterationCount = new IterationCount(1),
Children =
{
keyframe2,
keyframe1
}
};
var border = new Border()
{
Height = 100d,
Width = 50d
};
var propertyChangedCount = 0;
border.PropertyChanged += (sender, e) =>
{
if (e.Property == Control.WidthProperty)
{
propertyChangedCount++;
}
};
var clock = new TestClock();
var cancellationTokenSource = new CancellationTokenSource();
var animationRun = animation.RunAsync(border, clock, cancellationTokenSource.Token);
Assert.Equal(0, propertyChangedCount);
clock.Step(TimeSpan.FromSeconds(0));
Assert.False(animationRun.IsCompleted);
Assert.Equal(1, propertyChangedCount);
cancellationTokenSource.Cancel();
clock.Step(TimeSpan.FromSeconds(1));
clock.Step(TimeSpan.FromSeconds(2));
clock.Step(TimeSpan.FromSeconds(3));
//Assert.Equal(2, propertyChangedCount);
animationRun.Wait();
clock.Step(TimeSpan.FromSeconds(6));
Assert.True(animationRun.IsCompleted);
Assert.Equal(2, propertyChangedCount);
}
[Fact]
public void Cancellation_Of_Completed_Animation_Does_Not_Fail()
{
var keyframe1 = new KeyFrame()
{
Setters =
{
new Setter(Border.WidthProperty, 200d),
},
Cue = new Cue(1d)
};
var keyframe2 = new KeyFrame()
{
Setters =
{
new Setter(Border.WidthProperty, 100d),
},
Cue = new Cue(0d)
};
var animation = new Animation()
{
Duration = TimeSpan.FromSeconds(10),
Delay = TimeSpan.FromSeconds(0),
DelayBetweenIterations = TimeSpan.FromSeconds(0),
IterationCount = new IterationCount(1),
Children =
{
keyframe2,
keyframe1
}
};
var border = new Border()
{
Height = 100d,
Width = 50d
};
var propertyChangedCount = 0;
border.PropertyChanged += (sender, e) =>
{
if (e.Property == Control.WidthProperty)
{
propertyChangedCount++;
}
};
var clock = new TestClock();
var cancellationTokenSource = new CancellationTokenSource();
var animationRun = animation.RunAsync(border, clock, cancellationTokenSource.Token);
Assert.Equal(0, propertyChangedCount);
clock.Step(TimeSpan.FromSeconds(0));
Assert.False(animationRun.IsCompleted);
Assert.Equal(1, propertyChangedCount);
clock.Step(TimeSpan.FromSeconds(10));
Assert.True(animationRun.IsCompleted);
Assert.Equal(2, propertyChangedCount);
cancellationTokenSource.Cancel();
animationRun.Wait();
}
}
}

Loading…
Cancel
Save