Hooking into a Storyboard.Complete event with MVVM

2019-05-22 22:34发布

问题:

Basically, what I've got is a simple alert message setup. There is a view for the individual alerts with some data-binding to the VM for their text and color (green for success, red for error, etc.).

<Border Margin="0 0 0 20" Background="{Binding Path=BackgroundColor}" BorderBrush="#D4D4D4" BorderThickness="1" CornerRadius="8">
    <Border.Effect>
        <DropShadowEffect Color="DarkGray"/>
    </Border.Effect>
<Grid >
        <Button Content="X" HorizontalAlignment="Left" 
            Margin="268,10,0,0" VerticalAlignment="Top" Width="20" RenderTransformOrigin="-0.48,0.727"/>
        <TextBlock x:Name="Message" 
           Foreground="White" FontWeight="SemiBold" FontSize="13px"
           Text="{Binding Path=Message}" 
           HorizontalAlignment="Left" Width="250"
           TextWrapping="Wrap"
           VerticalAlignment="Center" RenderTransformOrigin="-4.395,-0.038" Margin="7,13,0,25"/>
    </Grid>
</Border>

The goal is to have them be dismissed when you click the 'X' button, with a nice fade animation. I've got a storyboard in to handle the fading:

<UserControl.Triggers>
    <EventTrigger RoutedEvent="Button.Click">
        <BeginStoryboard>
            <Storyboard>
                <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)">
                    <EasingDoubleKeyFrame KeyTime="0" Value="1"/>
                    <EasingDoubleKeyFrame KeyTime="0:0:1" Value="0"/>
                </DoubleAnimationUsingKeyFrames>
            </Storyboard>
        </BeginStoryboard>
    </EventTrigger>
</UserControl.Triggers>

Now, what I wanted to do is then have Caliburn.Micro hook into the storyboard's Completed event with a method to dimiss the actual view (there is a bindable collection of these alerts in another view so that the alerts will stack and then be dismissed at the user's desire).

I tried something like this at first, thinking that it would work the same as any of my other bindings:

<Storyboard cal:Message.Attach="[Event Completed] = [Action DismissMessage($source, $eventArgs)]">
<!-- rest of the codez... -->

But got the following error:

Cannot attach type "ActionMessage" to type "Storyboard". Instances of type "ActionMessage" can only be attached to objects of type "FrameworkElement".

And this makes well enough sense I guess...but then what is the best way to handle something like this binding? My mind inevitably starts to wander to attempting some hacky solution like calling the viewmodel from the view's code-behind, but it seems like that violates MVVM.

Any suggestions?

回答1:

As I've commented, an easier way out might be the aggregator.

When the StoryBoard completes, you can just fire an event via the aggregator (or a specific aggregator for dialogs even) and handle that in the VM to close the dialog

e.g. you will need something that provides the aggregator if you don't have one (not sure if you are using DI or not)

public static DialogEventAggregatorProvider  
{
    public static EventAggregator { get; set; } // Obviously instantiate this, I'll leave the code out for brevity
}

in your views codebehind:

public SomeView : UserControl
{
    private void StoryBoard_Completed(object sender, SomeEventArgs e)
    {
        DialogEventAggregatorProvider.EventAggregator.Publish(new CloseDialogMessage()); // Add some args or what have you if it helps identify what dialog to close, but try not to break MVVM ;)
    }
}

in the VM

public SomeViewModel : Screen, IHandle<CloseDialogMessage>
{
    public SomeViewModel() 
    {
        // Don't forget to subscribe or you'll be scratching your head
        DialogEventAggregatorProvider.EventAggregator.Subscribe(this);
    }

    public void Handle(CloseDialogMessage message) 
    { 
        // if(message.HasSomeValue) Here you could check the type of message etc.
           TryClose();
    }
}

It's not that pretty, but as long as you keep the implementation generic you can get away without violating MVVM principles



回答2:

I think the best way to avoid view code behind is to use a StoryboardCompletedTrigger from Blend interactivity library :

First include right references in your xaml :

xmlns:i="http://schemas.microsoft.com/expression/2010/
xmlns:ie="http://schemas.microsoft.com/expression/2010/interactions"   

Then add your interaction trigger to your view and set its action to CallMethodAction

<i:Interaction.Triggers>
  <ie:StoryboardCompletedTrigger Storyboard="{StaticResource MyStoryboard}">
    <ie:CallMethodAction MethodName="DismissMessage" TargetObject="{Binding}"/>
  </ie:StoryboardCompletedTrigger>
</i:Interaction.Triggers>


回答3:

you could bind that event to a command using Marlon Grech Attached Command Behaviors library.

the library can be downloaded from here: http://marlongrech.wordpress.com/2008/12/13/attachedcommandbehavior-v2-aka-acb/