|

Introduction
This is my first article. I hope that my code and findings are helpful to other
members of The Code Project. Much of this work was done as a learning
experience. Did my best to comment the code to support
NDoc.
Most of my programs are small utilities with just a few settings that I would
like to persist. Things like the items in a ListBox or the
selections made in a TreeView etc...
In the past, I have used the Win32 WritePrivateProfileString, GetPrivateProfileString
or a file of my own making. So when it was time to learn .NET, one of the first
things I wanted was a simple way to save my settings. When I didn't find what I
wanted, I decided to write a DLL to use in all my programs. My first impression
with the .NET Framework was how extensive it was. So I hope I haven't
re-invented a wheel here.
My options to save settings seemed to be:
-
Use Win32 API.
-
Pro:
Simple, human readable format (text).
-
Con: 16 Bit, requires interop, limited structure.
-
The
AppSettingsReader class.
Seems like a good solution for some things but did not suit all my needs.
-
Pro:
Simple, human readable format (XML).
-
Con: Casting is required on returned values.
-
Use the system registry.
-
Pro:
Can't think of one.
-
Con: Too dangerous. Plus the settings are not as easily moved around.
I don't like to mess with other people's registries. If I need to have a
customer make an edit to a setting for some reason, they will have to use
Regedit.
-
Use XML Serialization. Seemed like a good option at first, but I decided
against it. I will explain later.
-
Pro:
Simple, human readable format (XML).
-
Con: Casting is required because I am storing my settings in objects.
This may become a better solution with the new .NET 2.0.
-
Roll my own.
-
Pro:
The great thing about writing your own is when it stops working... you can fix
it.
-
Con: Writing your own code.
The Code Project was the first place I looked for some help on this topic and I
found lots of it. Much of it used the XmlSerializer. After working
with this class a bit, it seemed a little overkill so I decided not to use it.
The XmlSerializer seems to take a while to load and still did not
handle all my data types the way I wanted (like Color). If I knew
the exact structure and type of my data at design time, it may be a good
solution.
Because the user may want to save many different data types, it seemed that an
Object type was the way to go. Normally, I would never even think of
using an Object, but speed is not an issue here.
The trouble with the Object is all that
casting. Having the user (me) explicitly cast the return value each
time would be a pain.
Example
int ret = (int)MyIni.Sections["FormSettings"]["Left"];
The plan was to determine the data type when the user sets the value, and then
save that type in the XML file. Then when you read the data back, just use an
implicit cast to return it to the original type.
Example
int ret = MyIni.Sections["FormSettings"]["Left"];
Normally, an implicit cast is a bad thing. Implicit conversions can occur
without the programmer specifying them, so care must be taken to prevent
unpleasant surprises. In this case, we know the original type because we saved
it when we set the value. So it seems safe. Plus, you could always get to the
original Object by using the "Value"
property of the setting.
Example
object ret = MyIni.Sections["FormSettings"]["Left"].Value;
Having said that, with the new Generics feature, this may change. Generics are a
new feature in version 2.0 of the C# language and the common language runtime
(CLR). They are somewhat like Class Templates in C++.
This is a sample of one of the overloaded operators to cast to bool.
If you try to set a value of a type different than the original, it will throw
an error.
public static implicit operator bool(IniSetting v)
{
if(v.settingType!=Types.BOOLEAN)
{
throw new Exception("No conversion from " + v.settingType + " to bool." );
}
return (bool)v.settingValue;
}
Using the code
AppSettings class has the following members:
-
AppSettings() - Constructor, without a file specified. You will
have to specify one with the Save
method.
-
AppSettings(string fileName) - Constructor, with file
name specified. Just simply use the Save method with no file name.
-
bool Load()
- Loads the settings from disk using the default file.
-
bool Load(string filename)
- Loads the settings from disk using a specified file.
-
IniSetting GetVal(string sectionName, string settingName,object
oDefault)
- Read a value. Created on-demand.
-
IniSetting SetVal(string sectionName, string settingName, object
oValue)
- Set/create a value.
-
IniSetting SetVal(string sectionName, string settingName, object
oValue, string description)
- Set/create a value.
-
void Remove(string sectionName, string settingName)
- Remove a setting by name.
-
void Remove(string sectionName)
- Remove a section by name.
-
bool Save()
- Save settings to file specified in constructor.
-
bool Save(string pathAndFileName) - Save settings to
specific file.
To use the DLL, just make a reference to it and use the following syntax:
BackColor = MyIni.GetVal("FormSettings","BackColor",BackColor);
Font = MyIni.GetVal("FormSettings","Font",Font);
Size = MyIni.GetVal("FormSettings","Size",Size);
MyIni.SetVal("FormSettings","Left",Left);
int ret = MyIni.GetVal("FormSettings","Left");
ret = MyIni.Sections["FormSettings"]["Left"];
MyIni.Save([File]);
After the core functionality was finished, a utility class was created to handle
more complicated settings. The overloaded methods static bool
SaveItemsAndValues and static bool LoadItemsAndValues
were added. It currently supports ComboBox, ListBox and
TreeView. Sample code is provided for each.
At this point, the functionality is much like the old INI files. You have to
write code to read each setting when a form is opened, and then write code to
save the settings when the form is closed. That's not too bad but it could be
better.
The ability to mark a particular property to be saved and add it to a list would
be cool. That way, you could just tell the program to save all the marked
properties when you close the form. You could use "Dynamic Properties" to save
the settings to the app.config file but I chose not to interfere with
that built-in functionality.
Somehow, I needed to save a reference to a control and one of its properties.
Saving the name of a control and a property to XML is no problem. Getting a
reference to an instance of a control at run time was the problem. This was
done with Reflection. In case Reflection is new to you (it was (is) to me),
Reflection provides a way to discover everything about an assembly or module.
It also provides a way to do late binding and to invoke a method dynamically.
This is what I needed. These are the static methods that achieve this.
-
AddPropertyToBag(MyIni,tbTest,"Text"); Sets the propertied to be
persisted. A "PropertyBag" section is created.
-
SaveAllPropertiesInBag(MyIni, this);
Saves all persisted properties for all controls.
-
SetAllPropertiesFromBag(MyIni, this); Loads properties
for any persisted control properties on this parent control.
Points of Interest
The PropertyGrid support was created with lots of help from CP
contributors. I only have a basic understanding of the mechanism. The PropertyGrid
will be a great interface for editing values.
The code uses #region PropertyGrid Support to identify
code specific to the PG. Other than that, there are two extra PropertyDescriptor
classes to support collections in the PG. Any suggestions for better PropertyGrid
support is welcome.
Resources Used
History
-
V1.0.1.1
-
This is the initial release of this DLL and I expect trouble until I can do
some thorough testing.
-
V1.0.2.3
-
Fixed a bug that prevented Single types from working.
-
Re-wrote the reader to use the
XmlTextReader
class because we don't need the DOM and it should be faster.
-
Changed the XML structure to reduce the file size.
-
Added support for Encryption.
-
V1.0.2.3
-
Added properties to allow you to hide or disable settings and sections in the
PropertyGrid.
-
Fixed some typos in the comments based on user feedback.
-
Added globalization of some setting types based on user feedback.
-
Cleaned up the test form a little.
-
V1.0.2.9
-
Fixed a problem where empty sections caused read failure.
-
Fixed a problem where manually entered xml comments caused read failure.
-
Fixed a problem where XmlComments property was infinitly recursive.
Running the test program
-
Start the program and click LoadTestData.
-
Check some items in the tree and make some other changes.
-
Click SaveCtrlsToSettings. This saves the tree and combo to the
settings but not to disk.
-
Click SaveSettingsToDisk. This saves the settings to disk.
-
Close the program.
-
Start the program and click LoadSettingsFromDisk. You should see
your settings come back.
-
Click LoadJasonsSettings. You should see My sample settings.
| You must Sign In to use this message board. |
|
| | Msgs 1 to 25 of 38 (Total in Forum: 38) (Refresh) | FirstPrevNext |
|
 |
