将 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
}