WPF:双向绑定始终更新 - 单向绑定仅更新一次
WPF: TwoWay binding is ALWAYS updated - OneWay binding is ONLY ONCE updated
我知道你在想什么:都2017年了,请不要再提这个了,但我实在找不到任何有价值的解释。
请查看此 XAML-代码中的 ActiveNotes 属性。
我的 XAML 中有这个 TwoWay 绑定,效果很好。如果触发了 ScaleNotes 的 PropertyChanged 事件并且绑定设置为 TwoWay,它总是会更新。
<c:Keyboard
Grid.Row="2"
Grid.Column="0"
PlayCommand="{Binding PlayCommand}"
StopCommand="{Binding StopCommand}"
ActiveNotes="{Binding ScaleNotes, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
ViewModel 中的 ScaleNotes 属性 看起来像这样。每当它更改时,保证 将触发 PropertyChanged 事件。我检查并仔细检查了它。 ViewModel 中的业务逻辑有效。
private ReadOnlyCollection<eNote> _ScaleNotes;
public ReadOnlyCollection<eNote> ScaleNotes
{
get { return _ScaleNotes; }
set { SetField(ref _ScaleNotes, value); }
}
[DebuggerStepThrough]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
[DebuggerStepThrough]
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
到目前为止一切正常。每当 VM 中的 ScaleNotes 属性 发生变化时,目标 属性 ActiveNotes 就会更新。
现在的问题:
如果我只将 XAML 中的绑定更改为 OneWay 并且 VM 中的业务逻辑保持 100% 相同,则目标对象 中的 ActivesNotes 属性 是即使触发了 PropertyChanged 事件,也只更新一次。我检查并仔细检查了它。始终触发 ScaleNotes 属性 的 PropertyChanged 事件。
<c:Keyboard
Grid.Row="2"
Grid.Column="0"
PlayCommand="{Binding PlayCommand}"
StopCommand="{Binding StopCommand}"
ActiveNotes="{Binding ScaleNotes, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"/>
为了使这个完整,这里是目标对象中的 DP。
public static DependencyProperty ActiveNotesProperty = DependencyProperty.Register(
"ActiveNotes",
typeof(ReadOnlyCollection<eNote>),
typeof(Keyboard),
new PropertyMetadata(OnActiveNotesChanged));
public ReadOnlyCollection<eNote> ActiveNotes
{
get
{
return (ReadOnlyCollection<eNote>)GetValue(ActiveNotesProperty);
}
set
{
SetValue(ActiveNotesProperty, value);
}
}
private static void OnActiveNotesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Keyboard keyboard = (Keyboard)d;
keyboard.ActiveNotes = (ReadOnlyCollection<eNote>)e.NewValue;
if ((keyboard.ActiveNotes != null) && (keyboard.ActiveNotes.Count > 0))
{
keyboard.AllKeys.ForEach(k => { if ( k.Note != eNote.Undefined) k.IsActiveKey = true; });
keyboard.AllKeys.ForEach(k => { if ((k.Note != eNote.Undefined) && (!keyboard.ActiveNotes.Contains(k.Note))) k.IsActiveKey = false; });
}
else
{
keyboard.AllKeys.ForEach(k => { if (k.Note != eNote.Undefined) k.IsActiveKey = true; });
}
}
我不明白这个。据我所知,OneWay 和 TwoWay 只定义值的更新方向,而不是更新频率。
我无法理解,TwoWay 一切正常,业务逻辑保持 100% 相同,而 OneWay 是一个交易破坏者。
如果您问自己,为什么我想知道这个:此绑定计划为单向绑定。以任何方式更新源代码都是没有意义的。我只是把它改成了 TwoWay,因为 OneWay 没有按预期工作。
解决方案在@MikeStrobel 的帮助下:(见评论)
代码需要这样修改:
private static void OnActiveNotesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Keyboard keyboard = (Keyboard)d;
//THIS LINE BREAKED THE CODE, WHEN USING OneWay binding BUT NOT WITH TwoWay binding
//keyboard.ActiveNotes = (ReadOnlyCollection<eNote>)e.NewValue;
if ((keyboard.ActiveNotes != null) && (keyboard.ActiveNotes.Count > 0))
{
keyboard.AllKeys.ForEach(k => { if ( k.Note != eNote.Undefined) k.IsActiveKey = true; });
keyboard.AllKeys.ForEach(k => { if ((k.Note != eNote.Undefined) && (!keyboard.ActiveNotes.Contains(k.Note))) k.IsActiveKey = false; });
}
else
{
keyboard.AllKeys.ForEach(k => { if (k.Note != eNote.Undefined) k.IsActiveKey = true; });
}
}
使用单向绑定,OnActiveNotesChanged 事件处理程序方法中的分配删除或清除绑定。 Mike 说得对,这个赋值是完全没有必要的,因为此时值已经在 属性 中设置好了。所以它根本没有意义,无论我使用 OneWay 还是 TwoWay 绑定。
依赖属性有一个复杂的系统precedence。依赖项 属性 在任何给定时间的值可能来自各种来源:绑定、样式设置器、触发器设置器等。本地值具有最高优先级,当您设置本地值时,您会抑制来自的值其他来源。
在绑定的情况下,设置本地值将导致源到目标绑定(OneWay
或 OneTime
)*删除*。但是,当您使用目标到源绑定(TwoWay
或 OneWayToSource
)在 属性 上设置本地值时,将保留绑定,并且您分配的本地值将传播回源头。
对于您的情况,问题出在这里:
keyboard.ActiveNotes = (ReadOnlyCollection<eNote>)e.NewValue;
在您的 OnActiveNotesChanged
处理程序中,您正在为 ActiveNotes
分配一个新的本地值,这导致您的 OneWay
绑定被删除。幸运的是,解决方案很简单:您可以完全删除这一行,因为它是多余的。在大多数情况下,不需要在其自己的更改处理程序中分配依赖项 属性——新值已经应用。 (如果您希望有机会在应用建议值之前替换它,可以在 CoerceValueCallback
中进行替换,您也可以在 PropertyMetadata
中指定它。)
让我在这个问题上失眠后分享我的经验!
我在一个稍微不同的用例上偶然发现了这个问题,我找不到解决方案,但多亏了上面的建议,我终于可以解决它。
简而言之,我遇到了同样的绑定解离问题,但根本没有访问受控 属性 的代码!问题发生在 CustomControl
!
事实上,您还必须意识到,如果 CustomControl
声明了一个 DependencyProperty
,它被 消费者[=48] 在 Binding
中使用=] 的 CustomControl
和 Template 的 CustomControl
,那么你 必须 确保我们的 Binding in the Template of the CustomControl
is eighter of type TemplateBinding
or a regular binding using OneWay
or OneWayToSource
.
public static readonly DependencyProperty IsCheckedProperty = DependencyProperty.Register("IsChecked", typeof(bool), typeof(QAToggle), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.None));
public bool IsChecked
{
get => (bool)this.GetValue(IsCheckedProperty);
set => this.SetValue(IsCheckedProperty, value);
}
如果不是,更新时 UserControl
模板中使用的 Binding
也会为 CustomControl
设置一个新值 属性 从而删除外部CustomControl
.
的消费者设置的绑定
<Style TargetType="{x:Type ccont:QAToggle}">
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="MinHeight" Value="23" />
<Setter Property="MinWidth" Value="75" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ccont:QAToggle}">
<ToggleButton x:Name = "QAToggle"
Command = "{TemplateBinding Command}"
CommandParameter = "{TemplateBinding CommandParameter}"
FontSize = "{TemplateBinding FontSize}"
FontWeight = "{TemplateBinding FontWeight}"
FontFamily = "{TemplateBinding FontFamily}"
IsThreeState = "False"
IsChecked = "{Binding IsChecked, RelativeSource={RelativeSource AncestorType=ccont:QAToggle}}">
此处IsChecked = "{Binding IsChecked, RelativeSource={RelativeSource AncestorType=ccont:QAToggle}}"
行将打破外部绑定。为避免这种情况,将其更改为:
IsChecked = "{TemplateBinding IsChecked}"
或
IsChecked = "{Binding IsChecked, RelativeSource={RelativeSource AncestorType=ccont:QAToggle}, Mode=OneWay}"
我知道你在想什么:都2017年了,请不要再提这个了,但我实在找不到任何有价值的解释。
请查看此 XAML-代码中的 ActiveNotes 属性。
我的 XAML 中有这个 TwoWay 绑定,效果很好。如果触发了 ScaleNotes 的 PropertyChanged 事件并且绑定设置为 TwoWay,它总是会更新。
<c:Keyboard
Grid.Row="2"
Grid.Column="0"
PlayCommand="{Binding PlayCommand}"
StopCommand="{Binding StopCommand}"
ActiveNotes="{Binding ScaleNotes, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
ViewModel 中的 ScaleNotes 属性 看起来像这样。每当它更改时,保证 将触发 PropertyChanged 事件。我检查并仔细检查了它。 ViewModel 中的业务逻辑有效。
private ReadOnlyCollection<eNote> _ScaleNotes;
public ReadOnlyCollection<eNote> ScaleNotes
{
get { return _ScaleNotes; }
set { SetField(ref _ScaleNotes, value); }
}
[DebuggerStepThrough]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
[DebuggerStepThrough]
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
到目前为止一切正常。每当 VM 中的 ScaleNotes 属性 发生变化时,目标 属性 ActiveNotes 就会更新。
现在的问题:
如果我只将 XAML 中的绑定更改为 OneWay 并且 VM 中的业务逻辑保持 100% 相同,则目标对象 中的 ActivesNotes 属性 是即使触发了 PropertyChanged 事件,也只更新一次。我检查并仔细检查了它。始终触发 ScaleNotes 属性 的 PropertyChanged 事件。
<c:Keyboard
Grid.Row="2"
Grid.Column="0"
PlayCommand="{Binding PlayCommand}"
StopCommand="{Binding StopCommand}"
ActiveNotes="{Binding ScaleNotes, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"/>
为了使这个完整,这里是目标对象中的 DP。
public static DependencyProperty ActiveNotesProperty = DependencyProperty.Register(
"ActiveNotes",
typeof(ReadOnlyCollection<eNote>),
typeof(Keyboard),
new PropertyMetadata(OnActiveNotesChanged));
public ReadOnlyCollection<eNote> ActiveNotes
{
get
{
return (ReadOnlyCollection<eNote>)GetValue(ActiveNotesProperty);
}
set
{
SetValue(ActiveNotesProperty, value);
}
}
private static void OnActiveNotesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Keyboard keyboard = (Keyboard)d;
keyboard.ActiveNotes = (ReadOnlyCollection<eNote>)e.NewValue;
if ((keyboard.ActiveNotes != null) && (keyboard.ActiveNotes.Count > 0))
{
keyboard.AllKeys.ForEach(k => { if ( k.Note != eNote.Undefined) k.IsActiveKey = true; });
keyboard.AllKeys.ForEach(k => { if ((k.Note != eNote.Undefined) && (!keyboard.ActiveNotes.Contains(k.Note))) k.IsActiveKey = false; });
}
else
{
keyboard.AllKeys.ForEach(k => { if (k.Note != eNote.Undefined) k.IsActiveKey = true; });
}
}
我不明白这个。据我所知,OneWay 和 TwoWay 只定义值的更新方向,而不是更新频率。
我无法理解,TwoWay 一切正常,业务逻辑保持 100% 相同,而 OneWay 是一个交易破坏者。
如果您问自己,为什么我想知道这个:此绑定计划为单向绑定。以任何方式更新源代码都是没有意义的。我只是把它改成了 TwoWay,因为 OneWay 没有按预期工作。
解决方案在@MikeStrobel 的帮助下:(见评论)
代码需要这样修改:
private static void OnActiveNotesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Keyboard keyboard = (Keyboard)d;
//THIS LINE BREAKED THE CODE, WHEN USING OneWay binding BUT NOT WITH TwoWay binding
//keyboard.ActiveNotes = (ReadOnlyCollection<eNote>)e.NewValue;
if ((keyboard.ActiveNotes != null) && (keyboard.ActiveNotes.Count > 0))
{
keyboard.AllKeys.ForEach(k => { if ( k.Note != eNote.Undefined) k.IsActiveKey = true; });
keyboard.AllKeys.ForEach(k => { if ((k.Note != eNote.Undefined) && (!keyboard.ActiveNotes.Contains(k.Note))) k.IsActiveKey = false; });
}
else
{
keyboard.AllKeys.ForEach(k => { if (k.Note != eNote.Undefined) k.IsActiveKey = true; });
}
}
使用单向绑定,OnActiveNotesChanged 事件处理程序方法中的分配删除或清除绑定。 Mike 说得对,这个赋值是完全没有必要的,因为此时值已经在 属性 中设置好了。所以它根本没有意义,无论我使用 OneWay 还是 TwoWay 绑定。
依赖属性有一个复杂的系统precedence。依赖项 属性 在任何给定时间的值可能来自各种来源:绑定、样式设置器、触发器设置器等。本地值具有最高优先级,当您设置本地值时,您会抑制来自的值其他来源。
在绑定的情况下,设置本地值将导致源到目标绑定(OneWay
或 OneTime
)*删除*。但是,当您使用目标到源绑定(TwoWay
或 OneWayToSource
)在 属性 上设置本地值时,将保留绑定,并且您分配的本地值将传播回源头。
对于您的情况,问题出在这里:
keyboard.ActiveNotes = (ReadOnlyCollection<eNote>)e.NewValue;
在您的 OnActiveNotesChanged
处理程序中,您正在为 ActiveNotes
分配一个新的本地值,这导致您的 OneWay
绑定被删除。幸运的是,解决方案很简单:您可以完全删除这一行,因为它是多余的。在大多数情况下,不需要在其自己的更改处理程序中分配依赖项 属性——新值已经应用。 (如果您希望有机会在应用建议值之前替换它,可以在 CoerceValueCallback
中进行替换,您也可以在 PropertyMetadata
中指定它。)
让我在这个问题上失眠后分享我的经验!
我在一个稍微不同的用例上偶然发现了这个问题,我找不到解决方案,但多亏了上面的建议,我终于可以解决它。
简而言之,我遇到了同样的绑定解离问题,但根本没有访问受控 属性 的代码!问题发生在 CustomControl
!
事实上,您还必须意识到,如果 CustomControl
声明了一个 DependencyProperty
,它被 消费者[=48] 在 Binding
中使用=] 的 CustomControl
和 Template 的 CustomControl
,那么你 必须 确保我们的 Binding in the Template of the CustomControl
is eighter of type TemplateBinding
or a regular binding using OneWay
or OneWayToSource
.
public static readonly DependencyProperty IsCheckedProperty = DependencyProperty.Register("IsChecked", typeof(bool), typeof(QAToggle), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.None));
public bool IsChecked
{
get => (bool)this.GetValue(IsCheckedProperty);
set => this.SetValue(IsCheckedProperty, value);
}
如果不是,更新时 UserControl
模板中使用的 Binding
也会为 CustomControl
设置一个新值 属性 从而删除外部CustomControl
.
<Style TargetType="{x:Type ccont:QAToggle}">
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="MinHeight" Value="23" />
<Setter Property="MinWidth" Value="75" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ccont:QAToggle}">
<ToggleButton x:Name = "QAToggle"
Command = "{TemplateBinding Command}"
CommandParameter = "{TemplateBinding CommandParameter}"
FontSize = "{TemplateBinding FontSize}"
FontWeight = "{TemplateBinding FontWeight}"
FontFamily = "{TemplateBinding FontFamily}"
IsThreeState = "False"
IsChecked = "{Binding IsChecked, RelativeSource={RelativeSource AncestorType=ccont:QAToggle}}">
此处IsChecked = "{Binding IsChecked, RelativeSource={RelativeSource AncestorType=ccont:QAToggle}}"
行将打破外部绑定。为避免这种情况,将其更改为:
IsChecked = "{TemplateBinding IsChecked}"
或
IsChecked = "{Binding IsChecked, RelativeSource={RelativeSource AncestorType=ccont:QAToggle}, Mode=OneWay}"