C# 在WPF中使用数据绑定时如何避免递归循环? 作为一个有缺陷的例子,考虑一个简单的FX计算器,它有两种不同的货币量和一种隐藏在它们之间的比率。规则是,当任一金额发生变化时,计算汇率;如果汇率发生变化,则根据第一个金额和汇率计算第二个金额

C# 在WPF中使用数据绑定时如何避免递归循环? 作为一个有缺陷的例子,考虑一个简单的FX计算器,它有两种不同的货币量和一种隐藏在它们之间的比率。规则是,当任一金额发生变化时,计算汇率;如果汇率发生变化,则根据第一个金额和汇率计算第二个金额,c#,wpf,C#,Wpf,在下面的实现中,视图模型中包含所有交互逻辑,更改GUI中的任何数量都会导致一个相互递归的循环 尝试修复此问题的一种方法是在模型的setter上添加检查,以便在将属性设置为其现有值时不会引发事件,这在任何情况下都是良好的做法。但是,这本身并不是一个万无一失的解决方案,因为对于浮点数来说,总是有可能存在一个小的舍入错误,从而导致引发事件 在一个没有数据绑定的世界中,对模型和其他文本框的更新可以在发生更改的文本框的LostFocus事件中完成,这不会触发任何进一步的事件,因为我们只响应用户事件,而不响

在下面的实现中,视图模型中包含所有交互逻辑,更改GUI中的任何数量都会导致一个相互递归的循环

尝试修复此问题的一种方法是在模型的setter上添加检查,以便在将属性设置为其现有值时不会引发事件,这在任何情况下都是良好的做法。但是,这本身并不是一个万无一失的解决方案,因为对于浮点数来说,总是有可能存在一个小的舍入错误,从而导致引发事件

在一个没有数据绑定的世界中,对模型和其他文本框的更新可以在发生更改的文本框的LostFocus事件中完成,这不会触发任何进一步的事件,因为我们只响应用户事件,而不响应数据中的更改

我想到的另一种方法是使用标志来指示某个字段正在以编程方式更新,并在设置标志时忽略对该字段的更改,但当涉及大量字段时,这很快就会变得混乱

是否有任何标准的技术或模式用于解决WPF应用程序中的此问题

视图模型

namespace LoopingUpdates
{
    public class FxModel : INotifyPropertyChanged
    {
        private double _amountCcy1;
        private double _amountCcy2;
        private double _rate;

        public double AmountCcy1
        {
            get { return _amountCcy1;  }
            set
            {
                _amountCcy1 = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("AmountCcy1"));
            }
        }

        public double AmountCcy2
        {
            get { return _amountCcy2; }
            set
            {
                _amountCcy2 = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("AmountCcy2"));
            }
        }

        public double Rate
        {
            get { return _rate; }
            set
            {
                _rate = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Rate"));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }

    public class ViewModel
    {
        public FxModel FxModel { get; set; }

        public ViewModel()
        {
            FxModel = new FxModel() { AmountCcy1 = 100, AmountCcy2 = 200, Rate = 2 };
            FxModel.PropertyChanged += FxModel_PropertyChanged;
        }

        private void FxModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            switch (e.PropertyName) {
                case "AmountCcy1":
                    Debug.WriteLine("Amount Ccy 1 changed");
                    FxModel.Rate = FxModel.AmountCcy2 / FxModel.AmountCcy1;
                    break;

                case "AmountCcy2":
                    Debug.WriteLine("Amount Ccy 2 changed");
                    FxModel.Rate = FxModel.AmountCcy2 / FxModel.AmountCcy1;
                    break;

                case "Rate":
                    Debug.WriteLine("Rate 1 changed");
                    FxModel.AmountCcy2 = FxModel.AmountCcy1 * FxModel.Rate;
                    break;
            }
        }
    }
}
窗口xaml

<Window x:Class="LoopingUpdates.MainWindow"
        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:LoopingUpdates"
        mc:Ignorable="d"
        Title="MainWindow" Height="148.7" Width="255.556" Loaded="Window_Loaded">
    <Grid>
        <Label x:Name="label" Content="Amount Ccy 1" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top"/>
        <Label x:Name="label1" Content="Amount Ccy 2" HorizontalAlignment="Left" Margin="10,41,0,0" VerticalAlignment="Top"/>
        <Label x:Name="label2" Content="Rate" HorizontalAlignment="Left" Margin="10,72,0,0" VerticalAlignment="Top"/>
        <TextBox x:Name="txtAmountCcy1" Text="{Binding FxModel.AmountCcy1}" HorizontalAlignment="Left" Height="26" Margin="99,10,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="72" />
        <TextBox x:Name="txtAmountCcy2" Text="{Binding FxModel.AmountCcy2}" HorizontalAlignment="Left" Height="26" Margin="99,41,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="72"  />
        <TextBox x:Name="txtRate" Text="{Binding FxModel.Rate}" HorizontalAlignment="Left" Height="26" Margin="99,72,0,0" TextWrapping="Wrap"  VerticalAlignment="Top" Width="72" />
    </Grid>
</Window>

在属性设置器中设置一个复选框没有错,例如

if (property == value)
    return;
因此不会设置属性或引发属性更改事件。如果舍入是您害怕的,那么我也会处理ViewModel中的舍入。

好问题

我认为有两种方法可以解决这个问题:

  • 创建属性
    isupdateing
    ,如果
    isupdateing
    为true,则不要处理
    PropertyChanged
    。然后您可以“停用”更新过程
  • 为每个属性创建第二个属性(例如,
    RateInternal
    amountcy2internal
    ,…),该属性不调用属性已更改

  • 这些选项并不理想,但我不知道更好的方法。

    我总是避免递归循环检查ViewModel属性设置器中的
    (value!=\u privateField)

    如果您认为四舍五入可能有问题,我只需更改字段的值,如果四舍五入的值不同,则调用
    PropertyChanged

    public double AmountCcy1
    {
        get { return _amountCcy1; }
        set
        {
            if (Math.Round(value, 2) != Math.Round(_amountCcy1, 2))
            {
                _amountCcy1 = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("AmountCcy1"));
            }
        }
    }
    
    public double AmountCcy2
    {
        get { return _amountCcy2; }
        set
        {
            if (Math.Round(value, 2) != Math.Round(_amountCcy2, 2))
            {
                _amountCcy2 = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("AmountCcy2"));
            }
        }
    }
    

    非常感谢你的回答。我不确定四舍五入是否能完全解决这个问题。您可能会得到一个旧值为2.4999的情况。。。新的数字是2.5000。。。差别很小,但足以使两个数字四舍五入到不同的值。我见过像这样的有趣的事情发生在浮点数上。此外,您还依赖于这样一个事实,即逻辑将计算与原始更新相同的数字,尽管我想不出哪种情况不会是错误。如果您担心舍入错误,那么在setters中使用布尔标志。在这里,您不需要为每个属性设置单独的标志,只需要一个由所有属性共享的单一类范围标志。我最后做了一些类似于第一个选项的事情,但它需要添加大量的检查,这既健壮又简单。
    public double AmountCcy1
    {
        get { return _amountCcy1; }
        set
        {
            if (Math.Round(value, 2) != Math.Round(_amountCcy1, 2))
            {
                _amountCcy1 = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("AmountCcy1"));
            }
        }
    }
    
    public double AmountCcy2
    {
        get { return _amountCcy2; }
        set
        {
            if (Math.Round(value, 2) != Math.Round(_amountCcy2, 2))
            {
                _amountCcy2 = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("AmountCcy2"));
            }
        }
    }