Reputation: 3766
I have a class as follows:
public class Guardian : ModelBase, IDataErrorInfo
{
internal Guardian()
{
}
[Required]
[StringLength(50)]
[Display(Name = "Guardian's First Name")]
public string FirstName
{
get { return GetValue(() => FirstName); }
set { SetValue(() => FirstName, value); }
}
[Required]
[StringLength(50)]
[Display(Name = "Guardian's Last Name")]
public string LastName
{
get { return GetValue(() => LastName); }
set { SetValue(() => LastName, value); }
}
[USPhoneNumber]
[Display(Name = "Home Phone Number")]
public string HomePhone
{
get { return GetValue(() => HomePhone); }
set { SetValue(() => HomePhone, value.NormalizeNANPPhoneNumber()); }
}
[USPhoneNumber]
[Display(Name = "Personal Cell")]
public string PersonalCell
{
get { return GetValue(() => PersonalCell); }
set { SetValue(() => PersonalCell, value.NormalizeNANPPhoneNumber()); }
}
[Required]
[StringLength(100)]
[Display(Name = "Address")]
public string Address1
{
get { return GetValue(() => Address1); }
set { SetValue(() => Address1, value); }
}
[StringLength(100)]
public string Address2
{
get { return GetValue(() => Address2); }
set { SetValue(() => Address2, value); }
}
[Required]
[StringLength(100)]
public string City
{
get { return GetValue(() => City); }
set { SetValue(() => City, value); }
}
[Required]
[StringLength(100)]
public string State
{
get { return GetValue(() => State); }
set { SetValue(() => State, value); }
}
[Required]
[StringLength(20)]
[USPostalCode]
[Display(Name = "ZIP Code")]
public string Zip
{
get { return GetValue(() => Zip); }
set { SetValue(() => Zip, value); }
}
[Required]
[Display(Name = "Relationship to Children")]
public FamilyRole Relationship
{
get { return GetValue(() => Relationship); }
set { SetValue(() => Relationship, value); }
}
internal bool IsEmpty()
{
return
string.IsNullOrWhiteSpace(FirstName)
&& string.IsNullOrWhiteSpace(LastName)
&& string.IsNullOrWhiteSpace(HomePhone)
&& string.IsNullOrWhiteSpace(PersonalCell)
&& string.IsNullOrWhiteSpace(Address1)
&& string.IsNullOrWhiteSpace(Address2)
&& string.IsNullOrWhiteSpace(City)
&& string.IsNullOrWhiteSpace(State)
&& string.IsNullOrWhiteSpace(Zip)
&& Relationship == null
;
}
/// <summary>
/// Provides support for cross-cutting concerns without having to write
/// an attribute in Silverlight.
/// When time allows, convert to an Attribute. The code produced then
/// can be reused in other projects.
/// </summary>
/// <param name="listToAddTo"></param>
private void CustomValidation(List<ValidationResult> listToAddTo)
{
if (listToAddTo == null)
throw new ArgumentNullException("listToAddTo");
if (string.IsNullOrWhiteSpace(HomePhone) && string.IsNullOrWhiteSpace(PersonalCell))
listToAddTo.Add(new ValidationResult("At least one phone number must be filled in.", new string[] { "HomePhone" }));
}
#region IDataErrorInfo Members
public string Error
{
get
{
List<ValidationResult> results = new List<ValidationResult>();
this.IsValidObject(results);
CustomValidation(results);
if (results.Count > 0)
return results[0].ErrorMessage;
else
return null;
}
}
public string this[string columnName]
{
get
{
List<ValidationResult> results = new List<ValidationResult>();
this.IsValidObject(results);
CustomValidation(results);
var resultByColumn = results.Where(r => r.MemberNames.Contains(columnName)).ToList();
if (resultByColumn.Count > 0)
return resultByColumn[0].ErrorMessage;
else
return null;
}
}
#endregion
}
I implement IDataErrorInfo for this class. Everything works fine, my problem is really an annoyance, but one big enough that the Guy Who Pays The Bills says needs to be fixed. I have a separate void that does extra validation, which is called by the IDataErrorInfo members. It checks to see if at least one phone number is filled in.
An instance of this class is on my model, called CurrentGuardian, and the model is the DataContext for the following popup:
<controls:ChildWindow xmlns:my="clr-namespace:Microsoft.Windows.Controls"
x:Class="Tracktion.Controls.CheckInWindows.AddGuardian"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"
Width="575" Height="326"
Title="Add Parent/Guardian" HasCloseButton="False"
xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk">
<controls:ChildWindow.Resources>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="Black" />
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontSize" Value="12" />
</Style>
</controls:ChildWindow.Resources>
<Grid x:Name="LayoutRoot" Margin="2">
<Grid.RowDefinitions>
<RowDefinition Height="44" />
<RowDefinition Height="215*" />
<RowDefinition Height="45" />
</Grid.RowDefinitions>
<TextBlock Height="23" Name="textBlock1" Text="Please fill out the form below. Fields marked with an asterisk are required." VerticalAlignment="Top" TextAlignment="Center" />
<TextBlock Height="23" Margin="0,21,0,0" Name="textBlock2" Text="When done, click Add Another Guardian or Continue Adding Children below." VerticalAlignment="Top" TextAlignment="Center" />
<sdk:Label Grid.Row="1" Height="22" HorizontalAlignment="Left" Margin="0,11,0,0" Name="label1" VerticalAlignment="Top" Width="142" Content="* Guardian's First Name:" />
<sdk:Label Content="* Guardian's Last Name:" Height="22" HorizontalAlignment="Left" Margin="0,46,0,0" Name="label2" VerticalAlignment="Top" Width="142" Grid.Row="1" />
<sdk:Label Grid.Row="1" Height="28" HorizontalAlignment="Left" Margin="0,81,0,0" Name="label3" VerticalAlignment="Top" Width="142" Content="* Home Phone:" />
<sdk:Label Grid.Row="1" Height="28" HorizontalAlignment="Left" Margin="0,116,0,0" Name="label4" VerticalAlignment="Top" Width="120" Content="* Personal Cell:" />
<sdk:Label Grid.Row="1" Height="28" HorizontalAlignment="Left" Margin="302,11,0,0" Name="label5" VerticalAlignment="Top" Width="76" Content="* Address:" />
<sdk:Label Content="* What is your relationship to the child or children?" Height="23" HorizontalAlignment="Left" Margin="0,155,0,0" Name="label6" VerticalAlignment="Top" Width="360" Grid.Row="1" />
<TextBox Text="{Binding Path=CurrentGuardian.FirstName, Mode=TwoWay, ValidatesOnDataErrors=True}" Grid.Row="1" Height="28" HorizontalAlignment="Left" Margin="148,5,0,0" Name="textBox1" VerticalAlignment="Top" Width="135" />
<TextBox Text="{Binding Path=CurrentGuardian.LastName, Mode=TwoWay, ValidatesOnDataErrors=True}" Grid.Row="1" Height="28" HorizontalAlignment="Left" Margin="148,40,0,0" Name="textBox2" VerticalAlignment="Top" Width="135" />
<TextBox Text="{Binding Path=CurrentGuardian.HomePhone, Mode=TwoWay, ValidatesOnDataErrors=True}" Grid.Row="1" Height="28" HorizontalAlignment="Left" Margin="148,75,0,0" Name="txtHomePhone" VerticalAlignment="Top" Width="135" LostFocus="PhoneNumber_LostFocus" />
<TextBox Text="{Binding Path=CurrentGuardian.PersonalCell, Mode=TwoWay, ValidatesOnDataErrors=True}" Grid.Row="1" Height="28" HorizontalAlignment="Left" Margin="148,110,0,0" Name="txtCellPhone" VerticalAlignment="Top" Width="135" LostFocus="PhoneNumber_LostFocus" />
<my:WatermarkedTextBox Text="{Binding Path=CurrentGuardian.Address1, Mode=TwoWay, ValidatesOnDataErrors=True}" Grid.Row="1" Height="28" HorizontalAlignment="Left" Margin="366,5,0,0" x:Name="textBox5" VerticalAlignment="Top" Width="184" Watermark="Line 1" />
<my:WatermarkedTextBox Text="{Binding Path=CurrentGuardian.Address2, Mode=TwoWay, ValidatesOnDataErrors=True}" Grid.Row="1" Height="28" HorizontalAlignment="Left" Margin="366,40,0,0" x:Name="textBox6" VerticalAlignment="Top" Width="184" Watermark="Line 2" />
<my:WatermarkedTextBox Text="{Binding Path=CurrentGuardian.City, Mode=TwoWay, ValidatesOnDataErrors=True}" Grid.Row="1" Height="28" HorizontalAlignment="Left" Margin="366,75,0,0" x:Name="textBox7" VerticalAlignment="Top" Width="184" Watermark="City" />
<ComboBox ItemsSource="{Binding Path=States}" DisplayMemberPath="Abbreviation" SelectedValuePath="Abbreviation" SelectedValue="{Binding Path=CurrentGuardian.State, Mode=TwoWay, ValidatesOnDataErrors=True}" Grid.Row="1" Height="28" HorizontalAlignment="Left" Margin="366,110,0,0" Name="comboBox1" VerticalAlignment="Top" Width="88" />
<my:WatermarkedTextBox Text="{Binding Path=CurrentGuardian.Zip, Mode=TwoWay, ValidatesOnDataErrors=True}" Grid.Row="1" Height="28" HorizontalAlignment="Left" Margin="460,110,0,0" x:Name="textBox8" VerticalAlignment="Top" Width="90" Watermark="ZIP" />
<ComboBox DisplayMemberPath="Name" Height="28" HorizontalAlignment="Left" ItemsSource="{Binding Path=Relationships}" Margin="302,149,0,0" Name="comboBox2" SelectedItem="{Binding Path=CurrentGuardian.Relationship, Mode=TwoWay, ValidatesOnDataErrors=True}" VerticalAlignment="Top" Width="249" Grid.Row="1" />
<Button Content="Cancel" Grid.Row="2" Height="37" HorizontalAlignment="Left" Margin="0,8,0,0" Name="btnCancel" VerticalAlignment="Top" Width="93" Style="{StaticResource RedButton}" Click="btnCancel_Click" />
<StackPanel Orientation="Horizontal" Grid.Row="2" HorizontalAlignment="Right">
<Button Content="Add Another Guardian" Height="37" Margin="5,8,0,0" Name="btnAddGuardian" VerticalAlignment="Top" Style="{StaticResource OrangeButton}" HorizontalAlignment="Right" Width="159" Click="btnAddGuardian_Click" />
<Button Content="Continue" Height="37" Margin="5,8,0,0" Name="btnContinue" VerticalAlignment="Top" Style="{StaticResource GreenButton}" HorizontalAlignment="Right" Padding="20,0" Click="btnContinue_Click" />
<!-- TODO: Visibility set when accessing this screen through check in screen. -->
<Button Content="Check In" Margin="5,8,0,0" Name="btnCheckIn" Visibility="Collapsed" Style="{StaticResource GreenButton}" Click="btnCheckIn_Click" />
</StackPanel>
</Grid>
One phone number is required. You can enter both, but at least one is required. When the form binds to the empty CurrentGuardian, the first phone number field, Home Phone, is highlighted in red. It stays red after focusing and blurring that field, when both numbers have no data. If I enter a phone number, the field turns to black. Deleting the number turns it red. So far, so good - this is expected behavior. Now, if I do not enter a number for Home Phone, but then I enter a phone number for personal cell, when I tab off Personal Cell the Home Phone number stays highlighted in red until I tab to it. Once I tab to it, the red outline disappears. How do I make the field validate? I currently have the following code in the blur event for both fields:
private void PhoneNumber_LostFocus(object sender, RoutedEventArgs e)
{
KioskCheckIn2 model = (KioskCheckIn2)this.DataContext;
model.CurrentGuardian.IsValidObject(); // revalidate
txtHomePhone.GetBindingExpression(TextBox.TextProperty).UpdateSource();
txtCellPhone.GetBindingExpression(TextBox.TextProperty).UpdateSource();
}
I have the ValidationResult for checking both numbers only returning the HomePhone field as the offending control since I could not figure out how to force re-validation of the controls so both red outlines would disappear.
Thanks in advance!
Upvotes: 0
Views: 1046
Reputation: 3766
Implementing INotifyDataErrorInfo, rather than IDataErrorInfo, seemed to do the trick. IDataErrorInfo is nice but lacks the ability to let the UI know when other properties have changed besides the current one. I tried allowing both implementations but it got a bit weird, so I changed it so it only implemented INotifyDataErrorInfo.
public class Guardian : ModelBase, /*IDataErrorInfo,*/ INotifyDataErrorInfo
{
internal Guardian()
{
}
[Required]
[StringLength(50)]
[Display(Name = "Guardian's First Name")]
public string FirstName
{
get { return GetValue(() => FirstName); }
set { SetValue(() => FirstName, value); }
}
[Required]
[StringLength(50)]
[Display(Name = "Guardian's Last Name")]
public string LastName
{
get { return GetValue(() => LastName); }
set { SetValue(() => LastName, value); }
}
[USPhoneNumber]
[Display(Name = "Home Phone Number")]
public string HomePhone
{
get { return GetValue(() => HomePhone); }
set { SetValue(() => HomePhone, value.NormalizeNANPPhoneNumber()); }
}
[USPhoneNumber]
[Display(Name = "Personal Cell")]
public string PersonalCell
{
get { return GetValue(() => PersonalCell); }
set { SetValue(() => PersonalCell, value.NormalizeNANPPhoneNumber()); }
}
[Required]
[StringLength(100)]
[Display(Name = "Address")]
public string Address1
{
get { return GetValue(() => Address1); }
set { SetValue(() => Address1, value); }
}
[StringLength(100)]
public string Address2
{
get { return GetValue(() => Address2); }
set { SetValue(() => Address2, value); }
}
[Required]
[StringLength(100)]
public string City
{
get { return GetValue(() => City); }
set { SetValue(() => City, value); }
}
[Required]
[StringLength(100)]
public string State
{
get { return GetValue(() => State); }
set { SetValue(() => State, value); }
}
[Required]
[StringLength(20)]
[USPostalCode]
[Display(Name = "ZIP Code")]
public string Zip
{
get { return GetValue(() => Zip); }
set { SetValue(() => Zip, value); }
}
[Required]
[Display(Name = "Relationship to Children")]
public FamilyRole Relationship
{
get { return GetValue(() => Relationship); }
set { SetValue(() => Relationship, value); }
}
internal bool IsEmpty()
{
return
string.IsNullOrWhiteSpace(FirstName)
&& string.IsNullOrWhiteSpace(LastName)
&& string.IsNullOrWhiteSpace(HomePhone)
&& string.IsNullOrWhiteSpace(PersonalCell)
&& string.IsNullOrWhiteSpace(Address1)
&& string.IsNullOrWhiteSpace(Address2)
&& string.IsNullOrWhiteSpace(City)
&& string.IsNullOrWhiteSpace(State)
&& string.IsNullOrWhiteSpace(Zip)
&& Relationship == null
;
}
protected override void PropertyHasChanged(string propertyName)
{
base.PropertyHasChanged(propertyName);
if (ErrorsChanged != null)
this.GetType().GetProperties().ToList().ForEach(p => ErrorsChanged(this, new DataErrorsChangedEventArgs(p.Name)));
}
/// <summary>
/// Provides support for cross-cutting concerns without having to write
/// an attribute in Silverlight.
/// When time allows, convert to an Attribute. The code produced then
/// can be reused in other projects.
/// </summary>
/// <param name="listToAddTo"></param>
private void CustomValidation(List<ValidationResult> listToAddTo)
{
if (listToAddTo == null)
throw new ArgumentNullException("listToAddTo");
if (string.IsNullOrWhiteSpace(HomePhone) && string.IsNullOrWhiteSpace(PersonalCell))
listToAddTo.Add(new ValidationResult("At least one phone number must be filled in.", new string[] { "HomePhone", "PersonalCell" }));
}
List<ValidationResult> getErrorList()
{
List<ValidationResult> results = new List<ValidationResult>();
this.IsValidObject(results);
CustomValidation(results);
return results;
}
/*
#region IDataErrorInfo Members
public string Error
{
get
{
List<ValidationResult> results = getErrorList();
if (results.Count > 0)
return results[0].ErrorMessage;
else
return null;
}
}
public string this[string columnName]
{
get
{
List<ValidationResult> results = getErrorList();
var resultByColumn = results.Where(r => r.MemberNames.Contains(columnName)).ToList();
if (resultByColumn.Count > 0)
return resultByColumn[0].ErrorMessage;
else
return null;
}
}
#endregion
*/
#region INotifyDataErrorInfo Members
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public System.Collections.IEnumerable GetErrors(string propertyName)
{
List<ValidationResult> results = getErrorList();
return results.Where(e => e.MemberNames.Contains(propertyName));
}
public bool HasErrors
{
get
{
List<ValidationResult> results = getErrorList();
return results.Count > 0;
}
}
#endregion
}
Now, when I first go to the screen, both phone fields are highlighted. Entering a valid phone number in one field makes both phone number fields pass validation and the red line surrounding each disappears. Note that I put the second field back in under CustomValidation,
listToAddTo.Add(new ValidationResult("At least one phone number must be filled in.", new string[] { "HomePhone", "PersonalCell" }));
...so that both properties can show the red lines around them if the input is not in a correct format.
Here's some code I made to handle validation for both IDataErrorInfo and INotifyDataErrorInfo. You'll have to rework it a bit to fit your base class (if you have one) but I hope this helps someone out:
namespace CLARIA.Infrastructure
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
/// <summary>
/// Builds upon ModelBase with built-in validation.
/// </summary>
public abstract class ValidatableModelBase : ModelBase,
#if SILVERLIGHT
INotifyDataErrorInfo
#else
IDataErrorInfo
#endif
{
private List<ValidationResult> GetErrorList()
{
List<ValidationResult> results = new List<ValidationResult>();
this.IsValidObject(results);
CustomValidation(results);
return results;
}
/// <summary>
/// Allows the derived class to override and add custom validation.
/// The validation results generated from this method should be added
/// to the collection <see cref="addResultsToThisList"/>.
/// </summary>
/// <param name="addResultsToThisList"></param>
protected virtual void CustomValidation(List<ValidationResult> addResultsToThisList) {}
#if SILVERLIGHT
#region INotifyDataErrorInfo Members
protected override void PropertyHasChanged(string propertyName)
{
base.PropertyHasChanged(propertyName);
// Force re-validation of every property.
if (ErrorsChanged != null)
this.GetType().GetProperties().ToList().ForEach(p => ErrorsChanged(this, new DataErrorsChangedEventArgs(p.Name)));
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public System.Collections.IEnumerable GetErrors(string propertyName)
{
List<ValidationResult> results = GetErrorList();
return results.Where(e => e.MemberNames.Contains(propertyName));
}
public bool HasErrors
{
get
{
List<ValidationResult> results = GetErrorList();
return results.Count > 0;
}
}
#endregion
#else
#region IDataErrorInfo Members
public string Error
{
get
{
List<ValidationResult> results = GetErrorList();
if (results.Count > 0)
return results[0].ErrorMessage;
else
return null;
}
}
public string this[string columnName]
{
get
{
List<ValidationResult> results = GetErrorList();
var resultByColumn = results.Where(r => r.MemberNames.Contains(columnName)).ToList();
if (resultByColumn.Count > 0)
return resultByColumn[0].ErrorMessage;
else
return null;
}
}
#endregion
#endif
}
}
Upvotes: 1
Reputation: 11287
Try using the DataForm
not inside a ChildWindow
. This is known to be buggy. You can also try to apply these fixes. Maybe things have improved for Silverlight 5, I haven't checked that yet.
Generally, the DataForm
control is in the Toolkit's "Preview" quality band and the ChildWindow
isn't perfect either, so you can expect bugs in certain scenarios. You also have the source code though to make further fixes. ;)
Upvotes: 1