在WPF中使用数据绑定时如何避免递归循环?

时间:2017-08-01 11:37:05

标签: c# wpf

作为一个有点人为的例子,考虑一个简单的外汇计算器,它具有两种不同货币的金额以及它们之间的转换率。然后规则是当任何一个金额被改变时,计算费率,如果费率改变,那么第二个金额将从第一个金额和汇率计算。

下面的实现具有视图模型中的所有交互逻辑,更改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>

背后的窗口代码

namespace LoopingUpdates
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            DataContext = new ViewModel();
        }
    }
}

3 个答案:

答案 0 :(得分:1)

将支票放入您的物业设定器中是没有错的,例如

import UIKit

class ViewController: UIViewController {

    var mybool = true

    override func viewDidAppear(_ animated: Bool) {


        while(mybool){
            let firstFrame = CGRect(x: 160, y: 240, width: 100, height: 150)
            let firstView = UIView(frame: firstFrame)
            firstView.backgroundColor = UIColor.blue
            view.addSubview(firstView)

            let secondView = UIView(frame: firstFrame)
            secondView.backgroundColor = UIColor.black
            view.addSubview(secondView)

        }
    }


}

因此不设置属性或提升属性已更改事件。如果舍入是您所害怕的,那么我也会在ViewModel中处理舍入。

答案 1 :(得分:1)

我总是避免递归循环检查我的ViewModel属性的setter中是否(value != _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 :(得分:0)

好问题。

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

  1. 创建属性IsUpdating,如果PropertyChanged为真,则不处理IsUpdating。然后你可以“停用”更新过程......
  2. 为每个属性(例如RateInternalAmountCcy2Internal,...)创建第二个属性,该属性不会调用属性更改。
  3. 这些选项并不理想,但我不知道更好的方法。