I have an MVVM pattern application where I want the users to be able to enter dates, but also apply some validation on those dates. I do this by checking whatever they enter and overwriting it with the nearest valid date, if their entry is not valid. In order to let the user know that their date has been overwritten, I would have tried to animate the foreground of the date picker textbox, but I find that the animation is only visible on the first time their date is "corrected" in this way.
In the MainViewModel, I have a Ping property that notifies the UI each time it is set to "true" and a validation method that sets Ping = true
each time it has to overwrite a date:
public bool Ping
{
get => _ping;
set
{
if (value && !_ping)
{
_ping = value;
OnPropertyChanged();
_ping = false;
}
}
}
private DateTime _from;
//Bound to the Date input field in the UI
public DateTime From
{
get { return _from; }
set
{
if (_from != value)
{
_from = giveValidDate("From", value);
OnPropertyChanged();
}
}
}
private DateTime giveValidDate(string posn, DateTime givenDate)
{
DateTime validDate = new DateTime();
// [...A Load of validation that results in a valid Date output...] //
Ping = givenDate != validDate;
return validDate;
}
There is a TextBox style that I am using that has the animation on it:
<Style x:Key="PingableTextBox" TargetType="TextBox">
<Setter Property="TextBlock.FontSize" Value="18"/>
<Setter Property="TextElement.FontSize" Value="18"/>
<Setter Property="TextElement.Foreground" Value="{StaticResource Text_LightBrush}"/>
<Setter Property="TextElement.FontWeight" Value="Normal"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border BorderThickness="{TemplateBinding Border.BorderThickness}"
CornerRadius="2"
BorderBrush="{StaticResource Highlight_LightBrush}"
Background="{StaticResource Empty_DarkBrush}"
x:Name="border"
SnapsToDevicePixels="True">
<ScrollViewer HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"
Name="PART_ContentHost" Focusable="False" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="UIElement.IsMouseOver" Value="True">
<Setter Property="Border.BorderBrush" TargetName="border" Value="{StaticResource Good_MidBrush}"/>
<Setter Property="Cursor" Value="IBeam"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding Ping}" Value="true">
<DataTrigger.EnterActions>
<StopStoryboard BeginStoryboardName="Pinger"/>
<BeginStoryboard Name="Pinger">
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="Foreground.Color"
From="{StaticResource Bad_Bright}" To="{StaticResource Text_Light}" FillBehavior="Stop"
Duration="0:0:0:1.0"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<RemoveStoryboard BeginStoryboardName="Pinger"/>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
However, when I run the application, the trigger is only seen to act once (the brief red flash when an invalid date is selected):
I have seen many other questions on Stack Overflow about the same issue, but the solution has always been to add the line <StopStoryboard BeginStoryboardName="Pinger"/>
in the Enter Actions, to add the line <RemoveStoryboard BeginStoryboardName="Pinger"/>
to the Exit Actions or to add FillBehavior="Stop"
to the storyboard. I have tried every combination of each of these in all places I could think of and the problem still persists.
Is there some other explanation for the problem that I could have missed that would fix it for me, or something that I have failed to implement correctly.. In short, why is it only firing once?
PS - some of the questions I have used to implement the code you see above:
WPF Storyboard only fires once
WPF Fade In / Out only runs once
WPF MultiDataTrigger on Tag property only firing once
WPF - problem with DataTrigger which work only once
CodePudding user response:
You must raise the PropertyChanged
after you reset the Ping
property in order to trigger the Trigger.ExitAction
.
Setting the backing field _ping
does not propagate any change notification to the view.
This means your problem is not the problem that your quoted answer tries to solve.
You should also not define the Trigger.ExitAction
in your scenario. Since you have configured the animation to stop automatically once the timeline has completed (FillBehavior="Stop"
), you don't need to do anything to stop it. Note that RemoveStoryboard
does not do you any favor in your case. It would only complicate the logic to reset the property since RemoveStoryboard
would kill the animation immediately on the instant property toggle. This means, avoiding the Trigger.ExitAction
or to be more precise the RemoveStoryboard
allows you to toggle the property instantly:
// Trigger the animation
Ping = givenDate != validDate;
// Reset the property immediately to reset the animation trigger.
// Because the `Trigger.ExitAction` is deleted,
// the currently running animation will complete the timeline.
Ping = false;
If you want to implement the logic more gracefully you can toggle the Ping
property and define the Trigger.EnterAction
for the true
state and the Trigger.ExitAction
for the false
state (thus converting each state into a validation error signal):
public bool Ping
{
get => _ping;
set
{
if (value && !_ping)
{
_ping = value;
OnPropertyChanged();
}
}
}
private DateTime giveValidDate(string posn, DateTime givenDate)
{
DateTime validDate = new DateTime();
// [...A Load of validation that results in a valid Date output...] //
// Trigger the animation by toggling the property.
if (givenDate != validDate)
{
Ping ^= true;
}
return validDate;
}
<DataTrigger Binding="{Binding Ping}" Value="true">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="Foreground.Color"
From="{StaticResource Bad_Bright}" To="{StaticResource Text_Light}" FillBehavior="Stop"
Duration="0:0:0:1.0"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="Foreground.Color"
From="{StaticResource Bad_Bright}" To="{StaticResource Text_Light}" FillBehavior="Stop"
Duration="0:0:0:1.0"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>