Как обновить / преобразовать значение mumeri c TextBox при изменении единицы измерения с помощью ComboBox? Нормализация значения на основе текущей единицы? - PullRequest
0 голосов
/ 29 апреля 2020

Я хотел бы иметь конвертерную систему для моего проекта Xamarin и WPF. Я не хочу сохранять какие-либо единицы измерения в базе данных, поэтому я хочу напрямую преобразовывать значения текстовых полей при изменении пользователем единицы измерения.

enter image description here

I сделал publi c несколько наблюдаемых коллекций, таких как;

 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 задает это имя как строку в модели представления, чтобы узнать, какую переменную должна использовать функция преобразователя при изменении единицы измерения.

View

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

ViewModel

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

Может быть, кто-нибудь может вдохновить меня сделать это лучше.

Ответы [ 2 ]

1 голос
/ 29 апреля 2020

Следующий пример нормализует пользовательский ввод для базового блока м² :

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 и инкапсулирует логи нормализации c и дизайн элемента управления.

Следующий пользовательский элемент управления NormalizingNumericTextBox расширяет TextBox и преобразует два пути из ненормализованного значения в нормализовано и обратно.
Это в основном TextBox, выровненный с ComboBox как Unit селектор.
Возможно, он не идеален, но готов к использованию, и мне потребовалось около 10 минут, чтобы объединить предыдущий ответ с этим пользовательским элементом управления.

NormalizingNumericTextBox поддерживает любые типы единиц, описывающих значение цифры c.
Просто свяжите свойство NormalizingNumericTextBox.Units с коллекцией любого типа Unit реализация, например, вес, длина, валюта и т. д. c.

Привязка к NormalizingNumericTextBox.NormalizedValue чтобы получить / установить нормализованное значение. Установка этого свойства преобразует значение в текущий NormalizingNumericTextBox.SelectedUnit.
Привязать к NormalizingNumericTextBox.Text для необработанного входного значения.

Убедитесь, что значение по умолчанию Style (см. Ниже) добавлено к ResourceDictionary внутри / Темы / Generi c .xaml . Настройте это 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;
  }
}

Generi c .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>
0 голосов
/ 29 апреля 2020

Я не очень представляю, как повлияет ваш код, но я бы посоветовал вам попробовать следующий дизайн, в котором используется шаблон MVVM, который устраняет тесную связь между пользовательским интерфейсом и бэкендом. Я выделил вещи здесь

ваш 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
}

лучше использовать enum для единиц

...