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.UpgradeAgeはを使用して変換する必要があるため、アップグレードの実行方法がわかりません(age) => DateTime.Now.AddYears(-age)。したがって、Nameプロパティのみがアップグレードされ、DateOfBirthの値はDefault(DateTime)になります。

ApplicationSettingsBase.Upgradeしたがって、必要に応じて値を変換するアップグレードルーチンをオーバーライドして提供したいと思います。しかし、私は3つの問題に遭遇しました。

  1. を使用して前のバージョンの値にアクセスしようとするとApplicationSettingsBase.GetPreviousVersion、戻り値は現在のバージョンのオブジェクトになります。このオブジェクトには、Ageプロパティがなく、DateOfBirthプロパティが空です(AgeをDateOfBirthに逆シリアル化できないため)。
  2. アップグレードしているアプリケーションのバージョンを見つける方法が見つかりませんでした。v1からv2へのアップグレード手順とv2からv3への手順がある場合、ユーザーがv1からv3にアップグレードする場合は、両方のアップグレード手順を順番に実行する必要がありますが、ユーザーがv2からアップグレードする場合は、必要なのは2番目のアップグレード手順を実行します。
  3. 以前のバージョンのアプリケーションが何であるかを知っていて、以前の構造のユーザー設定にアクセスできたとしても(たとえば、生のXMLノードを取得するだけで)、アップグレード手順を連鎖させたい場合は(問題2で説明)、中間値はどこに保存しますか?v2からv3にアップグレードする場合、アップグレード手順はv2から古い値を読み取り、それらをv3の厳密に型指定された設定ラッパークラスに直接書き込みます。しかし、v1からアップグレードする場合、アプリケーションにはv3のラッパークラスしかないため、v1からv2へのアップグレード手順の結果をどこに配置しますか?

アップグレードコードがuser.configファイルで直接変換を実行する場合、これらすべての問題を回避できると思いましたが、以前のバージョンのuser.configの場所を取得する簡単な方法は見つかりませんでした。これLocalFileSettingsProvider.GetPreviousConfigFileName(bool)は、プライベートメソッドであるためです。

アプリケーションバージョン間でタイプを変更するユーザー設定をアップグレードするためのClickOnce互換ソリューション、できればバージョンのスキップをサポートできるソリューション(たとえば、ユーザーがv2をインストールする必要なしにv1からv3にアップグレードする)を持っている人はいますか?

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 が存在するかどうかを確認し、存在しない場合は年齢から計算して保存し、次にアクセスするときに保存します。すでに生年月日があり、年齢フィールドは無視できます。

年齢フィールドを古いものとしてマークして、将来使用しないようにすることができます。

必要に応じて、個人クラスにある種のプライベート バージョン フィールドを追加して、それ自体がどのバージョンであると見なされるかに応じて、内部的に自分自身を処理する方法を知ることができます。

古いバージョンのデータを引き続きサポートする必要があるため、設計が完全ではないオブジェクトが必要になる場合があります。

于 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 に答える