Dear visitor, thanks for stopping by! If you want, you can follow all updates on Snowball.be via RSS. You can also follow me on Twitter or Facebook. More interesting posts from other Microsoft Regional Directors can be found at The Region.
Gill Cleeren     .net | Silverlight | Silverlight Advent Calendar | sl4     December 20, 2009    

Next to the full support of the data binding engine for the IDataErrorInfo interface, Silverlight 4 adds a new interface called INotifyDataErrorInfo, offering more options for particular scenario's. In this post, we’ll look at a small example using this interface.

IDataErrorInfo allows retrieving the error on a per-property basis. It’s not possible however to validate all properties of the entity in one go. This becomes possible with the INotifyDataErrorInfo. Let’s look at the interface first.

public interface INotifyDataErrorInfo
{
    bool HasErrors { get; }
 
    event EventHandler ErrorsChanged;
 
    IEnumerable GetErrors(
                    string propertyName);
}

This interface has some nice advantages. As said, we can check if the entity as a whole is in an invalid state through the HasErrors property. Retrieving the errors using the GetErrors can now retrieve other things than just strings which was all that we could do with the IDataErrorInfo. On top of that, a property can have more than just one validation error at the same time

The ErrorsChanged event can come in handy if there’s a long running process needed to perform validation: assume that for some validation, we need to go to the database over a service. The ErrorsChanged event allows us to notify the UI if the validation errors change. If ValidatesOnNotifyDataErrors is set to true on the UI, Silverlight will listen for the ErrorsChanged event and will display any errors if they are added afterwards.

That’s it for this interface, let’s now look at it in an example. I have changed the example created for the IDataErrorInfo to work with INotifyDataErrorInfo, so the interface has remained the same.

The errors I want to return to my interface are more than just strings. I created a custom error as follows:

public class ChristmasSongError
{
    public ErrorLevel Severity{ get; set; }
    public string ErrorName { get; set; }
    public string ErrorMessage { get; set; }
 
    public override string ToString()
    {
        return ErrorName + ": " + ErrorMessage;
    }
}

The ChristmasSong class now implements the INotifyDataErrorInfo interface. Let’s focus on the Title property. When its value is set, it calls a validation method. In this method, I create an instance of the custom error, specifying all the property values. If there’s an error, here the title being empty, I add it to a Dictionary called errors using the AddError method. If validation is satisfied, it’s removed using the RemoveError method. The errrors Dictionary is used so it is possible for each property to have more than one error: each property has as its value a List.

public class ChristmasSong : INotifyDataErrorInfo
{
    private string title;
    private string performedBy;
    private TimeSpan duration;
    private DateTime published;
 
    private string error;
 
    public Dictionary<string, List> errors = 
      new Dictionary<string,List>();
 
    public event PropertyChangedEventHandler PropertyChanged;
 
