Click here to Skip to main content
Click here to Skip to main content

A Cross-platform C# Class for Using INI Files to Store Application Settings

By , 3 Sep 2013
Rate this:
Please Sign up or sign in to vote.

New version of this article

For anyone who may be interested, I posted an update version of this article hereSmile | :)

Introduction  

"INI" files are widely used throughout the Windows operating system. They were there before the Registry came around and are still there today.  

They are text files, usually (but not necessarily) with a ".ini" extension, and are devoted to storing various kind of settings.

Their structure is rather simple: settings are organized in "sections", with section headers identified by surrounding square brackets ("[" "]"). Each section contains one or more pairs of "key/value" strings. Each key is unique within a section, and identifies a specific setting.

An example of INI file content is:

[Section1]
Key1=somevalue
Key2=somevalue
...
[Section2]
Key1=somevalue
Key10=somevalue
...

Using INI files can be a convenient way to store your application settings, since the file format is easy to read, understand and modify simply by using a text editor.

Background  

Using INI files in a Windows application is usually achieved by means of some API calls exposed by the "kernel32.dll" library. This can be done in C# by using the "DllImport" directive:

#region "WIN32API"
 
    [DllImport("kernel32.dll")] private static extern int GetPrivateProfileInt
	([MarshalAs(UnmanagedType.LPStr)] string lpApplicationName,
	[MarshalAs(UnmanagedType.LPStr)] string lpKeyName,int nDefault,
	[MarshalAs(UnmanagedType.LPStr)] string lpFileName);
    [DllImport("kernel32.dll")] private static extern int GetPrivateProfileString
	([MarshalAs(UnmanagedType.LPStr)] string lpApplicationName,
	[MarshalAs(UnmanagedType.LPStr)] string lpKeyName,
	[MarshalAs(UnmanagedType.LPStr)] string lpDefault,
	byte[] lpReturnedString,int nSize,[MarshalAs(UnmanagedType.LPStr)] 
	string lpFileName);
    [DllImport("kernel32.dll")] private static extern int GetPrivateProfileString
	([MarshalAs(UnmanagedType.LPStr)] string lpApplicationName,
	[MarshalAs(UnmanagedType.LPStr)] string lpKeyName,
	[MarshalAs(UnmanagedType.LPStr)] string lpDefault,
	[MarshalAs(UnmanagedType.LPStr)] string lpReturnedString,
	int nSize,[MarshalAs(UnmanagedType.LPStr)] string lpFileName);
    [DllImport("kernel32.dll")] private static extern int WritePrivateProfileString
	([MarshalAs(UnmanagedType.LPStr)] string lpApplicationName,
	[MarshalAs(UnmanagedType.LPStr)] string lpKeyName,
	[MarshalAs(UnmanagedType.LPStr)] string lpString,
	[MarshalAs(UnmanagedType.LPStr)] string lpFileName);

#endregion  

I decided it would be fun to write my own cross-platform class to easily make use of INI files under both Mono and .NET.

The result is the INIFile class, which is compatible with standard Windows INI files, with a few exceptions: 

  1. Section and key names are case sensitive.
  2. It uses caching to boost performance.
  3. When changing or adding values, it does not preserve the original INI file structure.

Caching is performed by means of Dictionaries which hold in memory all sections and key/value pairs.

Using the Code

The INI file is read and parsed upon creation of an instance of the class, and its content is stored in the cache. The INI file name (complete path to the desired file) is passed to the constructor.

INIFile MyINIFile = new INIFile("myinifilename.ini");

The user program can then query the content of the cache by calling the GetValue() method, supplying a section and a key name.

string Value = MyINIFile.GetValue("Section", "Key", "Default value");

To keep compatibility with the behaviour of the Windows API calls, no exception is thrown if the INI file does not exist or if an inexisting section or key is requested. Instead, the default value supplied by the user will be returned. This allows to easily manage situations where settings are optionally modified by the end user and default values are hard-coded in the software.

There is also an option for lazy loading of the INI file content (simply pass true as second parameter to the constructor call), which tells the class instance to load the INI file content only at the first query or value modification. This can be useful to speed up application startup when necessary.

INIFile MyINIFile = new INIFile("myinifilename.ini", true);

Value can be changed by calling the SetValue() method.

MyINIFile.SetValue("Section", "Key", "Value");

If the supplied section or key are inexisting, they will be automatically added to the cache. Saving all changes back to the INI file is done by explicitly calling the Flush() method.

MyINIFile.Flush();

