< C++ .Net System Prog. 4 | Main | C++ .Net System Prog. 6 >


 

 

Early Stages of the C++ .Net 19

(Managed Extensions for C++)

 

 

The following are the topics available in this page.

  1. Writing to Configuration Files

  2. Per-User Configuration Files

  3. Versioning and Fusion

 

Writing to Configuration Files

 

The configuration file API is essentially a read-only API, which is a pity because you cannot programmatically change settings set via a user interface and have these persisted for the next run of the application. Of course, configuration files are just XML files, so you can use the .NET Framework XML classes to change the file. The System::Data::DataSet class provides a convenient way to do this because it presents the XML data in the form of a database. Here is a class that will do this work:

 

 

 

 

// write.cpp

__gc class ConfigWriter : public IDisposable

{

public:

   DataSet* data;

   bool bChanged;

   ConfigWriter()

   {

      data = new DataSet;

      data->ReadXml(AppDomain::CurrentDomain->SetupInformation->ConfigurationFile);

      bChanged = false;

   }

   void ChangeValue(String* name, String* value)

   {

      if (data == 0)

      {

         throw new ObjectDisposedException(S"data", S"Dataset object is disposed");

      }

      DataTable* dt = data->Tables->Item[S"appSettings"];

      if (dt != 0)

      {

         DataRelation* add;

         add = dt->ChildRelations->Item[S"appSettings_add"];

         if (add != 0)

         {

            // Iterate through each <add> looking for Value.

            DataTable* addTable;

            addTable = static_cast<DataTable*>(add->ChildTable);

            IEnumerator* e = addTable->Rows->GetEnumerator();

            bool bSucceeded = false;

            while (e->MoveNext())

            {

               DataRow* dr = static_cast<DataRow*>(e->Current);

               String* val = static_cast<String*>(dr->Item[S"key"]);

               if (val->Equals(name))

               {

                  // Set the value.

                  dr->Item[S"value"] = value;

                  bChanged = true;

                  bSucceeded = true;

                  break;

               }

            }

            if (!bSucceeded)

               throw new ArgumentException(String::Concat(S"Cannot find ", name));

         }

         else

         {

            throw new ConfigurationException(S"cannot find <add> section");

         }

      }

      else

      {

         throw new ConfigurationException(S"cannot find <appSettings> section");

      }

   }

   void Flush()

   {

      if (data == 0)

      {

         throw new ObjectDisposedException(S"data", S"Dataset object is disposed");

      }

      if (bChanged)

      {

         data->AcceptChanges();

         data->WriteXml(AppDomain::CurrentDomain->SetupInformation->ConfigurationFile);

      }

   }

   void Dispose()

   {

      Flush();

      if (data != 0) data->Dispose();

      data = 0;

   }

};

There are a few points to be made about this class. The class is based on a disposable resource, so it should also be disposable. Therefore, the class keeps a Boolean member that determines whether a change has been made, and if so, the Flush method will write the data to the configuration file. The constructor of this class opens the DataSet object using the configuration file. The name of this file is taken from the name of the default name for the current application domain. The ChangeValue method can be called to change any value in the <appSettings> section. First this method obtains the table named appSettings from the dataset, and then it checks to see whether there is a child table for all the <add> elements. To do this, it looks for a child relation named appSettings_add. If there is a child relation with this name, there will be a table where each row is an <add> element, and each column of these rows will be an attribute of the <add> element. The code simply checks the key column to see whether the element is the one that has been requested, and then the code changes the value column to the appropriate value. If the specified element does not exist, the class throws an exception. We were tempted to handle this situation by creating a new element with the suggested values. However, there is a bug in the DataTable class, so a new row is added to the child relation outside of the <appSettings> element. The source code for this module shows an alternative solution that uses the System::Xml classes to write to a config file that can add new elements. Here is some code that uses the ConfigWriter class to keep a count of how many times the application has been run:

void main()

{

   AppSettingsReader* reader = new AppSettingsReader;

   int i = *static_cast<Int32 __box*>(reader->GetValue(S"RunCount", __typeof(Int32)));

      i++;

      Console::WriteLine(S"This is run number {0}", __box(i));

 

      ConfigWriter* writer = new ConfigWriter;

      writer->ChangeValue(S"RunCount", i.ToString());

      writer->Dispose();

}

Bear in mind that the DataSet class has to parse the XML and this task does take a while. So although it is possible to write to configuration files, the message is clear: .NET configuration files were designed as read-only files.

 

Per-User Configuration Files

 

There is no mechanism to tell the runtime to read a configuration setting based on the currently logged-on user. We regard this as a serious deficiency in the configuration file API because without such a facility, all users will get the same settings. You might decide that only certain users should get particular features, or you might decide that you want to persist user settings such as the last file loaded by a word processing application. In this situation, the registry API excels: all you need to do is create a key under the HKEY_CURRENT_USER hive, and the API will determine the current user and store the data in a registry file specifically for that user. So how do you do this in .NET? There are several ways, and we will outline a few. The most obvious is to use the registry classes in the Microsoft.Win32 assembly to access the HKEY_CURRENT_USER hive. However, this strategy breaks the idea of XCOPY deployment and DEL un-installation because if you copy the application to another machine, you will not copy its configuration settings, and if you delete the application, its settings will still remain in the registry. You can obtain the currently logged-on user by reading the USERNAME environment variable, as shown here:

String* strUser = Environment::GetEnvironmentVariable(S"USERNAME");