    protected void NotifyPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(
                    this, new PropertyChangedEventArgs(propertyName));
    }
 
    public string Title
    {
        get
        {
            return title;
        }
        set
        {
            title = 
                    value;
            ValidateTitleProperty();
        }
    }
 
    private void ValidateTitleProperty()
    {
        ChristmasSongError christmasSongError =
new ChristmasSongError() 
        { Severity = ErrorLevel.Error, ErrorName =
"TitleRequired", 
          ErrorMessage = 
                    "Title should not be empty" };
        if (string.IsNullOrEmpty(title))
        {
            AddError(
                    "Title", christmasSongError);
        }
        else
        {
            RemoveError(
                    "Title", "TitleRequired");
        }
    }
 
    public string PerformedBy
    {
        get
        {
            return performedBy;
        }
        set
        {
            performedBy = 
                    value;
            ValidatePerformedByProperty();
        }
    }
 
    private void ValidatePerformedByProperty()
    {
        ChristmasSongError christmasSongError =
new ChristmasSongError() 
        { Severity = ErrorLevel.Error, ErrorName =
"PerformedByRequired", 
          ErrorMessage = 
                    "The artist should not be empty" };
        if (string.IsNullOrEmpty(performedBy))
        {
            AddError(
                    "PerformedBy", christmasSongError);
        }
        else
        {
            RemoveError(
                    "PerformedBy", "PerformedByRequired");
        }
    }
 
    public TimeSpan Duration
    {
        get
        {
            return duration;
        }
        set
        {
            duration = 
                    value;
            ValidateDuration();
        }
    }
 
    private void ValidateDuration()
    {
        ChristmasSongError durationNullError =
new ChristmasSongError() 
        { Severity = ErrorLevel.CriticalError, ErrorName =
"DurationNull", 
          ErrorMessage = 
                    "The duration should not be empty" };
        ChristmasSongError durationTooLongError =
new ChristmasSongError() 
        { Severity = ErrorLevel.Error, ErrorName =
"DurationTooLong", 
          ErrorMessage = 
                    "The duration is too long" };
        
        if (duration == TimeSpan.Zero)
        {
            AddError(
                    "Duration", durationNullError);
        }
        else
        {
            RemoveError(
                    "Duration", "DurationNull");
        }
 
        if (duration.TotalSeconds > 500)
        {
            AddError(
                    "Duration", durationTooLongError);
        }
        else
        {
            RemoveError(
                    "Duration", "DurationTooLong");
        }
    }
 
    public DateTime Published
    {
        get
        {
            return published;
        }
        set
        {
            published = 
                    value;
            VaidatePublished();
        }
    }
 
    private void VaidatePublished()
    {
        ChristmasSongError publishedTooLow =
new ChristmasSongError() 
        { Severity = ErrorLevel.CriticalError, ErrorName =
"PublishedTooLow", 
          ErrorMessage = 
                    "The date is too small" };
        ChristmasSongError publishedTooHigh =
new ChristmasSongError() 
        { Severity = ErrorLevel.CriticalError, ErrorName =
"PublishedTooHigh", 
          ErrorMessage = 
                    "The date is too high" };
 
        if (published < new DateTime(1900, 1, 1))
        {
            AddError(
                    "Published", publishedTooLow);
        }
        else
        {
            RemoveError(
                    "Published", "PublishedTooLow");
        }
 
        if (published > new DateTime( 2010, 1, 1))
        {
            AddError(
                    "Published", publishedTooHigh);
        }
        else
        {
            RemoveError(
                    "Published", "PublishedTooHigh");
        }
    }
 
    private void AddError(string propertyName, ChristmasSongError error)
    {
        
 
        if (!errors.ContainsKey(propertyName))
        {
            errors.Add(propertyName, 
                    new List() { error });
        }
        else// adding the error to the already existing list
        {
            var list = errors[propertyName];
            list.Add(error);
        }
 
        if (ErrorsChanged != null)
            ErrorsChanged(
                    this, new DataErrorsChangedEventArgs(propertyName));
    }
 
    private void RemoveError(string propertyName, string errorName)
    {
        if (errors.ContainsKey(propertyName))
        {
            var christmasSongError = errors[propertyName]
                .Where
(e => e.ErrorName == errorName).FirstOrDefault();
            var list = errors[propertyName];
            list.Remove(christmasSongError);
 
            if (list.Count == 0)//no more errors for this property 
            {
                errors.Remove(propertyName);
            }
 
            if (ErrorsChanged != null)
                ErrorsChanged(
                    this, new DataErrorsChangedEventArgs(propertyName));
        }
    }
 
    public event EventHandler ErrorsChanged;
 
    public System.Collections.IEnumerable GetErrors(string propertyName)
    {
        if (string.IsNullOrEmpty(propertyName))//retrieve
                        errors for entire entity
        {
            return errors.Values;
        }
        else
        {
            if (errors.ContainsKey(propertyName))
                return errors[propertyName];
            return null;
        }
    }
 
    public bool HasErrors
    {
        get
        {
            if (errors.Count == 0)
                return false;
            return true;
        }
    }
}

With the entity in place, we can look at the UI. The XAML code is similar to the IDataErrorInfo example, although we should now use the ValidatesOnNotifyDataErrors and set it to True. Below is part of the code, for the entire listing, see the download.

<TextBox x:Name="TitleTextBox" Grid.Row="1" Grid.Column="1" Width="200" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="2"

Text="{Binding Path=Title,Mode=TwoWay,ValidatesOnNotifyDataErrors=True,NotifyOnValidationError=True}" />

The complete sample can be downloaded here: SLINotifyDataErrorInfo.zip (602.56 KB)

  Posted on: Monday, December 21, 2009 12:42:27 AM (Romance Standard Time, UTC+01:00)   |   Comments [0]
         
9/2/2010   9:13:22 PM