使用组合框更改值单位时如何更新/转换数字文本框值?基于当前单位的值标准化?
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】:以下示例将用户输入规范化为基本单位m²:
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
并从非规范化值转换为规范化值和返回的两种方式。
它基本上是 TextBox
与 ComboBox
对齐的 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
最好对单位使用枚举
【讨论】:
感谢您的回答,但我已经知道了。我想为每个带有单元的文本框使用这些转换功能,并寻找更通用的功能以上是关于使用组合框更改值单位时如何更新/转换数字文本框值?基于当前单位的值标准化?的主要内容,如果未能解决你的问题,请参考以下文章