使用组合框更改值单位时如何更新/转换数字文本框值?基于当前单位的值标准化?

Posted

技术标签:

【中文标题】使用组合框更改值单位时如何更新/转换数字文本框值?基于当前单位的值标准化?【英文标题】:How to update/convert mumeric TextBox value when changing value's unit using a ComboBox? Value normalization based on current unit? 【发布时间】:2020-08-13 06:57:42 【问题描述】:

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

我公开了一些 Observable Collections,例如:

 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);
    


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

【问题讨论】:

如果我没记错的话,如果用户使用 ComboBox 更改单位,您想更新文本框中的值。还是我错过了什么? 对!这正是我想要的 您可以将单元封装到一个类似枚举的类中。然后创建一个专用的自定义控件来编辑单位。 您总是将值单位转换为单位。或者您的转换器逻辑是如何工作的,您是否总是转换为基本单位? 那你为什么要使用反射来访问一个字段? 【参考方案1】:

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

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 以获取/设置标准化值。设置此属性会将值转换为当前的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>

【讨论】:

非常好的标准化功能!!!但正如我评论的那样,我寻找更一般的转换。 你说的更一般是什么意思?在您的问题中,我没有找到任何关于通用转换的信息。请详细说明您所说的更笼统是什么意思? 我有多个带有单元组合框的文本框。每个带有单元组合框的文本框都应该使用这些功能。因此,我认为必须使用通用对话来查找与单击的组合框关联的文本框 我已经提供了可重用解决方案的核心。无论如何,我已经更新了我的答案,以展示如何将转换封装到一个方便且可自定义的控件中 - 免费。因为我是个好人。 非常感谢!!!我非常感谢。我没想到你会构建整个功能。【参考方案2】:

我不太了解您的代码影响,但我建议您尝试以下使用 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 类会是这样的

 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

最好对单位使用枚举

【讨论】:

感谢您的回答,但我已经知道了。我想为每个带有单元的文本框使用这些转换功能,并寻找更通用的功能

以上是关于使用组合框更改值单位时如何更新/转换数字文本框值?基于当前单位的值标准化?的主要内容,如果未能解决你的问题,请参考以下文章

在访问中更改组合框值时,可以更改/重新计算计算的文本框值

Datagridview 在更改列组合框值时执行代码

基于单选按钮选择 laravel 的文本框值更改

淘汰赛js根据文本框值更改div宽度

根据组合框选择更改文本框值

根据组合框选择 C# 更改组合框值