.NET Zone   
 
Featured Discussion

Use COM Interop to Read and Write to INI Files with .NET
from vb.dotnet.technical

Untitled
Join the Discussion
Go to this thread

vb.dotnet.technical

Discussions homepage

This featured discussion was drawn from content posted in one or more DevX discussion groups.


n the DevX vb.dotnet.technical group, reader "kriptos" asks how to read an INI file with .NET. Rather unhelpfully, someone answered anonymously that .NET applications use XML configuration files instead. It's true that .NET applications use configuration files, but unless you only need simple ungrouped key/value pairs, configuration files quickly become complicated, especially as you add custom configuration sections. But you can use the existing Win32 API calls to read and write from standard INI files using the DllImportAttribute with C#, or with the Declare Function statement in VB.NET; however there are a couple of tricks.

Author Note: The API calls to interact with INI files have been obsolete since the release of Windows 95, and are supported in Win32 for backward compatibility only. This featured discussion contains an INIWrapper class that wraps the most important API calls for interacting with INI files.

The API INI Functions
The API functions are usually paired; there's a Get and a Write version for most of the functions. The API contains special functions to read and write the win.ini file in the $Windows folder, those aren't discussed in this article; however, if you have a need to modify win.ini through .NET, you'll see enough here to declare the function prototypes yourself. For INI files associated with individual applications, the most important Win32 API INI-related functions are:

GetPrivateProfileString—retrieve an individual value associated with a named section and key.
WritePrivateProfileString—set an individual value associated with a named section and key.
GetPrivateProfileInt—retrieve an integer value associated with a named section and key.
WritePrivateProfileInt—set an integer value associated with a named section and key.
GetPrivateProfileSection—retrieve all the keys and values associated with a named section.
WritePrivateProfileSection—set all the keys and values associated with a named section.
GetPrivateProfileSectionNames—retrieve all the section names in an INI file.

For example, the GetPrivateProfileString API function retrieves an individual value from an INI file. You specify the file, the section, the key, a default value, a string buffer for the returned information, and the size of the buffer. In classic VB, you use a Declare Function statement to declare the API function.
' Classic VB declaration
Public Declare Function _
   GetPrivateProfileString _
   Lib "kernel32" _
   Alias "GetPrivateProfileStringA" _
   (ByVal lpApplicationName As String, _
   ByVal lpKeyName As Any, _
   ByVal lpDefault As String, _
   ByVal lpReturnedString As String, _
   ByVal nSize As Long, _
   ByVal lpFileName As String) As Long
In .NET the equivalent declaration is:
' VB.NET declaration
Private Declare Ansi Function _
   GetPrivateProfileString _
   Lib "KERNEL32.DLL" 
   Alias "GetPrivateProfileStringA" _
   (ByVal lpAppName As String, _
   ByVal lpKeyName As String, _
   ByVal lpDefault As String, _
   ByVal lpReturnedString As StringBuilder, _
   ByVal nSize As Integer, _
   ByVal lpFileName As String) As Integer
C# handles things a little differently. In C#, you use the DllImport attribute to declare function prototypes, so the equivalent declaration is:
// C# function prototype
[ DllImport("KERNEL32.DLL", 
   EntryPoint="GetPrivateProfileString")]
   protected internal static extern int 
GetPrivateProfileString(string lpAppName, 
   string lpKeyName, string lpDefault, 
   StringBuilder lpReturnedString, int nSize, 
   string lpFileName);
The interesting point here is that the lpReturnedString parameter expects a string buffer nSize in length. In .NET, you pass a StringBuilder object for the lpReturnedString parameter rather than a String object (remember to add a using System.Text; line to your class file, in VB.NET use the Imports System.Text statement). That's because strings in .NET are immutable, so while you can pass them into unmanaged code without errors, any changes made to the string buffer in unmanaged code aren't visible in your .NET code when the .NET framework marshals the data back into managed code. StringBuilder objects are both mutable strings and you can create them with a fixed buffer size. When calling unmanaged code that needs a fixed-size string buffer, try a StringBuilder first.