If any modification is present in the cache, all sections and key/value pairs will be written back to disk, overwriting the old file. If the file is not existing, it will be created. It is up to the user program to ensure integrity of the file contents by explicitly avoiding situations where two different instances of the INIFile class point to the same file. It's also interesting to note that any changes made by means of an external text editor after the INI file was loaded to the local cache will be lost after a call to Flush() (but only if a modification is present in the cache).

It is possible to force a refresh of the cache by calling the Refresh() method. The INI file will be parsed again and the content of the cache will be replaced by the file content. Any modification made to the cache after the last call to Flush() will be lost.

MyINIFile.Refresh();

In most typical situations, the best way to manage INI files is to create an instance of the INIFile class every time you need to read or write your application settings, and then destroy it. This will ensure that any changes made by means of a text editor will not be overwritten.

For example, you can create an instance of the class during the application startup to read the settings and store them, and then create another instance to write changes to the settings when the application closes, or when the user presses the "Ok" button on your "change settings" dialog, and so on.

Since all values are strings, the user program can easily encode any desired data type to a string in order to write it to the INI file, and decode it accordingly upon reading the same file. The INIFile class, however, offers a few getters and setters to automatically manage some of the basic data types, in order to cover all typical needs.

internal string GetValue(string SectionName, string Key, string DefaultValue)
internal bool GetValue(string SectionName, string Key, bool DefaultValue)
internal int GetValue(string SectionName, string Key, int DefaultValue)
internal double GetValue(string SectionName, string Key, double DefaultValue)
internal byte[] GetValue(string SectionName, string Key, byte[] DefaultValue)

internal void SetValue(string SectionName, string Key, string Value)
internal void SetValue(string SectionName, string Key, bool Value)
internal void SetValue(string SectionName, string Key, int Value)
internal void SetValue(string SectionName, string Key, double Value)
internal void SetValue(string SectionName, string Key, byte[] Value)

ints and doubles are simply converted to strings (using culture-neutral settings, so the file will be cross-culture compatible), while bools will be converted to integer values, where 0 means false and any other value means true. This makes it easy for the user to modify the values with a text editor.

There is also the possibility to store a byte array. This is extremely useful when storing hashes, uuids or any other kind of custom binary data. The framework offers methods for converting strings and other data to and from byte arrays, thus making it extremely easy to use this feature. Byte arrays are converted to hexadecimal strings when written to the INI file, which makes it possible to modify their values with a text editor. 

A Note on Thread-safety and Multi-process Access 

The class includes a basic locking system for thread-safety. All calls to the class methods are thread-safe.

Locking is done on accesses to the file and cache, and the same lock is used for both in order to guarantee the maximum possible data integrity in multi-threaded applications.

That said, you must remember that it's up to the user code to ensure full data integrity, since locking is done separately for each method call! So for example, having two different threads performing a few calls to SetValue() and then Flush() on the same instance of the class WILL require explicit locking by the user in order to prevent data loss. If the threads use different instances of the class to point to the same file, it's even more important to use locking to prevent not only data loss but also exceptions upon file access.

The same applies to accessing the file from different processes: you must either set up an interprocess locking system or be sure to manage all possible concurrency problems.

Source and Sample Code

You can download the class source code together with some sample code by means of the link in the upper part of this page.

It was developed in Mono 2.0 using MonoDevelop 2.0 Alpha 1, but is perfectly compatible with .NET - simply cut and paste the code into a Visual Studio project or just compile the source code with the .NET compiler.

Here is an extract of the sample code, which shows how to use the class features:

StreamReader sr = null;
try	
{
    Console.WriteLine("Creating INIFile object for \"test.ini\"...");
       INIFile MyINIFile = new INIFile("test.ini");
        Console.WriteLine("\nGetting values...\n");
     int Value1 = MyINIFile.GetValue("Section1","Value1",0);
    bool Value2 = MyINIFile.GetValue("Section1","Value2",false);
    double Value3 = MyINIFile.GetValue("Section1","Value3",(double)0);
    byte[] Value4 = MyINIFile.GetValue("Section1","Value4",(byte[])null);
     Console.Write("(int) Value1=");
    Console.WriteLine(Value1.ToString());
     Console.Write("(bool) Value2=");
    Console.WriteLine(Value2.ToString());
     Console.Write("(double) Value3=");
    Console.WriteLine(Value3.ToString());
     Console.Write("(byte[]) Value4=");
    Console.WriteLine(PrintByteArray(Value4));
     Console.WriteLine("\nSetting values...\n");
     Value1++;
    Value2 = !Value2;
    Value3 += 0.75;
    Value4 = new byte[] { 10, 20, 30, 40 };

    MyINIFile.SetValue("Section1","Value1", Value1);
    MyINIFile.SetValue("Section1","Value2", Value2);
    MyINIFile.SetValue("Section1","Value3", Value3);
    MyINIFile.SetValue("Section1","Value4", Value4);
     Console.Write("(int) Value1=");
    Console.WriteLine(Value1.ToString());
     Console.Write("(bool) Value2=");
    Console.WriteLine(Value2.ToString());
     Console.Write("(double) Value3=");
    Console.WriteLine(Value3.ToString());
     Console.Write("(byte[]) Value4=");
    Console.WriteLine(PrintByteArray(Value4));
     Console.WriteLine("\nFlushing cache...");
     MyINIFile.Flush();

    Console.WriteLine("\nFile content:\n");

    sr = new StreamReader("test.ini");
    string s;
    while ((s=sr.ReadLine()) != null) Console.WriteLine(s);

    Console.WriteLine("\nDone.");
}
catch (Exception ex)
{
    Console.Write("\n\nEXCEPTION: ");
    Console.WriteLine(ex.Message);
}
finally
{
    if (sr != null) sr.Close();
    sr = null;
}

