Skip to content

Commit e6f81bf

Browse files
Fix for 2596 - Align left margin for error message on outlined TextBox with helper text left margin (#2820)
* Fix left margin for error message on outlined TextBox The error message and helper text should have the same Margin.Left value for the outlined TextBox. * Adjusted error text left margin for filled text fields ComboBoxes, DatePicker and TimePicker all rely on the fields internally, and thus inherit these changes. * Added UI test for the filled style as well * Retain original bottom margin I want to make as few changes as possible to the original style (margin)
1 parent 76eab1e commit e6f81bf

File tree

5 files changed

+282
-3
lines changed

5 files changed

+282
-3
lines changed

MainDemo.Wpf/Domain/FieldsViewModel.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ public class FieldsViewModel : ViewModelBase
44
{
55
private string? _name;
66
private string? _name2;
7+
private string? _text1;
8+
private string? _text2;
9+
private string? _password1 = "password";
10+
private string? _password2 = "password";
711

812
public string? Name
913
{
@@ -17,6 +21,40 @@ public string? Name2
1721
set => SetProperty(ref _name2, value);
1822
}
1923

24+
public string? Text1
25+
{
26+
get => _text1;
27+
set => SetProperty(ref _text1, value);
28+
}
29+
30+
public string? Text2
31+
{
32+
get => _text2;
33+
set => SetProperty(ref _text2, value);
34+
}
35+
36+
public string? Password1
37+
{
38+
get => _password1;
39+
set
40+
{
41+
if (string.IsNullOrEmpty(value))
42+
throw new ArgumentException("Password cannot be empty");
43+
SetProperty(ref _password1, value);
44+
}
45+
}
46+
47+
public string? Password2
48+
{
49+
get => _password2;
50+
set
51+
{
52+
if (string.IsNullOrEmpty(value))
53+
throw new ArgumentException("Password cannot be empty");
54+
SetProperty(ref _password2, value);
55+
}
56+
}
57+
2058
public FieldsTestObject TestObject => new() { Name = "Mr. Test" };
2159
}
2260

MainDemo.Wpf/Domain/PasswordHelper.cs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
namespace MaterialDesignDemo.Domain;
2+
3+
/// <summary>
4+
/// Simple helper class to enable data-binding for <see cref="PasswordBox.Password"/> which is not a <see cref="DependencyProperty"/>
5+
/// </summary>
6+
internal static class PasswordHelper
7+
{
8+
public static readonly DependencyProperty PasswordProperty =
9+
DependencyProperty.RegisterAttached("Password",
10+
typeof(string), typeof(PasswordHelper),
11+
new FrameworkPropertyMetadata(string.Empty, OnPasswordPropertyChanged));
12+
13+
public static readonly DependencyProperty AttachProperty =
14+
DependencyProperty.RegisterAttached("Attach",
15+
typeof(bool), typeof(PasswordHelper), new PropertyMetadata(false, Attach));
16+
17+
private static readonly DependencyProperty IsUpdatingProperty =
18+
DependencyProperty.RegisterAttached("IsUpdating", typeof(bool),
19+
typeof(PasswordHelper));
20+
21+
public static void SetAttach(DependencyObject dp, bool value) => dp.SetValue(AttachProperty, value);
22+
23+
public static bool GetAttach(DependencyObject dp) => (bool)dp.GetValue(AttachProperty);
24+
25+
public static string GetPassword(DependencyObject dp) => (string)dp.GetValue(PasswordProperty);
26+
27+
public static void SetPassword(DependencyObject dp, string value) => dp.SetValue(PasswordProperty, value);
28+
29+
private static bool GetIsUpdating(DependencyObject dp) => (bool)dp.GetValue(IsUpdatingProperty);
30+
31+
private static void SetIsUpdating(DependencyObject dp, bool value) => dp.SetValue(IsUpdatingProperty, value);
32+
33+
private static void OnPasswordPropertyChanged(DependencyObject sender,
34+
DependencyPropertyChangedEventArgs e)
35+
{
36+
PasswordBox passwordBox = (PasswordBox) sender;
37+
passwordBox.PasswordChanged -= PasswordChanged;
38+
39+
if (!GetIsUpdating(passwordBox))
40+
{
41+
passwordBox.Password = (string)e.NewValue;
42+
}
43+
passwordBox.PasswordChanged += PasswordChanged;
44+
}
45+
46+
private static void Attach(DependencyObject sender,
47+
DependencyPropertyChangedEventArgs e)
48+
{
49+
PasswordBox passwordBox = (PasswordBox) sender;
50+
51+
if ((bool)e.OldValue)
52+
{
53+
passwordBox.PasswordChanged -= PasswordChanged;
54+
}
55+
56+
if ((bool)e.NewValue)
57+
{
58+
passwordBox.PasswordChanged += PasswordChanged;
59+
}
60+
}
61+
62+
private static void PasswordChanged(object sender, RoutedEventArgs e)
63+
{
64+
PasswordBox passwordBox = (PasswordBox) sender;
65+
SetIsUpdating(passwordBox, true);
66+
SetPassword(passwordBox, passwordBox.Password);
67+
SetIsUpdating(passwordBox, false);
68+
}
69+
}

MainDemo.Wpf/Fields.xaml

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@
296296
<Grid.RowDefinitions>
297297
<RowDefinition/>
298298
<RowDefinition/>
299+
<RowDefinition/>
299300
</Grid.RowDefinitions>
300301

301302
<Grid.ColumnDefinitions>
@@ -381,27 +382,67 @@
381382

382383
<smtx:XamlDisplay
383384
Grid.Row="1"
384-
Grid.Column="4"
385+
Grid.Column="3"
385386
UniqueKey="passwordFilled1"
386387
VerticalAlignment="Bottom">
387388
<PasswordBox
388389
Style="{StaticResource MaterialDesignFilledPasswordBox}"
389390
materialDesign:HintAssist.Hint="Password"
390391
materialDesign:HintAssist.HelperText="Helper text"/>
391392
</smtx:XamlDisplay>
393+
394+
<smtx:XamlDisplay
395+
Grid.Row="2"
396+
Grid.Column="2"
397+
UniqueKey="fieldFilled_with_validation"
398+
HorizontalAlignment="Left"
399+
Margin="0 24 0 0">
400+
<TextBox
401+
Style="{StaticResource MaterialDesignFilledTextBox}"
402+
VerticalAlignment="Top"
403+
materialDesign:HintAssist.Hint="Text (validated)"
404+
materialDesign:HintAssist.HelperText="Helper text">
405+
<TextBox.Text>
406+
<Binding Path="Text1" UpdateSourceTrigger="PropertyChanged">
407+
<Binding.ValidationRules>
408+
<domain1:NotEmptyValidationRule ValidatesOnTargetUpdated="True"/>
409+
</Binding.ValidationRules>
410+
</Binding>
411+
</TextBox.Text>
412+
</TextBox>
413+
</smtx:XamlDisplay>
414+
415+
<smtx:XamlDisplay
416+
Grid.Row="2"
417+
Grid.Column="3"
418+
UniqueKey="passwordFilled_with_validation"
419+
HorizontalAlignment="Left"
420+
Margin="0 24 0 0">
421+
<StackPanel Orientation="Vertical">
422+
<PasswordBox
423+
Style="{StaticResource MaterialDesignFilledPasswordBox}"
424+
materialDesign:HintAssist.Hint="Password (validated)"
425+
materialDesign:HintAssist.HelperText="Helper text"
426+
domain1:PasswordHelper.Attach="True"
427+
domain1:PasswordHelper.Password="{Binding Path=Password1, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnExceptions=True}">
428+
</PasswordBox>
429+
</StackPanel>
430+
</smtx:XamlDisplay>
392431
</Grid>
393432

394433
<Grid Margin="0 48 0 0">
395434
<Grid.RowDefinitions>
396435
<RowDefinition/>
397436
<RowDefinition/>
437+
<RowDefinition/>
398438
</Grid.RowDefinitions>
399439

400440
<Grid.ColumnDefinitions>
401441
<ColumnDefinition Width="Auto"/>
402442
<ColumnDefinition Width="Auto"/>
403443
<ColumnDefinition Width="Auto"/>
404444
<ColumnDefinition Width="Auto"/>
445+
<ColumnDefinition Width="Auto"/>
405446
</Grid.ColumnDefinitions>
406447

407448
<Grid.Resources>
@@ -426,6 +467,7 @@
426467

427468
<smtx:XamlDisplay
428469
Grid.Row="1"
470+
Grid.RowSpan="2"
429471
Grid.Column="0"
430472
UniqueKey="fields_26">
431473

@@ -448,6 +490,7 @@
448490

449491
<smtx:XamlDisplay
450492
Grid.Row="1"
493+
Grid.RowSpan="2"
451494
Grid.Column="1"
452495
UniqueKey="fields_29">
453496
<StackPanel>
@@ -486,14 +529,52 @@
486529

487530
<smtx:XamlDisplay
488531
Grid.Row="1"
489-
Grid.Column="4"
532+
Grid.Column="3"
490533
UniqueKey="passwordOutlined1"
491534
Margin="0 34 0 0">
492535
<PasswordBox
493536
Style="{StaticResource MaterialDesignOutlinedPasswordBox}"
494537
materialDesign:HintAssist.Hint="Password"
495538
materialDesign:HintAssist.HelperText="Helper text"/>
496539
</smtx:XamlDisplay>
540+
541+
<smtx:XamlDisplay
542+
Grid.Row="2"
543+
Grid.Column="2"
544+
UniqueKey="fieldOutlined_with_validation"
545+
HorizontalAlignment="Left"
546+
Margin="0 24 0 0">
547+
<TextBox
548+
Style="{StaticResource MaterialDesignOutlinedTextBox}"
549+
VerticalAlignment="Top"
550+
materialDesign:HintAssist.Hint="Text (validated)"
551+
materialDesign:HintAssist.HelperText="Helper text">
552+
<TextBox.Text>
553+
<Binding Path="Text2" UpdateSourceTrigger="PropertyChanged">
554+
<Binding.ValidationRules>
555+
<domain1:NotEmptyValidationRule ValidatesOnTargetUpdated="True"/>
556+
</Binding.ValidationRules>
557+
</Binding>
558+
</TextBox.Text>
559+
</TextBox>
560+
</smtx:XamlDisplay>
561+
562+
<smtx:XamlDisplay
563+
Grid.Row="2"
564+
Grid.Column="3"
565+
UniqueKey="passwordOutlined_with_validation"
566+
HorizontalAlignment="Left"
567+
Margin="0 24 0 0">
568+
<StackPanel Orientation="Vertical">
569+
<PasswordBox
570+
Style="{StaticResource MaterialDesignOutlinedPasswordBox}"
571+
materialDesign:HintAssist.Hint="Password (validated)"
572+
materialDesign:HintAssist.HelperText="Helper text"
573+
domain1:PasswordHelper.Attach="True"
574+
domain1:PasswordHelper.Password="{Binding Path=Password2, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnExceptions=True}">
575+
</PasswordBox>
576+
</StackPanel>
577+
</smtx:XamlDisplay>
497578
</Grid>
498579

499580
<Grid

MaterialDesignThemes.UITests/WPF/TextBoxes/TextBoxTests.cs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.ComponentModel;
3+
using System.Globalization;
34
using System.Linq;
45
using System.Threading.Tasks;
56
using System.Windows;
@@ -404,5 +405,91 @@ public async Task VerticalContentAlignment_ProperlyAlignsText()
404405

405406
recorder.Success();
406407
}
408+
409+
[Fact]
410+
[Description("Issue 2596")]
411+
public async Task OutlinedTextBox_ValidationErrorMargin_MatchesHelperTextMargin()
412+
{
413+
await using var recorder = new TestRecorder(App);
414+
415+
var stackPanel = await LoadXaml<StackPanel>(@"
416+
<StackPanel>
417+
<TextBox Style=""{StaticResource MaterialDesignOutlinedTextBox}""
418+
materialDesign:HintAssist.Hint=""Hint text""
419+
materialDesign:HintAssist.HelperText=""Helper text"">
420+
<TextBox.Text>
421+
<Binding RelativeSource=""{RelativeSource Self}"" Path=""Tag"" UpdateSourceTrigger=""PropertyChanged"">
422+
<Binding.ValidationRules>
423+
<local:NotEmptyValidationRule ValidatesOnTargetUpdated=""True""/>
424+
</Binding.ValidationRules>
425+
</Binding>
426+
</TextBox.Text>
427+
</TextBox>
428+
</StackPanel>
429+
", ("local", typeof(NotEmptyValidationRule)));
430+
431+
var textBox = await stackPanel.GetElement<TextBox>("/TextBox");
432+
433+
var errorViewer = await textBox.GetElement<Border>("DefaultErrorViewer");
434+
var helperTextTextBlock = await textBox.GetElement<TextBlock>("HelperTextTextBlock");
435+
436+
Thickness? errorMargin = await errorViewer.GetProperty<Thickness>(FrameworkElement.MarginProperty);
437+
Thickness? helperTextMargin = await helperTextTextBlock.GetProperty<Thickness>(FrameworkElement.MarginProperty);
438+
439+
Assert.True(errorMargin.HasValue);
440+
Assert.True(helperTextMargin.HasValue);
441+
Assert.True(Math.Abs(errorMargin.Value.Left - helperTextMargin.Value.Left) < double.Epsilon,
442+
$"Error text and helper text do not have the same Margin.Left values: Error text Margin.Left ({errorMargin.Value.Left}) == Helper text Margin.Left ({helperTextMargin.Value.Left})");
443+
444+
recorder.Success();
445+
}
446+
447+
[Fact]
448+
[Description("Issue 2596")]
449+
public async Task FilledTextBox_ValidationErrorMargin_MatchesHelperTextMargin()
450+
{
451+
await using var recorder = new TestRecorder(App);
452+
453+
var stackPanel = await LoadXaml<StackPanel>(@"
454+
<StackPanel>
455+
<TextBox Style=""{StaticResource MaterialDesignFilledTextBox}""
456+
materialDesign:HintAssist.Hint=""Hint text""
457+
materialDesign:HintAssist.HelperText=""Helper text"">
458+
<TextBox.Text>
459+
<Binding RelativeSource=""{RelativeSource Self}"" Path=""Tag"" UpdateSourceTrigger=""PropertyChanged"">
460+
<Binding.ValidationRules>
461+
<local:NotEmptyValidationRule ValidatesOnTargetUpdated=""True""/>
462+
</Binding.ValidationRules>
463+
</Binding>
464+
</TextBox.Text>
465+
</TextBox>
466+
</StackPanel>
467+
", ("local", typeof(NotEmptyValidationRule)));
468+
469+
var textBox = await stackPanel.GetElement<TextBox>("/TextBox");
470+
471+
var errorViewer = await textBox.GetElement<Border>("DefaultErrorViewer");
472+
var helperTextTextBlock = await textBox.GetElement<TextBlock>("HelperTextTextBlock");
473+
474+
Thickness? errorMargin = await errorViewer.GetProperty<Thickness>(FrameworkElement.MarginProperty);
475+
Thickness? helperTextMargin = await helperTextTextBlock.GetProperty<Thickness>(FrameworkElement.MarginProperty);
476+
477+
Assert.True(errorMargin.HasValue);
478+
Assert.True(helperTextMargin.HasValue);
479+
Assert.True(Math.Abs(errorMargin.Value.Left - helperTextMargin.Value.Left) < double.Epsilon,
480+
$"Error text and helper text do not have the same Margin.Left values: Error text Margin.Left ({errorMargin.Value.Left}) == Helper text Margin.Left ({helperTextMargin.Value.Left})");
481+
482+
recorder.Success();
483+
}
484+
}
485+
486+
public class NotEmptyValidationRule : ValidationRule
487+
{
488+
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
489+
{
490+
return string.IsNullOrWhiteSpace((value ?? "").ToString())
491+
? new ValidationResult(false, "Field is required.")
492+
: ValidationResult.ValidResult;
493+
}
407494
}
408495
}

MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.ValidationErrorTemplate.xaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,13 @@
9797
</DataTrigger>
9898

9999
<DataTrigger Binding="{Binding ElementName=Placeholder, Path=AdornedElement.(wpf:TextFieldAssist.HasOutlinedTextField)}" Value="True">
100-
<Setter TargetName="DefaultErrorViewer" Property="Margin" Value="0,2,0,0"/>
100+
<Setter TargetName="DefaultErrorViewer" Property="Margin" Value="16,2,0,0"/>
101101
<Setter TargetName="ValidationPopup" Property="VerticalOffset" Value="2"/>
102102
</DataTrigger>
103+
104+
<DataTrigger Binding="{Binding ElementName=Placeholder, Path=AdornedElement.(wpf:TextFieldAssist.HasFilledTextField)}" Value="True">
105+
<Setter TargetName="DefaultErrorViewer" Property="Margin" Value="16,2,0,2"/>
106+
</DataTrigger>
103107
</ControlTemplate.Triggers>
104108
</ControlTemplate>
105109
</ResourceDictionary>

0 commit comments

Comments
 (0)