使用组合框更改值单位时如何 update/convert 数字文本框值?基于当前单位的价值归一化?

How to update/convert mumeric TextBox value when changing value's unit using a ComboBox? Value normalization based on current unit?

我想要一个适用于我的 Xamarin 和 WPF 项目的转换器系统。我不想在数据库中保存任何单位,所以我想在用户更改单位时直接转换文本框值。

我制作了 public 一些 Observable 集合,例如;

 public class AreaList : ObservableCollection<Unit>
    {
        public AreaList() : base()
        {
            Add(new Unit("mm²"));
            Add(new Unit("cm²"));
            Add(new Unit("dm²"));
            Add(new Unit("m²"));
        }
    }

 public class Unit
    {
        private string name;

        public Unit(string name)
        {
            this.name = name;
        }

        public string Name
        {
            get { return name; }
            set { name = value; }
        }
    }

在视图中,我将集合绑定到我的组合框。我给了我的 TextBox 他绑定的名称 属性(Text="{Binding TxtBoxValue}" => x:Name="TxtBoxValue")。 ConvertUnitValueCommand 将此名称设置为视图模型中的字符串,以了解转换器函数在更改单位时应使用哪个变量。

查看

<UserControl.Resources>
        <c:AreaList x:Key="AreaListData" />
</UserControl.Resources>

<TextBox x:Name="TxtBoxValue"
         Text="{Binding Mode=TwoWay, Path=TxtBoxValue, UpdateSourceTrigger=PropertyChanged}">
</TextBox>

<ComboBox IsSynchronizedWithCurrentItem="True"
          IsEditable="False"
          DisplayMemberPath="Name"
          SelectedItem="{Binding Unit,Mode=OneWayToSource}"
          ItemsSource="{Binding Source={StaticResource AreaListData}}">
<i:Interaction.Triggers>
   <i:EventTrigger EventName="PreviewMouseLeftButtonDown">
         <i:InvokeCommandAction Command="{Binding ConvertUnitValueCommand}"
                                CommandParameter="{Binding ElementName=TxtBoxValue, Path=Name}" />
   </i:EventTrigger>
</i:Interaction.Triggers> 
</ComboBox>

视图模型

private string ConvertControlName;

private void ConvertUnitValue(object obj)
{
    ConvertControlName = obj.ToString();
}

