当存储的数据类型发生变化时,如何升级Settings.settings?

时间:2009-10-21 10:26:51

标签: c# .net clickonce application-settings

我有一个应用程序,用于在用户设置中存储对象集合,并通过ClickOnce部署。下一版本的应用程序具有已存储对象的修改类型。例如,以前版本的类型是:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

新版本的类型是:

public class Person
{
    public string Name { get; set; }
    public DateTime DateOfBirth { get; set; }
}

显然,ApplicationSettingsBase.Upgrade不知道如何执行升级,因为Age需要使用(age) => DateTime.Now.AddYears(-age)进行转换,因此只会升级Name属性,而DateOfBirth只会具有默认(日期时间)。

所以我想提供一个升级例程,通过覆盖ApplicationSettingsBase.Upgrade,根据需要转换值。但我遇到了三个问题:

  1. 当尝试使用ApplicationSettingsBase.GetPreviousVersion访问先前版本的值时,返回的值将是当前版本的对象,该对象没有Age属性并且具有空的DateOfBirth属性(因为它不能将年龄反序列化为DateOfBirth。。
  2. 我找不到找出我正在升级的应用程序版本的方法。如果存在从v1到v2的升级过程以及从v2到v3的过程,如果用户从v1升级到v3,我需要按顺序运行两个升级过程,但如果用户从v2升级,我只需要运行第二个升级程序。
  3. 即使我知道该应用程序的先前版本是什么,并且我可以访问其以前结构中的用户设置(例如通过获取原始XML节点),如果我想链接升级过程(如问题中所述) 2),我在哪里存储中间值?如果从v2升级到v3,升级过程将从v2读取旧值,并将它们直接写入v3中的强类型设置包装器类。但是如果从v1升级,我将把v1的结果放到v2升级程序中,因为应用程序只有v3的包装类?
  4. 如果升级代码直接在user.config文件上执行转换,我认为我可以避免所有这些问题,但我发现没有简单的方法来获取以前版本的user.config的位置,因为{{ 1}}是一种私有方法。

    是否有人使用ClickOnce兼容的解决方案来升级在应用程序版本之间更改类型的用户设置,最好是支持跳过版本的解决方案(例如,从v1升级到v3而不需要用户安装v2)?

3 个答案:

答案 0 :(得分:4)

我最终使用更复杂的方式进行升级,方法是从用户设置文件中读取原始XML,然后运行一系列升级例程,将数据重构为新版本的新版本。此外,由于我在ClickOnce的ApplicationDeployment.CurrentDeployment.IsFirstRun属性中找到的错误(您可以看到Microsoft Connect反馈here),我必须使用自己的IsFirstRun设置来了解何时执行升级。整个系统对我来说非常好(但由于一些非常顽固的障碍,它是由血液和汗水制成的)。忽略注释标记我的应用程序特定的内容,而不是升级系统的一部分。

using System;
using System.Collections.Specialized;
using System.Configuration;
using System.Xml;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using System.Reflection;
using System.Text;
using MyApp.Forms;
using MyApp.Entities;

namespace MyApp.Properties
{
    public sealed partial class Settings
    {
        private static readonly Version CurrentVersion = Assembly.GetExecutingAssembly().GetName().Version;

        private Settings()
        {
            InitCollections();  // ignore
        }

        public override void Upgrade()
        {
            UpgradeFromPreviousVersion();
            BadDataFiles = new StringCollection();  // ignore
            UpgradePerformed = true; // this is a boolean value in the settings file that is initialized to false to indicate that settings file is brand new and requires upgrading
            InitCollections();  // ignore
            Save();
        }

        // ignore
        private void InitCollections()
        {
            if (BadDataFiles == null)
                BadDataFiles = new StringCollection();

            if (UploadedGames == null)
                UploadedGames = new StringDictionary();

            if (SavedSearches == null)
                SavedSearches = SavedSearchesCollection.Default;
        }

        private void UpgradeFromPreviousVersion()
        {
            try
            {
                // This works for both ClickOnce and non-ClickOnce applications, whereas
                // ApplicationDeployment.CurrentDeployment.DataDirectory only works for ClickOnce applications
                DirectoryInfo currentSettingsDir = new FileInfo(ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal).FilePath).Directory;

                if (currentSettingsDir == null)
                    throw new Exception("Failed to determine the location of the settings file.");

                if (!currentSettingsDir.Exists)
                    currentSettingsDir.Create();

                // LINQ to Objects for .NET 2.0 courtesy of LINQBridge (linqbridge.googlecode.com)
                var previousSettings = (from dir in currentSettingsDir.Parent.GetDirectories()
                                        let dirVer = new { Dir = dir, Ver = new Version(dir.Name) }
                                        where dirVer.Ver < CurrentVersion
                                        orderby dirVer.Ver descending
                                        select dirVer).FirstOrDefault();

                if (previousSettings == null)
                    return;

                XmlElement userSettings = ReadUserSettings(previousSettings.Dir.GetFiles("user.config").Single().FullName);
                userSettings = SettingsUpgrader.Upgrade(userSettings, previousSettings.Ver);
                WriteUserSettings(userSettings, currentSettingsDir.FullName + @"\user.config", true);

                Reload();
            }
            catch (Exception ex)
            {
                MessageBoxes.Alert(MessageBoxIcon.Error, "There was an error upgrading the the user settings from the previous version. The user settings will be reset.\n\n" + ex.Message);
                Default.Reset();
            }
        }

