Introduction
This program demonstrates how to implement a user preferences property grid control with an arbitrary string name that works in conjunction with the VS2005 Project Settings page to create and update a user.config file.
Background
I was writing my first big application (a scientific visualization program) in C#, and got to the point where I wanted to implement a ‘user preferences’ facility. In my previous C++ efforts, I had used a tree view control to organize preferences into categories, but this time, I wanted to use the new ‘PropertyGrid
’ control and the ‘Project Settings’ feature in VS2005.
The ‘Project Settings’ feature in VS2005 is pretty cool. You define settings as either ‘application’ or ‘user’ settings via the dialog shown below. The utility knows about all the different object types you might need, including strings, fonts, colors, etc., and offers the appropriate editor for each one – very slick! Also, the IDE creates some magic so that the settings get saved to disk in XML files each time the app terminates, and then loads the settings from disk the next time the app is launched. Application settings get saved to a file called ‘app.config’ in the same folder as the executable, and user settings get saved to another file called ‘user.config’ in the user’s ‘Local Settings\ApplicationData’ folder (Note: This is *not* the ‘Application Data’ folder directly under the user name folder!). Part of the magic is the automatically generated ‘Settings
’ class comprised of a Settings.cs file and a Settings.Designer.cs file. The Settings.Designer.cs file contains the auto-generated class private members and the associated public properties for each project setting, along with the appropriate attribute entries for each.
The PropertyGrid
control is also way cool. Just by setting the ‘SelectedObject
’ property to a reference to the Settings
class object created automatically by the Project Settings feature, you get a very nice way of exposing project settings to the user. The user can edit user-scoped (but not application-scoped) settings conveniently, and any changes are written out to the appropriate .config file when the app terminates. Unfortunately, though, the PropertyGrid
/Project Settings magic has a major flaw – you can only use valid symbol names in the Project Settings grid, so the user is presented with a settings name like ‘default_plot_font’ instead of “Default Font for 2-D Plots”. Fortunately, thanks to the Bending the.NET PropertyGrid to Your Will article by Tony Allowatt, there is a way to trick the PropertyGrid
into displaying an arbitrary string rather than just property names.
While Tony’s article was very helpful, I still had to do some work and a fair amount of head-scratching to get to the point where I could present user-scoped project settings to the user with pleasing string titles. Having done so, I thought it would be worthwhile to contribute a small project to show the complete Project Settings – user edits – file storage setup.
Code Details
The snippets below show the Load
and btnOK_Click
event handlers for the main form (the one that contains the PropertyGrid
control). When you first use the Project->Settings form to create new project settings, a property definition is created for each property you enter, and the "System.Configuration.UserScopedSettingAttribute
" attribute is added to the property definition for any 'user' setting. The automatically-generated Settings
class also handles all the machinery for writing/reading settings to/from user.config.
The Load
handler uses .NET's Reflection apparatus to parse through all the properties in the Settings
class, looking for properties with the above attribute. First, it gets a reference to the static Settings
object EMWorkbench.Properties.Settings.Default
(also created automatically), then it gets a list of all the properties of the Settings
class Default
object, and then for each such property with the UserScopedSettingAttribute
attribute, it uses a case
statement to create a PropertySpec
object and adds it to the PropertyTable
table (PropertySpec
and PropertyTable
, classes courtesy of Tony Allowatt). The PropertySpec
class Name
field allows us to translate a formal property name like 'prefs_files_bscexec' to a more user-friendly string like "NEC-BSC CEM Code Location".
The OK button Click
handler turns the crank in the opposite direction, feeding any changes from the user's 'Preferences' page back to the static default Settings
class object. Then, the Settings
class takes care of writing the changes back out to the user.config XML file.
The OK button Click
handler works its way back up the PropertyGrid
structure until it gets a reference to the 'root' element. Then, it calls ParseGridItems(item)
on all top-level grid items, and ParseGridItems
takes care of transferring the preference item values back to the Settings
class. Note that ParseGridItems(item)
is a recursive function, calling itself as many times as necessary to work down through the PropertyGrid
tree structure. When ParseGridItems()
finds a grid item without the 'Category' type, it terminates the recursion and looks for a matching property description string in the case
statement. If a match is found, the corresponding Settings
class property is updated with the current value.
private void Form_Prefs_Load(object sender, EventArgs e)
{
PropertyTable proptable = new PropertyTable();
UserPrefs.Properties.Settings settings = UserPrefs.Properties.Settings.Default;
Type type = typeof(Properties.Settings);
MemberInfo[] pi = type.GetProperties();
foreach (MemberInfo m in pi)
{
Object[] myAttributes = m.GetCustomAttributes(true);
if (myAttributes.Length > 0)
{
for (int j = 0; j < myAttributes.Length; j++)
{
if( myAttributes[j].ToString() ==
"System.Configuration.UserScopedSettingAttribute")
{
PropertySpec ps = new PropertySpec("property name",
"System.String");
switch (m.Name)
{
case "prefs_files_bscexec":
ps = new PropertySpec(
"Bsc CEM Code",
"System.String",
"File Locations",
"NEC-BSC CEM Code Location",
settings.prefs_files_bscexec.ToString(),
typeof(System.Windows.Forms.Design.FileNameEditor),
typeof(System.Convert));
break;
case "pec_color":
ps = new PropertySpec(
"PEC Color",
typeof(System.Drawing.Color),
"Colors",
"Color used for PEC model elements",
settings.pec_color);
break;
case "default_plot_font":
ps = new PropertySpec(
"Default Plot Font",
typeof(Font),
"Fonts",
"Default font used for 2-D plots",
settings.default_plot_font);
break;
}
proptable.Properties.Add(ps);
}
}
}
}
this.pg_Prefs.SelectedObject = proptable;
}
private void btn_OK_Click(object sender, EventArgs e)
{
Button btn = (Button)sender;
Form_Prefs form = (Form_Prefs)btn.Parent;
PropertyGrid pg = form.pg_Prefs;
PropertyTable proptable = pg.SelectedObject as PropertyTable;
GridItem gi = pg.SelectedGridItem;
while (gi.Parent != null)
{
gi = gi.Parent;
}
foreach( GridItem item in gi.GridItems)
{
ParseGridItems(item);
}
this.Close();
}
private void ParseGridItems(GridItem gi)
{
UserPrefs.Properties.Settings settings = UserPrefs.Properties.Settings.Default;
if (gi.GridItemType == GridItemType.Category)
{
foreach (GridItem item in gi.GridItems)
{
ParseGridItems(item);
}
}
switch (gi.Label)
{
case "Bsc CEM Code":
settings.prefs_files_bscexec = gi.Value.ToString();
break;
case "PEC Color":
settings.pec_color = (Color)gi.Value;
break;
case "Default Plot Font":
settings.default_plot_font = (Font)gi.Value;
break;
default:
break;
}
}
Using the Code
To use the code, simply launch the executable. A form with a PropertyGrid
control and OK/Cancel buttons will appear, with three properties shown. Modify one or more properties, and click OK to save the changes to the user.config file. Clicking Cancel will exit the program without writing changes to the disk.
Points of Interest
Some additional notes on the ‘UserPrefs’ project.
- The line
UserPrefs.Properties.Settings.Default.Save();
must be executed when the application exits. Without it, the user.config file is not created (or updated). I placed the call in Program.cs just after the Run()
call, but it can be anywhere that gets executed as the program closes.
- I was initially stymied when attempting to implement a user setting for a file location, as I didn’t know what editor object to specify. I eventually found a post pointing to ‘
System.Windows.Forms.Design.FileNameEditor
’, but couldn’t get the code to compile even after adding a ‘using
’ statement for ‘System.Windows.Forms.Design
’ to the class file. The solution was to add ‘System.Design
’ as a project reference by right-clicking on ‘References’ under the project name in Solution Explorer, selecting ‘Add Reference…’, and selecting ‘System.Design’ from the list.
- I couldn’t figure out a really easy way to avoid the two ‘
case
’ blocks (one to load the table to be used by the PropertyGrid
, and the other to update the Settings
class properties from the PropertyGrid
). Therefore, adding a new user-scoped property requires the following steps:
- Add the new property via the Project -> Settings dialog, selecting ‘user’ for the Scope field.
- Add a new ‘
case
’ block to Form_Prefs_Load()
with the desired display name, description, and (if required) editor and/or conversion objects. Tony’s PropertyBag
class provides a number of constructors to handle just about any conceivable combination. Note that the string in ‘case <string-here"cs">>:
’ must be the property name defined in the Project Settings dialog.
- Add a new ‘
case
’ block to Form_Prefs_Btn_OK_Click()
to update the Settings
class property for each PropertyGrid
item. Here, the string in ‘case <string-here>:
’ must be the display name defined in Form_Prefs_Load()
.
- The user.config file location isn’t the same for debug and release versions, and may not even be the same for the release version running under debug (F5) control. On my system, the DEBUG version drops the user.config into C:\Documents and Settings\Frank\Local Settings\Application Data\UserPrefs\UserPrefs.vshost.exe_Url_m1s4iatnmp4yoimwn3wgal4p4kemi4u4\1.0.0.0. Note the ‘vshost.exe’ in the pathname – this is the giveaway that it is the DEBUG version (or at least running under debug control). If the program is launched by double-clicking on UserPrefs.exe in the Bin\Release folder, then the file goes to C:\Documents and Settings\Frank\Local Settings\Application Data\UserPrefs\UserPrefs.exe_Url_mzddu0ljbs451jxuvpl3xqzfdbzjtgwx\1.0.0.0.