Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

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

4.87/5 (14 votes)
1 Sep 2013CPOL9 min read 78.4K   4K  
The class runs under Mono or .NET and allows the use of Windows style INI files to store and retrieve application settings

Introduction  

A few years ago in a previous article I presented a very basic and rough version of a class to manage INI files under .NET and Mono: INIFile. The idea was to use Dictionaries to hold all the key/value pairs found in the file (caching), and of course parse the INI directly, without using any Windows API.

The main drawback with the approach was that the INI file was overwritten every time the content of the cache was dumped back to disk. This erased any commented-out line or any other custom content the user may have wanted to preserve beside the valid key/value pairs.

After writing the article, I dedicated some time to perfect the class, adding the necessary functionality to preserve the original file content, including comments and such, and then wrote a custom ApplicationSettings class which makes use of INIFile to persist application settings. These two classes have been the standard way to store application settings for all of my projects in the last few years, and I'm very happy with the way they work. :)

This second article was prompted by a recent post in the old one's forum, which reminded me I never updated it.

This new article is just a re-edit of the old one, so most of the content is identical. If you are familiar with the old one, you can just donwload the new version and have a look at the "Using the Code" paragraph - I changed it a bit and it also shows a new functionality: automatic flushing.

So, here goes - hopy you enjoy it! :)

What are INI files anyway ? 

 "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:

C#
#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.

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.

C#
INIFile MyINIFile = new INIFile("myinifilename.ini"); 
The user program can query the content of the cache by calling the GetValue() method, supplying a section and a key name.
C#
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 the first time you read or modify a value.

C#
INIFile MyINIFile = new INIFile("myinifilename.ini", true); // Activate lazy loading 
While most of the time you won't need lazy loading, it may be useful in some specific scenarios where you need to create the INIFile object and delay the actual parsing of the file, be it for performance or behavioural needs.

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

C#
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.
C#
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.

You can also provide a third Boolean parameter with value true to the constructor to activate automatic flushing.

C#
INIFile MyINIFile = new INIFile("myinifilename.ini", false, true); // Example: activate automatic flushing, but not lazy loading

Automatic flushing means the contents of the cache are dumped back to disk, overwriting the original file, every time you modify a value using SetValue(). This means you won't have to explicitly call the Flush() method when you're done with the modifications.

There is of course a considerable drop in performance if you use automatic flushing and modify a series of values in sequence. In that case, I strongly suggest leaving automatic flushing off and calling Flush().

If you are concerned with losing the modifications you already made because of an Exception, you should use the try...finally construct:  

C#
try
{
	MyINIFile.SetValue("Section1", "Key1", Value1);
	MyINIFile.SetValue("Section1", "Key2", Value2);
	
	// ... other code which may throw an exception

	MyINIFile.SetValue("Section2", "Key3", Value3);
}
finally
{
	MyINIFile.Flush();
} 
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() (or SetValue(), if automatic flushing is on) will be lost.
C#
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 <code>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.

C#
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 Cross-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 inorder 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.6.7 using MonoDevelop 2.4, but is perfectly compatible with all other Mono versions starting from 2.0 and with .NET - simply load the solution with Visual Studio, cut and paste the code into a new 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:

C#
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. 

You should now see the "test.ini" file in the same folder as the executable (INIFileTest/bin/Release or INIFileTest/bin/Debug).

Now edit it with a text editor like gedit, kate, vi, joe, and so on, and change its content, for example like this:

[MySection]
;This section is not used
;I like to use a ";" to start comments, but you can just write anything you like as long as it
;doesn't match a key/value pair you use in your application
 
[Section1]
Value1=3
Value2=0
 
;Here I'm commenting out and old value for "Value3" and inserting a new one to my liking
;Value3=2.25
Value3=12.34
 
Value4=0a141e28

Run the executable again and you should get this result:

Creating INIFile object for "test.ini"...
 
Getting values...
 
(int) Value1=3
(bool) Value2=False
(double) Value3=12,34
(byte[]) Value4=10, 20, 30, 40
 
Setting values...
 
(int) Value1=4
(bool) Value2=True
(double) Value3=13,09
(byte[]) Value4=10, 20, 30, 40
 
Flushing cache...
 
File content:
 
[MySection]
;This section is not used
;I like to use a ";" to start comments, but you can just write anything you like as long as it
;doesn't match a key/value pair you use in your application
 
[Section1]
Value1=4
Value2=1
 
;Here I'm commenting out and old value for "Value3" and inserting a new one to my liking
;Value3=2.25
Value3=13.09
 
Value4=0a141e28
 

Done. 

As you can see, the comments and all custom user content were preserved.

Points of Interest  

The class can be improved in many ways: implementing a better caching system, automatic flushing upon disposal implementing the IDisposable interface (although I personally don't  like this solution, so I abandoned the idea), 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! :)

History


  • 2013-09-01 - V2.1 - Posted new release which preserves the file content and offers automatic flushing
  • 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)