You could use this to create a configuration file with a name derived from the USERNAME variable. Under the covers, the configuration system appears to give some hope of specifying a configuration file other than the one derived from the application name. The AppDomain class has a read-only property named Setup­Information that is an AppDomainSetup object. The SetupInformation class has a read/write property named ConfigurationFile that gives the full path to the application configuration file. (We used this property in ConfigWriter in the previous section). Having the ConfigurationFile property read/write would imply that you could change this property and then create an AppSettingsReader object based on this new file. Sadly, you cannot do this operation because each AppSettingsReader object is actually created from the static ConfigurationSettings::AppSettings property, and AppSetings reads its values from the file determined by Con­figurationSettings::GetConfig the first time it is called. GetConfig obtains the configuration name from the current application domain and caches these values in a private member for future use. GetConfig is called by the runtime when an application is started, so from that point onward, the name of the application configuration file has been cached and cannot be changed. Your only option is to read in the values from a custom configuration file by reading the XML, with the DataSet class or with the XmlDocument class, as shown in this code:

AppDomainSetup* setup = AppDomain::CurrentDomain->SetupInformation;

String* strUser = Environment::GetEnvironmentVariable(S"USERNAME");

String* strConfig;

strConfig = String::Concat(setup->ApplicationBase, strUser, S".config");

DataSet* config = new DataSet;

config->ReadXml(strConfig);

// Read the per-user settings.

If you perform this operation, you do not get the advantage of the merging of configuration data. So if you determine that the per-user configuration file does not have a specific setting, you will have to explicitly check the application configuration for the setting to see whether there is a default value for all users. You could get around this problem by merging the XML for the section from the per-user configuration file, the application configuration file, and machine.config, but the details about how to perform this task are beyond the scope of this book. The advantage of the scheme we outlined above is that you can store the configuration file in the same folder as the application so that you preserve the idea behind XCOPY deployment and DEL un-installation. Another way to provide per-user configuration settings would be to add per-user sections to the application configuration file, as shown in this example:

<configuration>

   <users>

      <user name="Richard">

         <lastDoc>chapter5.doc</lastDoc>

      </user>

      <user name="Ellinor">

         <lastDoc>accounts2002.doc</lastDoc>

      </user>

   </users>

</configuration>

There are several disadvantages to this solution. Here are two. The first problem is that the <users> section needs to have a section handler to allow you to access the settings, so you need to write this class (and a corresponding class to write values to the appropriate section). Second, the settings for all users are stored in the same file. This means that if we copy the application and its configuration file to another machine, we get the settings for all users even if the only user on the new machine is me.

 

Versioning and Fusion

 

One of the goals of .NET is to solve the problem of DLL Hell. To a certain extent, if everyone plays by the rules, DLL Hell would never occur; however, few people know what the rules are and fewer still follow them. Put succinctly, the rules are that if you update a library, you should only add functionality. You should not remove or change functionality used by older applications. If you cannot guarantee this behavior, you should ensure that your library does not replace earlier versions and cannot be loaded by older applications. COM tried to solve this problem by basing versioning on absolute names of interfaces and classes with the implied rule that a new implementation means a new name (CLSID) for the implementation, but again, people broke that rule.

Windows introduced the idea of redirection files, empty files with the same name as the application file but with the extension .local, which indicated to the system to load DLLs from the local folder before following the LoadLibrary search algorithm. This system protects against loading the wrong version of a DLL, but it does not protect against replacing a DLL with a newer, incompatible version. To protect against this incompatibility, Windows introduced the idea of protected and shared DLLs. When a DLL is registered as shared, a reference count is maintained in the registry. When an installer installs an application that uses this DLL, the installer can update the reference count, and when the application is uninstalled, the reference count is decremented. Such a DLL is removed only when the count falls to zero. Also, during installation, the installer can check the version of the existing shared DLL and replace the DLL if the installer has a newer version. Windows also protects its own system DLLs by maintaining a copy of the official version. If you attempt to replace a system DLL, Windows will revert to the cached copy. Only a service pack can change a system DLL.

All of these facilities help to patch up a system that is suffering from DLL Hell, but they are essentially retrograde solutions applied after it became clear that a problem existed. The .NET Fusion technology is Microsoft’s attempt to solve the problem by designing versioning and location rules into the system so that you have rules to prevent the wrong library being loaded but you also have the flexibility to change the rules if necessary. Fusion comes with a tool named the Fusion Log Viewer (fuslogvw.exe). If Fusion cannot find a library, you can use the Fusion Log Viewer to see the search paths and the files that Fusion attempts to use, and from this, you can make an informed choice about how to solve the issue. Once you have fixed and run an application, the .NET Framework will store details about the application and the libraries it uses in an ini file in the folder:

\Document and Settings\<User>\Local Settings\Application Data\ApplicationHistory

where <User> is the currently logged-on user. This file is an INI file, but you should not read it directly. Instead, the .NET Framework setup will install a Microsoft Management Console (MMC) snap-in named Microsoft .NET Framework Configuration as shown below for version 2.0 installed on Windows XP Sp2 machine.

 

Running the .NET Framework 2.0 configuration MMC snap-in

 

Figure 15

 

.NET 2.0 Framework snap-in

 

Figure 16

 

To view the working versions of an application, you select the Applications node from the tree view and then select Fix An Application from the view pane. This action will give a list of all the .NET applications that you have ever run. You can select a particular application, and the tool will list date ranges when the application was run without assembly load problems as well as Application SafeMode, which is the original version of the assemblies that the application was first built and tested with. You can select one of these settings, and the tool will write values in the application’s configuration file to indicate the specific version of the library assembly that the application uses, and from this point onward, the application will use only those versions. You can edit the configuration file by hand to change these settings at a later stage, and if this configuration works, it will represent another entry in the Fix An Application dialog box.

 

Part 1 | Part 2 | Part 3 | Part 4 | Part 5 | Part 6 | Part 7 | Part 8

 


 

< C++ .Net System Prog. 4 | Main | C++ .Net System Prog. 6 >