Click here to Skip to main content
6,595,444 members and growing! (18,981 online)
Email Password   helpLost your password?
Languages » C# » Attributes     Beginner License: The Code Project Open License (CPOL)

Further attributes - method based attributes and data conversion for business objects

By Malisa Ncube

This article shows how you can use attribute based programming, reflection to perform data conversion on your business objects.
C# (C# 1.0, C# 2.0, C# 3.0), Architect
Version:4 (See All)
Posted:5 Jan 2009
Views:7,104
Bookmarked:21 times
Unedited contribution
Announcements
Loading...
 
Search    
Advanced Search
Add to IE Search
printPrint   add Share
      Discuss Discuss   Broken Article?Report  
6 votes for this article.
Popularity: 3.40 Rating: 4.38 out of 5

1

2
1 vote, 16.7%
3
1 vote, 16.7%
4
4 votes, 66.7%
5
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

        /// <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 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

        /// <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

    (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 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 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. Some of the techniques used in this article

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

8. Challenges and Limitations

    (a) You cannot use reflection .SetValue() on properties without setters specified, therefore your properties will have to be R/W.
    (b) DataBinding for convertible 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. Just as you cannot have variables on the attribute declarations.

9. History

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Malisa Ncube


Member
Malisa is a software engineer and trainer based in Uganda, who has been coding since 1995. Originally from Bulawayo in Zimbabwe.

Malisa mainly uses Microsoft technologies for development of business solutions. He has trained many developers in .NET, C#, ASP.NET, ADO.NET, Delphi, SQL Server, Oracle, Project Management and a lot of other stuff. He has spoken in many developer conferences about Microsoft technologies and Information technology.

Malisa loves braai(roasted meat) and occasional interaction with other humans.

Blog: http://geekswithblogs.net/malisancube
Twitter: http://www.twitter.com/malisancube
Occupation: Software Developer (Senior)
Company: Infectious Diseases Institute - Uganda
Location: Uganda Uganda

Other popular C# articles:

Article Top
You must Sign In to use this message board.
FAQ FAQ 
 
Noise Tolerance  Layout  Per page   
 Msgs 1 to 6 of 6 (Total in Forum: 6) (Refresh)FirstPrevNext
GeneralGood article Pinmemberwalera20:31 11 Jan '09  
GeneralRe: Good article PinmemberMalisa Ncube20:55 11 Jan '09  
Generalnice article PinmemberMember 229690820:22 11 Jan '09  
GeneralRe: nice article PinmemberMalisa Ncube20:27 11 Jan '09  
GeneralClearer image PinsupporterMark Nischalke10:21 5 Jan '09  
GeneralRe: Clearer image PinmemberMalisa Ncube20:09 5 Jan '09  

General General    News News    Question Question    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

PermaLink | Privacy | Terms of Use
Last Updated: 5 Jan 2009
Editor: Sean Ewington
Copyright 2009 by Malisa Ncube
Everything else Copyright © CodeProject, 1999-2009
Web22 | Advertise on the Code Project