How to store a collection of custom objects to an user.config file?

Dirk Vollmar picture Dirk Vollmar · Apr 9, 2009 · Viewed 22.9k times · Source

I would like to store a collection of custom objects in a user.config file and would like to add and remove items from the collection programmatically and then save the modified list back to the configuration file.

My items are of the following simple form:

class UserInfo
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }        
}

In my app.config I already created a custom section:

<configuration>
  <configSections>
    <section name="userInfo" type="UserInfoConfigurationHandler, MyProgram"/>

  </configSections>
  <userInfo>
    <User firstName="John" lastName="Doe" email="[email protected]" />
    <User firstName="Jane" lastName="Doe" email="[email protected]" />
  </userInfo>

</configuration>

I am also able to read in the settings by implementing IConfigurationSectionHandler:

class UserInfoConfigurationHandler : IConfigurationSectionHandler
{
    public UserInfoConfigurationHandler() { }

    public object Create(object parent, object configContext, System.Xml.XmlNode section)
    {
        List<UserInfo> items = new List<UserInfo>();
        System.Xml.XmlNodeList processesNodes = section.SelectNodes("User");

        foreach (XmlNode processNode in processesNodes)
        {
            UserInfo item = new UserInfo();
            item.FirstName = processNode.Attributes["firstName"].InnerText;
            item.LastName = processNode.Attributes["lastName"].InnerText;
            item.Email = processNode.Attributes["email"].InnerText;
            items.Add(item);
        }
        return items;
    }
}

I did all this following this article. However, using this approach I'm only able to read the settings from app.config into a List<UserInfo> collection, but I would also need to write a modified list back.

I was searching the documentation without success and now I'm kind of stuck. What am I missing?

Answer

Timothy Walters picture Timothy Walters · Apr 16, 2009

The way to add custom config (if you require more than just simple types) is to use a ConfigurationSection, within that for the schema you defined you need a ConfigurationElementCollection (set as default collection with no name), which contains a ConfigurationElement, as follows:

public class UserElement : ConfigurationElement
{
    [ConfigurationProperty( "firstName", IsRequired = true )]
    public string FirstName
    {
        get { return (string) base[ "firstName" ]; }
        set { base[ "firstName" ] = value;}
    }

    [ConfigurationProperty( "lastName", IsRequired = true )]
    public string LastName
    {
        get { return (string) base[ "lastName" ]; }
        set { base[ "lastName" ] = value; }
    }

    [ConfigurationProperty( "email", IsRequired = true )]
    public string Email
    {
        get { return (string) base[ "email" ]; }
        set { base[ "email" ] = value; }
    }

    internal string Key
    {
        get { return string.Format( "{0}|{1}|{2}", FirstName, LastName, Email ); }
    }
}

[ConfigurationCollection( typeof(UserElement), AddItemName = "user", CollectionType = ConfigurationElementCollectionType.BasicMap )]
public class UserElementCollection : ConfigurationElementCollection
{
    protected override ConfigurationElement CreateNewElement()
    {
        return new UserElement();
    }

    protected override object GetElementKey( ConfigurationElement element )
    {
        return ( (UserElement) element ).Key;
    }

    public void Add( UserElement element )
    {
        BaseAdd( element );
    }

    public void Clear()
    {
        BaseClear();
    }

    public int IndexOf( UserElement element )
    {
        return BaseIndexOf( element );
    }

    public void Remove( UserElement element )
    {
        if( BaseIndexOf( element ) >= 0 )
        {
            BaseRemove( element.Key );
        }
    }

    public void RemoveAt( int index )
    {
        BaseRemoveAt( index );
    }

    public UserElement this[ int index ]
    {
        get { return (UserElement) BaseGet( index ); }
        set
        {
            if( BaseGet( index ) != null )
            {
                BaseRemoveAt( index );
            }
            BaseAdd( index, value );
        }
    }
}

public class UserInfoSection : ConfigurationSection
{
    private static readonly ConfigurationProperty _propUserInfo = new ConfigurationProperty(
            null,
            typeof(UserElementCollection),
            null,
            ConfigurationPropertyOptions.IsDefaultCollection
    );

    private static ConfigurationPropertyCollection _properties = new ConfigurationPropertyCollection();

    static UserInfoSection()
    {
        _properties.Add( _propUserInfo );
    }

    [ConfigurationProperty( "", Options = ConfigurationPropertyOptions.IsDefaultCollection )]
    public UserElementCollection Users
    {
        get { return (UserElementCollection) base[ _propUserInfo ]; }
    }
}

I've kept the UserElement class simple, although it really should follow the pattern of declaring each property fully as described in this excellent CodeProject article. As you can see it represents the "user" elements in your config you provided.

The UserElementCollection class simply supports having more than one "user" element, including the ability to add/remove/clear items from the collection if you want to modify it at run-time.

Lastly there is the UserInfoSection which simply stats that it has a default collection of "user" elements.

Next up is a sample of the App.config file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <sectionGroup>
      <section
        name="userInfo"
        type="ConsoleApplication1.UserInfoSection, ConsoleApplication1"
        allowDefinition="Everywhere"
        allowExeDefinition="MachineToLocalUser"
      />
    </sectionGroup>
  </configSections>

  <userInfo>
    <user firstName="John" lastName="Doe" email="[email protected]" />
    <user firstName="Jane" lastName="Doe" email="[email protected]" />
  </userInfo>
</configuration>

As you can see, in this example I've included some userInfo/user elements in the App.config. I've also added settings to say they can be defined at machine/app/user/roaming-user levels.

Next we need to know how to update them at run-time, the following code shows an example:

Configuration userConfig = ConfigurationManager.OpenExeConfiguration( ConfigurationUserLevel.PerUserRoamingAndLocal );

var userInfoSection = userConfig.GetSection( "userInfo" ) as UserInfoSection;

var userElement = new UserElement();

userElement.FirstName = "Sample";
userElement.LastName = "User";
userElement.Email = "[email protected]";

userInfoSection.Users.Add( userElement );

userConfig.Save();

The above code will create a new user.config file if needed, buried deep inside the "Local Settings\Application Data" folder for the user.

If instead you want the new user added to the app.config file simply change the parameter for the OpenExeConfiguration() method to ConfigurationUserLevel.None.

As you can see, it's reasonably simple, although finding this information required a bit of digging.