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

All*_*nek 4 .net c# clickonce application-settings

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

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

新版本的类型是:

public class Person
{
    public string Name { get; set; }
    public DateTime DateOfBirth { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

显然,ApplicationSettingsBase.Upgrade不知道如何执行升级,因为 Age 需要使用 进行转换(age) => DateTime.Now.AddYears(-age),因此只有 Name 属性会被升级,而 DateOfBirth 将只有 Default(DateTime) 的值。

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

  1. 当尝试使用 访问先前版本的值时ApplicationSettingsBase.GetPreviousVersion,返回的值将是当前版本的对象,该对象没有 Age 属性并且具有空的 DateOfBirth 属性(因为它无法将 Age 反序列化为 DateOfBirth)。
  2. 我无法找到一种方法来找出我正在升级的应用程序版本。如果有从v1到v2的升级过程和从v2到v3的过程,如果用户从v1升级到v3,我需要按顺序运行两个升级过程,但如果用户从v2升级,我只需要运行第二次升级过程。
  3. 即使我知道应用程序的先前版本是什么,并且我可以访问其先前结构中的用户设置(例如通过获取原始 XML 节点),如果我想链接升级过程(如问题 2 中所述),我将在哪里存储中间值?如果从 v2 升级到 v3,升级过程将从 v2 读取旧值并将它们直接写入 v3 中的强类型设置包装类。但是,如果从 v1 升级,我会将 v1 到 v2 升级过程的结果放在哪里,因为应用程序只有 v3 的包装类?

我认为如果升级代码直接在 user.config 文件上执行转换,我可以避免所有这些问题,但我发现没有简单的方法来获取先前版本的 user.config 的位置,因为这LocalFileSettingsProvider.GetPreviousConfigFileName(bool)是一个私有方法。

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

All*_*nek 5

我最终使用了一种更复杂的方法来进行升级,即从用户设置文件中读取原始 XML,然后运行一系列升级例程,将数据重构为新的下一个版本中应有的方式。另外,由于我在 ClickOnce 的属性中发现了一个错误(您可以在此处ApplicationDeployment.CurrentDeployment.IsFirstRun查看 Microsoft Connect 反馈),我必须使用自己的 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");
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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);
            }

        }
    }
}
Run Code Online (Sandbox Code Playgroud)

使用以下 Main 方法让系统工作:

[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());
}
Run Code Online (Sandbox Code Playgroud)

我欢迎任何意见、批评、建议或改进。我希望这对某个地方的人有帮助。