Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/MaterialDesignThemes.Wpf/Internal/Shims/NotNullWhen.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@

#if NET462
namespace System.Diagnostics.CodeAnalysis;
//
// Summary:
// Specifies that when a method returns System.Diagnostics.CodeAnalysis.NotNullWhenAttribute.ReturnValue,
// the parameter will not be null even if the corresponding type allows it.
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
internal sealed class NotNullWhenAttribute : Attribute
{
//
// Summary:
// Initializes the attribute with the specified return value condition.
//
// Parameters:
// returnValue:
// The return value condition. If the method returns this value, the associated
// parameter will not be null.
public NotNullWhenAttribute(bool returnValue)
{
ReturnValue = returnValue;
}

//
// Summary:
// Gets the return value condition.
//
// Returns:
// The return value condition. If the method returns this value, the associated
// parameter will not be null.
public bool ReturnValue { get; }
}
#endif
16 changes: 11 additions & 5 deletions src/MaterialDesignThemes.Wpf/UpDownBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

namespace MaterialDesignThemes.Wpf;

using System.Diagnostics.CodeAnalysis;


#if NET8_0_OR_GREATER
using System.Numerics;
Expand All @@ -20,7 +22,7 @@ public class UpDownBase<T> : UpDownBase
private static T Clamp(T value, T min, T max) => T.Clamp(value, min, max);
private static T Add(T value1, T value2) => value1 + value2;
private static T Subtract(T value1, T value2) => value1 - value2;
private static bool TryParse(string text, IFormatProvider? formatProvider, out T? value)
private static bool TryParse(string text, IFormatProvider? formatProvider, [MaybeNullWhen(false)] out T value)
=> T.TryParse(text, formatProvider, out value);
private static int Compare(T value1, T value2) => value1.CompareTo(value2);
#else
Expand All @@ -39,8 +41,9 @@ public class UpDownBase<T, TArithmetic> : UpDownBase
private static T Clamp(T value, T min, T max) => _arithmetic.Max(_arithmetic.Min(value, max), min);
private static T Add(T value1, T value2) => _arithmetic.Add(value1, value2);
private static T Subtract(T value1, T value2) => _arithmetic.Subtract(value1, value2);
private static bool TryParse(string text, IFormatProvider? formatProvider, out T? value)
private static bool TryParse(string text, IFormatProvider? formatProvider, [NotNullWhen(true)] out T value)
=> _arithmetic.TryParse(text, formatProvider, out value);

private static int Compare(T value1, T value2) => _arithmetic.Compare(value1, value2);
#endif

Expand Down Expand Up @@ -134,7 +137,7 @@ private static void OnNumericValueChanged(DependencyObject d, DependencyProperty
if (value is T numericValue)
{
var upDownBase = ToUpDownBase(d);
numericValue = Clamp(numericValue, upDownBase.Minimum, upDownBase.Maximum);
numericValue = upDownBase.ClampValue(numericValue);
return numericValue;
}
return value;
Expand Down Expand Up @@ -213,7 +216,7 @@ private void OnTextBoxFocusLost(object sender, EventArgs e)
{
if (TryParse(textBoxField.Text, CultureInfo.CurrentUICulture, out T? value))
{
SetCurrentValue(ValueProperty, value);
SetCurrentValue(ValueProperty, ClampValue(value));
}
//NB: Because setting ValueProperty will coerce the value, we re-assign back to the textbox here.
textBoxField.Text = Value?.ToString();
Expand All @@ -228,6 +231,9 @@ private void OnTextBoxFocusLost(object sender, EventArgs e)

private void OnDecrease() => SetCurrentValue(ValueProperty, Clamp(Subtract(Value, ValueStep), Minimum, Maximum));

private T ClampValue(T value)
=> Clamp(value, Minimum, Maximum);

protected override void OnPreviewKeyDown(KeyEventArgs e)
{
if (e.Key == Key.Up)
Expand Down Expand Up @@ -364,6 +370,6 @@ public interface IArithmetic<T>

T Min(T value1, T value2);

bool TryParse(string text, IFormatProvider? formatProvider, out T? value);
bool TryParse(string text, IFormatProvider? formatProvider, [NotNullWhen(true)] out T value);
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<UserControl x:Class="MaterialDesignThemes.UITests.Samples.UpDownControls.BoundNumericUpDown"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:MaterialDesignThemes.UITests.Samples.UpDownControls"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DataContext="{d:DesignInstance local:BoundNumericUpDownViewModel,
IsDesignTimeCreatable=False}"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<materialDesign:NumericUpDown Value="{Binding Value}" />
<Button x:Name="btnToFocus" Content="Button to focus" />
</StackPanel>
</UserControl>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace MaterialDesignThemes.UITests.Samples.UpDownControls;

/// <summary>
/// Interaction logic for BoundNumericUpDown.xaml
/// </summary>
public partial class BoundNumericUpDown : UserControl
{
public BoundNumericUpDown()
{
ViewModel = new BoundNumericUpDownViewModel();
DataContext = ViewModel;
InitializeComponent();
}

//I expose the ViewModel here so I can access it in the tests
public BoundNumericUpDownViewModel ViewModel { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using CommunityToolkit.Mvvm.ComponentModel;

namespace MaterialDesignThemes.UITests.Samples.UpDownControls;

//If needed this can be refactored to be generic so all variations of the UpDownControl can use this as a VM
public partial class BoundNumericUpDownViewModel : ObservableObject
{
[ObservableProperty]
private int _value;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel;
using MaterialDesignThemes.UITests.Samples.UpDownControls;

namespace MaterialDesignThemes.UITests.WPF.UpDownControls;

Expand Down Expand Up @@ -156,4 +157,34 @@ public async Task InternalTextBoxIsFocused_WhenGettingKeyboardFocus()

recorder.Success();
}

[Fact(Skip = "Needs XAMLTest 1.2.3 or later")]
public async Task NumericUpDown_ValueSetGreaterThanMaximum_CoercesToMaximum()
{
await using var recorder = new TestRecorder(App);

//Arrange
var userControl = await LoadUserControl<BoundNumericUpDown>();
var numericUpDown = await userControl.GetElement<NumericUpDown>();
var buttonToFocus = await userControl.GetElement<Button>("btnToFocus");

//Set the current value to the maximum
await numericUpDown.SetMaximum(10);
await numericUpDown.SetValue(10);

//Act
//Input a number greater than the maximum
await numericUpDown.MoveKeyboardFocus();
await numericUpDown.SendKeyboardInput($"99");

await buttonToFocus.MoveKeyboardFocus();

//Assert
//The value and bound property in the VM should be set to the maximum
Assert.Equal(10, await numericUpDown.GetValue());
var viewModel = await userControl.GetProperty<BoundNumericUpDownViewModel>(nameof(BoundNumericUpDown.ViewModel));
Assert.Equal(10, viewModel?.Value);

recorder.Success();
}
}
Loading