Geeks With Blogs
Malisa Ncube - .NET Delights .NET Development ideas and things
MethodValidatorsAndConverters

1. Introduction

This is a follow-up to the "An attribute based approach to business object validation" article in which i introduced the use attributes and reflection to validate business objects. I also promised to write another article on data converters and method based validators, and here it is.

2. Background

In this article i will use an example to show how you can validate your business object using a method based attribute and how you can convert data assigned to properties when it is saved and re-convert it when the business object is read. All this will be based on attributes.

Sometimes a need arises, to store data differently from the way it is viewed. For example if you have a field which would have comments you may want to compress them on storage. This will certainly be good for your bandwidth - especially for us here in Africa.

3. The Business object

In this example lets use a Candidate object. I'm assuming a company considering interviewees.

I would like you to look closely at the attributes that we have used to decorate the object below. The attributes will enable validation of the business object much easier than having to write some instructions on the presentation layer. Some attributes below will help in converting the data so that it is stored differently from the way it is viewed.

    public class Candidate : EntityObject
{

#region Constructors

public Candidate(DataContext dataContext)
{
}

public Candidate()
{
}

#endregion

#region Properties

///
/// Property that describes the FirstName of this
/// We ensure that this is unique by using a method based validator.
///

[Required]
[MethodRule("Unique")]
public int CandidateNo { get; set; }

///
/// Property that describes the Title of this
/// This property defaults to "Mr" if the user does not specify a value.
///


[DefaultValue("Mr")]
public string Title { get; set; }

///
/// Property that describes the Firstname of this
///

[Required]
public string Firstname { get; set; }
///

/// Property that describes the Lastname of this
///

[Required]
public string Lastname { get; set; }
///
/// Property that describes the Age of this
///

[InRange(18, 95)]
[DefaultValue(30)]
public int? Age { get; set; }
///
/// Property that describes the RegistrationDate of this
/// We use a method to validate this property. I have placed the Validators
/// in their region below.
///

[MethodRule("ValidRegistration")]
public DateTime RegistrationDate { get; set; }
///
/// Property that describes the CV of this
/// In this attribute we have a DataConverter which should compress the CV
/// text before it is stored.
///

  [DataConversion(typeof(StringZipper))]
public string CV { get; set; }

#endregion

#region Methods
public override string ToString()
{
return string.Format(
"ObjectID: {0} \nName: {1} \nSurname : {2} \nAge: {3} \nRegistration Date: {4} \nCV: {5}",
ObjectID, Firstname, Lastname, Age, RegistrationDate, CV); }
#endregion

#region Method Based Validators

public void ValidRegistration(object sender, ValidateEventArgs e)
{
e.Valid = true;
if (RegistrationDate.Date < DateTime.Parse("01/01/2008 8:00:00"))
{
e.Valid = true;
e.ErrorMessage = "Impossible! The client could not have registered before we started this business.";

//Set to the minimum date - if you want
e.Property.SetValue(this, DateTime.Parse("01/01/2008 8:00:00"), null);
}

}

public void Unique(object sender, ValidateEventArgs e)
{
if (this.IsNew)
{
// A bit of LINQ to find if we have this candidate
var query = from candidate in DataSettings.DataContext.EntityObjects.OfType()
where candidate.CandidateNo == this.CandidateNo
select candidate;

e.Valid = query.Count() <= 1;
e.ErrorMessage = "The Candidate number must be unique.";
}
}

#endregion

}

The collection of attributes included in this object are:

  • Required - To ensure that the property value is entered.
  • InRange - A range validator.
  • DefaultValue - Indicates that if value is omitted, the default will be assigned to property.
  • MethodRule - Executes a method specified on saving the object.
  • DataConversion - converts the data assigned to property using a conversion class specifed.

In this example, i'm saving all my objects in a cache-like object, which i call DataSettings.DataContext.EntityObjects, and i use the code below to find an object.

             Guid CandidateID = (Guid)dataGridView1[9, e.RowIndex].Value;

Candidate candidate = new Candidate(); //You may decide to use static methods for this
candidate.Load(CandidateID);
if (candidate != null)
{
candidateBindingSource.DataSource = candidate;
cVTextBox.Text = candidate.CV;
}