        private static XmlElement ReadUserSettings(string configFile)
        {
            // PreserveWhitespace required for unencrypted files due to https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=352591
            var doc = new XmlDocument { PreserveWhitespace = true };
            doc.Load(configFile);
            XmlNode settingsNode = doc.SelectSingleNode("configuration/userSettings/MyApp.Properties.Settings");
            XmlNode encryptedDataNode = settingsNode["EncryptedData"];
            if (encryptedDataNode != null)
            {
                var provider = new RsaProtectedConfigurationProvider();
                provider.Initialize("userSettings", new NameValueCollection());
                return (XmlElement)provider.Decrypt(encryptedDataNode);
            }
            else
            {
                return (XmlElement)settingsNode;
            }
        }

        private static void WriteUserSettings(XmlElement settingsNode, string configFile, bool encrypt)
        {
            XmlDocument doc;
            XmlNode MyAppSettings;

            if (encrypt)
            {
                var provider = new RsaProtectedConfigurationProvider();
                provider.Initialize("userSettings", new NameValueCollection());
                XmlNode encryptedSettings = provider.Encrypt(settingsNode);
                doc = encryptedSettings.OwnerDocument;
                MyAppSettings = doc.CreateElement("MyApp.Properties.Settings").AppendNewAttribute("configProtectionProvider", provider.GetType().Name);
                MyAppSettings.AppendChild(encryptedSettings);
            }
            else
            {
                doc = settingsNode.OwnerDocument;
                MyAppSettings = settingsNode;
            }

            doc.RemoveAll();
            doc.AppendNewElement("configuration")
                .AppendNewElement("userSettings")
                .AppendChild(MyAppSettings);

            using (var writer = new XmlTextWriter(configFile, Encoding.UTF8) { Formatting = Formatting.Indented, Indentation = 4 })
                doc.Save(writer);
        }

        private static class SettingsUpgrader
        {
            private static readonly Version MinimumVersion = new Version(0, 2, 1, 0);

            public static XmlElement Upgrade(XmlElement userSettings, Version oldSettingsVersion)
            {
                if (oldSettingsVersion < MinimumVersion)
                    throw new Exception("The minimum required version for upgrade is " + MinimumVersion);

                var upgradeMethods = from method in typeof(SettingsUpgrader).GetMethods(BindingFlags.Static | BindingFlags.NonPublic)
                                     where method.Name.StartsWith("UpgradeFrom_")
                                     let methodVer = new { Version = new Version(method.Name.Substring(12).Replace('_', '.')), Method = method }
                                     where methodVer.Version >= oldSettingsVersion && methodVer.Version < CurrentVersion
                                     orderby methodVer.Version ascending 
                                     select methodVer;

                foreach (var methodVer in upgradeMethods)
                {
                    try
                    {
                        methodVer.Method.Invoke(null, new object[] { userSettings });
                    }
                    catch (TargetInvocationException ex)
                    {
                        throw new Exception(string.Format("Failed to upgrade user setting from version {0}: {1}",
                                                          methodVer.Version, ex.InnerException.Message), ex.InnerException);
                    }
                }

                return userSettings;
            }

            private static void UpgradeFrom_0_2_1_0(XmlElement userSettings)
            {
                // ignore method body - put your own upgrade code here

                var savedSearches = userSettings.SelectNodes("//SavedSearch");

                foreach (XmlElement savedSearch in savedSearches)
                {
                    string xml = savedSearch.InnerXml;
                    xml = xml.Replace("IRuleOfGame", "RuleOfGame");
                    xml = xml.Replace("Field>", "FieldName>");
                    xml = xml.Replace("Type>", "Comparison>");
                    savedSearch.InnerXml = xml;


                    if (savedSearch["Name"].GetTextValue() == "Tournament")
                        savedSearch.AppendNewElement("ShowTournamentColumn", "true");
                    else
                        savedSearch.AppendNewElement("ShowTournamentColumn", "false");
                }
            }
        }
    }
}

