Skip to content

Commit 581f74c

Browse files
Add initial InputValidationAdorner Sample test
(Should just have this be in the Nuget Package vs. sample...)
1 parent f1c8767 commit 581f74c

File tree

5 files changed

+382
-0
lines changed

5 files changed

+382
-0
lines changed

components/Adorners/samples/Adorners.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ The following example uses `IEditableObject` to control the editing lifecycle co
6060
6161
Adorners are template-based controls, but you can use a class-backed resource dictionary to better enable usage of x:Bind for easier creation and binding to the `AdornedElement`, as seen here.
6262

63+
## Input Validation Example
64+
65+
The custom adorner example above can be further extended to provide input validation feedback to the user using the standard `INotifyDataErrorInfo` interface.
66+
We use the `ObservableValidator` class from the `CommunityToolkit.Mvvm` package to provide validation rules for our view model properties.
67+
When the user submits invalid input, the adorner displays a red border around the text box and shows a tooltip with the validation error message:
68+
69+
> [!SAMPLE InputValidationAdornerSample]
70+
6371
## TODO: Resize Example
6472

6573
Another common use case for adorners is to allow a user to resize a visual element.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<!-- Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE file in the project root for more information. -->
2+
<ResourceDictionary x:Class="AdornersExperiment.Samples.InputValidation.InputValidationAdornerResources"
3+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
4+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
5+
xmlns:Windows11="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractPresent(Windows.Foundation.UniversalApiContract, 14)"
6+
xmlns:behaviors="using:CommunityToolkit.WinUI.Behaviors"
7+
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
8+
xmlns:local="using:AdornersExperiment.Samples.InputValidation"
9+
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
10+
xmlns:ui="using:CommunityToolkit.WinUI">
11+
12+
<!-- Implicitly applied default style -->
13+
<Style BasedOn="{StaticResource DefaultInputValidationAdornerStyle}"
14+
TargetType="local:InputValidationAdorner" />
15+
16+
<Style x:Key="DefaultInputValidationAdornerStyle"
17+
TargetType="local:InputValidationAdorner">
18+
<Setter Property="Template">
19+
<Setter.Value>
20+
<ControlTemplate TargetType="local:InputValidationAdorner">
21+
<ContentControl x:Name="ContentContainer"
22+
HorizontalContentAlignment="Stretch"
23+
VerticalContentAlignment="Stretch">
24+
<ContentControl.ContentTemplate>
25+
<DataTemplate x:DataType="local:InputValidationAdorner">
26+
<Grid Visibility="{x:Bind HasValidationFailed, Mode=OneWay}" >
27+
<Rectangle Margin="-4"
28+
HorizontalAlignment="Stretch"
29+
VerticalAlignment="Stretch"
30+
RadiusX="4"
31+
RadiusY="4"
32+
Stroke="Red"
33+
StrokeThickness="1"/>
34+
35+
<muxc:InfoBadge Width="20"
36+
MinHeight="20"
37+
Margin="0,0,-32,0"
38+
HorizontalAlignment="Right"
39+
VerticalAlignment="Center"
40+
Background="{ThemeResource SystemFillColorCriticalBrush}">
41+
<muxc:InfoBadge.IconSource>
42+
<muxc:FontIconSource FontFamily="{ThemeResource SymbolThemeFontFamily}"
43+
Glyph="&#xF13C;" />
44+
</muxc:InfoBadge.IconSource>
45+
<ToolTipService.ToolTip>
46+
<ToolTip Content="{x:Bind ValidationMessage, Mode=OneWay}" />
47+
</ToolTipService.ToolTip>
48+
</muxc:InfoBadge>
49+
</Grid>
50+
</DataTemplate>
51+
</ContentControl.ContentTemplate>
52+
</ContentControl>
53+
</ControlTemplate>
54+
</Setter.Value>
55+
</Setter>
56+
</Style>
57+
</ResourceDictionary>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
namespace AdornersExperiment.Samples.InputValidation;
6+
7+
public sealed partial class InputValidationAdornerResources : ResourceDictionary
8+
{
9+
// NOTICE
10+
// This file only exists to enable x:Bind in the resource dictionary.
11+
// Do not add code here.
12+
// Instead, add code-behind to your templated control.
13+
public InputValidationAdornerResources()
14+
{
15+
this.InitializeComponent();
16+
}
17+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<!-- Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE file in the project root for more information. -->
2+
<Page x:Class="AdornersExperiment.Samples.InputValidation.InputValidationAdornerSample"
3+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
4+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
5+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
6+
xmlns:local="using:AdornersExperiment.Samples.InputValidation"
7+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
8+
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
9+
xmlns:ui="using:CommunityToolkit.WinUI"
10+
mc:Ignorable="d">
11+
12+
<Page.Resources>
13+
<local:InputValidationAdornerResources />
14+
<!--
15+
Inner style copied here from above for convenience of the sample
16+
<Grid Visibility="{x:Bind HasValidationFailed, Mode=OneWay}" >
17+
<Rectangle Margin="-4"
18+
HorizontalAlignment="Stretch"
19+
VerticalAlignment="Stretch"
20+
RadiusX="4"
21+
RadiusY="4"
22+
Stroke="Red"
23+
StrokeThickness="1"/>
24+
25+
<muxc:InfoBadge Width="20"
26+
MinHeight="20"
27+
Margin="0,0,-32,0"
28+
HorizontalAlignment="Right"
29+
VerticalAlignment="Center"
30+
Background="{ThemeResource SystemFillColorCriticalBrush}">
31+
<muxc:InfoBadge.IconSource>
32+
<muxc:FontIconSource FontFamily="{ThemeResource SymbolThemeFontFamily}"
33+
Glyph="&#xF13C;" />
34+
</muxc:InfoBadge.IconSource>
35+
<ToolTipService.ToolTip>
36+
<ToolTip Content="{x:Bind ValidationMessage, Mode=OneWay}" />
37+
</ToolTipService.ToolTip>
38+
</muxc:InfoBadge>
39+
</Grid>
40+
-->
41+
</Page.Resources>
42+
43+
<StackPanel HorizontalAlignment="Left"
44+
Spacing="16">
45+
<TextBlock Text="Please fill out the form below and click Submit:" />
46+
47+
<!-- We set the DataContext here for our Adorner to retrieve the IEditableObject reference -->
48+
<!-- We use TwoWay binding to ensure the updates from the Adorner are reflected in the ViewModel -->
49+
<TextBox DataContext="{x:Bind ViewModel}"
50+
Header="Enter your first name:"
51+
PlaceholderText="First name"
52+
Text="{x:Bind ViewModel.FirstName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
53+
<ui:AdornerLayer.Xaml>
54+
<local:InputValidationAdorner PropertyName="FirstName"
55+
Style="{StaticResource DefaultInputValidationAdornerStyle}" />
56+
</ui:AdornerLayer.Xaml>
57+
</TextBox>
58+
59+
<TextBox DataContext="{x:Bind ViewModel}"
60+
Header="Enter your last name:"
61+
PlaceholderText="Last name"
62+
Text="{x:Bind ViewModel.LastName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
63+
<ui:AdornerLayer.Xaml>
64+
<local:InputValidationAdorner PropertyName="LastName"
65+
Style="{StaticResource DefaultInputValidationAdornerStyle}" />
66+
</ui:AdornerLayer.Xaml>
67+
</TextBox>
68+
69+
<TextBox DataContext="{x:Bind ViewModel}"
70+
Header="Enter your email address:"
71+
PlaceholderText="Email"
72+
Text="{x:Bind ViewModel.Email, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
73+
<ui:AdornerLayer.Xaml>
74+
<local:InputValidationAdorner PropertyName="Email"
75+
Style="{StaticResource DefaultInputValidationAdornerStyle}" />
76+
</ui:AdornerLayer.Xaml>
77+
</TextBox>
78+
79+
<TextBox DataContext="{x:Bind ViewModel}"
80+
Header="Enter your phone number:"
81+
PlaceholderText="Phone number"
82+
Text="{x:Bind ViewModel.PhoneNumber, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
83+
<ui:AdornerLayer.Xaml>
84+
<local:InputValidationAdorner PropertyName="PhoneNumber"
85+
Style="{StaticResource DefaultInputValidationAdornerStyle}" />
86+
</ui:AdornerLayer.Xaml>
87+
</TextBox>
88+
89+
<muxc:NumberBox DataContext="{x:Bind ViewModel}"
90+
Header="Enter your age:"
91+
LargeChange="10"
92+
PlaceholderText="Age"
93+
SmallChange="1"
94+
SpinButtonPlacementMode="Inline"
95+
Value="{x:Bind ViewModel.Age, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
96+
<ui:AdornerLayer.Xaml>
97+
<local:InputValidationAdorner PropertyName="Age"
98+
Style="{StaticResource DefaultInputValidationAdornerStyle}" />
99+
</ui:AdornerLayer.Xaml>
100+
</muxc:NumberBox>
101+
102+
<!-- Submit command -->
103+
<Button Command="{x:Bind ViewModel.SubmitCommand}"
104+
Content="Submit" />
105+
</StackPanel>
106+
</Page>
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using CommunityToolkit.Mvvm.ComponentModel;
6+
using CommunityToolkit.Mvvm.Input;
7+
using CommunityToolkit.WinUI;
8+
9+
using System.ComponentModel.DataAnnotations;
10+
11+
using INotifyDataErrorInfo = System.ComponentModel.INotifyDataErrorInfo;
12+
using DataErrorsChangedEventArgs = System.ComponentModel.DataErrorsChangedEventArgs;
13+
14+
namespace AdornersExperiment.Samples.InputValidation;
15+
16+
[ToolkitSample(id: nameof(InputValidationAdornerSample), "Input Validation Adorner", description: "A sample for showing how to use an Adorner for any Input Validation with INotifyDataErrorInfo.")]
17+
public sealed partial class InputValidationAdornerSample : Page
18+
{
19+
public ValidationFormWidgetViewModel ViewModel { get; } = new();
20+
21+
public InputValidationAdornerSample()
22+
{
23+
this.InitializeComponent();
24+
}
25+
}
26+
27+
/// <summary>
28+
/// ViewModel that shows using <see cref="INotifyDataErrorInfo"/> in conjunction with an Adorner.
29+
/// Via the <see cref="ObservableValidator"/> base class from the MVVM Toolkit.
30+
/// Example modified from the MVVM Toolkit Sample App.
31+
/// </summary>
32+
public partial class ValidationFormWidgetViewModel : ObservableValidator
33+
{
34+
public event EventHandler? FormSubmissionCompleted;
35+
public event EventHandler? FormSubmissionFailed;
36+
37+
[ObservableProperty]
38+
[Required]
39+
[MinLength(2)]
40+
[MaxLength(100)]
41+
public partial string? FirstName { get; set; }
42+
43+
[ObservableProperty]
44+
[Required]
45+
[MinLength(2)]
46+
[MaxLength(100)]
47+
public partial string? LastName { get; set; }
48+
49+
[ObservableProperty]
50+
[Required]
51+
[EmailAddress]
52+
public partial string? Email { get; set; }
53+
54+
[ObservableProperty]
55+
[Required]
56+
[Phone]
57+
public partial string? PhoneNumber { get; set; }
58+
59+
[ObservableProperty]
60+
[Required]
61+
[Range(13, 120)]
62+
public partial int Age { get; set; }
63+
64+
[RelayCommand]
65+
private void Submit()
66+
{
67+
ValidateAllProperties();
68+
69+
if (HasErrors)
70+
{
71+
FormSubmissionFailed?.Invoke(this, EventArgs.Empty);
72+
}
73+
else
74+
{
75+
FormSubmissionCompleted?.Invoke(this, EventArgs.Empty);
76+
}
77+
}
78+
}
79+
80+
/// <summary>
81+
/// An Adorner that shows an error message if Data Validation fails.
82+
/// The adorned control's <see cref="FrameworkElement.DataContext"/> must implement <see cref="INotifyDataErrorInfo"/>. It assumes that the return of <see cref="INotifyDataErrorInfo.GetErrors(string?)"/> is a <see cref="ValidationResult"/> collection.
83+
/// </summary>
84+
public sealed partial class InputValidationAdorner : Adorner<FrameworkElement>
85+
{
86+
/// <summary>
87+
/// Gets or sets the name of the property this adorner should look for errors on.
88+
/// </summary>
89+
public string PropertyName
90+
{
91+
get { return (string)GetValue(PropertyNameProperty); }
92+
set { SetValue(PropertyNameProperty, value); }
93+
}
94+
95+
/// <summary>
96+
/// Identifies the <see cref="PropertyName"/> dependency property.
97+
/// </summary>
98+
public static readonly DependencyProperty PropertyNameProperty =
99+
DependencyProperty.Register(nameof(PropertyName), typeof(string), typeof(InputValidationAdorner), new PropertyMetadata(null));
100+
101+
/// <summary>
102+
/// Gets or sets whether the popup is open.
103+
/// </summary>
104+
public bool HasValidationFailed
105+
{
106+
get { return (bool)GetValue(HasValidationFailedProperty); }
107+
set { SetValue(HasValidationFailedProperty, value); }
108+
}
109+
110+
/// <summary>
111+
/// Identifies the <see cref="HasValidationFailed"/> dependency property.
112+
/// </summary>
113+
public static readonly DependencyProperty HasValidationFailedProperty =
114+
DependencyProperty.Register(nameof(HasValidationFailed), typeof(bool), typeof(InputValidationAdorner), new PropertyMetadata(false));
115+
116+
/// <summary>
117+
/// Gets or sets the validation message for this failed property.
118+
/// </summary>
119+
public string ValidationMessage
120+
{
121+
get { return (string)GetValue(ValidationMessageProperty); }
122+
set { SetValue(ValidationMessageProperty, value); }
123+
}
124+
125+
/// <summary>
126+
/// Identifies the <see cref="ValidationMessage"/> dependency property.
127+
/// </summary>
128+
public static readonly DependencyProperty ValidationMessageProperty =
129+
DependencyProperty.Register(nameof(ValidationMessage), typeof(string), typeof(InputValidationAdorner), new PropertyMetadata(null));
130+
131+
private INotifyDataErrorInfo? _dataErrorInfo;
132+
133+
public InputValidationAdorner()
134+
{
135+
this.DefaultStyleKey = typeof(InputValidationAdorner);
136+
137+
// Uno workaround
138+
DataContext = this;
139+
}
140+
141+
protected override void OnApplyTemplate()
142+
{
143+
base.OnApplyTemplate();
144+
}
145+
146+
protected override void OnAttached()
147+
{
148+
base.OnAttached();
149+
150+
if (AdornedElement?.DataContext is INotifyDataErrorInfo iError)
151+
{
152+
_dataErrorInfo = iError;
153+
_dataErrorInfo.ErrorsChanged += this.INotifyDataErrorInfo_ErrorsChanged;
154+
}
155+
}
156+
157+
private void INotifyDataErrorInfo_ErrorsChanged(object? sender, DataErrorsChangedEventArgs e)
158+
{
159+
if (_dataErrorInfo is not null)
160+
{
161+
// Reset state
162+
if (!_dataErrorInfo.HasErrors)
163+
{
164+
HasValidationFailed = false;
165+
ValidationMessage = string.Empty;
166+
return;
167+
}
168+
169+
if (e.PropertyName == PropertyName)
170+
{
171+
HasValidationFailed = true;
172+
173+
StringBuilder message = new();
174+
foreach (ValidationResult result in _dataErrorInfo.GetErrors(e.PropertyName))
175+
{
176+
message.AppendLine(result.ErrorMessage);
177+
}
178+
179+
ValidationMessage = message.ToString().Trim();
180+
}
181+
}
182+
}
183+
184+
protected override void OnDetaching()
185+
{
186+
if (_dataErrorInfo is not null)
187+
{
188+
_dataErrorInfo.ErrorsChanged -= this.INotifyDataErrorInfo_ErrorsChanged;
189+
_dataErrorInfo = null;
190+
}
191+
192+
base.OnDetaching();
193+
}
194+
}

0 commit comments

Comments
 (0)