public Unit Unit
{
get => Get<Unit>();
set
{
     if (ConvertControlName != null)
     {
    FieldInfo variable = this.GetType().GetField(ConvertControlName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic);

    //Get the Value from setted Binding Variable
    double oldValue = (double)variable.GetValue(this);

    //Convert the value
    if (oldValue > 0)
    {
         double newValue = Converts.ConvertUnitValue(Unit, value, oldValue);
         variable.SetValue(this, newValue);
    }

    Set(value);
    }
}

也许任何人都可以给我一些灵感,让我做得更好。

我不太了解你的代码影响,但我建议你尝试下面的设计,它使用 MVVM 模式,消除了 UI 和后端之间的紧密耦合。 我把这里的东西分开了

您的 XAML 将具有类似

的代码
    <TextBox x:Name="unitTextbox"
     Text="{Binding Path=Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
    </TextBox>

    <ComboBox IsSynchronizedWithCurrentItem="True"
      IsEditable="False"
      DisplayMemberPath="Name"
      SelectedItem="{Binding SelectedUnit}"
      ItemsSource="{Binding AvailableUnits}">
    </ComboBox>

你的 ViewModel 会像

public class MainVm : Observable
{
    #region Private Fields
    private double _value;
    private ObservableCollection<Unit> _availableUnits;
    private Unit _selectedUnit;
    private Unit _previouslySelected;

    #endregion Private Fields

    #region Public Constructors

    public MainVm()
    {
        _availableUnits = new ObservableCollection<Unit>()
        {
          new Unit("mm²"),
          new Unit("cm²"),
          new Unit("dm²"),
          new Unit("m²")
        };
    }

    #endregion Public Constructors

    #region Public Properties

    public double Value
    {
        get
        {
            return _value;
        }
        set
        {
            if (_value != value)
            {
                _value = value;
                OnPropertyChanged();
            }
        }
    }

    public Unit SelectedUnit
    {
        get { return _selectedUnit; }
        set
        {

           _previouslySelected = _selectedUnit;
           _selectedUnit = value;
          // call to value conversion function
          // convert cm² to mm² or anything
           Value = UnitConvertor.Convert(_value, _previouslySelected.Name, _selectedUnit.Name);
           OnPropertyChanged();
        }
    }

    public ObservableCollection<Unit> AvailableUnits => _availableUnits;

    #endregion Public Properties
}

我的 Observable class 会像

 public class Observable : INotifyPropertyChanged
{
    #region Public Events

    public event PropertyChangedEventHandler PropertyChanged;

    #endregion Public Events

    #region Protected Methods

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    #endregion Protected Methods
}

最好对单位使用枚举

以下示例将用户输入规范化为基本单位 :

Unit.cs

public class Unit
{
  public Unit(string name, decimal baseFactor)
  {
    this.Name = name;
    this.BaseFactor = baseFactor;
  }

  #region Overrides of Object

  /// <inheritdoc />
  public override string ToString() => this.Name;

  #endregion

  public string Name { get; set; }
  public decimal BaseFactor { get; set; }
}

ViewModel.cs

public class ViewModel : INotifyPropertyChanged
{
  public ViewModel()
  {
    this.Units = new List<Unit>()
    {
      new Unit("mm²", (decimal) (1 / Math.Pow(1000, 2))),
      new Unit("cm²", (decimal) (1 / Math.Pow(100, 2))),
      new Unit("dm²", (decimal) (1 / Math.Pow(10, 2))),
      new Unit("m²", 1)
    };
  }

  private void NormalizeValue()
  {
    this.NormalizedValue = this.UnitValue * this.SelectedUnit.BaseFactor;
  }

  private List<Unit> units;
  public List<Unit> Units
  {
    get => this.units;
    set
    {
      this.units = value;
      OnPropertyChanged();
    }
  }

  private Unit selectedUnit;
  public Unit SelectedUnit
  {
    get => this.selectedUnit;
    set
    {
      this.selectedUnit = value;
      OnPropertyChanged();

      NormalizeValue();
    }
  }

  private decimal unitValue;
  public decimal UnitValue
  {
    get => this.unitValue;
    set
    {
      this.unitValue = value;
      OnPropertyChanged();

      NormalizeValue();
    }
  }

  private decimal normalizedValue;
  public decimal NormalizedValue
  {
    get => this.normalizedValue;
    set
    {
      this.normalizedValue = value;
      OnPropertyChanged();
    }
  }

  public event PropertyChangedEventHandler PropertyChanged;
  protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
  {
    this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  }
}

ManiWindow.xaml

<Window>
  <Window.DataContext>
    <ViewModel />
  </Window.DatContext>

  <StackPanel>

    <!-- Input -->
    <TextBox Text="{Binding UnitValue}" />
    <ComboBox ItemsSource="{Binding Units}" 
              SelectedItem="{Binding SelectedUnit}" />

    <TextBlock Text="{Binding NormalizedValue}" />
  </StackPanel>
</Window>

可重复使用的解决方案

一个可重用的解决方案是创建一个自定义控件,它派生自 TextBox 并封装了规范化逻辑和控件设计。

以下自定义控件 NormalizingNumericTextBox 扩展 TextBox 并将非规范化值和规范化值之间的双向转换。
它基本上是一个 TextBoxComboBox 作为 Unit 选择器对齐。
它可能并不完美,但它已经可以使用了,我只花了大约 10 分钟就将之前的答案合并到这个自定义控件中。

NormalizingNumericTextBox 支持描述数值的任何类型的单位。
只需将 NormalizingNumericTextBox.Units 属性 绑定到任何类型的 Unit 实现的集合,例如重量、长度、货币等

绑定到 NormalizingNumericTextBox.NormalizedValue 到 get/set 标准化值。设置此 属性 会将值转换为当前 NormalizingNumericTextBox.SelectedUnit.
为原始输入值绑定到 NormalizingNumericTextBox.Text

确保将默认的 Style(见下文)添加到 /Themes/Generic.xaml 内的 ResourceDictionary。自定义此 Style 以自定义外观。

ManiWindow.xaml

<Window>
  <Window.DataContext>
    <ViewModel />
  </Window.DatContext>

  <StackPanel>

    <!-- Input -->
      <NormalizingUnitTextBox NormalizedValue="{Binding NormalizedValue}" 
                              Units="{Binding Units}"
                              Width="180" />

    <!-- 
      Test to show/manipulate current normalized value of the view model.
      An entered normalized value will be converted back to the current NormalizingNumericTextBox.Unit -->
    <TextBox Background="Red" Text="{Binding NormalizedUnitValue}"/>
  </StackPanel>
</Window>

Unit.cs

public class Unit
{
  public Unit(string name, decimal baseFactor)
  {
    this.Name = name;
    this.BaseFactor = baseFactor;
  }

  #region Overrides of Object

  /// <inheritdoc />
  public override string ToString() => this.Name;

  #endregion

  public string Name { get; set; }
  public decimal BaseFactor { get; set; }
}

ViewModel.cs

public class ViewModel : INotifyPropertyChanged
{
  public ViewModel()
  {
    this.Units = new List<Unit>()
    {
      new Unit("m²", 1),
      new Unit("dm²", (decimal) (1/Math.Pow(10, 2))),
      new Unit("cm²", (decimal) (1/Math.Pow(100, 2))),
      new Unit("mm²", (decimal) (1/Math.Pow(1000, 2)))
    };
  }

  public List<Unit> Units { get; set; }

  private decimal normalizedValue;
  public decimal NormalizedValue
  {
    get => this.normalizedValue;
    set
    {
      this.normalizedValue = value;
      OnPropertyChanged();
    }
  }

  public event PropertyChangedEventHandler PropertyChanged;
  protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
  {
    this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  }
}

NormalizingNumericTextBox.cs

[TemplatePart(Name = "PART_UnitsItemsHost", Type = typeof(ItemsControl))]
public class NormalizingNumericTextBox : TextBox
{
  public static readonly DependencyProperty UnitsProperty = DependencyProperty.Register(
    "Units",
    typeof(IEnumerable<Unit>),
    typeof(NormalizingNumericTextBox),
    new PropertyMetadata(default(IEnumerable<Unit>), NormalizingNumericTextBox.OnUnitsChanged));

  public IEnumerable<Unit> Units
  {
    get => (IEnumerable<Unit>) GetValue(NormalizingNumericTextBox.UnitsProperty);
    set => SetValue(NormalizingNumericTextBox.UnitsProperty, value);
  }

  public static readonly DependencyProperty SelectedUnitProperty = DependencyProperty.Register(
    "SelectedUnit",
    typeof(Unit),
    typeof(NormalizingNumericTextBox),
    new FrameworkPropertyMetadata(
      default(Unit),
      FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
      NormalizingNumericTextBox.OnSelectedUnitChanged));

  public Unit SelectedUnit
  {
    get => (Unit) GetValue(NormalizingNumericTextBox.SelectedUnitProperty);
    set => SetValue(NormalizingNumericTextBox.SelectedUnitProperty, value);
  }

  public static readonly DependencyProperty NormalizedValueProperty = DependencyProperty.Register(
    "NormalizedValue",
    typeof(decimal),
    typeof(NormalizingNumericTextBox),
    new FrameworkPropertyMetadata(
      default(decimal),
      FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
      NormalizingNumericTextBox.OnNormalizedValueChanged));

  public decimal NormalizedValue
  {
    get => (decimal) GetValue(NormalizingNumericTextBox.NormalizedValueProperty);
    set => SetValue(NormalizingNumericTextBox.NormalizedValueProperty, value);
  }

  private ItemsControl PART_UnitsItemsHost { get; set; }
  private bool IsNormalizing { get; set; }

  static NormalizingNumericTextBox()
  {
    FrameworkElement.DefaultStyleKeyProperty.OverrideMetadata(
      typeof(NormalizingNumericTextBox),
      new FrameworkPropertyMetadata(typeof(NormalizingNumericTextBox)));
  }

  public NormalizingNumericTextBox()
  {
  }

  private static void OnNormalizedValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  {
    var _this = d as NormalizingNumericTextBox;
    _this.ConvertNormalizedValueToNumericText();
  }

  private static void OnSelectedUnitChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  {
    (d as NormalizingNumericTextBox).NormalizeNumericText();
  }

  private static void OnUnitsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  {
    var _this = d as NormalizingNumericTextBox;
    _this.SelectedUnit = _this.Units.FirstOrDefault();
  }

  /// <inheritdoc />
  public override void OnApplyTemplate()
  {
    base.OnApplyTemplate();
    this.PART_UnitsItemsHost = GetTemplateChild("PART_UnitsItemsHost") as ItemsControl;

    if (this.PART_UnitsItemsHost == null)
    {
      throw new InvalidOperationException($"{nameof(this.PART_UnitsItemsHost)} not found in ControlTemplate");
    }

    this.PART_UnitsItemsHost.SetBinding(
      Selector.SelectedItemProperty,
      new Binding(nameof(this.SelectedUnit)) {Source = this});
    this.PART_UnitsItemsHost.SetBinding(
      ItemsControl.ItemsSourceProperty,
      new Binding(nameof(this.Units)) {Source = this});
    this.SelectedUnit = this.Units.FirstOrDefault();
  }

  #region Overrides of TextBoxBase

  /// <inheritdoc />
  protected override void OnTextChanged(TextChangedEventArgs e)
  {
    base.OnTextChanged(e);
    if (this.IsNormalizing)
    {
      return;
    }

    NormalizeNumericText();
  }

  /// <inheritdoc />
  protected override void OnTextInput(TextCompositionEventArgs e)
  {
    // Suppress non numeric characters
    if (!decimal.TryParse(e.Text, NumberStyles.Number, CultureInfo.CurrentCulture, out decimal _))
    {
      e.Handled = true;
      return;
    }

    base.OnTextInput(e);
  }

  #endregion Overrides of TextBoxBase

  private void NormalizeNumericText()
  {
    this.IsNormalizing = true;
    if (decimal.TryParse(this.Text, NumberStyles.Number, CultureInfo.CurrentCulture, out decimal numericValue))
    {
      this.NormalizedValue = numericValue * this.SelectedUnit.BaseFactor;
    }

    this.IsNormalizing = false;
  }

  private void ConvertNormalizedValueToNumericText()
  {
    this.IsNormalizing = true;
    decimal value = this.NormalizedValue / this.SelectedUnit.BaseFactor;
    this.Text = value.ToString(CultureInfo.CurrentCulture);
    this.IsNormalizing = false;
  }
}

Generic.xaml

<ResourceDictionary>

  <Style TargetType="NormalizingNumericTextBox">
    <Setter Property="BorderThickness" Value="1" />
    <Setter Property="BorderBrush" Value="DarkGray" />
    <Setter Property="HorizontalAlignment" Value="Left"/>
    <Setter Property="VerticalContentAlignment" Value="Center"/>
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="local:NormalizingNumericTextBox">
          <Border BorderBrush="{TemplateBinding BorderBrush}"
                  BorderThickness="{TemplateBinding BorderThickness}"
                  Background="{TemplateBinding Background}"
                  Padding="{TemplateBinding Padding}">
            <Grid>
              <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="Auto" />
              </Grid.ColumnDefinitions>
              <ScrollViewer x:Name="PART_ContentHost" Grid.Column="0" Margin="0" />
              <ComboBox x:Name="PART_UnitsItemsHost" Grid.Column="1" BorderThickness="0" HorizontalAlignment="Right" />
            </Grid>
          </Border>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</ResourceDictionary>