4

我有一个在用户设置中存储对象集合的应用程序,并通过 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)?

4

3 回答 3

4

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

我欢迎任何意见、批评、建议或改进。我希望这对某个地方的人有所帮助。

于 2009-11-22T15:01:56.740 回答
1

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

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

为什么不继续拥有一个包含所有 3 个属性的 person 类。

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

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

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

如有必要,您可以向 person 类添加某种私有版本字段,以便在内部它知道如何根据它认为自己的版本来处理自己。

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

于 2009-10-21T10:44:59.857 回答
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 和公有 Age 都为 null 或 0,那么您还有另一个问题。在这种情况下,您始终可以将 DateofBirth 默认设置为 DateTime.Today。此外,如果您只有一个人的年龄,您将如何将他们的 DateOfBirth 告知这一天?

于 2009-10-29T19:34:58.990 回答