|
|
Hi Jason, Thanks for your work.
The AddPropertyToBag function in XmlAppUtils class:
get { //Only set the value if the value has not been set if(setting.Value==null) { setting.Value = control.GetType().GetProperty(propertyName).GetValue (control,null); } }
fail evaluating the setting value, because in the Value property of IniSetting class, settingValue and originalType of just created instance are null.
get { //Returns an object that will need to be cast. return Convert.ChangeType(settingValue,originalType); }
I suggest this modification:
get { //Returns an object that will need to be cast. if ((settingValue == null)||(originalType==null)) return null; else return Convert.ChangeType(settingValue,originalType); }
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Awesome demo program! I have recently just started to explore .NET and the use of their PropertyGrid. I am currently going through your code now, but I am trying to figure out where in the code the Grid knows how to display the Data in a nested fashion . For example, how does the grid know how to display A -> B -> C?
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
|
Actually, there is no need to cast values if you're doing XML Serialization correctly. It all depends on how you set up your XML serializable model, you can make it do anything you want. It's quick, flexible, and there's very little work involved. Read my article on Using the XML Serializer Attributes[^], I think you'll change your mind about the perceived 'cons' of XML serialization. It's one of the greatest tools available in .Net. And 2.0 didn't change XML serialization much either, it's always been a good tool for lots of things, though persisting settings is a simple use for it.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
I discovered that if there is an empty section (some of my sections are added without ever adding any settings, the the Load will get confused with depth levels and not properly process the non-empty section past the empty one.
Example (FOOBAR is an empty section, REPORTS is not):
<Section Name="FOOBAR" /> <Section Name="REPORTS"> <Setting Name="PrintReports" Type="IniSettings"> . .
In AppSettings.cs, function ReadEachSectionInSectons should look like this, in my opinion:
private void ReadEachSectionInSectons(IniSetting parentSetting,ref XmlTextReader xReader) { int nodeDepth=0; nodeDepth = xReader.Depth; //We must be at the top sections node while (xReader.Read()) { if(nodeDepth == xReader.Depth) return; if (xReader.NodeType == XmlNodeType.Element && xReader.Name == Tags.SECTION) { string SecName = xReader.GetAttribute(Tags.NAME);//"Name" if(SecName.Length==0)return;//Get out. No name.
IniSection thisSec; if(parentSetting==null)//No parent. Must be a top section { thisSec = Sections.Add(SecName); } else { //Create a section thisSec = Sections.Add(SecName); parentSetting.Value = thisSec; } /*new*/ if(!xReader.IsEmptyElement) /*new*/ { /*old*/ ReadEachSettingInSection(thisSec,ref xReader);//Each section /*new*/ } } } }
The suggested fix is wrapping the ReadEachSettingInSection call with the "if(!xReader.IsEmptyElement)" statement. I don't know if the property IsEmptyElement is part of 1.1 as I compiled this with 2.0. It took care of my problem.
|
| Sign In·View Thread·PermaLink | 1.00/5 (1 vote) |
|
|
|
 |
|
|
I concur, The changes you suggested have been submitted to CP. Thank you for reporting this problem. Jason
|
| Sign In·View Thread·PermaLink | 1.00/5 (1 vote) |
|
|
|
 |
|
|
Hi Jason, i use your tool quite a lot and found it very useful. Thanks for your work.
However, recently i encountered a weird bug. I gave one of my config-files to a collegue and added some comments (!<-- some comment --> to describe the values of a Setting element) and was somewhat astonished that this fails. Surprising is, that it gives no error and throws no exception, it just does competely the wrong thing. (in my special case the first value of the second section was filled in as the value for the last Setting element of the first section; the second section was then completely ignored).
You should eventually mention, that it is not designed to cope with xml-comments, otherwise this will eventually result in bugs, which are difficult to track down.
Regards Dietmar
|
| Sign In·View Thread·PermaLink | 1.00/5 (1 vote) |
|
|
|
 |
|
|
Detmar, I believe I have fixed the problem you describe and have uploaded the new code to CP Thank you for reporting this. Jason
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Hi, thank you for this super tool!
There is a small bug in AppSettings.cs line 129:
public string XmlComments { get { return XmlComments; //Endless recursive  } }
Correct would be:
return xmlComments; //So is it right 
Best regards: Georg Balog
Georg Balog info@primakut.hu
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Basically, my program needs to save and load the properties of all my dynamically created controls on the form (i.e labels, picture boxes, etc.) into and from the XML file. The saving aspect of the program works well. To get an idea of what my XML file sort of looks like, this is the output of a form with 2 labels in a splitPanel:
<?xml version="1.0" standalone="yes"?> <!--MacGen Programming www.CncEdit.com--> <!--XML Version=2.0--> <Sections> <Section Name="PropertyBag"> <Setting Name="frmMain.splitPanel..Label0" Type="IniSettings"> <Setting Name="Left" Type="Int32"> <Value>348</Value> </Setting> <Setting Name="Top" Type="Int32"> <Value>413</Value> </Setting> <Setting Name="Width" Type="Int32"> <Value>271</Value> </Setting> <Setting Name="Height" Type="Int32"> <Value>94</Value> </Setting> <Setting Name="Text" Type="String"> <Value>Sample Text 1</Value> </Setting> <Setting Name="Font" Type="Font"> <Value>Microsoft Sans Serif|48|Bold|Point|0|False</Value> </Setting> </Setting> <Setting Name="frmMain.splitPanel..Label1" Type="IniSettings"> <Setting Name="Left" Type="Int32"> <Value>301</Value> </Setting> <Setting Name="Top" Type="Int32"> <Value>60</Value> </Setting> <Setting Name="Width" Type="Int32"> <Value>284</Value> </Setting> <Setting Name="Height" Type="Int32"> <Value>323</Value> </Setting> <Setting Name="Text" Type="String"> <Value>Sample Text 2</Value> </Setting> <Setting Name="BackColor" Type="Color"> <Value>-65536</Value> </Setting> <Setting Name="BorderStyle" Type="BorderStyle"> <Value>Fixed3D</Value> </Setting> </Setting> </Section> </Sections>
Basic properties such as "Left", "Top", "Text", "BackColor", etc. loads correctly when I call the SetAllPropertiesFromBag(); however, properties which uses enumeration types like "BorderStyle" causes the function to return false. As a result my controls will not appear the way in which they were saved. I took a look at the source code (utilities.cs) and it appears as though the p.SetValue(curCtl,prop.Value,null); call won't work when prop.Value is an enumeration type. If anybody could find a solution to my problem, it would be much appreciated. Thanks!
|
| Sign In·View Thread·PermaLink | 5.00/5 (1 vote) |
|
|
|
 |
|
|
I know that giving a "pro" for using the registry is taboo these days, but there are very good reasons to elect to use the registry as a repository for application and user settings, even in .Net. I think you may want to add this entry to the "pros" for the registry, since you weren't able to think of a pro when you wrote this. 
BIGGEST Pro for using the registry: GPOs
HKLM\Software\Policies and HKCU\Software\Policies can contain "true policy" software settings (via Administrative Templates) which can be created and modified by an administrator in Group Policy Objects deployable through Active Directory. This is extremely handy and makes creating and maintaining uniform settings across all desktops in a domain very easy.
This is also generally true for the rest of HKLM\Software and HKCU\Software, with the exception that because of registry DACLs, these are not "true policies" but are considered "preferences." In other words, these are settings which will be at a known state at system reboot and/or user logon, but can be changed by the user during their active session. These settings may be reverted at each GPO refresh, depending on how your domain's GPO refresh cycle is set up.
...
There are many pros and cons for the registry as any other settings repository, but one should not fear the registry. It is only "dangerous" if you have administrative access and don't know what you are doing (ie, you can nuke important settings). As a standard user, the registry is as safe as any other settings repository. The user can nuke their own settings, but everybody else is okay.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Good work, nice article! It appears that more people have more or less the same needs with storing some data. I have described in an article almost the same problem with a different solution: http://www.codeproject.com/dotnet/IsolatedStorageMadeEasy.asp
Edwin.
|
| Sign In·View Thread·PermaLink | 4.60/5 (2 votes) |
|
|
|
 |
|
|
most my files have been saved as cookie(tiny stuff), txt/ini files, or access dba, thanks for the info on this xml dude, real appreacte it 
///////////////// Thus spake the master programmer:
``A well-written program is its own heaven; a poorly-written program is its own hell.''
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
In the AppSettings dll project settings, I suggest changing this line:
StartProgram = "C:\Documents and Settings\Jason\My Documents\Dev\DOTNET\SaveSettings\bin\Debug\TestXmlAppSettings.exe"
to a relative path like so, for non-Jason users 
StartProgram = "..\bin\Debug\TestXmlAppSettings.exe"
***
Also a typo fix for AppSetting.cs
"differend" => "different"
Perry2
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
I see in the comments some bug reports, and a message by author that he won't upload some fix until later.
Is there any place to get fixed versions? Any public cvs or subversion repository? Any history information (versions, dates, changes, stuff like that)?
Thanks for any info!
Also, "spcified" should be "specified" (in AppSetting.cs, at least in the currently distributed version).
Perry2
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Perry2, I just uploaded the latest version that includes the fixes you requested. You should see a change in the History section when it is published. Jason
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Hi there,
I found SaveSettingsToXml pretty handy to save information (not only settings) to XML, even though it misses a pure tree implementation, i.e. more levels than just the section / name levels.
Anyway, I also found that the files generated by this library are not portable across locales. The reason for this is that some locale-dependant values (DateTime, decimal, double and Single) are save in a locale-dependant manner. For instance, a double will be represented like '7.76' in English, whereas it will be represented like '7,76' in French. Dates are even more noticable: the 4th of July 2005 in American English will be '07/04/2005', but in British English, it will be '04/07/2005'.
In order to ensure the portability of files generated by SaveSettingsToXml across locale, I had to modify two functions, in order to save the types above in a locale-indenpendant manner.
In WriteXMLSection(), I had to add four cases in the switch: case Types.DATETIME: if(iniSetting!=null) xWriter.WriteElementString(Tags.VALUE,((DateTime)iniSetting).ToString(DateTimeFormatInfo.InvariantInfo)); break; case Types.DECIMAL: if(iniSetting!=null) xWriter.WriteElementString(Tags.VALUE,((decimal)iniSetting).ToString(NumberFormatInfo.InvariantInfo)); break; case Types.DOUBLE: if(iniSetting!=null) xWriter.WriteElementString(Tags.VALUE,((double)iniSetting).ToString(NumberFormatInfo.InvariantInfo)); break; case Types.SINGLE: if(iniSetting!=null) xWriter.WriteElementString(Tags.VALUE,((Single)iniSetting).ToString(NumberFormatInfo.InvariantInfo)); break;
In ConvertFromXmlToType(), I had to modify the same four cases: case Types.DATETIME: return Convert.ToDateTime(valueFromXML, DateTimeFormatInfo.InvariantInfo); case Types.DECIMAL: return Convert.ToDecimal(valueFromXML, NumberFormatInfo.InvariantInfo); case Types.DOUBLE: return Convert.ToDouble(valueFromXML, NumberFormatInfo.InvariantInfo); ... case Types.SINGLE: return Convert.ToSingle(valueFromXML, NumberFormatInfo.InvariantInfo);
By doing this, I can ensure that all of the locale-dependant types are saved in a locale independant way, so that the files generated by SaveSettingsToXml are portable across locales.
In a more general way, it is always a good idea to save locale-dependant types in a locale-independant way, so that the files are portable across locales. This can be achieve either by using the trick I highlighted here, or by using a binary format. Fred. --- "Programming is the most fun you can have with your clothes on, although clothes are not mandatory."
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
|
This is a nice class. However, I think it could be a little better. The application settings are common values for the entire application. Hence if someone changes a value, such change should be reflected in every single instance of the object, since they're kind of global values.
Consider the following scenario. In my settings file, I have a setting: DSN for database connection. Then in the main app, with one instance of AppSetting class I start a new thread which will be performing some database operations, and hence will need the DSN setting for such task. However, while this thread is being executed endlessly (well, until the app is ended), the user decides to change the database connections, because --say-- the IP of the host changed. I then use AppSettings to change the settings... however, the original instance being ran within the thread didn't receive the changes. See the problemo? I will have to find a way to update all the instances of the class!
The best solution? One of the most famous design patterns: singleton. As you might know, a singleton is a class that only allows one single instance of it running. Usually, this pattern is implemented by holding a static member to the class itself, and a public member --say Instance()-- which will initialize the static member if it hasn't been initialized yet, and will return a reference to the static member. Of course, the constructor is always private (or protected perhaps). Then the only way to have an instance of the class is through the static Instance member. In such manner, all instance of the class will actually be references to the same object. If I change it in one place (say, the thread in our example), it will change everywhere.
And it won't take you too much coding. Only make the constructors private, and add something like:
private static AppSetting g_settingInstance; public static AppSetting Instance() { if (g_settingInstance == null) { g_settingInstance = new AppSetting(); // whatever initialization needed }
return g_settingInstance; }
What do you think about this feature?
Regards, KK.
One day you'll find that I have gone, for tomorrow may rain, so I'll follow the sun...
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
|
Hi, I have been using your old (first) version for several weeks, and I just switched to the new one. I received the following error:
An unhandled exception of type 'System.NullReferenceException' occurred in xmlsettings.dll
Additional information: Object reference not set to an instance of an object.
the routine is: public static implicit operator bool(IniSetting v)
My Load routine: m_bListViewChecksVisible = MyIni.GetVal("FormSettings", "ListViewVisible"); My Closing routine: MyIni.SetVal("FormSettings", "ListViewVisible", this.m_bListViewChecksVisible);
bool m_bListViewChecksVisible;
MyIni:
<!--Application settings-->
I like the (old) version very much!
Don
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Here is MyIni (without the HTML stuff!) xml version="1.0" standalone="yes" --Application settings-- Sections Section Name="PropertyBag" Setting Name="DonCkView.txtBankBalance" Type="IniSettings" Values Setting Name="Text" Type="String" Value 0 Value Setting Values Setting Section Section Name="FormSettings" Setting | | | | | |