所有带有用户界面元素的操作都通过主应用程序线程的调度程序队列发生。
下面我写的不是一个精确的,而是一个大概的工作场景。
当您更改 ViewModel 中的任何属性时,会创建一个任务来更改 UI 元素的属性(通过绑定),并将此任务推送到 Dispatcher 队列。
下次更改 ViewModel 属性时,下一个任务将排队。
如果您很快更改 ViewModel 属性的值,则下一个任务可能会在前一个任务之前排队。
当第一个任务启动时,它会读取 ViewModel 属性的当前值,这将不再是第一个属性值,而是第二个。
绑定会改变UI元素的属性值,这个改变会触发相应的事件,也会在同一个任务中执行。
处理完事件后,第一个任务就完成了。
稍等片刻,第二个任务就开始了。
绑定将重新分配 ViewModel 属性的当前值。
但是元素的 UI 属性已经包含了这个值。
并且由于 UI 元素的属性值没有发生变化,因此也就不会产生关于其变化的事件。
应要求添加:
如果您可以提供仅 XAML 的解决方案(这将有助于保持逻辑和设计分离),那就太好了!
实施。
要在 XAML 中使用,您需要修改 DataTrigger 以检查 PropertyChanged 事件流上的值。
但是更改 DataTrigger 很棘手。
它的许多成员被声明为internal。
因此,您必须创建自己的中间触发器 (CockingTrigger),该触发器将根据给定条件触发。
DataTrigger 将绑定到 CockedTrigger 属性。
完成动作后,必须解除扳机。
为此,必须创建动画中使用的附加属性。
CockedTrigger 将在 Resources 中创建并需要绑定,因此它必须从 Freezable 派生。
此外,它在 Dispatcher 线程中部分不起作用,因此,需要实现 INotifyPropertyChanged,并且所有必要的 DependecyProperties 值都额外存储在私有字段中。
有很多代码。
using System;
using System.ComponentModel;
using System.Reflection;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media.Animation;
namespace Proxy
{
public class CockedTrigger : Freezable, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected override Freezable CreateInstanceCore()
=> new CockedTrigger();
/// <summary>The binding source.</summary>
public object Source
{
get => GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
/// <summary><see cref="DependencyProperty"/> for property <see cref="Source"/>.</summary>
public static readonly DependencyProperty SourceProperty =
DependencyProperty.Register(nameof(Source), typeof(object), typeof(CockedTrigger), new PropertyMetadata(null, SourceChanged));
private static void SourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
CockedTrigger trigget = (CockedTrigger)d;
if (e.OldValue is INotifyPropertyChanged oldTarget)
{
oldTarget.PropertyChanged -= trigget.OnObservablePropertyChanged;
}
if (e.NewValue is INotifyPropertyChanged newTarget)
{
trigget.SetPropertyFunc();
newTarget.PropertyChanged += trigget.OnObservablePropertyChanged;
}
}
public Func<object> PropertyFunc
{
get => _propertyFunc; private set
{
if (!Equals(_propertyFunc, value))
{
_propertyFunc = value;
PropertyChanged?.Invoke(this, PropertyFuncPropertyChangedEventArgs);
}
}
}
private Func<object> _propertyFunc;
private static readonly PropertyChangedEventArgs PropertyFuncPropertyChangedEventArgs
= new PropertyChangedEventArgs(nameof(PropertyFunc));
private object fieldSource;
private string fieldProperty;
private void SetPropertyFunc()
{
fieldSource = Source;
fieldProperty = Property;
PropertyFunc = GetValuePropertyFunc(Source, Property);
}
public static Func<object> GetValuePropertyFunc(object source, string propertyName)
{
if (source == null || string.IsNullOrWhiteSpace(propertyName))
{
return null;
}
Type type = source.GetType();
PropertyInfo property = type.GetProperty(propertyName);
if (property == null)
{
return null;
}
if (property.PropertyType.IsClass ||
property.PropertyType.IsInterface ||
property.PropertyType.IsArray)
{
return (Func<object>)property?.GetMethod.CreateDelegate(typeof(Func<object>), source);
}
if (property.PropertyType.IsValueType ||
property.PropertyType.IsEnum)
{
Type funcType = typeof(Func<>);
Type funcTypeGeneric = funcType.MakeGenericType(property.PropertyType);
Delegate funcDelegate = property.GetMethod.CreateDelegate(funcTypeGeneric, source);
return () => funcDelegate.DynamicInvoke();
}
throw new NotImplementedException($"Not implemented for property type {property.PropertyType.FullName}.");
}
private void OnObservablePropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (!string.IsNullOrWhiteSpace(fieldProperty) &&
e.PropertyName == fieldProperty &&
PropertyFunc != null &&
Equals(PropertyFunc(), fieldValue))
{
IsTriggerCocked = true;
}
}
/// <summary>Name observable Property.</summary>
public string Property
{
get => (string)GetValue(PropertyProperty);
set => SetValue(PropertyProperty, value);
}
/// <summary><see cref="DependencyProperty"/> for property <see cref="Property"/>.</summary>
public static readonly DependencyProperty PropertyProperty =
DependencyProperty.Register(nameof(Property), typeof(string), typeof(CockedTrigger), new PropertyMetadata(null,
(d, e) => ((CockedTrigger)d).SetPropertyFunc()));
/// <summary>Comparison value.</summary>
public object Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
/// <summary><see cref="DependencyProperty"/> for property <see cref="Value"/>.</summary>
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(nameof(Value), typeof(object), typeof(CockedTrigger), new PropertyMetadata(null,
(d, e) => ((CockedTrigger)d).fieldValue = e.NewValue));
private object fieldValue;
/// <summary>
/// Read-only property.
/// Returns true if, after changing the observed property, its value is equal to the expected value.
/// Can return to false only with the UnloadTrigger method.
/// </summary>
public bool IsTriggerCocked
{
get => _isTriggerCocked; private set
{
if (!Equals(_isTriggerCocked, value))
{
_isTriggerCocked = value;
PropertyChanged?.Invoke(this, IsTriggerCockedPropertyChangedEventArgs);
}
}
}
private bool _isTriggerCocked;
private static readonly PropertyChangedEventArgs IsTriggerCockedPropertyChangedEventArgs
= new PropertyChangedEventArgs(nameof(IsTriggerCocked));
public void UnloadTrigger() => IsTriggerCocked = false;
public static CockedTrigger GetResetWhenCompleted(Timeline animation)
{
return (CockedTrigger)animation.GetValue(ResetWhenCompletedProperty);
}
public static void SetResetWhenCompleted(Timeline animation, CockedTrigger value)
{
animation.SetValue(ResetWhenCompletedProperty, value);
}
// Using a DependencyProperty as the backing store for ResetWhenCompleted. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ResetWhenCompletedProperty =
DependencyProperty.RegisterAttached("ResetWhenCompleted", typeof(CockedTrigger), typeof(CockedTrigger), new PropertyMetadata(null, ResetWhenCompletedChanged));
private static void ResetWhenCompletedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is Timeline animation))
{
throw new NotImplementedException($"Implemented only for classes derived from {nameof(Timeline)}.");
}
if (e.OldValue is CockedTrigger oldTrigger)
{
animation.Completed -= oldTrigger.UnloadTrigger;
}
if (e.NewValue is CockedTrigger newTrigger)
{
animation.Completed += newTrigger.UnloadTrigger;
}
}
private void UnloadTrigger(object sender, EventArgs e)
=> UnloadTrigger();
protected override bool FreezeCore(bool isChecking)
{
if (isChecking)
return true;
object source = fieldSource;
object value = fieldValue;
string property = fieldProperty;
BindingOperations.ClearBinding(this, SourceProperty);
BindingOperations.ClearBinding(this, ValueProperty);
BindingOperations.ClearBinding(this, PropertyProperty);
Source = source;
Value = value;
Property = property;
return base.FreezeCore(isChecking);
}
}
}
使用示例。
一个简单的 ViewModel,其中,在给定的时间间隔内,AnyProperty 属性将尽可能短的时间设置为 true,然后立即重置为 false。
为了方便测试,还显示了值下一次跳转的剩余间隔。
using Simplified;
using System;
using System.Timers;
namespace CheckingProxyTrigger
{
public class CptViewModel : BaseInpc
{
private bool _anyProperty;
private double _tickInterval = 10; // 10 s
private double _intervalToNextTick;
public bool AnyProperty { get => _anyProperty; set => Set(ref _anyProperty, value); }
public double TickInterval { get => _tickInterval; set => Set(ref _tickInterval, value); }
public double IntervalToNextTick { get => _intervalToNextTick; private set => Set(ref _intervalToNextTick, value); }
private DateTime nextTime;
private readonly Timer timer = new Timer()
{
Interval = 10 // 10 ms
};
public CptViewModel()
{
nextTime = DateTime.Now.AddSeconds(TickInterval);
timer.Elapsed += OnTick;
timer.Start();
}
private void OnTick(object sender, ElapsedEventArgs e)
{
IntervalToNextTick = (nextTime - DateTime.Now).TotalSeconds;
if (IntervalToNextTick <= 0)
{
timer.Stop();
AnyProperty = true;
AnyProperty = false;
nextTime = DateTime.Now.AddSeconds(TickInterval);
timer.Start();
}
}
}
}
该窗口由几个TextBlock 组成,用于跟踪各种状态。
其中一个使用数据上下文绑定到AnyProperty 并处理其更改事件以计算值跳转。
在每次跳转时,事件(根据逻辑)应该被调用两次。
一次为True,第二次为False。
但是因为有队列,只处理了一个事件。
下面有两个Border。
一个被设置为从跳跃属性开始。
它只适用于第一次跳跃。
第二个来自资源中的 CockedTrigger。
它适用于每次跳跃。
<Window x:Class="CheckingProxyTrigger.CptWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:CheckingProxyTrigger" xmlns:proxy="clr-namespace:Proxy;assembly=Common"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d"
Title="CptWindow" Height="450" Width="800">
<FrameworkElement.DataContext>
<local:CptViewModel/>
</FrameworkElement.DataContext>
<FrameworkElement.Resources>
<proxy:CockedTrigger x:Key="trigger"
Source="{Binding Mode=OneWay}"
Property="AnyProperty">
<proxy:CockedTrigger.Value>
<sys:Boolean>True</sys:Boolean>
</proxy:CockedTrigger.Value>
</proxy:CockedTrigger>
</FrameworkElement.Resources>
<StackPanel>
<TextBlock>
<Run Text="Interval until next tick:"/>
<Run Text="{Binding IntervalToNextTick, Mode=OneWay, StringFormat=\{0:F1\}}"/>
</TextBlock>
<TextBlock DataContext="{Binding AnyProperty}" DataContextChanged="TextBlock_DataContextChanged">
<Run Text="Count of property value changes:"/>
<Run x:Name="countChanges" Text="0"/>
</TextBlock>
<TextBlock>
<Run Text="Trigger cocked:"/>
<Run Text="{Binding IsTriggerCocked, Mode=OneWay, Source={StaticResource trigger}}"/>
</TextBlock>
<Border Height="50" Width="50">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="Red"/>
<Style.Triggers>
<Trigger Property="ActualWidth" Value="50">
<Setter Property="Background" Value="Green"/>
</Trigger>
<DataTrigger Binding="{Binding AnyProperty}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Width"
From="400"
To="50"
Duration="0:0:2"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
<Border Height="50" Width="50" Margin="10">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="Red"/>
<Style.Triggers>
<Trigger Property="ActualWidth" Value="50">
<Setter Property="Background" Value="Green"/>
</Trigger>
<DataTrigger Binding="{Binding IsTriggerCocked, Mode=OneWay, Source={StaticResource trigger}}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Width"
From="400"
To="50"
Duration="0:0:2"
proxy:CockedTrigger.ResetWhenCompleted="{StaticResource trigger}"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
</StackPanel>
</Window>
using System.Windows;
namespace CheckingProxyTrigger
{
public partial class CptWindow : Window
{
public CptWindow()
{
InitializeComponent();
}
private int count;
private void TextBlock_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (countChanges != null)
{
count++;
countChanges.Text = count.ToString();
}
}
}
}