2009-10-21 79 views
3

我有一个应用程序,用于存储用户设置中的对象集合,并通过ClickOnce进行部署。下一版本的应用程序对于存储的对象具有修改后的类型。例如,以前版本的类型是:当存储的数据类型更改时,如何升级Settings.settings?

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只会具有Default(DateTime)的值。

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

  1. 当试图访问使用ApplicationSettingsBase.GetPreviousVersion以前版本的值,返回值将是当前版本中,它没有年龄属性的对象,有一个空DateOfBirth属性(因为它无法将年龄序列化为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)?

回答

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

下定制extention方法和辅助类中使用:使用

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

太棒了!我是那个“某个地方的人”...... :-) – rymdsmurf 2017-05-12 14:14:52

1

这可能不是您正在寻找的答案,但它听起来像是在试图将此问题作为升级进行管理而不是继续支持旧版本时过度复杂化。

问题不是简单地说一个字段的数据类型正在改变,问题是你完全改变了对象背后的业务逻辑,并且需要支持具有与旧业务逻辑和新业务逻辑有关的数据的对象。

为什么不只是继续拥有一个拥有所有3个属性的人类。

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

当用户升级到新版本,年龄仍保存,所以当你访问出生日期场你只是检查是否有出生日期存在,如果它不从年龄计算它和保存它,所以当你下次访问它时,它已经有一个出生日期,并且年龄字段可以被忽略。

,所以你要记住不要在将来使用它你可以标记年龄字段为过时。

如有必要,您可以添加某种私人版本场对人的类,所以在内部它知道如何处理自己根据什么版本,它认为自己是。

有时候,你必须有不在设计完美的对象,因为你还必须支持从旧版本的数据。

+1

我想我的例子被过分简化了。存储在用户设置文件中的对象是嵌套对象的更复杂的对象。其中一个嵌套类型被重构,其中一个字段从枚举更改为自定义对象,基本上使用适当的设计模式表示相同的数据。该字段甚至具有相同的名称。我考虑保留旧领域,但是当考虑到长期后果时,我的组装会有许多过时的成员和类,这些成员和类曾经存储在用户设置中,这可能会造成严重的维护问题。 – 2009-10-21 11:18:50

+0

这是一个棘手的问题,我看到你正在尝试做什么,但有时你最终会过时的成员,以继续支持旧版本。 – 2009-10-21 11:30:04

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

如果专用_dob并且公众年龄为零或0,则您还有其他问题。在这种情况下,您可以始终将DateofBirth设置为DateTime.Today。此外,如果您拥有的只是个人的年龄,那么您如何将自己的生日告诉到当天?

+0

这就是我想过使用摆在首位的解决方案,但我有一个问题 - 基类业务对象的存储I变化,等等的XMLSerializer无法反序列化的新形式。所以这个方法只适用于非常基本的类。我目前正在研究一个更复杂(有点冒险)的方案来执行这样的升级,到目前为止它似乎运作良好。当我完成后会在这里发布。 – 2009-10-30 10:15:27

相关问题