2009-10-21 82 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

相關問題