【问题标题】:Change property from setter and notify从 setter 更改属性并通知
【发布时间】:2017-05-21 13:28:18
【问题描述】:

在属性的设置器中,有时我必须将属性的值更改为当前设置的值以外的值。无论如何,这对于 Windows 应用商店应用程序而言,仅通过引发 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 调用引起的。这导致我寻求替代解决方案。

有没有更好的方法来从其设置器中更改属性的值并将更改通知 UI?我目前正在开发一个 Windows 应用商店应用程序,所以我想在这种情况下得到答案。但我最终会将它移植到 UWP。这也是UWP的问题吗?如果是这样,我也想要一个解决方案(可能相同)。

【问题讨论】:

  • 所以我假设您的视图模型触发了事件,您的屏幕捕获了该事件,但该屏幕已被导航离开?
  • @CodingYoshi 是的,在这种情况下,视图模型会从属性的设置器中触发事件。
  • 我的意思是你为什么需要OnPropertyChanged_Delayed?这是代码气味。如果您想通知属性已更改,则只需通知即可。为什么要推迟?
  • @HappyNomad 虽然您说“有时我必须将属性值更改为当前设置的值以外的值”,但您没有解释为什么应该延迟触发 PropertyChanged 事件。 Mark给出的答案显示了如何同步进行。
  • 所以你在设置你的属性时有一个竞争条件?是否有另一个控件试图更改属性但您也想更改它?当您更改值时,您应该阻止该控制器的事件处理程序。我能给出的最便宜的方法是使用布尔辅助值来检查是否必须通知属性。 (问题的根源是,当你不想要它时,你不应该首先设置属性。)

标签: c# xaml windows-store-apps uwp inotifypropertychanged


【解决方案1】:

正如我在上面的评论中所解释的,我在处理 ComboBox 控件时遇到了这类问题。我想更改设置器中所选项目的值,但视图忽略了我的新值。延迟 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>

在您的视图模型中,不要尝试覆盖设置器中的选定值。相反,在 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} />

【讨论】:

  • 也请看我的回答。
【解决方案2】:

我的问题是:如何从属性的设置器中更改正在设置的值并让 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 解决问题,但他实际上并未展示如何更改设置器中的值。我会在这里展示。

<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 属性的设置器中,像这样更改值:

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
}

【讨论】:

    【解决方案3】:

    我在 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();
            }
        }
    }
    

    您的房产是什么样的?

    【讨论】:

    • TextBox.TextSelector.SelectedItem 的双向绑定会出现问题。
    • 如果您能发布相关代码,我可能会提供更多帮助。
    • 我添加了一个简单的例子来演示这个问题。
    • 对于它的价值...在 UWP 中,如果您使用 x:Bind 而不是 Binding,这将按预期工作。我并不是说这是一个解决方案……只是指出一个区别。看起来您有几种方法可以用来实现您的目标。
    猜你喜欢
    • 2021-04-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多