Using Reflection for binding classes to .ini files


I’m sure the title of the article will appear strange and weird for you. Why should I use ".ini" files nowadays having all the required configuration infrastructure and a pile of Microsoft patterns for doing the configuration stuff. I definitely need to provide some background…

2 of my recent jobs were tightly connected with gambling industry. Me and my partner Alex (you can jump to his space from my "Friends" space section) were working under enormous pressure developing software for video slots and roulette tables, hardware programming and Windows XP Embedded targeting. Guess I’ll dwell on it some other day when I’ll be in a good mood for speaking about XNA and OpenGL, maths, combinational and permutation algorithms, etc.

Once we were finishing another video slot machine. We’ve been working for a whole a day and a night in a non-stop mode and at last when all the tests were performed except one. The machine couldn’t survive the power failure test. All the settings were lost, settings files were damaged and registry hives remained unchanged. It took me several hours to find the solution for that and at the end I made a strong decision for  ".ini" files usage as the best approach for storing configuration settings when speaking of embedded systems.

Weak sides of the configuration approaches when getting power failure situation

1. Registry

I must state that registry really sucks when speaking about sensitive information. It’s caching driver mechanism lives it’s own life. It usually saves information on Windows "Reset" or "Shut Down" performed in a proper way. Try to configure desktop and plug off the power manually. You’ll see the old preferences and nothing will be saved. So registry takes the first place to be avoided.

2. Application Settings

When speaking .net application settings I must admit that the older framework becomes the harder it gets to understand what is going on with the lowest layers. It happens very often that you have to dig all the abstraction layers, providers, wrappers and services to find out "Where the hell is IO operations"? As always Reflector stays the best tool to find out the truth. This approach doesn’t survive the power failures either. Almost all the times you get the damaged data because of incomplete flush operations.

3. IO operations and file streams

The same as above. You can do everything you want with the file streams and use any classes from System.IO namespace but the result is the same as mentioned. The files damage from time to time because of incomplete flush operations though not so often as using Application Settings approach.

You may wonder why do I argue against the above approaches and what kind of code did I do that flushing was my horror. The answer is quite simple.

According to some strict hardware specifications my application had to control the work of bill acceptor, coin acceptor and several money counters. Information should be accepted processed and sent at least every 7-10 ms at a separate thread. So each 7-10 ms I had a strong need of dumping and caching to hard drive to protect machine from "power failure hacks". I’ve realized at the end of testing that the best way of solving the task that was keeping closer to kernel.

After testing the ".ini" via the kernel32 I found out the amazing thing that our machine survived all the power failures. We couldn’t damage our settings with that approach anymore.

Though I’ll give you not the same sample we used, guess you’ll get the main idea. I’ve made it more object oriented and more complicated to cover the reflection power as well.

 

Going unmanaged

The ".ini" processing is rather simple and has always the same declaration to remember

