当存储的数据类型发生变化时,如何升级Settings.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只会升级具有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)?

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

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

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

以下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()); } 

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

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

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

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

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

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

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

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

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

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

 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告诉当天?