2017-08-01 96 views
0

作为一个有点人为的例子考虑一个简单的外汇计算器有两种不同货币的金额和一个汇率之间进行转换。然后,规则是在任何一个金额改变时计算利率,如果利率改变,则第二金额是从第一金额和汇率中计算出来的。如何避免在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(); 
     } 
    } 
} 
+0

如果你担心舍入误差,则在设置器中使用布尔标志。对于你在这里做的事情,你不需要每个属性都有一个单独的标志,只有一个由所有属性共享的类级标志。 –

回答

0

好问题。

我看到两种方式来面对这个问题:

  1. 创建属性IsUpdating和不处理PropertyChanged如果IsUpdating是真实的。然后,您可以“停用”更新过程...
  2. 为不调用属性更改的每个属性创建第二个属性(例如RateInternal,AmountCcy2Internal,...)。

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

+0

我最终做了类似于第一个选项的事情,同时它需要添加大量的检查,我的健壮和简单。 – Shane

1

窗口代码有什么错把一个检查你的财产器例如

if (property == value) 
    return; 

因此不设置属性或提高属性更改事件。如果舍入是你害怕的,那么我会把视图模型中的四舍五入处理掉。

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")); 
     } 
    } 
} 
+0

非常感谢您的回答。我不确定四舍五入是否能完全解决问题。你可能会得到一个旧值为2.4999 ...和新值为2.5000的情况......差别很小,但它足以使两个数字变成不同的值。我已经看到了这样的有趣的事情发生在浮点数字。你也依赖于这样一个事实,即逻辑会计算出与原始更新相同的数字,尽管我无法想象这种情况不会是一个错误。 – Shane