Skip to content

Commit c5136e2

Browse files
authored
Merge pull request #74 from CommunityToolkit/dev/observable-validator-sample
Add ObservableValidator sample
2 parents a136eeb + 0b55ad2 commit c5136e2

19 files changed

+704
-7
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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 System.Threading.Tasks;
6+
7+
namespace MvvmSample.Core.Services;
8+
9+
/// <summary>
10+
/// The default <see langword="interface"/> for a service that shows dialogs
11+
/// </summary>
12+
public interface IDialogService
13+
{
14+
/// <summary>
15+
/// Shows a message dialog with a title and custom content.
16+
/// </summary>
17+
/// <param name="title">The title of the message dialog.</param>
18+
/// <param name="message">The content of the message dialog.</param>
19+
Task ShowMessageDialogAsync(string title, string message);
20+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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 MvvmSample.Core.Services;
6+
7+
namespace MvvmSample.Core.ViewModels;
8+
9+
public partial class ObservableValidatorPageViewModel : SamplePageViewModel
10+
{
11+
public ObservableValidatorPageViewModel(IFilesService filesService)
12+
: base(filesService)
13+
{
14+
}
15+
}

samples/MvvmSample.Core/ViewModels/Widgets/SubredditWidgetViewModel.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using System.Collections.ObjectModel;
77
using System.Threading.Tasks;
88
using CommunityToolkit.Mvvm.ComponentModel;
9-
using CommunityToolkit.Mvvm.DependencyInjection;
109
using CommunityToolkit.Mvvm.Input;
1110
using MvvmSample.Core.Models;
1211
using MvvmSample.Core.Services;
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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 System;
6+
using System.ComponentModel.DataAnnotations;
7+
using System.Linq;
8+
using CommunityToolkit.Mvvm.ComponentModel;
9+
using CommunityToolkit.Mvvm.Input;
10+
using MvvmSample.Core.Services;
11+
12+
namespace MvvmSample.Core.ViewModels.Widgets;
13+
14+
/// <summary>
15+
/// A viewmodel for the validation widget.
16+
/// </summary>
17+
public partial class ValidationFormWidgetViewModel : ObservableValidator
18+
{
19+
private readonly IDialogService DialogService;
20+
21+
public ValidationFormWidgetViewModel(IDialogService dialogService)
22+
{
23+
DialogService = dialogService;
24+
}
25+
26+
public event EventHandler? FormSubmissionCompleted;
27+
public event EventHandler? FormSubmissionFailed;
28+
29+
[ObservableProperty]
30+
[Required]
31+
[MinLength(2)]
32+
[MaxLength(100)]
33+
private string? firstName;
34+
35+
[ObservableProperty]
36+
[Required]
37+
[MinLength(2)]
38+
[MaxLength(100)]
39+
private string? lastName;
40+
41+
[ObservableProperty]
42+
[Required]
43+
[EmailAddress]
44+
private string? email;
45+
46+
[ObservableProperty]
47+
[Required]
48+
[Phone]
49+
private string? phoneNumber;
50+
51+
[ICommand]
52+
private void Submit()
53+
{
54+
ValidateAllProperties();
55+
56+
if (HasErrors)
57+
{
58+
FormSubmissionFailed?.Invoke(this, EventArgs.Empty);
59+
}
60+
else
61+
{
62+
FormSubmissionCompleted?.Invoke(this, EventArgs.Empty);
63+
}
64+
}
65+
66+
[ICommand]
67+
private void ShowErrors()
68+
{
69+
string message = string.Join(Environment.NewLine, GetErrors().Select(e => e.ErrorMessage));
70+
71+
_ = DialogService.ShowMessageDialogAsync("Validation errors", message);
72+
}
73+
}

samples/MvvmSampleUwp/App.xaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
1212
<ResourceDictionary Source="Controls/InteractiveSample.xaml" />
1313
<ResourceDictionary Source="Controls/DocumentationBlock.xaml" />
14+
<ResourceDictionary Source="Controls/ValidationTextBox.xaml" />
1415
</ResourceDictionary.MergedDictionaries>
1516

1617
<!-- Misc resources -->

samples/MvvmSampleUwp/App.xaml.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ protected override void OnLaunched(LaunchActivatedEventArgs e)
4545
Ioc.Default.ConfigureServices(
4646
new ServiceCollection()
4747
//Services
48+
.AddSingleton<IDialogService, DialogService>()
4849
.AddSingleton<IFilesService, FilesService>()
4950
.AddSingleton<ISettingsService, SettingsService>()
5051
.AddSingleton(RestService.For<IRedditService>("https://www.reddit.com/"))
@@ -55,6 +56,8 @@ protected override void OnLaunched(LaunchActivatedEventArgs e)
5556
.AddTransient<IocPageViewModel>()
5657
.AddTransient<MessengerPageViewModel>()
5758
.AddTransient<ObservableObjectPageViewModel>()
59+
.AddTransient<ObservableValidatorPageViewModel>()
60+
.AddTransient<ValidationFormWidgetViewModel>()
5861
.AddTransient<RelayCommandPageViewModel>()
5962
.AddTransient<SamplePageViewModel>()
6063
.BuildServiceProvider());

samples/MvvmSampleUwp/Controls/DocumentationBlock.xaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls"
44
xmlns:local="using:MvvmSampleUwp.Controls">
55

6-
<!-- Operator button -->
76
<Style TargetType="local:DocumentationBlock">
87
<Setter Property="HorizontalAlignment" Value="Stretch" />
98
<Setter Property="HorizontalContentAlignment" Value="Stretch" />

samples/MvvmSampleUwp/Controls/InteractiveSample.xaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
xmlns:local="using:MvvmSampleUwp.Controls"
66
xmlns:muxc="using:Microsoft.UI.Xaml.Controls">
77

8-
<!-- Operator button -->
98
<Style TargetType="local:InteractiveSample">
109
<Setter Property="HorizontalAlignment" Value="Stretch" />
1110
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
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 System.ComponentModel;
6+
using System.ComponentModel.DataAnnotations;
7+
using System.Linq;
8+
using Windows.UI.Xaml;
9+
using Windows.UI.Xaml.Controls;
10+
11+
namespace MvvmSampleUwp.Controls;
12+
13+
/// <summary>
14+
/// A simple control that acts as a container for a documentation block.
15+
/// </summary>
16+
[TemplatePart(Name = "PART_TextBox", Type = typeof(TextBox))]
17+
[TemplatePart(Name = "PART_WarningIcon", Type = typeof(FontIcon))]
18+
public sealed class ValidationTextBox : ContentControl
19+
{
20+
/// <summary>
21+
/// The <see cref="TextBox"/> instance in use.
22+
/// </summary>
23+
private TextBox textBox;
24+
25+
/// <summary>
26+
/// The <see cref="MarkdownTextBlock"/> instance in use.
27+
/// </summary>
28+
private FontIcon warningIcon;
29+
30+
/// <summary>
31+
/// The previous data context in use.
32+
/// </summary>
33+
private INotifyDataErrorInfo oldDataContext;
34+
35+
public ValidationTextBox()
36+
{
37+
DataContextChanged += ValidationTextBox_DataContextChanged;
38+
}
39+
40+
/// <inheritdoc/>
41+
protected override void OnApplyTemplate()
42+
{
43+
base.OnApplyTemplate();
44+
45+
textBox = (TextBox)GetTemplateChild("PART_TextBox");
46+
warningIcon = (FontIcon)GetTemplateChild("PART_WarningIcon");
47+
48+
textBox.TextChanged += TextBox_TextChanged;
49+
}
50+
51+
/// <summary>
52+
/// Gets or sets the <see cref="string"/> representing the text to display.
53+
/// </summary>
54+
public string Text
55+
{
56+
get => (string)GetValue(TextProperty);
57+
set => SetValue(TextProperty, value);
58+
}
59+
60+
/// <summary>
61+
/// The <see cref="DependencyProperty"/> backing <see cref="Text"/>.
62+
/// </summary>
63+
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
64+
nameof(Text),
65+
typeof(string),
66+
typeof(ValidationTextBox),
67+
new PropertyMetadata(default(string)));
68+
69+
/// <summary>
70+
/// Gets or sets the <see cref="string"/> representing the header text to display.
71+
/// </summary>
72+
public string HeaderText
73+
{
74+
get => (string)GetValue(HeaderTextProperty);
75+
set => SetValue(HeaderTextProperty, value);
76+
}
77+
78+
/// <summary>
79+
/// The <see cref="DependencyProperty"/> backing <see cref="HeaderText"/>.
80+
/// </summary>
81+
public static readonly DependencyProperty HeaderTextProperty = DependencyProperty.Register(
82+
nameof(HeaderText),
83+
typeof(string),
84+
typeof(ValidationTextBox),
85+
new PropertyMetadata(default(string)));
86+
87+
/// <summary>
88+
/// Gets or sets the <see cref="string"/> representing the placeholder text to display.
89+
/// </summary>
90+
public string PlaceholderText
91+
{
92+
get => (string)GetValue(PlaceholderTextProperty);
93+
set => SetValue(PlaceholderTextProperty, value);
94+
}
95+
96+
/// <summary>
97+
/// The <see cref="DependencyProperty"/> backing <see cref="PlaceholderText"/>.
98+
/// </summary>
99+
public static readonly DependencyProperty PlaceholderTextProperty = DependencyProperty.Register(
100+
nameof(PlaceholderText),
101+
typeof(string),
102+
typeof(ValidationTextBox),
103+
new PropertyMetadata(default(string)));
104+
105+
/// <summary>
106+
/// Gets or sets the <see cref="string"/> representing the text to display.
107+
/// </summary>
108+
public string PropertyName
109+
{
110+
get => (string)GetValue(PropertyNameProperty);
111+
set => SetValue(PropertyNameProperty, value);
112+
}
113+
114+
/// <summary>
115+
/// The <see cref="DependencyProperty"/> backing <see cref="PropertyName"/>.
116+
/// </summary>
117+
public static readonly DependencyProperty PropertyNameProperty = DependencyProperty.Register(
118+
nameof(PropertyName),
119+
typeof(string),
120+
typeof(ValidationTextBox),
121+
new PropertyMetadata(PropertyNameProperty, OnPropertyNamePropertyChanged));
122+
123+
/// <summary>
124+
/// Invokes <see cref="RefreshErrors"/> whenever <see cref="PropertyName"/> changes.
125+
/// </summary>
126+
private static void OnPropertyNamePropertyChanged(object sender, DependencyPropertyChangedEventArgs args)
127+
{
128+
if (args.NewValue is not string { Length: > 0 } propertyName)
129+
{
130+
return;
131+
}
132+
133+
((ValidationTextBox)sender).RefreshErrors();
134+
}
135+
136+
/// <summary>
137+
/// Updates the bindings whenever the data context changes.
138+
/// </summary>
139+
private void ValidationTextBox_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
140+
{
141+
if (oldDataContext is not null)
142+
{
143+
oldDataContext.ErrorsChanged -= DataContext_ErrorsChanged;
144+
}
145+
146+
if (args.NewValue is INotifyDataErrorInfo dataContext)
147+
{
148+
oldDataContext = dataContext;
149+
150+
oldDataContext.ErrorsChanged += DataContext_ErrorsChanged;
151+
}
152+
153+
RefreshErrors();
154+
}
155+
156+
/// <summary>
157+
/// Invokes <see cref="RefreshErrors"/> whenever the data context requires it.
158+
/// </summary>
159+
private void DataContext_ErrorsChanged(object sender, DataErrorsChangedEventArgs e)
160+
{
161+
RefreshErrors();
162+
}
163+
164+
/// <summary>
165+
/// Updates <see cref="Text"/> when needed.
166+
/// </summary>
167+
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
168+
{
169+
Text = ((TextBox)sender).Text;
170+
}
171+
172+
/// <summary>
173+
/// Refreshes the errors currently displayed.
174+
/// </summary>
175+
private void RefreshErrors()
176+
{
177+
if (this.warningIcon is not FontIcon warningIcon ||
178+
PropertyName is not string propertyName ||
179+
DataContext is not INotifyDataErrorInfo dataContext)
180+
{
181+
return;
182+
}
183+
184+
ValidationResult result = dataContext.GetErrors(propertyName).OfType<ValidationResult>().FirstOrDefault();
185+
186+
warningIcon.Visibility = result is not null ? Visibility.Visible : Visibility.Collapsed;
187+
188+
if (result is not null)
189+
{
190+
ToolTipService.SetToolTip(warningIcon, result.ErrorMessage);
191+
}
192+
}
193+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:local="using:MvvmSampleUwp.Controls">
2+
3+
<Style TargetType="local:ValidationTextBox">
4+
<Setter Property="HorizontalAlignment" Value="Left" />
5+
<Setter Property="HorizontalContentAlignment" Value="Left" />
6+
<Setter Property="Template">
7+
<Setter.Value>
8+
<ControlTemplate TargetType="local:ValidationTextBox">
9+
<Grid ColumnSpacing="12">
10+
<Grid.ColumnDefinitions>
11+
<ColumnDefinition Width="560" />
12+
<ColumnDefinition Width="Auto" />
13+
</Grid.ColumnDefinitions>
14+
<TextBox
15+
Name="PART_TextBox"
16+
HorizontalAlignment="Stretch"
17+
Header="{TemplateBinding HeaderText}"
18+
IsSpellCheckEnabled="False"
19+
PlaceholderText="{TemplateBinding PlaceholderText}"
20+
Text="{TemplateBinding Text}" />
21+
<FontIcon
22+
Name="PART_WarningIcon"
23+
Grid.Column="1"
24+
Margin="0,32,0,0"
25+
VerticalAlignment="Center"
26+
FontSize="18"
27+
Foreground="Orange"
28+
Glyph="&#xE814;"
29+
Visibility="Collapsed" />
30+
</Grid>
31+
</ControlTemplate>
32+
</Setter.Value>
33+
</Setter>
34+
</Style>
35+
36+
</ResourceDictionary>

0 commit comments

Comments
 (0)