Domanda

Ho un'applicazione che memorizza un insieme di oggetti nelle impostazioni utente, e viene distribuito tramite ClickOnce. La prossima versione delle applicazioni ha un tipo modificato per gli oggetti memorizzati. Ad esempio, il tipo di versione precedente era:

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

E il tipo della nuova versione è:

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

Ovviamente, ApplicationSettingsBase.Upgrade non saprebbe come eseguire un aggiornamento, in quanto Età deve essere convertito utilizzando (age) => DateTime.Now.AddYears(-age), in modo che solo la proprietà Nome sarebbe stato aggiornato, e DateOfBirth sarebbe solo avere il valore di default (DateTime).

Così mi piacerebbe fornire una routine di aggiornamento, sovrascrivendo ApplicationSettingsBase.Upgrade, che sarebbe convertire i valori, se necessario. Ma ho incontrato tre problemi:

  1. Quando si cerca di accedere alla versione precedente del valore utilizzando ApplicationSettingsBase.GetPreviousVersion, il valore restituito sarebbe un oggetto della versione attuale, che non hanno la proprietà Age e ha una proprietà DateOfBirth vuota (in quanto non può deserializzare Età in DateOfBirth ).
  2. non riuscivo a trovare un modo per scoprire da quale versione dell'applicazione che sto aggiornando. Se c'è una procedura di aggiornamento da v1 a v2 e una procedura da v2 a v3, se un utente è l'aggiornamento da V1 a V3, ho bisogno di eseguire sia l'aggiornamento delle procedure in ordine, ma se l'utente è l'aggiornamento da v2, ho solo bisogno per eseguire la seconda procedura di aggiornamento.
  3. Anche se sapevo quello che la versione precedente dell'applicazione è, e ho potuto accedere alle impostazioni utente nella loro struttura precedente (diciamo semplicemente ottenendo un nodo XML grezzo), se volevo procedure di aggiornamento a catena (come descritto in circolazione 2), dove sarei memorizzare i valori intermedi? Se l'aggiornamento da v2 a v3, la procedura di aggiornamento avrebbe letto i vecchi valori da V2 e scriverli direttamente alla classe impostazioni involucro fortemente tipizzato in v3. Ma se l'aggiornamento da v1, dove avrei messo i risultati del v1 a v2 procedura di aggiornamento, dal momento che l'applicazione ha solo una classe wrapper per v3?

Ho pensato che avrei potuto evitare tutti questi problemi se il codice di aggiornamento sarebbe eseguire la conversione direttamente sul file user.config, ma ho trovato un modo semplice per ottenere la posizione del user.config della versione precedente, dal momento che è LocalFileSettingsProvider.GetPreviousConfigFileName(bool) un metodo privato.

Qualcuno ha una soluzione compatibile con ClickOnce per l'aggiornamento delle impostazioni utente che cambiano tipo tra le versioni delle applicazioni, preferibilmente una soluzione in grado di supportare le versioni saltare (ad esempio l'aggiornamento da V1 a V3 senza richiedere all'utente di installare in v2)?

È stato utile?

Soluzione

Ho finito per usare un modo più complesso per fare gli aggiornamenti, leggendo l'XML grezzo dal file di impostazioni utente, quindi eseguire una serie di routine di aggiornamento che refactoring i dati al modo in cui si suppone che sia nella nuova versione successiva. Inoltre, a causa di un bug che ho trovato nella proprietà ApplicationDeployment.CurrentDeployment.IsFirstRun di ClickOnce (si può vedere il Microsoft Connect retroazione qui ), ho dovuto usare la mia impostazione isFirstRun sapere quando eseguire l'aggiornamento. L'intero sistema funziona molto bene per me (ma è stata fatta con il sangue e il sudore a causa di un paio di strappi molto testardo). Ignorare i commenti segnano ciò che è specifico per la mia domanda e non fa parte del sistema di aggiornamento.

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

I seguenti metodi extention personalizzate e classi di supporto sono stati utilizzati:

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

        }
    }
}

Il seguente metodo principale è stato utilizzato per consentire al sistema di funzionare:

[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());
}

Accolgo con favore qualsiasi ingresso, critiche, suggerimenti o miglioramenti. Spero che questo aiuta qualcuno da qualche parte.

Altri suggerimenti

Questo non può essere davvero la risposta che state cercando, ma suona come si sta overcomplicating il problema cercando di gestire questo come un aggiornamento in cui non si ha intenzione di continuare a sostenere la vecchia versione.

Il problema non è semplicemente che il tipo di dati di un campo sta cambiando, il problema è che si sta cambiando totalmente la logica di business dietro l'oggetto e la necessità di sostenere oggetti che hanno i dati relativi sia alla logica di business vecchio e nuovo.

Perché non solo continuare ad avere una classe persona che ha tutte 3 proprietà su di esso.

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

Quando l'utente aggiorna alla nuova versione, l'età è ancora conservati, in modo che quando si accede al campo DateOfBirth basta controllare se un DateOfBirth esiste, e se non lo fa si calcola che dall'età e salvarlo in modo che quando alla prossima accedervi, ha già una data di nascita e il campo di età può essere ignorato.

Si potrebbe contrassegnare il campo età come obsoleti in modo da ricordare non usarlo in futuro.

Se necessario, si potrebbe aggiungere un qualche tipo di campo versione privata alla classe persona in modo internamente sa come gestire se stesso a seconda della versione che si considera.

A volte non avere oggetti che non sono perfette nel design, perché si devono ancora sostenere i dati da vecchie versioni.

So che questo è già stato risposto, ma ho accarezzato con questo e voluto aggiungere un modo ho trattato una simile (non lo stesso) situazione con Tipi personalizzati:

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

Se sia il _dob privato e Age pubblico è nullo o 0, si dispone di un altro problema tutti insieme. Si può sempre impostare DatadiNascita a DateTime.Today per impostazione predefinita in questo caso. Inoltre, se hai a disposizione solo l'età di un individuo, come farà a dire la loro DateOfBirth fino al giorno?

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top