Click here to Skip to main content
15,880,405 members
Articles / Programming Languages / C#
Article

Really Lazy Properties

Rate me:
Please Sign up or sign in to vote.
2.56/5 (9 votes)
6 Jan 20042 min read 39.9K   11   7
Make the task of defining properties a little less typing intensive. It should provide a convenient location to track changes to entities or whatever you can think of.

Introduction

Greetings reader, this article sets out to make the task of defining properties a little less typing intensive. It should provide a convenient location to track changes to entities or whatever you can think of.

In short, you'll be able to write properties that look like this:

C#
public DateTime DateCreated{
    get{ return (DateTime)propertyValue; }
    set{ propertyValue = value; }
}

The above code is a very simple example, it doesn't do much more than save you defining the underlying variable (i.e. DateTime dateCreated = DateTime.MinValue;).

Let's look at what's needed to get this going.

The base class

C#
[Serializable]
public abstract class Persistable {
    public Persistable(){
        properties = (PropertyValue[]) 
          GetType().GetCustomAttributes(typeof(PropertyValue), true);

        for (int i = 0; i < properties.Length; i++)
            properties[i].parent = this;
    }

    PropertyValue[] properties;

    //The main point of entry/exit for our entities data
    protected object propertyValue{
        set{
            PropertyValue property = PropertyValue;

            if (property == null) 
                throw new Exception("Property not found");                
            property.SetValue(value);
        }
        get{
            PropertyValue property = PropertyValue;                
            return property == null ? null : property.Value;
        }
    }

    //To only be used by the propertyValue property
    //Finds the PropertyValue attribute for the currently executing property
    PropertyValue PropertyValue{
        get{
            System.Diagnostics.StackTrace st = 
                 new System.Diagnostics.StackTrace();

            if (st.FrameCount > 0){
                string propertyName = 
                  st.GetFrame(2).GetMethod().Name;//Jump TWO steps back
                propertyName = propertyName.Substring(4);
                    
                return FindProperty(propertyName);
            }

            return null;
        }
    }

    //Scans through the PropertyValue array 
    //till it finds the requested PropertyValue.
    PropertyValue FindProperty(string name){
        for (int i = 0; i < properties.Length; i++){
            if (properties[i].Name.Equals(name)){
                return properties[i];
            }
        }

        return null;
    }

    [Serializable]
    [AttributeUsage(AttributeTargets.Class, AllowMultiple=true)]
    public class PropertyValue : Attribute{
        public PropertyValue(string name){
            this.name = name;
        }

        protected object value;
        string name;
        internal Persistable parent;

        public string Name{
            get{ return name; }
        }

        public virtual object Value{
            get { return value; }
        }

        internal virtual void SetValue(object value){
            this.value = value;
        }
    }
}

Using the code

Here's a simple example:

C#
[Serializable]
[Persistable.PropertyValue("ApplicationNumber")]
public class ApplicationMessage : Persistable {
    #region ApplicationMessage Properties
    public string ApplicationNumber{
        get{ return (string)propertyValue; }
        set{ propertyValue = value; }
    }
    #endregion
}

Important: Note that the first argument to the PropertyValue constructor is the same as the name of the property. This is how the relationship is made and these two values must match.

Now in order to make PropertyValue more than just a performance degrader, you need to add some useful functionality. Let's take another look at the implementation of PropertyValue::SetValue(object value).

Suppose we need an audit log of changes made to our "Persistable" entities. First we need to centralize the persistence of our entities.

C#
///Persistable
public virtual Persist(){
    ...
    //Custom persistence in override OR
    //I like to pass "this" to the Data Layer
    //ie. id = MyObjPersister.Persist(this);
}

Next add supporting objects for logging.

C#
//Persistable
StringBuilder changeLog;

long id;//Standardise Persistable ids.

//Depending on how you run things you can
//get away with no having a Set'er on the ID
public long ID{
    get{ return id; }
    set{ id = value; }
}

//Appends the supplied string to the log
void LogChange(string detail){
    if (changeLog == null){
        changeLog = new StringBuilder();

        if (id == 0)
            changeLog.AppendFormat("{0}: Instance" +
                " of {1} was created.\n",
                DateTime.Now, GetType());
        else
            changeLog.AppendFormat("{0}: Log started" +
              " for instance of {1} with ID : {2}.\n",
              DateTime.Now, GetType(), id);
    }

    changeLog.AppendFormat("{0}: {1}\n", DateTime.Now, detail);
}

//Returns the body of the changelog
string ChangeLog{
 get{ return changeLog == null ? string.Empty : changeLog.ToString(); }
}

Now we change the implementation of PropertyValue::SetValue(object value).

C#
//PropertyValue

internal virtual void SetValue(object value){
    parent.LogChange(string.Format("Property {0} " +
      "was changed from {1} to {2}.", this.value, value));

    this.value = value;
}

Now we're logging our changes to a StringBuilder. We need to output them somewhere, sometime. To do this, we need to make one final tweak to Persistable::Persist().

C#
//Persistable

public virtual Persist(){
    ...//persist the object

    Log.LogAudit(ChangeLog);
}

Log.LogAudit could do anything. Personally, I like to send to the Event Log. Here's a simple e.g.:

