In my Application.Resources I have the following Storyboard defined.
<Application.Resources>
<!--Storyboard animation for fading out a UI element-->
<Storyboard x:Key="FadeOutAnimation">
<DoubleAnimation From="1"
To="0"
Duration="0:0:0.25"
Storyboard.TargetProperty="Opacity"
AutoReverse="False" />
</Storyboard>
</Application.Resources>
In code-behind I'm using this to fade out some TextBlocks when the user taps on them.
// Get the storyboard from application resources
Storyboard sb = (Storyboard)App.Current.Resources["FadeOutAnimation"];
// Setup the animation target for fade out
Storyboard.SetTarget( sb.Children.ElementAt( 0 ) as DoubleAnimation, myTextBlock );
// Set the animation completed handler
sb.Completed += ( s, e1 ) => {
// Stop the Storyboard
sb.Stop();
// Hide the TextBlock
myTextBlock.Visibility = Visibility.Collapsed;
};
// Start the Storyboard
sb.begin();
The question is, do I need to somehow 'unhook' myTextBlock from being the target of the DoubleAnimation?
If yes, how do I do it?
The reason I'm asking is I'm worried about a reference to that TextBlock hanging around until this Storyboard is used again.
Thanks for your help!
We don't always have to use Xaml in sliverlight if its getting in our way:-
public static AnimationHelper
{
public static void FadeOutAndCollapse(UIElement target)
{
DoubleAnimation da = new DoubleAnimation();
da.From = 1.0;
da.To = 0.0;
da.Duration = TimeSpan.FromSeconds(0.25);
da.AutoReverse = false;
StoryBoard.SetTargetProperty(da, new PropertyPath("Opacity"));
StoryBoard.SetTarget(da, target);
StoryBoard sb = new StoryBoard();
sb.Children.Add(da);
EventHandler eh = null;
eh = (s, args) =>
{
target.Visiblity = Visibility.Collapsed;
sb.Stop();
sb.Completed -= eh;
}
sb.Completed += eh;
sb.Begin();
}
}
With this in place you can fade out and collapse any UI element with:-
AnimationHelper.FadeOutAndCollapse(myTextBox);
I'd been inclined to remove the From = 1.0 to make it more general so that elements that have a lower starting opacity don't suddenly flash to full opacity before disappearing.
Don't worry about dangling references to lightweight user interface elements; they will be garbage collected when there are no more references. Your more pressing problem is that a single storyboard is being used for multiple text objects and if they overlap, it will do the wrong thing.
For example, if you start one animation and then start another, then they will both stop at the same time because there is only one storyboard and your handler calls stop. Either associate a separate storyboard with each text element in XAML or create a new storyboard in the code-behind for each animation that you want to do.
Also, if you were to use a single storyboard, you should be careful to remove your completed event handler because currently you will keep accumulating them and old handlers will be called when the storyboard re-completes.
Related
I subscribed to the wpf window's Loaded event: Loaded += loaded; and try to change the opacity of some controls in code behind.
I notice that in the method loaded the controls are not painted by wpf yet. So the code has no effect, the rendering of the controls occurs only after the method is exited.
1) Is there another event e.g. Rendered that I can subscribe to?
EDIT: I just discovered that there is an OnContentRendered event and the following code works:
Although an animation is probably preferrable.
protected override void OnContentRendered(EventArgs e)
{
base.OnContentRendered(e);
for (int i = 0; i < 100; i++)
{
Parentpanel.Opacity += 0.01;
Splashscreen.Opacity -= 0.01;
Dispatcher.Invoke(new Action(() => { }), DispatcherPriority.ContextIdle, null);
Thread.Sleep(50);
}
}
Otherwise I probably have to use an animation that changes the opacity of usercontrol1 from 0.1 to 1.0 and of usercontrol2 from 1.0 to 0.0.
2) Do you know an example for such an animation?
In your Loaded handler you can post the UI altering operation ( e.g. void ChangeOpacity() ) on the dispatcher:
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(ChangeOpacity));
It will execute after the rendering is done.
Edit
I see you simply need an animation to start when the window opens. It's easily done in XAML, here's a working example generated in Blend:
<Window x:Class="WpfApplication3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="100" Width="200">
<Window.Resources>
<Storyboard x:Key="myStoryboard">
<DoubleAnimationUsingKeyFrames
Storyboard.TargetProperty="(UIElement.Opacity)"
Storyboard.TargetName="myControl">
<EasingDoubleKeyFrame KeyTime="0:0:2" Value="0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</Window.Resources>
<Window.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard Storyboard="{StaticResource myStoryboard}"/>
</EventTrigger>
</Window.Triggers>
<StackPanel>
<TextBox x:Name="myControl" Text="I'm disappearing..." />
</StackPanel>
</Window>
I just recently was having issues while trying to render some standardized animation to WPF usercontrols when the visibility changed. In my app, I have a couple singleton static classes. In one, I added a static method "VisibleFader" and you pass in the framework element control and it automatically attaches the event handler to the double-animation against the opacity property. It works great and does not require ANY changes to any other Styles, Control Templates, or any other theme implementations.
public static DoubleAnimation da;
public static void VisibleFader(FrameworkElement fe)
{
if (da == null)
{
da = new DoubleAnimation();
da.From = 0;
da.To = 1;
da.Duration = new Duration(TimeSpan.FromSeconds(.7));
}
fe.IsVisibleChanged += myFader;
}
private static void myFader(object sender, DependencyPropertyChangedEventArgs e)
{
((FrameworkElement)sender).BeginAnimation(FrameworkElement.OpacityProperty, da);
}
Then, in my class (such as your Loaded Event), I just call this static method with that "userControl" object.
MySingletonClass.VisibleFader( this.whateverUserControl );
Done... So, when the visibility changes, it fades IN from nothing to 1. And if something was having its visibilty to hidden, it's gone anyhow.
I have a a custom Panel that raises a RoutedEvent which is defined globally in a static class :
public class CompressItemsToFitStackPanel : StackPanel
{
protected override Size ArrangeOverride(Size arrangeSize)
{
// some logic
// Raise Attached Event
CustomEventManager.RaiseArrangeEvent(this);
return base.ArrangeOverride(arrangeSize);
}
}
My attached Event:
public static class CustomEventManager
{
public static readonly RoutedEvent ArrangeEvent = EventManager.RegisterRoutedEvent("Arrange",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(CustomEventManager));
internal static void RaiseArrangeEvent(UIElement target)
{
var args = new RoutedEventArgs(ArrangeEvent);
target.RaiseEvent(args);
}
}
this Panel is the items panel for an Items control ,
the ItemsTemplate as an EventTrigger which i wan't to fire when the attached event is raised:
<DataTemplate DataType="{x:Type local:Checker}" x:Key="CheckerTempalte">
<Ellipse x:Name="ellipse" Style="{StaticResource checkerStyle}">
<Ellipse.Triggers>
<EventTrigger RoutedEvent="local:CustomEventManager.Arrange">
<BeginStoryboard>
<Storyboard>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.Y)"
Storyboard.TargetName="ellipse"
>
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:1" Value="{Binding Val}" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Ellipse.Triggers>
</Ellipse>
The Event trigger is not triggered ,
Maybe i'm not using AttachedEvents correctly or it is not Declared correctly
i need the event from the panel to propagate and trigger the EventTriggers in the Child elements ,
any ideas what i'm doing wrong ?
EDIT :
After dkozl's insight i came to the conclusion that i need an AddXXXHandler and RemoveXXXHandler in order for the XAML to add/ remove the handler for the EventTrigger
public static void AddArrangeHandler(DependencyObject d, RoutedEventHandler handler)
{
UIElement uie = d as UIElement;
if (uie != null)
{
uie.AddHandler(CustomEventManager.ArrangeEvent, handler);
}
}
public static void RemoveArrangeHandler(DependencyObject d, RoutedEventHandler handler)
{
UIElement uie = d as UIElement;
if (uie != null)
{
uie.RemoveHandler(CustomEventManager.ArrangeEvent, handler);
}
}
but still nothing happens , and i never reach these methods .
EDIT 2 :
thanks to dkozl's comments below ,
the Event is raised for each child element , since he the ellipses are down the
Visual Tree .
protected override Size ArrangeOverride(Size arrangeSize)
{
foreach (UIElement child in children)
{
CustomEventManager.RaiseArrangeEvent(child);
}
}
but still nothing happens , iv'e also tested the 'StoryBoard' by giving the 'EventTrigger'
the 'MouseEnter' event and moving my mouse over the element , it works fine.
still , even raising the event on each Ellipse still does not work ...
any ideas ?
Another point of interest is that the child elements are actually the 'Checker' type
and not the Ellipse which the DataTemplate represents , i don't get how 'Checker' is considered a UIElement .?
What you've created is standard RoutedEvent, not an attached one. Add/Remove handler is different for attached events. You'll need two separate methods (AddArrangeChildrenHandler and RemoveArrangeChildrenHandler). It has been explained on MSDN site
UPDATE:
I've copied updated Ellipse definition and CustomEventManager class into my application, added test button which calls CustomEventManager.RaiseArrangeEvent(ellipse); when clicked and it works for me.
I also had to add Ellipse.RenderTransorm of TransformGroup, fourth transformation being TranslateTransform to make it work like in the example
UPDATE2: Event is raised on the panel where the Ellipse is placed which means that bubbling event would never reach it as it will start from the panel and go up the visual tree to Window never reaching children of the panel
You should maybe consider a code only approach to this problem. If you wish to define the animations in XAML you can possibly StaticResource them into properties on the CompressToFitStackPanel. But if your DataTemplate is referring to a specific transform by index you are definitely losing the power and abstraction of DataTemplate'ing.
Reevaluate your problem space and what the design is supposed to solve. Use ILSpy or Reflector to analyze how other Panels solved problems with properties or Attached Dependency Properties (Grid.Column, Grid.Row).
Place more of the burden on the new layout panel and less on those who use it.
Good luck.
I came across a different behaviour in Silverlight and WPF when an animated property values is held ("filled") after the animation has ended. The Remarks section in the documentation of the FillBehavior
property says
The filling behavior can create the illusion that a property is
unsettable at runtime if you are not careful about stopping unintended
animations. Attempting to change the animated value coming from a
filling animation in code will appear to have no effect until the
filling animation is stopped.
However, in Silverlight this seems not to be true. In the small example below I animate the Opacity of a filled Rectangle to zero when the "Fade Out" button is clicked. Although the animation's FillBehavior is set to HoldEnd, resetting the Opacity to 1 in the "Reset" button click handler works and the Button reappears. If I do the same in a WPF application it behaves as expected, namely clicking the "Reset" button has no visual effect.
My question is, has anybody else observed this behavior? Is it perhaps a bug in Silverlight, or am I missing something?
I'm using Silverlight 5 and WPF 4.5 here.
<StackPanel HorizontalAlignment="Center">
<Rectangle Name="rect" Width="200" Height="100" Fill="DarkGreen"/>
<Button Content="Fade Out" Click="FadeOutButtonClick"/>
<Button Content="Reset" Click="ResetButtonClick"/>
</StackPanel>
-
private void FadeOutButtonClick(object sender, RoutedEventArgs e)
{
var animation = new DoubleAnimation
{
To = 0d,
Duration = TimeSpan.FromSeconds(1),
FillBehavior = FillBehavior.HoldEnd
};
Storyboard.SetTargetProperty(animation, new PropertyPath(UIElement.OpacityProperty));
Storyboard.SetTarget(animation, rect);
var storyboard = new Storyboard();
storyboard.Children.Add(animation);
storyboard.Begin();
}
private void ResetButtonClick(object sender, RoutedEventArgs e)
{
rect.Opacity = 1d;
}
You can do a reverse animation on the reset :
private void ResetButtonClick(object sender, RoutedEventArgs e)
{
var animation = new DoubleAnimation
{
To = 1d,
Duration = TimeSpan.FromSeconds(0),
FillBehavior = FillBehavior.HoldEnd
};
Storyboard.SetTargetProperty(animation, new PropertyPath(UIElement.OpacityProperty));
Storyboard.SetTarget(animation, rect);
var storyboard = new Storyboard();
storyboard.Children.Add(animation);
storyboard.Begin();
Hope This will help.
Suppose I have a ListBox bound to an ObservableCollection and I want to animate adding/removing of ListBoxItems eg. FadeIn/Out, SlideDown/Up etc. How can I do that?
After spending mad hours hunting the wilds of Google, I figure I should share how I solved this problem since it seems to be a pretty d*mn simple thing to need and yet WPF makes it ridiculously frustrating until you intimately understand how animation is implemented. Once you do, you realize FrameworkElement.Unloaded is a useless event for animation. I've seen many versions of this question all over StackOverflow (amongst others), with all sorts of hackish ways to solve this. Hopefully I can provide a most simple example that you can then fancy up for your many purposes.
I will not show Fade In example since that is covered by plenty of examples using the Loaded routed event already. It is Fading Out on item removal that is the royal pain in the *#$.
The major problem here stems from how Storyboards just get weird when you put them into Control/Data Templates / Styles. It is impossible to bind the DataContext (and thus your object's ID) to the Storyboard. The Completed event fires with zero idea of who it just finished on. Diving the visual tree is useless since all your data templated items have the same names for their containers! So sure, you could write up a function that goes and searches the entire collection for objects that have their removal flag property set, but that is ugly and honestly, just not something you ever want to admit writing on purpose. And it won't work if you have several objects being removed within the length of your animation of each other (which is my case). You could also just write a cleanup thread that does similar things and get lost in timing hell. No fun. I digress. On to the solution.
Assumptions:
You are using ObservableCollection populated with some custom objects
You use a DataTemplate to give these a custom look, hence why you want to animate their removal
You bind the ObservableCollection to a ListBox (or something simple like it)
You have INotifyPropertyChanged implemented on the class of objects in your OC.
Then the solution is quite simple really, painfully so if you spent any long amount of time trying to solve this.
Create a Storyboard that animates your fade out in the Window.Resources section of your window (above the DataTemplate).
(Optional) Define the Duration separately as a resource so you can avoid hard coding as much. Or just hard code the durations.
Make a public boolean property in your object class called "Removing", "isRemoving", whatev. Make sure you raise a Property Changed event for this field.
Create a DataTrigger that binds to your "Removing" property and on True plays the fade out storyboard.
Create a private DispatcherTimer object in your object class and implement a simple timer that has the same duration as your fade out animation and removes your object from the list in its tick handler.
Code example is below, which hopefully makes it all easy to grasp. I simplified the example as much as possible so you'll need to adapt it to your environment as it suits you.
Code Behinds
public partial class MainWindow : Window
{
public static ObservableCollection<Missiles> MissileRack = new ObservableCollection<Missiles>(); // because who doesn't love missiles?
public static Duration FadeDuration;
// main window constructor
public MainWindow()
{
InitializeComponent();
// somewhere here you'll want to tie the XAML Duration to your code-behind, or if you like ugly messes you can just skip this step and hard code away
FadeDuration = (Duration)this.Resources["cnvFadeDuration"];
//
// blah blah
//
}
public void somethread_ShootsMissiles()
{
// imagine this is running on your background worker threads (or something like it)
// however you want to flip the Removing flag on specific objects, once you do, it will fade out nicely
var missilesToShoot = MissileRack.Where(p => (complicated LINQ search routine).ToList();
foreach (var missile in missilesToShoot)
{
// fire!
missile.Removing = true;
}
}
}
public class Missiles
{
public Missiles()
{}
public bool Removing
{
get { return _removing; }
set
{
_removing = value;
OnPropertyChanged("Removing"); // assume you know how to implement this
// start timer to remove missile from the rack
start_removal_timer();
}
}
private bool _removing = false;
private DispatcherTimer remove_timer;
private void start_removal_timer()
{
remove_timer = new DispatcherTimer();
// because we set the Interval of the timer to the same length as the animation, we know the animation will finish running before remove is called. Perfect.
remove_timer.Interval = MainWindow.TrackFadeDuration.TimeSpan; // I'm sure you can find a better way to share if you don't like global statics, but I am lazy
remove_timer.Tick += new EventHandler(remove_timer_Elapsed);
remove_timer.Start();
}
// use of DispatcherTimer ensures this handler runs on the GUI thread for us
// this handler is now effectively the "Storyboard Completed" event
private void remove_timer_Elapsed(object sender, EventArgs e)
{
// this is the only operation that matters for this example, feel free to fancy this line up on your own
MainWindow.MissileRack.Remove(this); // normally this would cause your object to just *poof* before animation has played, but thanks to timer,
}
}
XAMLs
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Test" Height="300" Width="300">
<Window.Resources>
<Duration x:Key="cnvFadeDuration">0:0:0.3</Duration> <!-- or hard code this if you really must -->
<Storyboard x:Key="cnvFadeOut" >
<DoubleAnimation Storyboard.TargetName="cnvMissile"
Storyboard.TargetProperty="Opacity"
From="1" To="0" Duration="{StaticResource cnvFadeDuration}"
/>
</Storyboard>
<DataTemplate x:Key="MissileTemplate">
<Canvas x:Name="cnvMissile">
<!-- bunch of pretty missile graphics go here -->
</Canvas>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=Removing}" Value="true" >
<DataTrigger.EnterActions>
<!-- you could actually just plop the storyboard right here instead of calling it as a resource, whatever suits your needs really -->
<BeginStoryboard Storyboard="{StaticResource cnvFadeOut}" />
</DataTrigger.EnterActions>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</Window.Resources>
<Grid>
<ListBox /> <!-- do your typical data binding and junk -->
</Grid>
</Window>
Huzzah!~
Dr TJ's answer is right enough. Going down that route you'd have to wrap ObservableCollection<T> and implement a BeforeDelete event,..then you could use an EventTrigger to control the storyboards.
That's a right pain though. You're probably better creating a DataTemplate and handling the FrameworkElement.Loaded and FrameworkElement.Unloaded events in an EventTrigger.
I've put a quick sample together for you below. You'd have to sort out the remove code yourself but I'm sure you're up to it.
<ListBox>
<ListBox.ItemsSource>
<x:Array Type="sys:String">
<sys:String>One</sys:String>
<sys:String>Two</sys:String>
<sys:String>Three</sys:String>
<sys:String>Four</sys:String>
<sys:String>Five</sys:String>
</x:Array>
</ListBox.ItemsSource>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"
Opacity="0">
<TextBlock.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
Duration="00:00:02"
From="0"
To="1" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="FrameworkElement.Unloaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
Duration="00:00:02"
From="1"
To="0" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</TextBlock.Triggers>
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
HTH, Stimul8d
Fade-out is likely to be impossible without re-writing the ItemsControl base implementation. The problem is that when the ItemsControl receives the INotifyCollectionChanged event from the collection it immediately (and within deep private code) marks the item container as not visible (IsVisible is a readonly property that gets its value from a hidden cache so cannot be accessed).
You can easily implement the fade-in in this way:
public class FadingListBox : ListBox
{
protected override void PrepareContainerForItemOverride(
DependencyObject element, object item)
{
var lb = (ListBoxItem)element;
DoubleAnimation anm = new DoubleAnimation(0, 1,
TimeSpan.FromMilliseconds(500));
lb.BeginAnimation(OpacityProperty, anm);
base.PrepareContainerForItemOverride(element, item);
}
}
But the 'fade-out' equivalent never works as the container is already invisible and cannot be reset.
public class FadingListBox : ListBox
{
protected override void ClearContainerForItemOverride(
DependencyObject element, object item)
{
var lb = (ListBoxItem) element;
lb.BringIntoView();
DoubleAnimation anm = new DoubleAnimation(
1, 0, TimeSpan.FromMilliseconds(500));
lb.BeginAnimation(OpacityProperty, anm);
base.ClearContainerForItemOverride(element, item);
}
}
Even if you have your own custom container generator, you cannot overcome this issue
protected override DependencyObject GetContainerForItemOverride()
{
return new FadingListBoxItem();
}
And this kind of makes sense, because if the container was still visible after the data it represents has disappeared, then you could theoretically click on the container (kicking off triggers, events etc) and experience some subtle bugs perhaps.
The accepted answer works for animating the addition of new items, but not for the removal of existing ones. This is because by the time the Unloaded event fires, the item has already been removed. The key to getting deletion to work is to add a "marked for deletion" concept. Being marked for deletion should trigger the animation, and the completion of the animation should trigger the actual deletion. There are probably a bunch of ways this idea could be implemented, but I got it to work by creating an attached behavior and by tweaking my viewmodels a bit. The behavior exposes three attached properties, all of which must be set on each ListViewItem:
"Storyboard" of type Storyboard. This is the actual animation you want to run when an item is removed.
"PerformRemoval" of type ICommand. This is a command that will be executed when the animation is done running. It should execute code to actually remove the element from the databound collection.
"IsMarkedForRemoval" of type bool. Set this to true when you decide to remove an item from the list (e.g. in a button click handler). As soon as the attached behavior sees this property change to true, it will begin the animation. And when the animation's Completed event fires, it will Execute the PerformRemoval command.
Here is a link to full source for the behavior and example usage (if it's bad form to direct to your own blog, I'll remove the link. I'd paste the code here, but it's fairly lengthy. I don't receive any money from the thing, if that makes a difference).
For me FrameworkElement.Unloaded event doesn't work - the item just disappears instantly. I can hardly believe that years of experience with WPF haven't produced anything prettier, but looks like the only way this can work is a hack described here: Animating removed item in Listbox ?..
Heh. Since accepted solution is not work, let's try another round ;)
We can't use Unloaded event because ListBox (or other control) remove item from visual tree when it removed from original list. So main idea is to create shadow copy of provided ObservableCollection and bind list to it.
First of all - XAML:
<ListBox ItemsSource="{Binding ShadowView}" IsSynchronizedWithCurrentItem="True">
<ListBox.ItemTemplate>
<DataTemplate>
<Border Loaded="OnItemViewLoaded">
<TextBlock Text="{Binding}"/>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Create ListBox, bind it to our shadow copy, set IsSynchronizedWithCurrentItem for correct support ICollectionView.CurrentItem (very useful interface), and set Loaded event on item view. This event handler need to associate view (which will be animated) and item (which will be removed).
private void OnItemViewLoaded (object sender, RoutedEventArgs e)
{
var fe = (FrameworkElement) sender ;
var dc = (DependencyObject) fe.DataContext ;
dc.SetValue (ShadowViewSource.ViewProperty, fe) ;
}
Initialize everything:
private readonly ShadowViewSource m_shadow ;
public ICollectionView ShadowView => m_shadow.View ;
public MainWindow ()
{
m_collection = new ObservableCollection<...> () ;
m_view = CollectionViewSource.GetDefaultView (m_collection) ;
m_shadow = new ShadowViewSource (m_view) ;
InitializeComponent ();
}
And last, but not least, ShadowViewSource class (yeah, it's not perfect, but as proof-of-concept it works):
using System ;
using System.Collections.Generic ;
using System.Collections.ObjectModel ;
using System.Collections.Specialized ;
using System.ComponentModel ;
using System.Linq ;
using System.Windows ;
using System.Windows.Data ;
using System.Windows.Media.Animation ;
namespace ShadowView
{
public class ShadowViewSource
{
public static readonly DependencyProperty ViewProperty = DependencyProperty.RegisterAttached ("View", typeof (FrameworkElement), typeof (ShadowViewSource)) ;
private readonly ICollectionView m_sourceView ;
private readonly IEnumerable<object> m_source ;
private readonly ICollectionView m_view ;
private readonly ObservableCollection<object> m_collection ;
public ShadowViewSource (ICollectionView view)
{
var sourceChanged = view.SourceCollection as INotifyCollectionChanged ;
if (sourceChanged == null)
throw new ArgumentNullException (nameof (sourceChanged)) ;
var sortChanged = view.SortDescriptions as INotifyCollectionChanged ;
if (sortChanged == null)
throw new ArgumentNullException (nameof (sortChanged)) ;
m_source = view.SourceCollection as IEnumerable<object> ;
if (m_source == null)
throw new ArgumentNullException (nameof (m_source)) ;
m_sourceView = view ;
m_collection = new ObservableCollection<object> (m_source) ;
m_view = CollectionViewSource.GetDefaultView (m_collection) ;
m_view.MoveCurrentTo (m_sourceView.CurrentItem) ;
m_sourceView.CurrentChanged += OnSourceCurrentChanged ;
m_view.CurrentChanged += OnViewCurrentChanged ;
sourceChanged.CollectionChanged += OnSourceCollectionChanged ;
sortChanged.CollectionChanged += OnSortChanged ;
}
private void OnSortChanged (object sender, NotifyCollectionChangedEventArgs e)
{
using (m_view.DeferRefresh ())
{
var sd = m_view.SortDescriptions ;
sd.Clear () ;
foreach (var desc in m_sourceView.SortDescriptions)
sd.Add (desc) ;
}
}
private void OnSourceCollectionChanged (object sender, NotifyCollectionChangedEventArgs e)
{
var toAdd = m_source.Except (m_collection) ;
var toRemove = m_collection.Except (m_source) ;
foreach (var obj in toAdd)
m_collection.Add (obj) ;
foreach (DependencyObject obj in toRemove)
{
var view = (FrameworkElement) obj.GetValue (ViewProperty) ;
var begintime = 1 ;
var sb = new Storyboard { BeginTime = TimeSpan.FromSeconds (begintime) } ;
sb.Completed += (s, ea) => m_collection.Remove (obj) ;
var fade = new DoubleAnimation (1, 0, new Duration (TimeSpan.FromMilliseconds (500))) ;
Storyboard.SetTarget (fade, view) ;
Storyboard.SetTargetProperty (fade, new PropertyPath (UIElement.OpacityProperty)) ;
sb.Children.Add (fade) ;
var size = new DoubleAnimation (view.ActualHeight, 0, new Duration (TimeSpan.FromMilliseconds (250))) ;
Storyboard.SetTarget (size, view) ;
Storyboard.SetTargetProperty (size, new PropertyPath (FrameworkElement.HeightProperty)) ;
sb.Children.Add (size) ;
size.BeginTime = fade.Duration.TimeSpan ;
sb.Begin () ;
}
}
private void OnViewCurrentChanged (object sender, EventArgs e)
{
m_sourceView.MoveCurrentTo (m_view.CurrentItem) ;
}
private void OnSourceCurrentChanged (object sender, EventArgs e)
{
m_view.MoveCurrentTo (m_sourceView.CurrentItem) ;
}
public ICollectionView View => m_view ;
}
}
And final words. First of all it works. Next - this approach don't require any changes in existing code, workarounds via Deleting property, etc, etc, etc. Especially when implemented as single custom control. You have ObservableCollection, add items, remove, do whatever you want, UI will always try to correctly reflect this changes.
Create two story boards for fade-in and fade-out and bind its value to the brush you've created for the OpacityMask of your ListBox
I'm working on a 3D carousel of flat, square tiles that will contain information. I'm working on animating this carousel to rotate when a person presses Next and Previous buttons.
I've gotten it to work by using BeginAnimation on the Rotation property of the RotateTransform3D I applied to the carousel, but I can't seem to make a Storyboard version of the same animation work. The reason I need the Storyboard version is for the HandOffBehavior.Compose parameter because without it, multiple clicks of my next and previous buttons results in a misaligned carousel.
Here is the code for the Storyboard:
RotateTransform3D tempTransform = (RotateTransform3D)wheel.Transform;
AxisAngleRotation3D rotation = (AxisAngleRotation3D)tempTransform.Rotation;
Storyboard storyboard = new Storyboard();
DoubleAnimation animation = new DoubleAnimation();
animation.By = defaultAngle;
animation.Duration = TimeSpan.FromSeconds(.5);
Storyboard.SetTarget(animation, rotation);
Storyboard.SetTargetProperty(animation, new PropertyPath("Angle"));
storyboard.Children.Add(animation);
storyboard.Duration = animation.Duration;
storyboard.Begin(new FrameworkContentElement(), HandoffBehavior.Compose);
For some reason, this code results in absolutely nothing. I followed the examples I had to the letter, so I am quite frustrated. Any help is greatly appreciated. I am also completely open to using BeginAnimation if I can replicate HandOffBehavior.Compose.
My experience comes from 2D animation, but I guess the problem is the same.
For some stupid reason (probably relating to an unhealthy focus on XAML), Storyboards can only animate Freezable objects by looking them up by name. (See example in Storyboards Overview.) Thus although you provide a reference to your 'rotation' object when you call Storyboard.SetTarget(animation, rotation), the Storyboard only wants to remember and use a name, which it does not have.
The solution is:
Create a naming scope around the element that will govern the transform.
Call RegisterName() for each Freezable object being animated.
Pass the element to Storyboard.Begin()
Which would make your code look something like this (not tested):
FrameworkContentElement element = new FrameworkContentElement();
NameScope.SetNameScope(element, new NameScope());
RotateTransform3D tempTransform = (RotateTransform3D)wheel.Transform;
AxisAngleRotation3D rotation = (AxisAngleRotation3D)tempTransform.Rotation;
element.RegisterName("rotation", rotation);
Storyboard storyboard = new Storyboard();
DoubleAnimation animation = new DoubleAnimation();
animation.By = defaultAngle;
animation.Duration = TimeSpan.FromSeconds(.5);
Storyboard.SetTarget(animation, rotation);
Storyboard.SetTargetProperty(animation, new PropertyPath("Angle"));
storyboard.Children.Add(animation);
storyboard.Duration = animation.Duration;
storyboard.Begin(element, HandoffBehavior.Compose);
None of this is necessary in XAML because your objects are automatically registered.
EDIT: But then I worked out that you can simplify things by leaving out the Storyboard altogether:
var T = new TranslateTransform(40, 0);
Duration duration = new Duration(new TimeSpan(0, 0, 0, 1, 0);
DoubleAnimation anim = new DoubleAnimation(30, duration);
T.BeginAnimation(TranslateTransform.YProperty, anim);