代码之家  ›  专栏  ›  技术社区  ›  Allon Guralnek

存储的数据类型更改时如何升级settings.settings?

  •  3
  • Allon Guralnek  · 技术社区  · 15 年前

    我有一个应用程序,它在用户设置中存储一组对象,并通过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) => DateTime.Now.AddYears(-age) ,因此只升级name属性,dateofbirth的值将为默认值(datetime)。

    所以我想通过覆盖 应用程序设置Base.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兼容的解决方案来升级在应用程序版本之间更改类型的用户设置,最好是支持跳过版本的解决方案(例如,在不要求用户安装v2的情况下从v1升级到v3)?

    3 回复  |  直到 15 年前
        1
  •  4
  •   Allon Guralnek    15 年前

    我最终使用了一种更复杂的方法进行升级,从用户设置文件中读取原始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());
    }
    

    我欢迎任何意见、评论、建议或改进。我希望这能帮助别人。

        2
  •  1
  •   Simon P Stevens    15 年前

    这可能不是您真正想要的答案,但听起来您试图将其作为一个升级来管理,从而使问题变得过于复杂,因为您不打算继续支持旧版本。

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

    为什么不继续使用一个包含所有3个属性的Person类呢?

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

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

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

    如有必要,您可以向Person类添加某种私有版本字段,这样它就可以在内部知道如何处理自己,这取决于它认为自己是什么版本。

    有时候你必须有设计上不完美的对象,因为你仍然需要支持旧版本的数据。

        3
  •  0
  •   sidney.andrews    15 年前

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

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

    如果私人出生日期和公共年龄都为空或0,那么您将共同面临另一个问题。您可以始终将出生日期设置为datetime。在这种情况下,默认为今天。另外,如果你只有一个人的年龄,你怎么知道他们的出生日期到今天?