10/21/2010

Save your Settings.settings to a Known Location

By default, when you call Save() on your settings, the LocalFileSettingsProvider (the only provider that .NET supplies) stores the settings in a path that is version-dependent.

This means that when you deploy a new version of your application, the path will change and the previous settings will be lost. Not entirely lost, of course, the LocalFileSettingsProvider has a method to “migrate” the old settings: Upgrade(), and this may exactly the way you want things to work, especially when you change the structure of your settings from one version to the next.

Where this begins to get annoying is when you write add-in for other application, in my case an Outlook add-in: here the provider uses the version of Outlook as part of the path to the user.config file; this means that when you get an update of the hosting application and it's version number changes, you loose your settings!

The only way I found to work around this was to implement my own settings provider, which is fairly simple. Once you have the class created (all it needs to do is inherit from System.Configuration.SettingsProvider) you can specify it as the provider for any setting you define in you application: select the setting and enter the fully qualified class name as the Provider in the Properties window.

I found the following information helpful to build my own settings provider:

http://www.codeproject.com/KB/vb/CustomSettingsProvider.aspx
http://msdn.microsoft.com/en-us/library/ms230624%28VS.90%29.aspx

To get you started writing your own settings provider here’s the code that I use, which is based on the articles mentioned above:

using System;



using System.Collections.Generic;



using System.Linq;



using System.Text;



 



using System.Configuration;



using System.Collections;



using System.IO;



using System.Xml;



using System.Collections.Specialized;



using log4net;



 



namespace MyNamespace



{



    class MySettingsProvider : SettingsProvider



    {



        #region Logging



        // log4net



        private static readonly ILog Log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);



        #endregion



 



        /// <summary>



        /// Constructor



        /// </summary>



        public MySettingsProvider()



        {



        }



 



        /// <summary>



        /// We use the product name (AssemblyProduct from AssemblyInfo.cs)



        /// as the application name



        /// </summary>



        public override string ApplicationName



        {



            get



            {



                string assemblyFileName = System.Reflection.Assembly.GetExecutingAssembly().Location;



                System.Diagnostics.FileVersionInfo fvi = System.Diagnostics.FileVersionInfo.GetVersionInfo(assemblyFileName);



                return fvi.ProductName;



            }



            set { }



        }



 



        /// <summary>



        /// Creates the path to the settings file, which is be design not version dependant



        /// (in contrast to the default provider).



        /// The path is built as user's application data directory, then a subdirectory based



        /// on the application name, and the file name is always user.config similar to the



        /// default provider.



        /// </summary>



        private string GetSavingPath



        {



            get



            {



                string retVal = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + Path.DirectorySeparatorChar + 



                    ApplicationName + Path.DirectorySeparatorChar + "user.config";



                return retVal;



            }



        }



 



        /// <summary>



        /// Here we just call the base class initializer



        /// </summary>



        /// <param name="name"></param>



        /// <param name="col"></param>



        public override void Initialize(string name, NameValueCollection col)



        {



            base.Initialize(this.ApplicationName, col);



        }



 



        /// <summary>



        /// SetPropertyValue is invoked when ApplicationSettingsBase.Save is called



        /// ASB makes sure to pass each provider only the values marked for that provider -



        /// You need to manually set the provider for each setting.



        /// </summary>



        /// <param name="context"></param>



        /// <param name="propvals"></param>



        public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection propvals)



        {



            string dir = Path.GetDirectoryName(GetSavingPath);



            if (!Directory.Exists(dir))



            {



                Log.Info("Settings directory does not exist, creating it");



                try



                {



                    Directory.CreateDirectory(dir);



                }



                catch (Exception fe)



                {



                    Log.ErrorFormat("Failed to create directory {0}", dir);



                    Log.Error("Exception", fe);



                }



            }



            try



            {



                using (XmlTextWriter tw = new XmlTextWriter(this.GetSavingPath, Encoding.Unicode))



                {



                    tw.WriteStartDocument();



                    tw.WriteStartElement(ApplicationName);



 



                    foreach (SettingsPropertyValue propertyValue in propvals)



                    {



                        if (IsUserScoped(propertyValue.Property) && propertyValue.SerializedValue != null)



                        {



                            tw.WriteStartElement(propertyValue.Name);



                            tw.WriteValue(propertyValue.SerializedValue);



                            tw.WriteEndElement();



                        }



                    }



 



                    tw.WriteEndElement();



                    tw.WriteEndDocument();



                }



            }



            catch (Exception e)



            {



                Log.Error("Unable to save settings", e);



            }



        }



 



        /// <summary>



        /// First instantiates all settings with their default values, then tries to



        /// retrieve the values from the settigs file. If the latter fails, the



        /// default values are returned.



        /// </summary>



        /// <param name="context"></param>



        /// <param name="props"></param>



        /// <returns></returns>



        public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext context, SettingsPropertyCollection props)



        {



            // Create new collection of values



            SettingsPropertyValueCollection values = new SettingsPropertyValueCollection();



 



            // Iterate through the settings to be retrieved (use their default values)



            foreach (SettingsProperty setting in props)



            {



                SettingsPropertyValue value = new SettingsPropertyValue(setting);



                value.IsDirty = false;



                /*value.SerializedValue = setting.DefaultValue;



                value.PropertyValue = setting.DefaultValue;*/



                values.Add(value);



            }



 



            if (!File.Exists(this.GetSavingPath))



            {



                Log.Debug("Settings file does not exist (yet) - returning default values");



                return values;



            }



 



            try



            {



                using (XmlTextReader tr = new XmlTextReader(this.GetSavingPath))



                {



                    try



                    {



                        tr.ReadStartElement(ApplicationName);



                        foreach (SettingsPropertyValue value in values)



                        {



                            if (IsUserScoped(value.Property))



                            {



                                try



                                {



                                    tr.ReadStartElement(value.Name);



                                    value.SerializedValue = tr.ReadContentAsObject();



                                    value.Deserialized = false;



                                    tr.ReadEndElement();



                                }



                                catch (XmlException xe1)



                                {



                                    Log.Error("Failed to read value from settings file", xe1);



                                }



                            }



                        }



                        tr.ReadEndElement();



                    }



                    catch (XmlException xe2)



                    {



                        Log.Error("Failed to read section from settings file", xe2);



                    }



                }



            }



            catch (Exception e)



            {



                Log.Error("Failed to read settings file", e);



            }



 



            return values;



        }



 



        // Helper method: walks the "attribute bag" for a given property



        // to determine if it is user-scoped or not.



        // Note that this provider does not enforce other rules, such as



        //   - unknown attributes



        //   - improper attribute combinations (e.g. both user and app - this implementation



        //     would say true for user-scoped regardless of existence of app-scoped)



        private bool IsUserScoped(SettingsProperty prop)



        {



            foreach (DictionaryEntry d in prop.Attributes)



            {



                Attribute a = (Attribute)d.Value;



                if (a.GetType() == typeof(UserScopedSettingAttribute))



                    return true;



            }



            return false;



        }



 



    }



}


2 comments:

brd said...

Thanks for good code
I thought that it is only my problem with LocalSettingsProvider path :)

Anonymous said...

I'm running into the same problem with the version of Outlook changing which is causing my addin to create a new user.config file. My first thought was if there was a way to override a 'version-like' field in LocalFileSettingsProvider which is the default. Then this problem could be resolved in a single line or two of code. Unfortunately that doesn't seem to be the case as you're doing the serialization yourself. Oh well. Great info though!

adaxas Web Directory