NumericUpDown.cs 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998
  1. using System;
  2. using System.Globalization;
  3. using System.IO;
  4. using System.Linq;
  5. using Avalonia.Controls.Primitives;
  6. using Avalonia.Data;
  7. using Avalonia.Input;
  8. using Avalonia.Interactivity;
  9. using Avalonia.Threading;
  10. using Avalonia.Utilities;
  11. namespace Avalonia.Controls
  12. {
  13. /// <summary>
  14. /// Control that represents a TextBox with button spinners that allow incrementing and decrementing numeric values.
  15. /// </summary>
  16. public class NumericUpDown : TemplatedControl
  17. {
  18. /// <summary>
  19. /// Defines the <see cref="AllowSpin"/> property.
  20. /// </summary>
  21. public static readonly StyledProperty<bool> AllowSpinProperty =
  22. ButtonSpinner.AllowSpinProperty.AddOwner<NumericUpDown>();
  23. /// <summary>
  24. /// Defines the <see cref="ButtonSpinnerLocation"/> property.
  25. /// </summary>
  26. public static readonly StyledProperty<Location> ButtonSpinnerLocationProperty =
  27. ButtonSpinner.ButtonSpinnerLocationProperty.AddOwner<NumericUpDown>();
  28. /// <summary>
  29. /// Defines the <see cref="ShowButtonSpinner"/> property.
  30. /// </summary>
  31. public static readonly StyledProperty<bool> ShowButtonSpinnerProperty =
  32. ButtonSpinner.ShowButtonSpinnerProperty.AddOwner<NumericUpDown>();
  33. /// <summary>
  34. /// Defines the <see cref="ClipValueToMinMax"/> property.
  35. /// </summary>
  36. public static readonly DirectProperty<NumericUpDown, bool> ClipValueToMinMaxProperty =
  37. AvaloniaProperty.RegisterDirect<NumericUpDown, bool>(nameof(ClipValueToMinMax),
  38. updown => updown.ClipValueToMinMax, (updown, b) => updown.ClipValueToMinMax = b);
  39. /// <summary>
  40. /// Defines the <see cref="CultureInfo"/> property.
  41. /// </summary>
  42. public static readonly DirectProperty<NumericUpDown, CultureInfo> CultureInfoProperty =
  43. AvaloniaProperty.RegisterDirect<NumericUpDown, CultureInfo>(nameof(CultureInfo), o => o.CultureInfo,
  44. (o, v) => o.CultureInfo = v, CultureInfo.CurrentCulture);
  45. /// <summary>
  46. /// Defines the <see cref="FormatString"/> property.
  47. /// </summary>
  48. public static readonly StyledProperty<string> FormatStringProperty =
  49. AvaloniaProperty.Register<NumericUpDown, string>(nameof(FormatString), string.Empty);
  50. /// <summary>
  51. /// Defines the <see cref="Increment"/> property.
  52. /// </summary>
  53. public static readonly StyledProperty<double> IncrementProperty =
  54. AvaloniaProperty.Register<NumericUpDown, double>(nameof(Increment), 1.0d, validate: OnCoerceIncrement);
  55. /// <summary>
  56. /// Defines the <see cref="IsReadOnly"/> property.
  57. /// </summary>
  58. public static readonly StyledProperty<bool> IsReadOnlyProperty =
  59. AvaloniaProperty.Register<NumericUpDown, bool>(nameof(IsReadOnly));
  60. /// <summary>
  61. /// Defines the <see cref="Maximum"/> property.
  62. /// </summary>
  63. public static readonly StyledProperty<double> MaximumProperty =
  64. AvaloniaProperty.Register<NumericUpDown, double>(nameof(Maximum), double.MaxValue, validate: OnCoerceMaximum);
  65. /// <summary>
  66. /// Defines the <see cref="Minimum"/> property.
  67. /// </summary>
  68. public static readonly StyledProperty<double> MinimumProperty =
  69. AvaloniaProperty.Register<NumericUpDown, double>(nameof(Minimum), double.MinValue, validate: OnCoerceMinimum);
  70. /// <summary>
  71. /// Defines the <see cref="ParsingNumberStyle"/> property.
  72. /// </summary>
  73. public static readonly DirectProperty<NumericUpDown, NumberStyles> ParsingNumberStyleProperty =
  74. AvaloniaProperty.RegisterDirect<NumericUpDown, NumberStyles>(nameof(ParsingNumberStyle),
  75. updown => updown.ParsingNumberStyle, (updown, style) => updown.ParsingNumberStyle = style);
  76. /// <summary>
  77. /// Defines the <see cref="Text"/> property.
  78. /// </summary>
  79. public static readonly DirectProperty<NumericUpDown, string> TextProperty =
  80. AvaloniaProperty.RegisterDirect<NumericUpDown, string>(nameof(Text), o => o.Text, (o, v) => o.Text = v,
  81. defaultBindingMode: BindingMode.TwoWay);
  82. /// <summary>
  83. /// Defines the <see cref="Value"/> property.
  84. /// </summary>
  85. public static readonly DirectProperty<NumericUpDown, double> ValueProperty =
  86. AvaloniaProperty.RegisterDirect<NumericUpDown, double>(nameof(Value), updown => updown.Value,
  87. (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay);
  88. /// <summary>
  89. /// Defines the <see cref="Watermark"/> property.
  90. /// </summary>
  91. public static readonly StyledProperty<string> WatermarkProperty =
  92. AvaloniaProperty.Register<NumericUpDown, string>(nameof(Watermark));
  93. private IDisposable _textBoxTextChangedSubscription;
  94. private double _value;
  95. private string _text;
  96. private bool _internalValueSet;
  97. private bool _clipValueToMinMax;
  98. private bool _isSyncingTextAndValueProperties;
  99. private bool _isTextChangedFromUI;
  100. private CultureInfo _cultureInfo;
  101. private NumberStyles _parsingNumberStyle = NumberStyles.Any;
  102. /// <summary>
  103. /// Gets the Spinner template part.
  104. /// </summary>
  105. private Spinner Spinner { get; set; }
  106. /// <summary>
  107. /// Gets the TextBox template part.
  108. /// </summary>
  109. private TextBox TextBox { get; set; }
  110. /// <summary>
  111. /// Gets or sets the ability to perform increment/decrement operations via the keyboard, button spinners, or mouse wheel.
  112. /// </summary>
  113. public bool AllowSpin
  114. {
  115. get { return GetValue(AllowSpinProperty); }
  116. set { SetValue(AllowSpinProperty, value); }
  117. }
  118. /// <summary>
  119. /// Gets or sets current location of the <see cref="ButtonSpinner"/>.
  120. /// </summary>
  121. public Location ButtonSpinnerLocation
  122. {
  123. get { return GetValue(ButtonSpinnerLocationProperty); }
  124. set { SetValue(ButtonSpinnerLocationProperty, value); }
  125. }
  126. /// <summary>
  127. /// Gets or sets a value indicating whether the spin buttons should be shown.
  128. /// </summary>
  129. public bool ShowButtonSpinner
  130. {
  131. get { return GetValue(ShowButtonSpinnerProperty); }
  132. set { SetValue(ShowButtonSpinnerProperty, value); }
  133. }
  134. /// <summary>
  135. /// Gets or sets if the value should be clipped when minimum/maximum is reached.
  136. /// </summary>
  137. public bool ClipValueToMinMax
  138. {
  139. get { return _clipValueToMinMax; }
  140. set { SetAndRaise(ClipValueToMinMaxProperty, ref _clipValueToMinMax, value); }
  141. }
  142. /// <summary>
  143. /// Gets or sets the current CultureInfo.
  144. /// </summary>
  145. public CultureInfo CultureInfo
  146. {
  147. get { return _cultureInfo; }
  148. set { SetAndRaise(CultureInfoProperty, ref _cultureInfo, value); }
  149. }
  150. /// <summary>
  151. /// Gets or sets the display format of the <see cref="Value"/>.
  152. /// </summary>
  153. public string FormatString
  154. {
  155. get { return GetValue(FormatStringProperty); }
  156. set { SetValue(FormatStringProperty, value); }
  157. }
  158. /// <summary>
  159. /// Gets or sets the amount in which to increment the <see cref="Value"/>.
  160. /// </summary>
  161. public double Increment
  162. {
  163. get { return GetValue(IncrementProperty); }
  164. set { SetValue(IncrementProperty, value); }
  165. }
  166. /// <summary>
  167. /// Gets or sets if the control is read only.
  168. /// </summary>
  169. public bool IsReadOnly
  170. {
  171. get { return GetValue(IsReadOnlyProperty); }
  172. set { SetValue(IsReadOnlyProperty, value); }
  173. }
  174. /// <summary>
  175. /// Gets or sets the maximum allowed value.
  176. /// </summary>
  177. public double Maximum
  178. {
  179. get { return GetValue(MaximumProperty); }
  180. set { SetValue(MaximumProperty, value); }
  181. }
  182. /// <summary>
  183. /// Gets or sets the minimum allowed value.
  184. /// </summary>
  185. public double Minimum
  186. {
  187. get { return GetValue(MinimumProperty); }
  188. set { SetValue(MinimumProperty, value); }
  189. }
  190. /// <summary>
  191. /// Gets or sets the parsing style (AllowLeadingWhite, Float, AllowHexSpecifier, ...). By default, Any.
  192. /// </summary>
  193. public NumberStyles ParsingNumberStyle
  194. {
  195. get { return _parsingNumberStyle; }
  196. set { SetAndRaise(ParsingNumberStyleProperty, ref _parsingNumberStyle, value); }
  197. }
  198. /// <summary>
  199. /// Gets or sets the formatted string representation of the value.
  200. /// </summary>
  201. public string Text
  202. {
  203. get { return _text; }
  204. set { SetAndRaise(TextProperty, ref _text, value); }
  205. }
  206. /// <summary>
  207. /// Gets or sets the value.
  208. /// </summary>
  209. public double Value
  210. {
  211. get { return _value; }
  212. set
  213. {
  214. value = OnCoerceValue(value);
  215. SetAndRaise(ValueProperty, ref _value, value);
  216. }
  217. }
  218. /// <summary>
  219. /// Gets or sets the object to use as a watermark if the <see cref="Value"/> is null.
  220. /// </summary>
  221. public string Watermark
  222. {
  223. get { return GetValue(WatermarkProperty); }
  224. set { SetValue(WatermarkProperty, value); }
  225. }
  226. /// <summary>
  227. /// Initializes new instance of <see cref="NumericUpDown"/> class.
  228. /// </summary>
  229. public NumericUpDown()
  230. {
  231. Initialized += (sender, e) =>
  232. {
  233. if (!_internalValueSet && IsInitialized)
  234. {
  235. SyncTextAndValueProperties(false, null, true);
  236. }
  237. SetValidSpinDirection();
  238. };
  239. }
  240. /// <summary>
  241. /// Initializes static members of the <see cref="NumericUpDown"/> class.
  242. /// </summary>
  243. static NumericUpDown()
  244. {
  245. CultureInfoProperty.Changed.Subscribe(OnCultureInfoChanged);
  246. FormatStringProperty.Changed.Subscribe(FormatStringChanged);
  247. IncrementProperty.Changed.Subscribe(IncrementChanged);
  248. IsReadOnlyProperty.Changed.Subscribe(OnIsReadOnlyChanged);
  249. MaximumProperty.Changed.Subscribe(OnMaximumChanged);
  250. MinimumProperty.Changed.Subscribe(OnMinimumChanged);
  251. TextProperty.Changed.Subscribe(OnTextChanged);
  252. ValueProperty.Changed.Subscribe(OnValueChanged);
  253. }
  254. /// <inheritdoc />
  255. protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
  256. {
  257. if (TextBox != null)
  258. {
  259. TextBox.PointerPressed -= TextBoxOnPointerPressed;
  260. _textBoxTextChangedSubscription?.Dispose();
  261. }
  262. TextBox = e.NameScope.Find<TextBox>("PART_TextBox");
  263. if (TextBox != null)
  264. {
  265. TextBox.Text = Text;
  266. TextBox.PointerPressed += TextBoxOnPointerPressed;
  267. _textBoxTextChangedSubscription = TextBox.GetObservable(TextBox.TextProperty).Subscribe(txt => TextBoxOnTextChanged());
  268. }
  269. if (Spinner != null)
  270. {
  271. Spinner.Spin -= OnSpinnerSpin;
  272. }
  273. Spinner = e.NameScope.Find<Spinner>("PART_Spinner");
  274. if (Spinner != null)
  275. {
  276. Spinner.Spin += OnSpinnerSpin;
  277. }
  278. SetValidSpinDirection();
  279. }
  280. /// <inheritdoc />
  281. protected override void OnKeyDown(KeyEventArgs e)
  282. {
  283. switch (e.Key)
  284. {
  285. case Key.Enter:
  286. var commitSuccess = CommitInput();
  287. e.Handled = !commitSuccess;
  288. break;
  289. }
  290. }
  291. /// <summary>
  292. /// Called when the <see cref="CultureInfo"/> property value changed.
  293. /// </summary>
  294. /// <param name="oldValue">The old value.</param>
  295. /// <param name="newValue">The new value.</param>
  296. protected virtual void OnCultureInfoChanged(CultureInfo oldValue, CultureInfo newValue)
  297. {
  298. if (IsInitialized)
  299. {
  300. SyncTextAndValueProperties(false, null);
  301. }
  302. }
  303. /// <summary>
  304. /// Called when the <see cref="FormatString"/> property value changed.
  305. /// </summary>
  306. /// <param name="oldValue">The old value.</param>
  307. /// <param name="newValue">The new value.</param>
  308. protected virtual void OnFormatStringChanged(string oldValue, string newValue)
  309. {
  310. if (IsInitialized)
  311. {
  312. SyncTextAndValueProperties(false, null);
  313. }
  314. }
  315. /// <summary>
  316. /// Called when the <see cref="Increment"/> property value changed.
  317. /// </summary>
  318. /// <param name="oldValue">The old value.</param>
  319. /// <param name="newValue">The new value.</param>
  320. protected virtual void OnIncrementChanged(double oldValue, double newValue)
  321. {
  322. if (IsInitialized)
  323. {
  324. SetValidSpinDirection();
  325. }
  326. }
  327. /// <summary>
  328. /// Called when the <see cref="IsReadOnly"/> property value changed.
  329. /// </summary>
  330. /// <param name="oldValue">The old value.</param>
  331. /// <param name="newValue">The new value.</param>
  332. protected virtual void OnIsReadOnlyChanged(bool oldValue, bool newValue)
  333. {
  334. SetValidSpinDirection();
  335. }
  336. /// <summary>
  337. /// Called when the <see cref="Maximum"/> property value changed.
  338. /// </summary>
  339. /// <param name="oldValue">The old value.</param>
  340. /// <param name="newValue">The new value.</param>
  341. protected virtual void OnMaximumChanged(double oldValue, double newValue)
  342. {
  343. if (IsInitialized)
  344. {
  345. SetValidSpinDirection();
  346. }
  347. if (ClipValueToMinMax)
  348. {
  349. Value = MathUtilities.Clamp(Value, Minimum, Maximum);
  350. }
  351. }
  352. /// <summary>
  353. /// Called when the <see cref="Minimum"/> property value changed.
  354. /// </summary>
  355. /// <param name="oldValue">The old value.</param>
  356. /// <param name="newValue">The new value.</param>
  357. protected virtual void OnMinimumChanged(double oldValue, double newValue)
  358. {
  359. if (IsInitialized)
  360. {
  361. SetValidSpinDirection();
  362. }
  363. if (ClipValueToMinMax)
  364. {
  365. Value = MathUtilities.Clamp(Value, Minimum, Maximum);
  366. }
  367. }
  368. /// <summary>
  369. /// Called when the <see cref="Text"/> property value changed.
  370. /// </summary>
  371. /// <param name="oldValue">The old value.</param>
  372. /// <param name="newValue">The new value.</param>
  373. protected virtual void OnTextChanged(string oldValue, string newValue)
  374. {
  375. if (IsInitialized)
  376. {
  377. SyncTextAndValueProperties(true, Text);
  378. }
  379. }
  380. /// <summary>
  381. /// Called when the <see cref="Value"/> property value changed.
  382. /// </summary>
  383. /// <param name="oldValue">The old value.</param>
  384. /// <param name="newValue">The new value.</param>
  385. protected virtual void OnValueChanged(double oldValue, double newValue)
  386. {
  387. if (!_internalValueSet && IsInitialized)
  388. {
  389. SyncTextAndValueProperties(false, null, true);
  390. }
  391. SetValidSpinDirection();
  392. RaiseValueChangedEvent(oldValue, newValue);
  393. }
  394. /// <summary>
  395. /// Called when the <see cref="Increment"/> property has to be coerced.
  396. /// </summary>
  397. /// <param name="baseValue">The value.</param>
  398. protected virtual double OnCoerceIncrement(double baseValue)
  399. {
  400. return baseValue;
  401. }
  402. /// <summary>
  403. /// Called when the <see cref="Maximum"/> property has to be coerced.
  404. /// </summary>
  405. /// <param name="baseValue">The value.</param>
  406. protected virtual double OnCoerceMaximum(double baseValue)
  407. {
  408. return Math.Max(baseValue, Minimum);
  409. }
  410. /// <summary>
  411. /// Called when the <see cref="Minimum"/> property has to be coerced.
  412. /// </summary>
  413. /// <param name="baseValue">The value.</param>
  414. protected virtual double OnCoerceMinimum(double baseValue)
  415. {
  416. return Math.Min(baseValue, Maximum);
  417. }
  418. /// <summary>
  419. /// Called when the <see cref="Value"/> property has to be coerced.
  420. /// </summary>
  421. /// <param name="baseValue">The value.</param>
  422. protected virtual double OnCoerceValue(double baseValue)
  423. {
  424. return baseValue;
  425. }
  426. /// <summary>
  427. /// Raises the OnSpin event when spinning is initiated by the end-user.
  428. /// </summary>
  429. /// <param name="e">The event args.</param>
  430. protected virtual void OnSpin(SpinEventArgs e)
  431. {
  432. if (e == null)
  433. {
  434. throw new ArgumentNullException(nameof(e));
  435. }
  436. var handler = Spinned;
  437. handler?.Invoke(this, e);
  438. if (e.Direction == SpinDirection.Increase)
  439. {
  440. DoIncrement();
  441. }
  442. else
  443. {
  444. DoDecrement();
  445. }
  446. }
  447. /// <summary>
  448. /// Raises the <see cref="ValueChanged"/> event.
  449. /// </summary>
  450. /// <param name="oldValue">The old value.</param>
  451. /// <param name="newValue">The new value.</param>
  452. protected virtual void RaiseValueChangedEvent(double oldValue, double newValue)
  453. {
  454. var e = new NumericUpDownValueChangedEventArgs(ValueChangedEvent, oldValue, newValue);
  455. RaiseEvent(e);
  456. }
  457. /// <summary>
  458. /// Converts the formatted text to a value.
  459. /// </summary>
  460. private double ConvertTextToValue(string text)
  461. {
  462. double result = 0;
  463. if (string.IsNullOrEmpty(text))
  464. {
  465. return result;
  466. }
  467. // Since the conversion from Value to text using a FormatString may not be parsable,
  468. // we verify that the already existing text is not the exact same value.
  469. var currentValueText = ConvertValueToText();
  470. if (Equals(currentValueText, text))
  471. {
  472. return Value;
  473. }
  474. result = ConvertTextToValueCore(currentValueText, text);
  475. if (ClipValueToMinMax)
  476. {
  477. return MathUtilities.Clamp(result, Minimum, Maximum);
  478. }
  479. ValidateMinMax(result);
  480. return result;
  481. }
  482. /// <summary>
  483. /// Converts the value to formatted text.
  484. /// </summary>
  485. /// <returns></returns>
  486. private string ConvertValueToText()
  487. {
  488. //Manage FormatString of type "{}{0:N2} °" (in xaml) or "{0:N2} °" in code-behind.
  489. if (FormatString.Contains("{0"))
  490. {
  491. return string.Format(CultureInfo, FormatString, Value);
  492. }
  493. return Value.ToString(FormatString, CultureInfo);
  494. }
  495. /// <summary>
  496. /// Called by OnSpin when the spin direction is SpinDirection.Increase.
  497. /// </summary>
  498. private void OnIncrement()
  499. {
  500. var result = Value + Increment;
  501. Value = MathUtilities.Clamp(result, Minimum, Maximum);
  502. }
  503. /// <summary>
  504. /// Called by OnSpin when the spin direction is SpinDirection.Decrease.
  505. /// </summary>
  506. private void OnDecrement()
  507. {
  508. var result = Value - Increment;
  509. Value = MathUtilities.Clamp(result, Minimum, Maximum);
  510. }
  511. /// <summary>
  512. /// Sets the valid spin directions.
  513. /// </summary>
  514. private void SetValidSpinDirection()
  515. {
  516. var validDirections = ValidSpinDirections.None;
  517. // Zero increment always prevents spin.
  518. if (Increment != 0 && !IsReadOnly)
  519. {
  520. if (Value < Maximum)
  521. {
  522. validDirections = validDirections | ValidSpinDirections.Increase;
  523. }
  524. if (Value > Minimum)
  525. {
  526. validDirections = validDirections | ValidSpinDirections.Decrease;
  527. }
  528. }
  529. if (Spinner != null)
  530. {
  531. Spinner.ValidSpinDirection = validDirections;
  532. }
  533. }
  534. /// <summary>
  535. /// Called when the <see cref="CultureInfo"/> property value changed.
  536. /// </summary>
  537. /// <param name="e">The event args.</param>
  538. private static void OnCultureInfoChanged(AvaloniaPropertyChangedEventArgs e)
  539. {
  540. if (e.Sender is NumericUpDown upDown)
  541. {
  542. var oldValue = (CultureInfo)e.OldValue;
  543. var newValue = (CultureInfo)e.NewValue;
  544. upDown.OnCultureInfoChanged(oldValue, newValue);
  545. }
  546. }
  547. /// <summary>
  548. /// Called when the <see cref="Increment"/> property value changed.
  549. /// </summary>
  550. /// <param name="e">The event args.</param>
  551. private static void IncrementChanged(AvaloniaPropertyChangedEventArgs e)
  552. {
  553. if (e.Sender is NumericUpDown upDown)
  554. {
  555. var oldValue = (double)e.OldValue;
  556. var newValue = (double)e.NewValue;
  557. upDown.OnIncrementChanged(oldValue, newValue);
  558. }
  559. }
  560. /// <summary>
  561. /// Called when the <see cref="FormatString"/> property value changed.
  562. /// </summary>
  563. /// <param name="e">The event args.</param>
  564. private static void FormatStringChanged(AvaloniaPropertyChangedEventArgs e)
  565. {
  566. if (e.Sender is NumericUpDown upDown)
  567. {
  568. var oldValue = (string)e.OldValue;
  569. var newValue = (string)e.NewValue;
  570. upDown.OnFormatStringChanged(oldValue, newValue);
  571. }
  572. }
  573. /// <summary>
  574. /// Called when the <see cref="IsReadOnly"/> property value changed.
  575. /// </summary>
  576. /// <param name="e">The event args.</param>
  577. private static void OnIsReadOnlyChanged(AvaloniaPropertyChangedEventArgs e)
  578. {
  579. if (e.Sender is NumericUpDown upDown)
  580. {
  581. var oldValue = (bool)e.OldValue;
  582. var newValue = (bool)e.NewValue;
  583. upDown.OnIsReadOnlyChanged(oldValue, newValue);
  584. }
  585. }
  586. /// <summary>
  587. /// Called when the <see cref="Maximum"/> property value changed.
  588. /// </summary>
  589. /// <param name="e">The event args.</param>
  590. private static void OnMaximumChanged(AvaloniaPropertyChangedEventArgs e)
  591. {
  592. if (e.Sender is NumericUpDown upDown)
  593. {
  594. var oldValue = (double)e.OldValue;
  595. var newValue = (double)e.NewValue;
  596. upDown.OnMaximumChanged(oldValue, newValue);
  597. }
  598. }
  599. /// <summary>
  600. /// Called when the <see cref="Minimum"/> property value changed.
  601. /// </summary>
  602. /// <param name="e">The event args.</param>
  603. private static void OnMinimumChanged(AvaloniaPropertyChangedEventArgs e)
  604. {
  605. if (e.Sender is NumericUpDown upDown)
  606. {
  607. var oldValue = (double)e.OldValue;
  608. var newValue = (double)e.NewValue;
  609. upDown.OnMinimumChanged(oldValue, newValue);
  610. }
  611. }
  612. /// <summary>
  613. /// Called when the <see cref="Text"/> property value changed.
  614. /// </summary>
  615. /// <param name="e">The event args.</param>
  616. private static void OnTextChanged(AvaloniaPropertyChangedEventArgs e)
  617. {
  618. if (e.Sender is NumericUpDown upDown)
  619. {
  620. var oldValue = (string)e.OldValue;
  621. var newValue = (string)e.NewValue;
  622. upDown.OnTextChanged(oldValue, newValue);
  623. }
  624. }
  625. /// <summary>
  626. /// Called when the <see cref="Value"/> property value changed.
  627. /// </summary>
  628. /// <param name="e">The event args.</param>
  629. private static void OnValueChanged(AvaloniaPropertyChangedEventArgs e)
  630. {
  631. if (e.Sender is NumericUpDown upDown)
  632. {
  633. var oldValue = (double)e.OldValue;
  634. var newValue = (double)e.NewValue;
  635. upDown.OnValueChanged(oldValue, newValue);
  636. }
  637. }
  638. private void SetValueInternal(double value)
  639. {
  640. _internalValueSet = true;
  641. try
  642. {
  643. Value = value;
  644. }
  645. finally
  646. {
  647. _internalValueSet = false;
  648. }
  649. }
  650. private static double OnCoerceMaximum(NumericUpDown upDown, double value)
  651. {
  652. return upDown.OnCoerceMaximum(value);
  653. }
  654. private static double OnCoerceMinimum(NumericUpDown upDown, double value)
  655. {
  656. return upDown.OnCoerceMinimum(value);
  657. }
  658. private static double OnCoerceIncrement(NumericUpDown upDown, double value)
  659. {
  660. return upDown.OnCoerceIncrement(value);
  661. }
  662. private void TextBoxOnTextChanged()
  663. {
  664. try
  665. {
  666. _isTextChangedFromUI = true;
  667. if (TextBox != null)
  668. {
  669. Text = TextBox.Text;
  670. }
  671. }
  672. finally
  673. {
  674. _isTextChangedFromUI = false;
  675. }
  676. }
  677. private void OnSpinnerSpin(object sender, SpinEventArgs e)
  678. {
  679. if (AllowSpin && !IsReadOnly)
  680. {
  681. var spin = !e.UsingMouseWheel;
  682. spin |= ((TextBox != null) && TextBox.IsFocused);
  683. if (spin)
  684. {
  685. e.Handled = true;
  686. OnSpin(e);
  687. }
  688. }
  689. }
  690. private void DoDecrement()
  691. {
  692. if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Decrease) == ValidSpinDirections.Decrease)
  693. {
  694. OnDecrement();
  695. }
  696. }
  697. private void DoIncrement()
  698. {
  699. if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Increase) == ValidSpinDirections.Increase)
  700. {
  701. OnIncrement();
  702. }
  703. }
  704. public event EventHandler<SpinEventArgs> Spinned;
  705. private void TextBoxOnPointerPressed(object sender, PointerPressedEventArgs e)
  706. {
  707. if (e.Device.Captured != Spinner)
  708. {
  709. Dispatcher.UIThread.InvokeAsync(() => { e.Device.Capture(Spinner); }, DispatcherPriority.Input);
  710. }
  711. }
  712. /// <summary>
  713. /// Defines the <see cref="ValueChanged"/> event.
  714. /// </summary>
  715. public static readonly RoutedEvent<NumericUpDownValueChangedEventArgs> ValueChangedEvent =
  716. RoutedEvent.Register<NumericUpDown, NumericUpDownValueChangedEventArgs>(nameof(ValueChanged), RoutingStrategies.Bubble);
  717. /// <summary>
  718. /// Raised when the <see cref="Value"/> changes.
  719. /// </summary>
  720. public event EventHandler<SpinEventArgs> ValueChanged
  721. {
  722. add { AddHandler(ValueChangedEvent, value); }
  723. remove { RemoveHandler(ValueChangedEvent, value); }
  724. }
  725. private bool CommitInput()
  726. {
  727. return SyncTextAndValueProperties(true, Text);
  728. }
  729. /// <summary>
  730. /// Synchronize <see cref="Text"/> and <see cref="Value"/> properties.
  731. /// </summary>
  732. /// <param name="updateValueFromText">If value should be updated from text.</param>
  733. /// <param name="text">The text.</param>
  734. private bool SyncTextAndValueProperties(bool updateValueFromText, string text)
  735. {
  736. return SyncTextAndValueProperties(updateValueFromText, text, false);
  737. }
  738. /// <summary>
  739. /// Synchronize <see cref="Text"/> and <see cref="Value"/> properties.
  740. /// </summary>
  741. /// <param name="updateValueFromText">If value should be updated from text.</param>
  742. /// <param name="text">The text.</param>
  743. /// <param name="forceTextUpdate">Force text update.</param>
  744. private bool SyncTextAndValueProperties(bool updateValueFromText, string text, bool forceTextUpdate)
  745. {
  746. if (_isSyncingTextAndValueProperties)
  747. return true;
  748. _isSyncingTextAndValueProperties = true;
  749. var parsedTextIsValid = true;
  750. try
  751. {
  752. if (updateValueFromText)
  753. {
  754. if (!string.IsNullOrEmpty(text))
  755. {
  756. try
  757. {
  758. var newValue = ConvertTextToValue(text);
  759. if (!Equals(newValue, Value))
  760. {
  761. SetValueInternal(newValue);
  762. }
  763. }
  764. catch
  765. {
  766. parsedTextIsValid = false;
  767. }
  768. }
  769. }
  770. // Do not touch the ongoing text input from user.
  771. if (!_isTextChangedFromUI)
  772. {
  773. var keepEmpty = !forceTextUpdate && string.IsNullOrEmpty(Text);
  774. if (!keepEmpty)
  775. {
  776. var newText = ConvertValueToText();
  777. if (!Equals(Text, newText))
  778. {
  779. Text = newText;
  780. }
  781. }
  782. // Sync Text and textBox
  783. if (TextBox != null)
  784. {
  785. TextBox.Text = Text;
  786. }
  787. }
  788. if (_isTextChangedFromUI && !parsedTextIsValid)
  789. {
  790. // Text input was made from the user and the text
  791. // represents an invalid value. Disable the spinner in this case.
  792. if (Spinner != null)
  793. {
  794. Spinner.ValidSpinDirection = ValidSpinDirections.None;
  795. }
  796. }
  797. else
  798. {
  799. SetValidSpinDirection();
  800. }
  801. }
  802. finally
  803. {
  804. _isSyncingTextAndValueProperties = false;
  805. }
  806. return parsedTextIsValid;
  807. }
  808. private double ConvertTextToValueCore(string currentValueText, string text)
  809. {
  810. double result;
  811. if (IsPercent(FormatString))
  812. {
  813. result = decimal.ToDouble(ParsePercent(text, CultureInfo));
  814. }
  815. else
  816. {
  817. // Problem while converting new text
  818. if (!double.TryParse(text, ParsingNumberStyle, CultureInfo, out var outputValue))
  819. {
  820. var shouldThrow = true;
  821. // Check if CurrentValueText is also failing => it also contains special characters. ex : 90°
  822. if (!double.TryParse(currentValueText, ParsingNumberStyle, CultureInfo, out var _))
  823. {
  824. // extract non-digit characters
  825. var currentValueTextSpecialCharacters = currentValueText.Where(c => !char.IsDigit(c));
  826. var textSpecialCharacters = text.Where(c => !char.IsDigit(c));
  827. // same non-digit characters on currentValueText and new text => remove them on new Text to parse it again.
  828. if (currentValueTextSpecialCharacters.Except(textSpecialCharacters).ToList().Count == 0)
  829. {
  830. foreach (var character in textSpecialCharacters)
  831. {
  832. text = text.Replace(character.ToString(), string.Empty);
  833. }
  834. // if without the special characters, parsing is good, do not throw
  835. if (double.TryParse(text, ParsingNumberStyle, CultureInfo, out outputValue))
  836. {
  837. shouldThrow = false;
  838. }
  839. }
  840. }
  841. if (shouldThrow)
  842. {
  843. throw new InvalidDataException("Input string was not in a correct format.");
  844. }
  845. }
  846. result = outputValue;
  847. }
  848. return result;
  849. }
  850. private void ValidateMinMax(double value)
  851. {
  852. if (value < Minimum)
  853. {
  854. throw new ArgumentOutOfRangeException(nameof(Minimum), string.Format("Value must be greater than Minimum value of {0}", Minimum));
  855. }
  856. else if (value > Maximum)
  857. {
  858. throw new ArgumentOutOfRangeException(nameof(Maximum), string.Format("Value must be less than Maximum value of {0}", Maximum));
  859. }
  860. }
  861. /// <summary>
  862. /// Parse percent format text
  863. /// </summary>
  864. /// <param name="text">Text to parse.</param>
  865. /// <param name="cultureInfo">The culture info.</param>
  866. private static decimal ParsePercent(string text, IFormatProvider cultureInfo)
  867. {
  868. var info = NumberFormatInfo.GetInstance(cultureInfo);
  869. text = text.Replace(info.PercentSymbol, null);
  870. var result = decimal.Parse(text, NumberStyles.Any, info);
  871. result = result / 100;
  872. return result;
  873. }
  874. private bool IsPercent(string stringToTest)
  875. {
  876. var PIndex = stringToTest.IndexOf("P", StringComparison.Ordinal);
  877. if (PIndex >= 0)
  878. {
  879. //stringToTest contains a "P" between 2 "'", it's considered as text, not percent
  880. var isText = stringToTest.Substring(0, PIndex).Contains("'")
  881. && stringToTest.Substring(PIndex, FormatString.Length - PIndex).Contains("'");
  882. return !isText;
  883. }
  884. return false;
  885. }
  886. }
  887. }