Как вы обновляете Settings.settings, когда тип хранимых данных изменяется?

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

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

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

3 ответа

Решение

В итоге я использовал более сложный способ обновления, прочитав необработанный XML-файл из файла настроек пользователя, а затем выполнил серию процедур обновления, которые реорганизуют данные так, как это должно быть в новой следующей версии. Кроме того, из-за ошибки, которую я нашел в ClickOnce's 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, у которого есть все 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 вплоть до дня?

Другие вопросы по тегам