Search code examples
wpfxamleventtrigger

How to Set a Control's user-editable property with a Button click only in XAML


Specifically, I'm trying to create a "reset" button that will set a slider's value to zero. I don't want code-behind because the template will be customizable.

I tried the following. Without FillBehavior="Stop" the slider is no longer movable by the user. With FillBehavior="Stop" the value immediately returns to the preceding value (giving the appearance it does nothing at all)

    <Slider Name="MySlider" Minimum="-5" Maximum="5" Value="{Binding FloatProperty}"></Slider>
    <Button Content="Reset">
        <Button.Triggers>
            <EventTrigger RoutedEvent="Button.Click">
                <BeginStoryboard>
                    <Storyboard FillBehavior="Stop">
                        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="MySlider" Storyboard.TargetProperty="Value">
                            <DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="0"/>
                        </DoubleAnimationUsingKeyFrames>
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger>
        </Button.Triggers>
    </Button>

I also tried using a DataTrigger on the Slider binding to the button's IsPressed property, but I wasn't able to get that to work either. It would return to the prior value.

Note that the Value of the Slider is bound to a property of the data.

Thanks!


Solution

  • Minor edits to your XAML seems to do the trick. You need to filter the Button.Click events by a specific source name, like shown below. Also, removing the Binding from Slider.Value seems to help.

    I'm not quite sure how best to maintain the Binding and still be able to make this work right.

    <Slider Name="MySlider"
            Maximum="5"
            Minimum="-5"/>
    <Button Name="myButton" Content="Reset">
        <Button.Triggers>
            <EventTrigger RoutedEvent="Button.Click" SourceName="myButton">
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation Duration="0"
                                         Storyboard.TargetName="MySlider"
                                         Storyboard.TargetProperty="Value"
                                         To="0" />
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger>
        </Button.Triggers>
    </Button>
    

    If the goal is to have Slider.Value data bound to something, like...

    <Slider Value="{Binding FloatProperty}"/>
    

    ... then I suspect (though I'm not 100% sure) that what you are trying to do may not be possible.

    First, let me explain why you are seeing the slider being "frozen". This is not quite a bug - I just needed to read the docs a bit carefully to realize what was going on.

    When FillBehavior is set to Stop, the Animation will revert the value (to whatever it was before the Animation started) when the TimeLine is completed.

    On the other hand, when FillBehavior is HoldEnd (which is the default behavior you get when FillBehavior is not specified explicitly), the DoubleAnimation will continue to hold the end value and protect against changes from other sources (like slider move by hand). So every time you move the slider, the Animation resets it back to 0 becuase of HoldEnd. This is why you see the slider acting as if it were "frozen" at 0.

    I investigated using a DataTrigger on Button.IsPressed = True as an alternative. This is typically done within a Style or a ControlTemplate - and it would look something like this:

             <Slider Maximum="5"
                    Minimum="-5"
                    Value="{Binding FloatProperty}">
                <Slider.Template>
                    <ControlTemplate TargetType="Slider">
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto" />
                                <RowDefinition Height="Auto" />
                            </Grid.RowDefinitions>
                            <Slider x:Name="Slider"
                                    Grid.Row="0"
                                    Maximum="{TemplateBinding Maximum}"
                                    Minimum="{TemplateBinding Minimum}"
                                    Value="{Binding RelativeSource={RelativeSource TemplatedParent},
                                                    Path=Value,
                                                    Mode=TwoWay}" />
                            <Button x:Name="ResetButton"
                                    Grid.Row="1"
                                    Content="Reset" />
                        </Grid>
                        <ControlTemplate.Triggers>
                            <DataTrigger Binding="{Binding ElementName=ResetButton, Path=IsPressed}" Value="True">
                                <Setter TargetName="Slider" Property="Value" Value="0" />
                            </DataTrigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Slider.Template>
            </Slider>
    

    Unfortunately, the Dependency Property Value Precedence rules hold that TemplatedParent template properties have a higher precedence than Triggers and Style setters. Because of this, clicking on the 'Reset' button would momentarily change the slider value to 0, but would be immediately reset back to its original value that is backed by the corresponding Binding.

    Having exhausted Animation and DataTriggers, I'm lead to conclude that this may not be possible. That said, given the vast range of capabilities present in WPF, I could very well be wrong...


    Edit: 12/5/2015

    I think I have a solution that will satisfy your key requirement - namely allowing a designer to modify the UI, including the meaning of "reset" Value of the Slider. That said, this solution requires code-behind - but such a code-behind would not prevent the XAML to be fully expressive of your intent.

    The idea is to write a Behavior, and use that Behavior to associate a Button with a Slider, and indicate that this Button should trigger the 'reset' action.

    The basic outline of the Behavior looks like this:

        /// <remarks>
        /// Requires System.Windows.Interactivity.dll to be 
        /// added to the References section of the Project
        /// </remarks>
        public class SliderResetBehavior: Behavior<Button>
        {
            protected override void OnAttached()
            {
                // AssociatedObject refers to a Button instance
                // to which this Behavior is attached
                AssociatedObject.Click += OnClick;
            }
    
            /// <summary>
            /// Upon receiving a Click event, reset the Slider 
            /// </summary>
            private void OnClick(object sender, RoutedEventArgs e)
            {
                if(Slider != null)
                {
                    Slider.Value = ResetValue;
                }
            }
    
            #region Dependency Property - ResetValue
    
            /// <summary>
            /// Stores a double that will act as the definition of 'reset' 
            /// value for the Slider associated with this Behavior. 
            /// </summary>
            public double ResetValue
            {
                get { return (double)GetValue(ResetValueProperty); }
                set { SetValue(ResetValueProperty, value); }
            }
    
            // Using a DependencyProperty as the backing store for ResetValue.
            // This enables animation, styling, binding, etc...
            public static readonly DependencyProperty ResetValueProperty =
                DependencyProperty.Register(
                    "ResetValue", 
                    typeof(double), 
                    typeof(SliderResetBehavior), 
                    new PropertyMetadata(0.0));
    
            #endregion
    
            /// <summary>
            /// Maintains a System.Windows.Controls.Slider object upon 
            /// which this Behavior will act upon. 
            /// </summary>
            #region Dependency Property - Slider 
            public Slider Slider
            {
                get { return (Slider)GetValue(SliderProperty); }
                set { SetValue(SliderProperty, value); }
            }
    
            // Using a DependencyProperty as the backing store for Slider.
            // This enables animation, styling, binding, etc...
            public static readonly DependencyProperty SliderProperty =
                DependencyProperty.Register(
                    "Slider", 
                    typeof(Slider), 
                    typeof(SliderResetBehavior), 
                    new PropertyMetadata(default(Slider)));
    
            #endregion
        }
    

    The XAML can now be quite simple, like this:

    <StackPanel>
        <Slider x:Name="Slider" Minimum="-5" Maximum="5" Value="{Binding SliderValue}"/>
        <!-- 
           Requires xmlns entries like this:
            xmlns:local="clr-namespace:<Namespace containing SliderResetBehavior>"
            xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        -->
        <Button x:Name="button" Content="Reset">
            <i:Interaction.Behaviors>
                <local:SliderResetBehavior ResetValue="0" Slider="{Binding ElementName=Slider}"/>
            </i:Interaction.Behaviors>
        </Button>
    </StackPanel>
    

    This XAML shown above retains the key properties of being fully customizable by a designer using loose XAML only, and allowing data-binding of Slider.Value as well.