簡體   English   中英

當存儲的數據類型發生變化時,如何升級Settings.settings?

[英]How do you upgrade Settings.settings when the stored data type changes?

我有一個應用程序,它在用戶設置中存儲一組對象,並通過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<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);
            }

        }
    }
}

使用以下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告訴當天?

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM