In an attempt to maintain proper MVVM separation I found and modified a 'StoryboardManager' class that is supposed to attach a property to a Storyboard which can be scripted in the View's XAML and manipulated from the ViewMode. This works just fine one time. After that there is an exception thrown complaining about attempts to change a frozen entity. I've tried multiple approaches but just cannot get the darned thing to work.
First, background. In XAML, a Storyboard is created as a UserControl resource and targeted at a particular control named LockImage. This Storyboard is extended as a result of the existence of a static StoryboardManager class which attaches an ID property to it. The XAML code sets a value into this attached property. The StoryboardManager sees when the ID changes and takes that opportunity to save the associated Storyboard object in a collection where the ID is a key. This is how the ViewModel is able to access a Storyboard that was defined in the View's XAML script. The ViewModel tells the StoryboardManager to start the Storyboard whose name is some string. The StoryboardManager uses that name as a key and looks in its collection of Storyboards. It finds the Storyboard and calls its Begin method.
Each Storyboard has a Completed event. When the VM starts a Storyboard its Completed event is loaded with a lambda callback that does two things. First, it removes the callback from the Storyboard's Completed event collection. Secondly, it calls an optional completed notification that can be passed in from the VM.
All of the above works. Once. The second time around the Storyboard is considered to be frozen and an attempt to add a callback event handler results in an exception.
Here are the essential parts of the StoryboardManager class.
public static class StoryboardManager { public delegate void Callback(object state); private enum StoryboardState { None, Inactive, Running } /// <summary> /// This is a name/value pair where the key is the name of the storyboard and the data is /// the actual Storyboard object itself. /// </summary> public static DependencyProperty IDProperty = DependencyProperty.RegisterAttached("ID", typeof(string), typeof(StoryboardManager), new PropertyMetadata(null, IDChanged)); static Dictionary<string, Storyboard> _storyboards = new Dictionary<string, Storyboard>(); static Dictionary<string, StoryboardState> _storyboardState = new Dictionary<string, StoryboardState>(); public static void SetID(DependencyObject obj, string id) { obj.SetValue(IDProperty, id); } public static string GetID(DependencyObject obj) { return obj.GetValue(IDProperty) as string; } private static void IDChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) { // Ensure that the passing in object can be coerced into being a Storyboard. Storyboard sb = obj as Storyboard; if (sb == null) { // Not the sort of object we can work with. return; } // Does the collction already have an entry with the specified key? // This must be determined because an attempt to add an existing key will // generate an exception being thrown. string key = e.NewValue as string; if (_storyboards.ContainsKey(key)) { // Yes. Just update it with the new data. _storyboards[key] = sb; } else { // No. Safe to add. _storyboards.Add(key, sb); SetStoryboardState(key, StoryboardState.Inactive); } } private static bool IsInactive(string id) { bool bReturn = false; do { if (string.IsNullOrEmpty(id)) { break; } // Is there an entry for this storyboard in the state collection? if (!_storyboardState.ContainsKey(id)) { // No. Cannot report anything one way or another about this one. break; } // Anything other than running is considered to be inactive. bReturn = (_storyboardState[id] != StoryboardState.Running); } while (false); return bReturn; } public static void PlayStoryboard(string id, Callback callback, object state) { do { try { // Does the collection know about this particular Storyboard? if (!_storyboards.ContainsKey(id)) { // No. Just invoke the completion callback, if it exists. if (callback != null) { callback(state); } break; } // Gain access to the actual storyboard itself. Storyboard sb = _storyboards[id]; if (sb == null) { // Oops. No valid Storyboard. Just invoke the completion callback, if it exists. if (callback != null) { callback(state); } break; } // Make a copy of the Storyboard so the the event handler and // freeze related considerations can be ignored. //sb = sb.Clone(); // Preclude cross thread access. lock (sb) { // Is this storyboard inactive? if (IsInactive(id)) { // Yes. Generate a lambda expression that will invoke the // handler and callback for this storyboard. EventHandler handler = null; handler = delegate { // First remove the handler from the Completed event. sb.Completed -= handler; // Invoke the method that handles both the callback and the closing // of the Storyboard state. PlayStoryboardCallback( id, callback, state ); }; sb.Completed += handler; // Set a flag to indicate that this storyboard is active. SetStoryboardState( id, StoryboardState.Running ); // Start the storyboard. sb.Begin(); } } } catch (Exception ex) { // Just contain any exceptions that may be generated. DebugLogFile.WriteLine(ex.ToString()); } } while (false); } private static void PlayStoryboardCallback(string id, Callback callback, object state) { do { try { // Invoke the completion callback, if it exists. if (callback != null) { callback(state); } // Change the run state of the storyboard. if (_storyboardState.ContainsKey(id)) { StoryboardState ss = _storyboardState[id]; _storyboardState[id] = StoryboardState.Inactive; } } catch (Exception ex) { // Just contain any exceptions that may be generated. DebugLogFile.WriteLine(ex.ToString()); } } while (false); } private static void SetStoryboardState(string id, StoryboardState sbState) { do { try { if (_storyboardState.ContainsKey(id)) { _storyboardState[id] = sbState; } else { _storyboardState.Add(id, sbState); } } catch { // Just eat the exception so that the app is not trashed. } } while (false); } }
The storyboard is defined like this:
<!-- 'winutils is a namespace defined elsewhere. --><!-- This is where the StoryboardManager is defined. --><UserControl.Resources><Storyboard x:Key="LockStoryboard" winutils:StoryboardManager.ID="LockStoryboard"><DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)" Storyboard.TargetName="LockImage"><EasingDoubleKeyFrame KeyTime="0:0:0.5" Value="0"/><EasingDoubleKeyFrame KeyTime="0:0:1" Value="64"/></DoubleAnimationUsingKeyFrames></Storyboard></ResourceDictionary></UserControl.Resources><!-- Later on, the image "LockImage" is specified --><!-- in the body of the XAML script. -->
Finally, the Storyboard is actually started in the ViewModel.
StoryboardManager.PlayStoryboard( Constants.LockStoryboard, null, null );
As I said, this all works once and just once. The second time around an exception is thrown in the StoryboardManager when the Storyboard's COmpleted event is updated.
sb.Completed += handler;
"Specified value of type 'System.Windows.Media.Animation.Storyboard' must have IsFrozen set to false to modify."
So, why is this thing working only a single time and how do I fix it?
Richard Lewis Haggard