Как обновить Settings.settings при изменении типа хранимых данных?

StackOverflow https://stackoverflow.com/questions/1599945

Вопрос

У меня есть приложение, которое хранит коллекцию объектов в настройках пользователя и развертывается через 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) => 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), где мне хранить промежуточные значения?При обновлении с версии 2 до версии 3 процедура обновления будет считывать старые значения из версии 2 и записывать их непосредственно в класс-оболочку строго типизированных настроек в версии 3.Но при обновлении с версии 1 куда мне поместить результаты процедуры обновления с версии 1 до версии 2, поскольку приложение имеет только класс-оболочку для версии 3?

Я думал, что смогу избежать всех этих проблем, если код обновления будет выполнять преобразование непосредственно в файле user.config, но я не нашел простого способа получить местоположение файла user.config предыдущей версии, поскольку LocalFileSettingsProvider.GetPreviousConfigFileName(bool) это частный метод.

Есть ли у кого-нибудь решение, совместимое с ClickOnce, для обновления пользовательских настроек, которые меняют тип между версиями приложения, желательно решение, которое может поддерживать пропуск версий (например,обновление с версии v1 до версии 3 без необходимости установки пользователем версии 2)?

Это было полезно?

Решение

В итоге я использовал более сложный способ обновления: считывал необработанный 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());
}

Я приветствую любой вклад, критику, предложения или улучшения.Я надеюсь, что это поможет кому-то где-нибудь.

Другие советы

Возможно, это не тот ответ, который вы ищете, но похоже, что вы слишком усложняете проблему, пытаясь рассматривать это как обновление, при котором вы не собираетесь продолжать поддерживать старую версию.

Проблема не просто в том, что тип данных поля меняется, проблема в том, что вы полностью меняете бизнес-логику объекта и вам необходимо поддерживать объекты, содержащие данные, относящиеся как к старой, так и к новой бизнес-логике.

Почему бы просто не продолжать использовать класс person, в котором есть все три свойства.

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

Если и частный _dob, и общедоступный возраст равны нулю или 0, у вас есть еще одна проблема.В этом случае вы всегда можете установить для DateofBirth значение DateTime.Today по умолчанию.Кроме того, если все, что у вас есть, это возраст человека, как вы определите его дату рождения с точностью до дня?

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top