The EntryPoint parameter in the C# declaration above isn't strictly required. The EntryPoint parameter contains the name (or the index) of the function you want to declare. You only need to include the EntryPoint parameter if the name of your .NET function isn't the same, because .NET looks for a function named identically to the .NET function if you don't include the parameter. However, if you were to rename the .NET function shown above to getAppInitValue, you would have to include the EntryPoint parameter.
[ DllImport("KERNEL32.DLL", 
   EntryPoint="GetPrivateProfileString")]
   protected internal static extern int 
getAppInitValue(string lpAppName, 
   string lpKeyName, string lpDefault, 
   StringBuilder lpReturnedString, int nSize, 
   string lpFileName);
Author Note: I declared the prototypes in the C# class using the protected internal static accessibility level, which means they're only visible from this project and any derived classes—you may want to change the accessibility level, depending on how you want to use the functions.

The sample code includes an INI file named "INIinterop.ini" (see Listing 1) that looks like this:
 [textvalues]
1=item1
2=item2
3=item3

[intvalues]
1=101
2=102
3=103
After setting up the imported GetPrivateProfileString function definition, you can call it just like any other function. For example, using the INI file shown above and assuming it was saved as c:\INIinterop.ini, the following code would retrieve the value of the item in the [textvalues] section with the key "1"—the string "item1"—and write it to the Output window. Note that you don't need to instantiate an instance of the INIWrapper class, because all the methods are class-level methods (static methods in C#, shared methods in VB.NET).
// C# code
StringBuilder buffer = new StringBuilder(256);
int bufLen = INIWrapper.GetPrivateProfileString
   ("textvalues", "1", "", buffer, buffer.Capacity, 
   "c:\\INIinterop.ini");
Debug.WriteLine(buffer.ToString());

' VB.NET code
Dim buffer As StringBuilder = New StringBuilder(256)
Dim sDefault As String = ""
Dim bufLen As Integer = _
  INIWrapper.GetPrivateProfileString _
  ("textvalues", "1", "", buffer, buffer.Capacity, _
  "c:\INIinterop.ini") <> 0) 
Debug.WriteLine(buffer.ToString())
In contrast, you can write a new value without using a StringBuilder, because you don't need a return value.
// using C#
INIFileInterop.WritePrivateProfileString
   ("textvalues", "1", "new Item 1", 
   "c:\\INIinterop.ini")

' using VB.NET
INIFileInterop.WritePrivateProfileString
   ("textvalues", "1", "new Item 1", 
   "c:\\INIinterop.ini")