#region External API
[DllImport("KERNEL32.DLL", EntryPoint = "GetPrivateProfileStringW", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
private static extern int GetPrivateProfileString(
  string lpAppName,
  string lpKeyName,
  string lpDefault,
  string lpReturnString,
  int nSize,
  string lpFilename);

[DllImport("KERNEL32.DLL", EntryPoint = "WritePrivateProfileStringW", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
private static extern int WritePrivateProfileString(
  string lpAppName,
  string lpKeyName,
  string lpString,
  string lpFilename);        
#endregion

GetPrivateProfileStringW gets the value from file and WritePrivateProfileStringW writes a value to it.

So as you can see you can quickly save the required data to file from any part of code. But imagine that you have about 300 values to be processed all over the application. Even if you create one class for holding it, the process of loading and saving properties’ values will bring you crazy.

As I went crazy very quickly I’ve decided to automate the process using one custom attribute and all the power of reflection.

 

Moving params to custom attribute

You have to remember 4 strings to store any property to file: Filename (physical path of ".ini" file, will be created if not found), Application Name (anything that will be wrapped with "[ ]" brackets, don’t know why MS didn’t call it category or something like that), Key Name and Key Value. First 3 of them are usually static for an application. So the idea is very simple – why not to move them to a custom attribute.

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class IniFileKeyBinding : Attribute
{
    private string _Section;
    public string Section
    {
        get { return _Section; }
        set { _Section = value; }
    }

    private string _Key;        
    public string Key
    {
        get { return _Key; }
        set { _Key = value; }
    }

    private string _DefaultValue;
    public string DefaultValue
    {
        get { return _DefaultValue; }
        set { _DefaultValue = value; }
    }


    public IniFileKeyBinding(string section, string key)
    {
        _Section = section;
        _Key = key;            
    }
}

So now you can mark some properties in our class to be ready for saving/loading. Here comes my settings provider to hold this stuff. There will be no supplementary information because it is overcommented I think 🙂

 

IniFileSettingsProvider.cs

using System;
using System.Reflection;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;

namespace IniSettings
{
    public class IniFileSettingsProvider
    {
        #region External API
        [DllImport("KERNEL32.DLL", EntryPoint = "GetPrivateProfileStringW", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
        private static extern int GetPrivateProfileString(
          string lpAppName,
          string lpKeyName,
          string lpDefault,
          string lpReturnString,
          int nSize,
          string lpFilename);

        [DllImport("KERNEL32.DLL", EntryPoint = "WritePrivateProfileStringW", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
        private static extern int WritePrivateProfileString(
          string lpAppName,
          string lpKeyName,
          string lpString,
          string lpFilename);        
        #endregion

        /// <summary>
        /// This is a flag for denying data changes. It is mandatory setting public properties using reflection.
        /// </summary>
        protected bool locked;

        #region Local properties
        private string _FileName;
        public string FileName
        {
            get { return _FileName; }
            protected set { _FileName = value; }
        } 
        #endregion

        #region Constructors
        public IniFileSettingsProvider(string filename)
        {
            if (string.IsNullOrEmpty(filename))
                throw new ArgumentNullException("filename");

            _FileName = filename;
            DataBind();
        } 
        #endregion

        /// <summary>
        /// Load and setup all the properties marked with IniFileKeyBinding custom attribute
        /// </summary>
        protected virtual void DataBind()
        {
            // Get current object type
            Type t = this.GetType();

            // Get our mandatory field for temporary locking of data changing
            FieldInfo f_locked = t.GetField("locked", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase);

            // If there's no locking field we don't support data binding
            if (f_locked == null)
                throw new NotSupportedException("Data binding is not supported for objects that do not support property locking!");

            // Lock data changes by setting our lock to true
            f_locked.SetValue(this, true);

            // Get the type of our custom attribute
            Type aType = typeof(IniFileKeyBinding);
            // Get all the public propeties of the required type
            PropertyInfo[] properties = t.GetProperties(); 

            // Configure a flag determining whether to load default values for properties if requested ini file was not found
            bool defaultOnly = !File.Exists(_FileName);

            // Start looping the public properties
            foreach (PropertyInfo pi in properties)
            {
                // Skip processing readonly properties
                if (!pi.CanWrite) continue;

                // If the propety exposes our IniFileKeyBinding attribute...
                if (pi.IsDefined(aType, false))
                {
                    // cee1faffe2ebffe5ec efe5f0e5ece5ededf3fe e4ebff f5f0e0ede5ede8ff e7ede0f7e5ede8ff e8e7 ini f4e0e9ebe0
                    // String holder for ini file value (values are always got as string)
                    string iniValue;
                    // Object holder for the final value or the property
                    object propValue;                    

                    // Extract our IniFileKeyBinding attribute info for current property looped
                    IniFileKeyBinding attrInfo = (IniFileKeyBinding)pi.GetCustomAttributes(typeof(IniFileKeyBinding), false)[0];                    
                    
                    // If we don't require default value initialization
                    if (!defaultOnly)
                        // Load ini file value based on attribute info
                        iniValue = GetValue(this._FileName, attrInfo.Section, attrInfo.Key, attrInfo.DefaultValue);
                    else
                        // else set the predefined default value
                        iniValue = attrInfo.DefaultValue;
                    
                    // If we got an empty string... we get the empty string in 2 cases:
                    // 1. Value doesn't exist at all in the ini file
                    // 2. Value is supposed to be empty
                    // We must test this in order to avoid problems...
                    if (string.IsNullOrEmpty(iniValue))
                    {
                        // If current property is of string type
                        if (pi.PropertyType == typeof(string))
                            // If the default value is not defined in the custom attribute we set null value otherwise the predefined attribute value
                            propValue = (attrInfo.DefaultValue == null) ? null : attrInfo.DefaultValue;
                        else
                            // Otherwise we setup the possible default value of given type
                            propValue = @default(pi.PropertyType);
                    }
                    else
                        // Change the type of received value to the current propery type
                        propValue = Convert.ChangeType(iniValue, pi.PropertyType, CultureInfo.InvariantCulture);
                    
                    // Finally set the property value
                    pi.SetValue(this, propValue, null);
                }
            }

            // Unlock the data changing by removing the lock
            f_locked.SetValue(this, false);
        }
                
        /// <summary>
        /// Svae the property value to corresponding ini file. Property should be marked with IniFileKeyBinding attribute to be saved.
        /// </summary>
        /// <param name="propertyName">The name of the property to be saved.</param>
        protected virtual void SaveProperty(string propertyName)
        {
            // Skip the save procedure in case of lock enabled
            if (locked) return;

            if (string.IsNullOrEmpty(propertyName))
                throw new ArgumentNullException("propertyName");

            // Get the type for current object
            Type t = this.GetType();    

            // Extract all the neccessary information for the given property
            PropertyInfo pi = t.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);

            if (pi == null)
                throw new NullReferenceException("Class does not contain property named '" + propertyName + "'.");

            // Get the type of our custom attribute
            Type aType = typeof(IniFileKeyBinding);

            if (!pi.IsDefined(aType, false))
                throw new NotSupportedException("Saving properties not marked with IniFileKeyBinding attribute is not supported by this method.");

            // Extract our custom attribute from the given property
            IniFileKeyBinding attrInfo = (IniFileKeyBinding)pi.GetCustomAttributes(typeof(IniFileKeyBinding), false)[0];
            // Get the property's value
            object propValue = pi.GetValue(this, null);
            // Convert the property value to string for saving to ini file
            string iniValue = (string)Convert.ChangeType(propValue, typeof(string), CultureInfo.InvariantCulture);
            // Finally save the string value to ini file
            WritePrivateProfileString(attrInfo.Section, attrInfo.Key, iniValue, _FileName);
        }

        /// <summary>
        /// Save all the properties marked with IniFileKeyBinding attribute to file.
        /// </summary>
        public virtual void Save()
        {
            Save(_FileName);
        }

        /// <summary>
        /// Save all the properties marked with IniFileKeyBinding attribute to file.
        /// </summary>
        /// <param name="filename">File to be used for saving the properties.</param>
        public virtual void Save(string filename)
        {
            if (string.IsNullOrEmpty(filename))
                throw new ArgumentNullException("filename");

            Type t = this.GetType();
            Type aType = typeof(IniFileKeyBinding);
            PropertyInfo[] properties = t.GetProperties();

            foreach (PropertyInfo pi in properties)
            {
                if (pi.IsDefined(aType, false))
                {
                    IniFileKeyBinding attrInfo = (IniFileKeyBinding)pi.GetCustomAttributes(typeof(IniFileKeyBinding), false)[0];
                    object propValue = pi.GetValue(this, null);
                    string iniValue = (string)Convert.ChangeType(propValue, typeof(string), CultureInfo.InvariantCulture);
                    WritePrivateProfileString(attrInfo.Section, attrInfo.Key, iniValue, filename);
                }
            }
        }

        #region Utils
        // This method loads the value from the ini file
        private static string GetValue(string filename, string section, string key, string defaultValue)
        {
            string retval = new string(' ', 1024);
            GetPrivateProfileString(section, key, defaultValue, retval, 1024, filename);
            return retval.Split('')[0];            
        }

        // Get the default value for the type provided
        public static object @default(Type t)
        {
            if (!t.IsValueType)
                return null;
            else
                return Activator.CreateInstance(t);
        }
        #endregion
    }
}

This will be the base class for your own configuration holders. All is left to do for you is implementation of your own settings providers based on it. I’ll give you the basic sample.

MySettings.cs

using System;

namespace SomeMyProjectNamespace
{
    public class MySettings : IniFileSettingsProvider
    {
        public MySettings(string filename) : base(filename) { }

        private int _ScreenWidth;
        [IniFileKeyBinding("Graphics", "ScreenWidth", DefaultValue="1024")]
        public int ScreenWidth
        {
            get { return _ScreenWidth; }
            set 
            { 
                _ScreenWidth = value;
                SaveProperty("ScreenWidth");                
            }
        }

        private int _ScreenHeight;
        [IniFileKeyBinding("Graphics", "ScreenHeight")]
        public int ScreenHeight
        {
            get { return _ScreenHeight; }
            set 
            { 
                _ScreenHeight = value;
                SaveProperty("ScreenHeight");
            }
        }
    }
}

All the properties are automatically bound upon class creation. All your properties are automatically saved upon value changes. Values are being saved according to an attribute configuration. You should only take care of what to mark and what default values to provide.

This approach saved a lot of my time and hope will help someone too.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s