Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

C# User Preferences with PropertyGrid

0.00/5 (No votes)
30 Jan 2009 1  
Implementing a user preferences form in C# with the PropertyGrid control.

PrefsPage.jpg

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.

UserSettings_small.GIF

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)
{
    //01/20/09 build table of properties using current values from Settings object
    // all this is required because PropertyGrid doesn't allow arbitrary string property
    // names by default.
    // Uses the PropertySpec & PropertyTable classes from PropertyBag.cs, 
    // courtesy of Tony Allowatt 
    // the basic constructor signature is:
    // public PropertySpec(string propname, string name, string type, string category, 
    // string description, object defaultValue, Type editor, Type typeConverter)
    // 'propname' values come from Settings.Designer.cs, which is managed automatically
    // by the project settings dialog

    //create & fill the table. 
    PropertyTable proptable = new PropertyTable();
    //Construct PropertyTable entries from Settings class user-scoped properties 
    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)
                    {
                        //Files category
                    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;
                        //Colors
                    case "pec_color":
                        ps = new PropertySpec(
                            "PEC Color",
                            typeof(System.Drawing.Color),
                            "Colors",
                            "Color used for PEC model elements",
                            settings.pec_color);
                        break;
                        //Fonts
                    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 line binds the PropertyTable object to the preferences PropertyGrid control
    this.pg_Prefs.SelectedObject = proptable;
}

private void btn_OK_Click(object sender, EventArgs e)
{
    //write property values back to Settings object properties
    Button btn = (Button)sender;
    Form_Prefs form = (Form_Prefs)btn.Parent;
    PropertyGrid pg = form.pg_Prefs;
    PropertyTable proptable = pg.SelectedObject as PropertyTable;
    //EMWorkbench.Properties.Settings settings = EMWorkbench.Properties.Settings.Default;
    //get the grid root
    GridItem gi = pg.SelectedGridItem;
    while (gi.Parent != null)
    {
        gi = gi.Parent;
    }
    //transfer all grid item values to Settings class properties
    foreach( GridItem item in gi.GridItems)
    {
        ParseGridItems(item); //recursive
    }
    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); //terminates at 1st Property
        }
    }
    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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here