使用了以下自定义扩展方法和帮助程序类:

using System;
using System.Windows.Forms;
using System.Collections.Generic;
using System.Xml;


namespace MyApp
{
    public static class ExtensionMethods
    {
        public static XmlNode AppendNewElement(this XmlNode element, string name)
        {
            return AppendNewElement(element, name, null);
        }
        public static XmlNode AppendNewElement(this XmlNode element, string name, string value)
        {
            return AppendNewElement(element, name, value, null);
        }
        public static XmlNode AppendNewElement(this XmlNode element, string name, string value, params KeyValuePair<string, string>[] attributes)
        {
            XmlDocument doc = element.OwnerDocument ?? (XmlDocument)element;
            XmlElement addedElement = doc.CreateElement(name);

            if (value != null)
                addedElement.SetTextValue(value);

            if (attributes != null)
                foreach (var attribute in attributes)
                    addedElement.AppendNewAttribute(attribute.Key, attribute.Value);

            element.AppendChild(addedElement);

            return addedElement;
        }
        public static XmlNode AppendNewAttribute(this XmlNode element, string name, string value)
        {
            XmlAttribute attr = element.OwnerDocument.CreateAttribute(name);
            attr.Value = value;
            element.Attributes.Append(attr);
            return element;
        }
    }
}

namespace MyApp.Forms
{
    public static class MessageBoxes
    {
        private static readonly string Caption = "MyApp v" + Application.ProductVersion;

        public static void Alert(MessageBoxIcon icon, params object[] args)
        {
            MessageBox.Show(GetMessage(args), Caption, MessageBoxButtons.OK, icon);
        }
        public static bool YesNo(MessageBoxIcon icon, params object[] args)
        {
            return MessageBox.Show(GetMessage(args), Caption, MessageBoxButtons.YesNo, icon) == DialogResult.Yes;
        }

        private static string GetMessage(object[] args)
        {
            if (args.Length == 1)
            {
                return args[0].ToString();
            }
            else
            {
                var messegeArgs = new object[args.Length - 1];
                Array.Copy(args, 1, messegeArgs, 0, messegeArgs.Length);
                return string.Format(args[0] as string, messegeArgs);
            }

        }
    }
}

以下主要方法用于允许系统工作:

[STAThread]
static void Main()
{
        // Ensures that the user setting's configuration system starts in an encrypted mode, otherwise an application restart is required to change modes.
        Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal);
        SectionInformation sectionInfo = config.SectionGroups["userSettings"].Sections["MyApp.Properties.Settings"].SectionInformation;
        if (!sectionInfo.IsProtected)
        {
            sectionInfo.ProtectSection(null);
            config.Save();
        }

        if (Settings.Default.UpgradePerformed == false)
            Settings.Default.Upgrade();

        Application.Run(new frmMain());
}

我欢迎任何意见,批评,建议或改进。我希望这可以帮助某个人。

答案 1 :(得分:1)

这可能不是您正在寻找的答案,但听起来您通过尝试将其作为升级进行管理而不会继续支持旧版本而使问题过于复杂。

问题不仅仅是字段的数据类型正在发生变化,问题在于您完全改变了对象背后的业务逻辑,需要支持具有与新旧业务逻辑相关的数据的对象。

为什么不继续拥有一个拥有全部3个属性的人员类。

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public DateTime DateOfBirth { get; set; }
}

当用户升级到新版本时,仍会存储年龄,因此当您访问DateOfBirth字段时,您只需检查DateOfBirth是否存在,如果不存在,则从年龄开始计算并保存,以便你下次访问它,它已经有一个出生日期,年龄字段可以忽略。

您可以将年龄字段标记为过时,以便记住以后不要使用它。

如果有必要,您可以在person类中添加某种私有版本字段,以便在内部知道如何处理自身,具体取决于它认为自己的版本。

有时您必须拥有设计不完善的对象,因为您仍然需要支持旧版本的数据。

答案 2 :(得分:0)

我知道这已经得到了解答,但我一直在玩弄这个并希望添加一种方法来处理与自定义类型相似(不一样)的情况:

public class Person
{

    public string Name { get; set; }
    public int Age { get; set; }
    private DateTime _dob;
    public DateTime DateOfBirth
    {
        get
        {
            if (_dob is null)
            { _dob = DateTime.Today.AddYears(Age * -1); }
            else { return _dob; }     
        }
        set { _dob = value; }
    }
 }

如果private _dob和public Age都为null或0,则您还有另一个问题。在这种情况下,默认情况下,您可以将DateofBirth设置为DateTime.Today。此外,如果您拥有的只是一个人的年龄,那么您如何将他们的DateOfBirth告诉当天呢?