You can retrieve and write integer values with the GetPrivateProfileInt and WritePrivateProfileInt methods. See Listing 2 (C#) or Listing 3 (VB.NET) for the full declarations. To call the GetPrivateProfileInt function, you pass the section name, key name, a default integer value (returned if the key doesn't exist), and the name of the INI file. For example, the following code writes "101" to the Output window.
//using C#
int result = INIWrapper.GetPrivateProfileString
   ("intvalues", "1", 0, "c:\\ INIinterop.ini");
Debug.WriteLine(result.ToString());

' using VB.NET
dim result as Integer = _
INIWrapper.GetPrivateProfileString _
   ("intvalues", "1", 0, "c:\INIinterop.ini")
Debug.WriteLine(result.ToString())
Unfortunately calling the GetPrivateProfileSection function isn't quite as easy. The function returns a buffer filled with a null-delimited list of all the keys and values (items) in a specified section, with an additional trailing null character after the last item, so the returned buffer looks like this, where the \0 characters denote nulls:

1 = i t e m 1 \0 2 = i t e m 2 \ 0 3 = i t e m 3 \0 \0

You would expect to declare the function using a StringBuilder object with a pre-defined length for the lpReturnedString buffer parameter, just as with the GetPrivateProfileString function—but that doesn't work. When you call the function, it returns the proper number of characters, but the Stringbuilder contains only the first item, "item1=first". However, the return value of the function contains 16, which is correct—the length of the text of the two items in the [textvalues] section plus one null character after each item. In other words, the StringBuilder buffer contains the second item—but you can't reach it; the first null character in the StringBuilder buffer determines the length of the contents available, and the StringBuilder throws an error if you attempt to index a character past that point. Obviously, you need to pass a managed type that isn't quite so sensitive to null-delimited strings.

Using a Char array doesn't work either—the function doesn't alter the array, even though it still returns the correct number of characters. Instead, after much fiddling with the problem, it turns out that you can use a Byte array. You can see the full declaration in Listing 2 (C#) or Listing 3 (VB.NET).

When the function call returns, the byte array contains the entire set of items, separated with null characters, as expected. I haven't found a truly simple way to convert the byte array to a set of strings; the best method I've found is to iterate through the byte array creating the individual strings using a StringBuilder object. The sample GetINISection method below wraps the call to the GetPrivateProfileSection API, converts the returned items to strings, collects them in a StringCollection and returns that to the calling code.
// C# code
public static StringCollection GetINISection
   (String filename, String section) {
   StringCollection items = new StringCollection();
   byte[] buffer = new byte[32768];
   int bufLen=0;
   bufLen = GetPrivateProfileSection(section, 
      buffer, buffer.GetUpperBound(0), filename);
   if (bufLen > 0) {
      StringBuilder sb = new StringBuilder();
      for(int i=0; i < bufLen; i++) {               
         if (buffer[i] != 0) {
            sb.Append((char) buffer[i]);
         }
         else {
            if (sb.Length > 0) {
               items.Add(sb.ToString());
               sb = new StringBuilder();
            }
         }
      }
   }
   return items;
} 

' VB.NET code
Public Shared Function GetINISection(ByVal filename _
   As String, ByVal section As String) _
   As StringCollection
   Dim items As StringCollection = New _
      StringCollection()
   Dim buffer(32768) As Byte
   Dim bufLen As Integer = 0
   Dim sb As StringBuilder
   Dim i As Integer
   bufLen = GetPrivateProfileSection(section, _
      buffer, buffer.GetUpperBound(0), filename)
   If bufLen > 0 Then
      sb = New StringBuilder()
      For i = 0 To bufLen - 1
         If buffer(i) <> 0 Then
            sb.Append(ChrW(buffer(i)))
         Else
            If sb.Length > 0 Then
               items.Add(sb.ToString())
               sb = New StringBuilder()
            End If
         End If
      Next
   End If
   Return items
End Function
To use the method, add the line using System.Collections.Specialized; (C#) or Imports System.Collections.Specialized (VB.NET) to the top of the calling class, and then you can write code such as this:
// C# code
StringCollection items = 
   INIFileInterop.GetINISection
   ("c:\\INIinterop.ini", "textvalues");
foreach(String s in items) {
   Debug.WriteLine(s);
}

' VB.NET code
Dim s As String
Dim items as StringCollection = _
   INIFileInterop.GetINISection _
   ("c:\INIinterop.ini", "textvalues")
For Each s In items
   Debug.WriteLine(s)
Next
The sample code consists of two projects. The first is a class library project with an INIWrapper class (INIWrapper.cs in C#, INIWrapper.vb in VB.NET) that contains the API function prototypes and some wrapper methods to simplify calling the APIs. The classes work with any INI file. A second Windows Form project INIFileInteropTest (the C# version) or INIFileInteropTestVB (the VB.NET version) contains a full set of examples for using the INIWrapper classes, but the sample code depends on the INIInterop.ini file. You can save the INI file anywhere, but you must specify where it is in the INI File text field on the form before clicking the Start button (see Figure 1). The project prints the results to the large text field at the bottom of the form.

There are a few other Win32 API calls that work with INI files, and over the years, I've found it useful to add wrapper functions that, for example, return just list of keys in a section, or just the list of values. I've also found it useful to write wrapper functions to insert comments at various places. You can probably think of many more extensions to these simple classes. Finally, this code is meant for example use only. You should add error-trapping and checking (see the DllImportAttribute.SetLastError field in the documentation, and the Marshal.GetLastWin32Error method in the documentation for more information.)
 
Do you agree?



 








 
Sponsored Links

Advertising Info  |   Member Services  |   Contact Us  |   Help  |   Feedback  |   Site Map
Jupiterweb networks

internet.comearthweb.comDevx.comClickZ

Search Jupiterweb:

Jupitermedia Corporation has four divisions:
JupiterWeb, JupiterResearch, JupiterEvents, and JupiterImages

Copyright 2004 Jupitermedia Corporation All Rights Reserved.
Legal Notices, Licensing, Reprints, & Permissions, Privacy Policy.

Jupitermedia Corporate Info | Newsletters | Tech Jobs | E-mail Offers

Copyright Information/Privacy Statement