将 setter 更改为 属性 并通知
Change property from setter and notify
在 属性 的 setter 中,有时我必须将 属性 的值更改为当前设置的值以外的值。对于 Windows Store 应用程序,仅通过引发 PropertyChanged
事件是行不通的。我可以更改值,但 UI 不会根据该更改自行更新。
<TextBox Text="{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
视图模型:
class MyViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string text;
public string Text
{
get { return text; }
set
{
if ( text != value ) {
text = value == "0" ? "" : value;
OnPropertyChanged();
}
}
}
protected void OnPropertyChanged( [CallerMemberName] string propertyName = null )
{
PropertyChanged?.Invoke( this, new PropertyChangedEventArgs( propertyName ) );
}
}
我写了下面的方法,很好地解决了这个问题:
protected void OnPropertyChanged_Delayed( [CallerMemberName] string propertyName = null )
{
Task.Delay( 1 ).ContinueWith(
t => OnPropertyChanged( propertyName ),
TaskScheduler.FromCurrentSynchronizationContext()
);
}
我叫这个而不是 OnPropertyChanged
。但我最近在我的 Windows 商店应用程序中发现了一个间歇性错误,该错误是由用户离开页面后发生的 OnPropertyChanged_Delayed
调用引起的。这促使我寻求替代解决方案。
是否有更好的方法从 setter 中更改 属性 的值并将更改通知 UI?我目前正在开发一个 Windows Store 应用程序,所以我想在这方面得到一个答案。但我最终会将其移植到 UWP。这也是 UWP 上的问题吗?如果是这样,我也想要一个解决方案(可能是相同的)。
我在 UWP 中执行此操作没有问题。我不知道 Windows 商店应用程序有任何不同。
private string _informativeMessage;
public string InformativeMessage
{
get { return _informativeMessage; }
set
{
if (_informativeMessage != value)
{
if(value == SomeValueThatMakesMeNeedToChangeIt)
{
_informativeMessage = "Overridden Value";
}
else
{
_informativeMessage = value;
}
RaisePropertyChanged();
}
}
}
你的 属性 长什么样?
正如我在上面的评论中所解释的那样,我在处理 ComboBox 控件时 运行 遇到过此类问题。我想更改 setter 中所选项目的值,但视图忽略了我的新值。延迟 PropertyChanged 事件是一种丑陋的解决方法,可能会导致意外的副作用。
但解决此问题的另一种方法是将 SelectionChanged 事件绑定到视图模型中的命令。
这个 XAML 像往常一样绑定到 SelectedItem,但也使用 System.Windows.Interactivity 绑定 SelectionChanged 事件:
<ComboBox ItemsSource="{Binding MyItems}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding SelectionChangedCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</ComboBox>
在您的视图模型中,不要尝试覆盖 setter 中的选定值。相反,在 ICommand 实现中更改它:
string _selectedItem;
public string SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
PropertyChanged(this, new PropertyChangedEventArgs("SelectedItem"));
}
}
// RelayCommand is just a custom ICommand implementation which calls the specified delegate when the command is executed
public RelayCommand SelectionChangedCommand
{
get
{
return new RelayCommand(unused =>
{
if (_selectedItem == MyItems[2])
_selectedItem = MyItems[0];
PropertyChanged(this, new PropertyChangedEventArgs("SelectedItem"));
});
}
}
如我所说,我只观察到 SelectedItem 属性 的这种行为。如果您在使用 TextBox 时遇到问题,那么您可以尝试像这样更改绑定的 UpdateSourceTrigger:
<TextBox Text={Binding MyText, Mode=TwoWay, UpdatedSourceTrigger=PropertyChanged} />
我的问题是:在 属性 的 setter 中,如何更改设置的值并让 UI 识别该更改?
双向 XAML 绑定假定设置到源的目标值也是源实际存储的值。它忽略了它刚刚设置的 属性 的 PropertyChanged
事件,因此它不会在更新后检查源的实际值。
文本框
最简单的解决方案是:进行单向绑定,而不是在 TextChanged
处理程序中更新源。
<TextBox Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}"
TextChanged="TextBox_TextChanged"/>
事件处理程序:
void TextBox_TextChanged( object sender, TextChangedEventArgs e )
{
var tb = (TextBox)sender;
var pm = (MyViewModel)DataContext;
pm.Text = tb.Text; //update one-way binding's source
}
当 UI 的文本更改时,通过设置视图模型的文本来处理事件。视图文本的单向绑定意味着它随后接收实际设置的值。这样做可以避免上述 XAML 绑定的限制。
可以保留双向绑定,并且上述解决方案仍然有效。但是如果想让双向绑定更新它的源码,那么代码就稍微长一点:
void TextBox_TextChanged( object sender, TextChangedEventArgs e )
{
var tb = (TextBox)sender;
var pm = (MyViewModel)DataContext;
tb.GetBindingExpression( TextBox.TextProperty ).UpdateSource();
if ( tb.Text != pm.Text )
tb.Text = pm.Text; //correct two-way binding's target
}
如果您不需要 UpdateSourceTrigger=PropertyChanged
那么另一种选择是:
<TextBox Text="{Binding Text, Mode=TwoWay}" LostFocus="TextBox_LostFocus"/>
使用事件处理程序:
void TextBox_LostFocus( object sender, RoutedEventArgs e )
{
var tb = (TextBox)sender;
var pm = (MyViewModel)DataContext;
tb.GetBindingExpression( TextBox.TextProperty ).UpdateSource();
pm.OnPropertyChanged( "Text" );
}
组合框
@RogerN 的回答有助于说明使用 SelectionChanged
可以解决问题,但他实际上并没有说明如何更改 setter 中的值。我会在这里展示。
<ComboBox ItemsSource="{Binding Items}"
SelectedItem="{Binding Text, Mode=TwoWay}"
SelectionChanged="ComboBox_SelectionChanged"/>
将此 属性 添加到视图模型:
public IList<string> Items
{
get { return items; }
}
readonly IList<string> items = new ObservableCollection<string> { "first", "second", "third", "forth" };
在 Text
属性 的 setter 中,像这样更改值:
text = value == "third" ? "forth" : value;
上面显示的 TextBox
的第一种方法不适用于 ComboBox
,但第二种方法有效。
void ComboBox_SelectionChanged( object sender, SelectionChangedEventArgs e )
{
var cb = (ComboBox)sender;
var pm = (MyViewModel)DataContext;
cb.GetBindingExpression( ComboBox.SelectedItemProperty ).UpdateSource();
if ( (string)cb.SelectedItem != pm.Text )
cb.SelectedItem = pm.Text; //correct two-way binding's target
}
在 属性 的 setter 中,有时我必须将 属性 的值更改为当前设置的值以外的值。对于 Windows Store 应用程序,仅通过引发 PropertyChanged
事件是行不通的。我可以更改值,但 UI 不会根据该更改自行更新。
<TextBox Text="{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
视图模型:
class MyViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string text;
public string Text
{
get { return text; }
set
{
if ( text != value ) {
text = value == "0" ? "" : value;
OnPropertyChanged();
}
}
}
protected void OnPropertyChanged( [CallerMemberName] string propertyName = null )
{
PropertyChanged?.Invoke( this, new PropertyChangedEventArgs( propertyName ) );
}
}
我写了下面的方法,很好地解决了这个问题:
protected void OnPropertyChanged_Delayed( [CallerMemberName] string propertyName = null )
{
Task.Delay( 1 ).ContinueWith(
t => OnPropertyChanged( propertyName ),
TaskScheduler.FromCurrentSynchronizationContext()
);
}
我叫这个而不是 OnPropertyChanged
。但我最近在我的 Windows 商店应用程序中发现了一个间歇性错误,该错误是由用户离开页面后发生的 OnPropertyChanged_Delayed
调用引起的。这促使我寻求替代解决方案。
是否有更好的方法从 setter 中更改 属性 的值并将更改通知 UI?我目前正在开发一个 Windows Store 应用程序,所以我想在这方面得到一个答案。但我最终会将其移植到 UWP。这也是 UWP 上的问题吗?如果是这样,我也想要一个解决方案(可能是相同的)。
我在 UWP 中执行此操作没有问题。我不知道 Windows 商店应用程序有任何不同。
private string _informativeMessage;
public string InformativeMessage
{
get { return _informativeMessage; }
set
{
if (_informativeMessage != value)
{
if(value == SomeValueThatMakesMeNeedToChangeIt)
{
_informativeMessage = "Overridden Value";
}
else
{
_informativeMessage = value;
}
RaisePropertyChanged();
}
}
}
你的 属性 长什么样?
正如我在上面的评论中所解释的那样,我在处理 ComboBox 控件时 运行 遇到过此类问题。我想更改 setter 中所选项目的值,但视图忽略了我的新值。延迟 PropertyChanged 事件是一种丑陋的解决方法,可能会导致意外的副作用。
但解决此问题的另一种方法是将 SelectionChanged 事件绑定到视图模型中的命令。
这个 XAML 像往常一样绑定到 SelectedItem,但也使用 System.Windows.Interactivity 绑定 SelectionChanged 事件:
<ComboBox ItemsSource="{Binding MyItems}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding SelectionChangedCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</ComboBox>
在您的视图模型中,不要尝试覆盖 setter 中的选定值。相反,在 ICommand 实现中更改它:
string _selectedItem;
public string SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
PropertyChanged(this, new PropertyChangedEventArgs("SelectedItem"));
}
}
// RelayCommand is just a custom ICommand implementation which calls the specified delegate when the command is executed
public RelayCommand SelectionChangedCommand
{
get
{
return new RelayCommand(unused =>
{
if (_selectedItem == MyItems[2])
_selectedItem = MyItems[0];
PropertyChanged(this, new PropertyChangedEventArgs("SelectedItem"));
});
}
}
如我所说,我只观察到 SelectedItem 属性 的这种行为。如果您在使用 TextBox 时遇到问题,那么您可以尝试像这样更改绑定的 UpdateSourceTrigger:
<TextBox Text={Binding MyText, Mode=TwoWay, UpdatedSourceTrigger=PropertyChanged} />
我的问题是:在 属性 的 setter 中,如何更改设置的值并让 UI 识别该更改?
双向 XAML 绑定假定设置到源的目标值也是源实际存储的值。它忽略了它刚刚设置的 属性 的 PropertyChanged
事件,因此它不会在更新后检查源的实际值。
文本框
最简单的解决方案是:进行单向绑定,而不是在 TextChanged
处理程序中更新源。
<TextBox Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}"
TextChanged="TextBox_TextChanged"/>
事件处理程序:
void TextBox_TextChanged( object sender, TextChangedEventArgs e )
{
var tb = (TextBox)sender;
var pm = (MyViewModel)DataContext;
pm.Text = tb.Text; //update one-way binding's source
}
当 UI 的文本更改时,通过设置视图模型的文本来处理事件。视图文本的单向绑定意味着它随后接收实际设置的值。这样做可以避免上述 XAML 绑定的限制。
可以保留双向绑定,并且上述解决方案仍然有效。但是如果想让双向绑定更新它的源码,那么代码就稍微长一点:
void TextBox_TextChanged( object sender, TextChangedEventArgs e )
{
var tb = (TextBox)sender;
var pm = (MyViewModel)DataContext;
tb.GetBindingExpression( TextBox.TextProperty ).UpdateSource();
if ( tb.Text != pm.Text )
tb.Text = pm.Text; //correct two-way binding's target
}
如果您不需要 UpdateSourceTrigger=PropertyChanged
那么另一种选择是:
<TextBox Text="{Binding Text, Mode=TwoWay}" LostFocus="TextBox_LostFocus"/>
使用事件处理程序:
void TextBox_LostFocus( object sender, RoutedEventArgs e )
{
var tb = (TextBox)sender;
var pm = (MyViewModel)DataContext;
tb.GetBindingExpression( TextBox.TextProperty ).UpdateSource();
pm.OnPropertyChanged( "Text" );
}
组合框
@RogerN 的回答有助于说明使用 SelectionChanged
可以解决问题,但他实际上并没有说明如何更改 setter 中的值。我会在这里展示。
<ComboBox ItemsSource="{Binding Items}"
SelectedItem="{Binding Text, Mode=TwoWay}"
SelectionChanged="ComboBox_SelectionChanged"/>
将此 属性 添加到视图模型:
public IList<string> Items
{
get { return items; }
}
readonly IList<string> items = new ObservableCollection<string> { "first", "second", "third", "forth" };
在 Text
属性 的 setter 中,像这样更改值:
text = value == "third" ? "forth" : value;
上面显示的 TextBox
的第一种方法不适用于 ComboBox
,但第二种方法有效。
void ComboBox_SelectionChanged( object sender, SelectionChangedEventArgs e )
{
var cb = (ComboBox)sender;
var pm = (MyViewModel)DataContext;
cb.GetBindingExpression( ComboBox.SelectedItemProperty ).UpdateSource();
if ( (string)cb.SelectedItem != pm.Text )
cb.SelectedItem = pm.Text; //correct two-way binding's target
}