WPF - Validation ValidationRule và Vấn Đề Về Thông Báo Lỗi Trực Quan Phần I


Bài viết này cho ta thấy làm thế nào để có thể sử dụng một ErrorTemplate và kích hoạt một Style để cung cấp thông tin phản hồi trực quan nhằm thông báo cho người dùng khi một giá trị không hợp lệ được nhập vào, dựa trên một quy tắc xác nhận nào đó.




Chúng ta sẽ làm một ví dụ như sau: Tôi có một lớp Persional chứa thông tin một người nào đó với hai thuộc tính là NameYearOld, tôi sẽ Binding dữ liệu vào hai TextBox. Yêu cầu khi người dùng nhập tên và tuổi vào hai TextBox kia là tên không nhỏ hơn 2 kí tự hoặc rỗng, tuổi phải lớn hơn >= 18. Nếu nhập sai sẽ có ToolTip thông báo lỗi và chuyển màu TexBox.


Tôi tiến hành tạo Style cho TextBox với những yêu cầu sau:


- Có phần Header với dữ liệu được BinDing từ Tag ( là một property) của TextBox.


- Màu nền của Header trùng với màu viền của TextBox (mặc định là Blue)


- Trigger: Nếu sảy ra một Validation.HasError (=True) thì màu nền Header và viền TextBox chuyển sang Red và có sẽ có ToolTip thông báo lỗi cụ thể.


Tôi đặc style này vào phần Window.Resources như sau.




[code language="xml"]

<Window.Resources>
<SolidColorBrush x:Key="TextBox.Static.Border" Color="#FFABAdB3"/>
<SolidColorBrush x:Key="TextBox.MouseOver.Border" Color="#FF7EB4EA"/>
<SolidColorBrush x:Key="TextBox.Focus.Border" Color="#FF569DE5"/>
<Style x:Key="TxtStyleValidation" TargetType="{x:Type TextBox}">
<Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
<Setter Property="BorderBrush" Value="{StaticResource TextBox.Static.Border}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="KeyboardNavigation.TabNavigation" Value="None"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="AllowDrop" Value="true"/>
<Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst"/>
<Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid MinWidth="100" x:Name="grdHeader" Background="{TemplateBinding BorderBrush}">
<Label x:Name="lbHeader" Content="{TemplateBinding Tag}" Padding="5,0,5,2" VerticalContentAlignment="Center" Foreground="White" ></Label>
</Grid>
<ScrollViewer x:Name="PART_ContentHost" Focusable="false" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden" Grid.Column="1" VerticalContentAlignment="Center" Padding="5,2,2,1"/>

</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="BorderBrush" TargetName="border" Value="{StaticResource TextBox.MouseOver.Border}"/>
</Trigger>
<Trigger Property="IsKeyboardFocused" Value="true">
<Setter Property="BorderBrush" TargetName="border" Value="{StaticResource TextBox.Focus.Border}"/>
</Trigger>

<!-- Phần chính-->
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
<Setter Property="Background" TargetName="grdHeader" Value="Red"/>
<Setter Property="BorderBrush" TargetName="border" Value="Red"/>
</Trigger>
<!-- Phần chính-->

</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsInactiveSelectionHighlightEnabled" Value="true"/>
<Condition Property="IsSelectionActive" Value="false"/>
</MultiTrigger.Conditions>
<Setter Property="SelectionBrush" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightBrushKey}}"/>
</MultiTrigger>
</Style.Triggers>
</Style>
</Window.Resources>

[/code]

Và Phần thể hiện nội dung chính được thiết kế như sau:




[code language="xml"]
<Window x:Class="Validation.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">

<!--Chèn phần style trên vào đây -->

<StackPanel x:Name="Stack" Margin="10">
<TextBox x:Name="txtName" Height="23" TextWrapping="Wrap"
Text="{Binding Name,UpdateSourceTrigger=PropertyChanged,ValidatesOnDataErrors=True}" Tag="My Name"
VerticalAlignment="Top" Padding="5,1,2,1" Style="{DynamicResource TxtStyleValidation}"
BorderBrush="#FF0630AE" Margin="0,3,0,0"/>
<TextBox x:Name="txtYear" Height="23" TextWrapping="Wrap"
Text="{Binding YearOld, UpdateSourceTrigger=PropertyChanged,ValidatesOnDataErrors=True}" Tag="Year-Old"
VerticalAlignment="Top" Padding="5,1,2,1" Style="{DynamicResource TxtStyleValidation}"
BorderBrush="#FF0630AE" Margin="0,3,0,0"/>
</StackPanel>
</Window>
[/code]

Kết quả cho style




[caption id="" align="aligncenter" width="525"]tuanphamdg Phạm Tuân[/caption]

Chúng ta đến phần quan trọng nhất là khởi tạo lớp Persional


Chúng ta tạo lớp Persional và phải kế thừa từ hai interface sau IDataErrorInfo INotifyPropertyChanged. Ta có hình hài lớp này như sau (Tham khảo INotifyPropertyChanged để hiểu hơn về method OnPropertyChanged)




[code language="csharp"]
class Personal : IDataErrorInfo,INotifyPropertyChanged
{
private string _name;
private int _yearold;

public int YearOld
{
set
{
if (value != _yearold)
{
_yearold = value;
}
}
get { return _yearold; }
}
public string Name
{
set
{
value = value.Trim();
if (value !=_name)
{
_name = value;
}
}
get { return _name; }
}

// IDataErrorInfo
public string Error
{
get { throw new NotImplementedException(); }
}

public string this[string columnName]
{
get { throw new NotImplementedException(); }
}

//INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string property){}
}
[/code]

Hoàn thành lớp Persional như sau




[code language="csharp"]
class Personal:IDataErrorInfo,INotifyPropertyChanged
{
private string _name;
private int _yearold;

public int YearOld
{
set
{
if (value != _yearold)
{
_yearold = value;
OnPropertyChanged("YearOld");
}
}
get { return _yearold; }
}
public string Name
{
set
{
value = value.Trim();
if (value !=_name)
{
_name = value;
OnPropertyChanged("Name");
}
}
get { return _name; }
}

//IDataErrorInfo
public string Error
{
get { throw new NotImplementedException(); }
}

public string this[string columnName]
{
get
{
string result = String.Empty;
switch (columnName)
{
case "Name":
if (String.IsNullOrEmpty(Name) || Name.Length < 2)
{
result = "Lỗi nhập liệu: tên không chính xác";
}
break;
case "YearOld":
if (YearOld < 18)
{
result = "Lỗi nhập liệu: Tuổi quá nhỏ";
}
break;
}

return result;
}
}

// INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;

public void OnPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged.Invoke(this,new PropertyChangedEventArgs(property));
}
}
}
[/code]

Hoàn thành phần View và ViewModel xong ta tiến hành một bước đơn giản sau trước khi chạy thử ứng dụng

[code language="csharp"]
public MainWindow()
{
InitializeComponent();

Stack.DataContext = new Personal() { Name = "Phạm Tuân", YearOld = 21 };
}
[/code]

Kết quả như sau




[caption id="" align="aligncenter" width="525"]tuanphamdg Phạm Tuân[/caption]

Các bạn có thể tham khảo SourceCode tại đây, Phần tiếp theo nói về ValidationRule.


Chúc các bạn thành công!
Tuân Phạm


Chúc các bạn thành công!
PHẠM TUÂN