C#
//
//IN ORDER FOR AN APPLICATION TO AUTOMATICALLY 
//CREATE AN EVENT LOG CERTAIN (i dunno) PRIVILEGES ARE NEEDED.
//The alternative is to create it manually and just let the app add to it.

    public sealed class Log    {
        static readonly string applicationName = 
          ConfigurationSettings.AppSettings["ApplicationName"];
        static readonly string logSourceName = 
          ConfigurationSettings.AppSettings["LogName"];

        static Log(){
            if (!EventLog.Exists(applicationName))
                EventLog.CreateEventSource(logSourceName, applicationName);
        }

        public static void LogAudit(string info) {
            string output = string.Format("{0}\n", info);
            
            EventLog.WriteEntry(logSourceName, 
              output, EventLogEntryType.Information, 1);
        }
    }

So now every time you Persist a Persistable entity, any changes that have been made are conveniently logged to the event log (or elsewhere). Incidentally, I've adapted all/most of this code from an application whilst writing this article, so nothing has been tested :D

Points of Interest

In the future (post Whidbey), it'd be sweet to change PropertyValue to support Generics. That way it would know what data type it was storing. This would make it possible to do more cool stuff like put constraints on the property etc.

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


Written By
Web Developer
Australia Australia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralInteresting Pin
Colin Breame8-May-06 10:38
Colin Breame8-May-06 10:38 
GeneralRe: Interesting Pin
worldspawn8-May-06 14:43
worldspawn8-May-06 14:43 
GeneralA bit pointless Pin
Broken God8-Jan-04 6:32
Broken God8-Jan-04 6:32 
GeneralRe: A bit pointless Pin
dog_spawn8-Jan-04 7:54
dog_spawn8-Jan-04 7:54 
GeneralRe: A bit pointless Pin
worldspawn8-Jan-04 11:18
worldspawn8-Jan-04 11:18 
QuestionWhat about performance ? Pin
Pedro_Gomes6-Jan-04 21:04
Pedro_Gomes6-Jan-04 21:04 
AnswerRe: What about performance ? Pin
worldspawn6-Jan-04 22:50
worldspawn6-Jan-04 22:50 
I know its a major pain and I run into it all the time. You can always adapt the approach into your own framework. As for performance, well its not so great.

Below is a rather simple test. The numbers it displays are pretty meaningless but it's easy to see that the stackwalk takes adds quite a bit of time. I've tries using new StackFrame(1, false).GetMethod().Name instead of creating StackTrace class but it hasn't made any impact.

*shrug*

using System.Diagnostics;
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;

namespace StackTest
{
	/// <summary>
	/// Summary description for Form1.
	/// </summary>
	public class Form1 : System.Windows.Forms.Form
	{
		private System.Windows.Forms.Button button1;
		private System.Windows.Forms.Button button2;
		/// <summary>
		/// Required designer variable.
		/// </summary>
		private System.ComponentModel.Container components = null;

		public Form1()
		{
			//
			// Required for Windows Form Designer support
			//
			InitializeComponent();

			//
			// TODO: Add any constructor code after InitializeComponent call
			//
		}

		/// <summary>
		/// Clean up any resources being used.
		/// </summary>
		protected override void Dispose( bool disposing )
		{
			if( disposing )
			{
				if (components != null) 
				{
					components.Dispose();
				}
			}
			base.Dispose( disposing );
		}

		#region Windows Form Designer generated code
		/// <summary>
		/// Required method for Designer support - do not modify
		/// the contents of this method with the code editor.
		/// </summary>
		private void InitializeComponent()
		{
			this.button1 = new System.Windows.Forms.Button();
			this.button2 = new System.Windows.Forms.Button();
			this.SuspendLayout();
			// 
			// button1
			// 
			this.button1.Location = new System.Drawing.Point(16, 56);
			this.button1.Name = "button1";
			this.button1.Size = new System.Drawing.Size(80, 56);
			this.button1.TabIndex = 0;
			this.button1.Text = "Stack Test";
			this.button1.Click += new System.EventHandler(this.button1_Click);
			// 
			// button2
			// 
			this.button2.Location = new System.Drawing.Point(176, 56);
			this.button2.Name = "button2";
			this.button2.Size = new System.Drawing.Size(88, 64);
			this.button2.TabIndex = 1;
			this.button2.Text = "Normal Test";
			this.button2.Click += new System.EventHandler(this.button2_Click);
			// 
			// Form1
			// 
			this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
			this.ClientSize = new System.Drawing.Size(292, 266);
			this.Controls.Add(this.button2);
			this.Controls.Add(this.button1);
			this.Name = "Form1";
			this.Text = "Form1";
			this.ResumeLayout(false);

		}
		#endregion

		/// <summary>
		/// The main entry point for the application.
		/// </summary>
		[STAThread]
		static void Main() 
		{
			Application.Run(new Form1());
		}

		public string stacktest{
			get{ 
				return new StackFrame(1, false).GetMethod().Name;
			}
		}

		public string normaltest{
			get{
				return DateTime.Now.ToString();
			}
		}

		private void button1_Click(object sender, System.EventArgs e) {
			DateTime then = DateTime.Now;

			for (int i = 0; i < 100000; i++){
				string x = stacktest;
			}

			MessageBox.Show(DateTime.Now.Subtract(then).Seconds.ToString());
		}

		private void button2_Click(object sender, System.EventArgs e) {
			DateTime then = DateTime.Now;

			for (int i = 0; i < 100000; i++){
				string x = normaltest;
			}

			MessageBox.Show(DateTime.Now.Subtract(then).Seconds.ToString());
		}
	}
}


[worldspawn]

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.