Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Further Attributes - Method Based Attributes and Data Conversion for Business Objects

0.00/5 (No votes)
5 Jan 2009 1  
This article shows how you can use attribute based programming, reflection to perform data conversion on your business objects.
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, let's 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
    
    /// <summary>
    /// Property that describes the FirstName of this <see cref=""Candidate"">
    /// We ensure that this is unique by using a method based validator.
    /// </see></summary>
    [Required]
    [MethodRule("Unique")]
    public int CandidateNo { get; set; }
    
    /// <summary>
    /// Property that describes the Title of this <see cref=""Candidate"">
    /// This property defaults to "Mr" if the user does not specify a value.
    /// </see></summary>
    [DefaultValue("Mr")]
    public string Title { get; set; } 
    
    /// <summary>
    /// Property that describes the Firstname of this <see cref=""Candidate"">
    /// </see></summary>
    [Required]
    public string Firstname { get; set; }
    
    /// <summary>
    /// Property that describes the Lastname of this <see cref=""Candidate"">
    /// </see></summary>
    [Required]
    public string Lastname { get; set; }
    
    /// <summary>
    /// Property that describes the Age of this <see cref=""Candidate"">
    /// </see></summary>
    [InRange(18, 95)]
    [DefaultValue(30)]
    public int? Age { get; set; }
    
    /// <summary>
    /// Property that describes the RegistrationDate of this <see cref=""Candidate"">
    /// We use a method to validate this property. I have placed the Validators 
    /// in their region below.
    /// </see></summary>
    [MethodRule("ValidRegistration")]
    public DateTime RegistrationDate { get; set; }
    
    /// <summary>
    /// Property that describes the CV of this <see cref=""Candidate"">
    /// In this attribute we have a DataConverter which should compress the CV
    /// text before it is stored.
    /// </see></summary>
    [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<candidate>()
                        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 specified.

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
    
    /// <summary>
    /// The Errors collection to keep the errors. The validation method populates this.
    /// </summary>
    public readonly List<error> Errors = new List<error>();
    
    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<validateeventargs> eventHandler = 
                    Delegate.CreateDelegate(typeof(EventHandler<validateeventargs>),
                    this, (customAttribute as MethodRuleAttribute).ValidationMethod) as 
                        EventHandler<validateeventargs>;
                    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:

  1. Go to next Property. 
  2. If not exists, exit.
  3. Find associated attributes.
  4. If the attribute is a MethodValidator proceed, else jump to 9.
  5. Create delegate, bind it to the method indicated on MethodValidator.
  6. Create new Event arguments of validation type.
  7. Execute associated eventhandler, using the event arguments.
  8. If result on the event arguments is not valid, add Error to this.Errors collection.
  9. Go to 1.

The main thing behind the method based validator is the ability to bind the event 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 parameter. We then invoke that event handler using event arguments, which would then help us to obtain a response from the event handler which is then used to add associated Error to the object.Errors collection of this object.

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

//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 delegate 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 it's 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 administrators.

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);
    }
}

The code below shows how you can load the 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. Some of the Techniques Used in this Article

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

8. Challenges and Limitations

  1. You cannot use reflection .SetValue() on properties without setters specified, therefore your properties will have to be R/W.
  2. DataBinding for convertible properties is not supported, else the user will see some garbled text on some controls.
  3. I don't know whether to call it a limitation - typesafety and generics are not available on attributes. Just as you cannot have variables on the attribute declarations.

9. History

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here