If we save the data we would have the following.

Compressed

4. The Business object Base

In this example i would like you to focus on the area where i create a delegate using Delegate.CreateDelegate(typeof(EventHandler)....

    public class EntityObject : IEntityObject
{

#region Internal Fields

internal Guid objectID;
public bool IsNew { get; set; }
public bool IsDirty { get; set; }

#endregion

#region Public Properties

///
/// The Errors collection to keep the errors. The validation method populates this.
///

public readonly List Errors = new List();

public DataContext dataContext;

public Guid ObjectID
{
get
{
return objectID;
}
set
{
objectID = value;
}
}

#endregion

#region Constructors

public EntityObject()
{
if (dataContext == null)
{
if (DataSettings.DataContext == null) DataSettings.DataContext = new DataContext();
this.dataContext = DataSettings.DataContext;
}

//Create unique object identifier
objectID = Guid.NewGuid();
IsNew = true;
IsDirty = false;
}

public EntityObject(DataContext dataContext)
{
if (dataContext == null)
{
if (DataSettings.DataContext == null) DataSettings.DataContext = new DataContext();
this.dataContext = DataSettings.DataContext;
}
else
this.dataContext = dataContext;

}

#endregion




.....



public virtual void Validate(object sender, ValidateEventArgs e)
{
//Initialise the error collection
Errors.Clear();

//Enable calling the OnValidate event before validation takes place
if (this.OnValidate != null) this.OnValidate(this, new ValidateEventArgs());
try
{
foreach (PropertyInfo prop in this.GetType().GetProperties())
{
/* Get property value assigned to property */
object data = prop.GetValue(this, null);

#region Default Value setting
...
#endregion

#region IsRequired Validation
...
#endregion

#region InRange Validation
...
#endregion

#region MethodBasedValidation
/* Check if property value is Method Based Validation */
foreach (object customAttribute in prop.GetCustomAttributes(typeof(MethodRuleAttribute), true))
{
//Create event handler dynamically
EventHandler eventHandler = Delegate.CreateDelegate(typeof(EventHandler), this,
(customAttribute as MethodRuleAttribute).ValidationMethod) as EventHandler;
ValidateEventArgs args = new ValidateEventArgs(prop, string.Format("Value assigned to {0} is invalid.", prop.Name));

eventHandler(this, args); // Execute event handler
if (!args.Valid)
{
Errors.Add(new Error(this, prop.Name, args.ErrorMessage));
}
}
#endregion

#region Data Converters

/* Check if property value is required */
foreach (object customAttribute in prop.GetCustomAttributes(typeof(DataConversionAttribute), true))
{
Type conversionType = (customAttribute as DataConversionAttribute).ConverterType;
prop.SetValue(this, Converter.Instance(conversionType).Change.ToPersistentType(data), null);
}

#endregion
}
}
catch (Exception ex)
{
//
throw new Exception("Could not validate Object!", ex);
}
finally
{
//Enable calling the OnValidated event after validation has taken place
if (this.OnValidated != null) this.OnValidated(this, new ValidateEventArgs());
}

}


5. Method Based validators.

A delegate is a pointer to a method or event handler.

The .NET framework has a very interesting feature which enables us to create delegates at runtime and bind them to event handlers.

The following is the algorithm for a method based validator

    (a) Go to next Property.
    (b) If not exists exit.
    (c) Find associated attributes
    (d) If the attribute is a MethodValidator proceed, else jump to (i).
    (e) Create delegate, bind it to the method indicated on MethodValidator.
    (f) Create new Event arguments of validation type.
    (g) Execute associated eventhandler, using the event arguments.
    (h) If result on the event arguments is not valid, add Error to this.Errors collection.
    (i) Go to (a).

The main thing behing the method based validator is the ability to bind the evant at runtime to a property, and thanks again to reflection. We can find attributes associated with a property and then we create a delegate which points to the event handler presented at the attribute. We then invoke the eventhandler using event arguments, which would then help us to obtain response from the method call. The response from the event handler is then used to add associated Error to the Errors collection of this object.

                       //Create delegate and map it to ValidationMethod of current property
EventHandler eventHandler = Delegate.CreateDelegate(typeof(EventHandler), this,
(customAttribute as MethodRuleAttribute).ValidationMethod) as EventHandler;

//Create event arguments which we will inject into handler, and then review the modifications on it
ValidateEventArgs args = new ValidateEventArgs(prop, string.Format("Value assigned to {0} is invalid.", prop.Name));

eventHandler(this, args); // Execute event handler through the delegate
if (!args.Valid) // Read argument after it has been affected by the event handler
{
Errors.Add(new Error(this, prop.Name, args.ErrorMessage));
}

For more information on dynamic delagate creation, consult the MDSN documentation (here).

6. The Data Converters

The data converters enable us to change the data before it is stored. They inherit from the DataConverter class below.

We made the methods virtual so that you can override them for any implementation of conversion. The ToPersistentType(object value) is to enable conversion to storage type while FromPersistentType(object value) converts data back to viewable type.

     public class DataConverter
{
public virtual object ToPersistentType(object value)
{
throw new NotImplementedException();
}

public virtual object FromPersistentType(object value)
{
throw new NotImplementedException();
}
}

The class below is one data converter, that may be used to compress string data. You may decide to create another one to compress other data types or encrypt certain data when its stored. That reminds me of a payroll application which i built and the clients did not want the column which stores salary to be readable to database admnistrators.

     class StringZipper: DataConverter
{
public override object ToPersistentType(object value)
{
return value == null ? null : (object)Zipper.Compress((string)value);
}

public override object FromPersistentType(object value)
{
return value == null ? null : (object)Zipper.Decompress((string)value);
}
}

Code below shows how you can load the and object within the base class.

         public void Load(Guid entityObjectID)
{
EntityObject entityObject = new EntityObject(DataSettings.DataContext);
entityObject = dataContext.Load(entityObjectID);
entityObject.IsNew = false;
entityObject.IsDirty = false;

if (entityObject != null)
{
foreach (PropertyInfo prop in entityObject.GetType().GetProperties())
{
/* Get property value assigned to property */
object data = prop.GetValue(entityObject, null);

#region Data Converters

if ((prop.Attributes == PropertyAttributes.None) && (data != null))
prop.SetValue(this, data, null);

/* Check if property value is required */
foreach (object customAttribute in prop.GetCustomAttributes(typeof(DataConversionAttribute), true))
{
Type conversionType = (customAttribute as DataConversionAttribute).ConverterType;
prop.SetValue(this, Converter.Instance(conversionType).Change.FromPersistentType(data), null);
}
#endregion
}
}
else
{
Console.WriteLine("Could not find object!");
}
}

7. Techniques used in this article

  • Reflection
  • Generics
  • Anonymous types
  • Delegates / Dynamic Delegates
  • LINQ

8. Challenges and Limitations

    (a) Using reflection .SetValue() on attributes without setters specified.
    (b) DataBinding for convertable properties is not supported, else the user will see some garbled text on some controls.
    (c) I don't know whether to call it a limitation - typesafety and generics are not available on attributes.

9. History

Published on 03/01/2009, follow-up of Attribute based validation

10. Source Code

All source code for this article is available for download from CodeProject

 

Technorati Tags: ,,
Posted on Monday, January 5, 2009 5:05 AM C# | Back to top


Comments on this post: Further attributes - method based validation and data conversion for business objects

# re: Further attributes - method based validation and data conversion for business objects
Requesting Gravatar...
It gives me object must be of type decimal for validating the InRange of a decimal type. I'm not how to cast the type so that there wouldn't be this error.
Left by Howard on Jun 18, 2009 8:27 AM

# re: Further attributes - method based validation and data conversion for business objects
Requesting Gravatar...
Hi Howard

The best thing is to overload the constructor of the the InRangeAttribute class and have something like

public InRangeAttribute(double min, double max)
{
this.min = min;
this.max = max;
}
Left by Malisa Ncube on Jun 18, 2009 6:30 PM

Your comment:
 (will show your gravatar)


Copyright © Malisa L. Ncube | Powered by: GeeksWithBlogs.net