This is the printout of the results for running the example for the first time:

Creating INIFile object for "test.ini"...

Getting values...

(int) Value1=0
(bool) Value2=False
(double) Value3=0
(byte[]) Value4=

Setting values...

(int) Value1=1
(bool) Value2=True
(double) Value3=0,75
(byte[]) Value4=10, 20, 30, 40

Flushing cache...

File content:

[Section1]
Value1=1
Value2=1
Value3=0.75
Value4=0a141e28

Done.

Points of Interest

The class can be improved in many ways: implementing a better caching system, automatic flushing upon disposal (although I personally don't like this solution, so I abandoned the idea), writing only modified values to the file (loss of performance but in some cases a better way to ensure data integrity), a way to automatically encode any kind of data by using generics and wrapper classes, or even better a standard way to dump and reload classes (using reflection) to the INI file, mapping them to the section/key structure, and so on.

The basics are all there, and it should be easy enough to add flavour to the class. Enjoy! Smile | :)

History

  • 2009-04-13 - V1.0 - First release

License

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

About the Author

Moreno Airoldi
Software Developer S.T.A. snc
Italy Italy
Been coding since I was 10, started out on a Philips game console in pseudo-assembler and then moved on to a C64, C128, and lots of Amigas.
 
Been working in the field of software development for industrial automation since 1989.
 
Now I'm running a small software house together with my wonderful wife. We specialise in the development of software solutions for industrial automation, data acquisition, automated production management and ERP integration with factory automation.

Comments and Discussions

 
QuestionNew version of this article PinmemberMoreno Airoldi1-Sep-13 4:19 
For anyone who may be interested, I posted an updated version of this article here[^]. Smile | :)
In theory, there is no difference between theory and practice, but not in practice. - Anonymous
 
A computer is a stupid machine with the ability to do incredibly smart things, while computer programmers are smart people with the ability to do incredibly stupid things. They are, in short, a perfect match. - B. Bryson

Questionescape character? Pinmemberabcayad30-Aug-13 22:56 
AnswerRe: escape character? PinmemberMoreno Airoldi31-Aug-13 0:48 
QuestionExcellent!! Pinmembernbaction26-Aug-13 12:42 
AnswerRe: Excellent!! PinmemberMoreno Airoldi26-Aug-13 14:20 
GeneralMy vote of 5 PinmemberJari Petays31-Jul-12 1:57 
GeneralInteresting PinmemberxComaWhitex6-Nov-09 21:15 
GeneralRe: Interesting PinmemberMoreno Airoldi8-Nov-09 21:43 
Generalgood work PinmemberDonsw16-May-09 12:22 
GeneralRe: good work PinmemberMoreno Airoldi16-May-09 22:08 
GeneralNicely done! PinmemberShayd21-Apr-09 7:26 
GeneralRe: Nicely done! PinmemberMoreno Airoldi21-Apr-09 9:18 
GeneralRe: Nicely done! PinmemberShayd21-Apr-09 10:07 
GeneralRe: Nicely done! PinmemberMoreno Airoldi21-Apr-09 11:21 
GeneralRe: Nicely done! PinmemberMoreno Airoldi26-Aug-13 14:23 
GeneralRe: Nicely done! PinmemberShayd26-Aug-13 14:41 
GeneralRe: Nicely done! PinmemberMoreno Airoldi1-Sep-13 4:14 

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

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

| Advertise | Privacy | Mobile
Web01 | 2.8.140415.2 | Last Updated 3 Sep 2013
Article Copyright 2009 by Moreno Airoldi
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid