たいちょーの雑記

ぼくが3日に一度くらい雑記をかくところ

VisualStudioの設定ダイアログを作る

またVSIX

ほぼ備忘録になってる
まぁぼくは忘れやすいのでOKOK

それにしてもsurface3みたいなスペックだと実験インスタンス起動するのに時間かかってつらい

Visual Studioの設定ダイアログを作る

メニューバーの[ツール][オプション]で開くやつ正式名称はわからない・・・

d1
こういうやつ

これを自分の作ったVSIX用に作る

参考はここ

Creating an Options Page

プロジェクトを作る

今回は空のVSPackageに作っていく。自分のVSIXに実装するときは新しくVSPackageを追加する必要はない

まずは[ファイル]-[新規作成]-[プロジェクト]-[Visual C#]-[Extensibility]-[VSIX Project]を選んでプロジェクトを作る

できたらソリューションエクスプローラーで右クリックして[追加]-[新しい項目]
d2

Visual Studio Packageを選ぶ

d2

テンプレートが展開されるまでちょっと待つ

実装する

ダイアログページを作るにはSystem.ComponentModel名前空間が必要なのでusingする

using System.ComponentModel;

次にオプションで設定したい値を持ってるクラスを作る。今回はOptionDialogクラスにした。ネーミングセンス・・・・

public class OptionDialog : DialogPage {

}

このクラスにはDialogPageクラスを継承する必要があるのに気を付けよう

次にPackageクラスを継承してるクラスに属性を追加する。たぶんこのVSIXはオプションページがありますよ~~~ってことなんだと思うたぶん

/*他の属性は略*/
[ProvideOptionPage(typeof(OptionDialog),"Test Dialog","General",0,0,true)]
public sealed class Dialog : Package {
    /*略*/
}

d2

ちなみにこの属性の引数はこんな感じらしい。categoryNameはオプションウィンドウのサイドバーの項目で、pageNameはその下の全般とかのことっぽい

今のままだと設定したい項目が0なので追加していく今回はIntを1つもたせることにした

public class OptionDialog : DialogPage {
    [Category("かてごりー")]
    [DisplayName("ひょうじめい")]
    [Description("せつめい")]
    public int Num { get; set; }
}

DialogPageではプロパティとかにした値がそのまま設定項目として表示される。XmlとかJsonシリアライズしたりするのと似ていてちょっとうれしかった

実は実装はこれで終わりで、あとはビルドするだけでちゃんと動くのだ!最高!

d2

プロパティにつけていた属性の意味は上の画像と照らし合わせればわかると思う。これは僕も驚いたところだけど、これ値を変えてから再起動しても値が変更されたままになる。どこに書き出しているかはリファレンスをちゃんと読んでないのでわからない(読む気もない)。まあとにかく簡単である

xmlに書き出してみる

xmlファイルの準備

こういうのはjsonとかxmlに書き出しておきたいっていうのがあります(あります)
なので今回はxmlに書き出すのを目標にしてみる

まずはソリューションエクスプローラーからxmlを追加しておく。そして一度自力で書くか、適当なコンソールアプリケーションを書いてこのxmlを書いておく。書くのはOptionDialogクラスと同じようなプロパティを持ったクラスをシリアライズしたやつ。こういう時にC#インタラクティブが便利だと思うんだけどXmlSerializerがroslynでは死ぬのでダメだった・・・ざんねん・・・
なので自分はいつも以下みたいなのを書いてる。面倒

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;

namespace ConsoleApp2 {
    class Program {
        static void Main(string[] args) {
            var path = @"c:\users\user\source\repos\TestDialog\TestDialog\Resources\config.xml";
            var xml = new XmlSerializer(typeof(Store));
            using(var sw=new StreamWriter(path, false, Encoding.UTF8)) {
                xml.Serialize(sw, new Store { Num = 0 });
            }
        }

    }
        public class Store {
            public int Num { get; set; }
        }

}

追加したxmlはプロパティウィンドウでビルドアクションをコンテンツに、Include in VSIXをTrueにしておくこと。これが分からなくてINF分失った

Load/Saveイベント

またイベントの話だけどダイアログが開かれたとき、または閉じられたときにLoadとSaveが行われる。DialogPageにはこれをするLoad/SaveSettingsToStorageメソッドがあるのでオーバーライドしてやるといい

パスの取得

さっきビルドした拡張機能がどこにインストールされてるのか探してみたらC:\Users\USER\AppData\Local\Microsoft\VisualStudio\15.0_29065328Exp\Extensions\xztaityozx\TestDialog\1.0にあった。上の設定をしていればこの下にxmlも置かれるのでこのパスが分からないといけない。でもユーザー名とか変わるし決め打ちはできないのでどうにかして実行中に取得したい

ggっていたらドンピシャなstackoverflowを見つけた
stackoverflow.com

CodeBase?ってやつを取得すると実行中のファイルがわかるっぽい
そしてどうやらPackageクラスにも同じような値が入っているらしいけど何階層か上までしかわからなかったのでダメだった

なのでこんな風にかいた

public class OptionDialog : DialogPage {
    [Category("かてごりー")]
    [DisplayName("ひょうじめい")]
    [Description("せつめい")]
    public int Num { get; set; }

    public string Path {
        get {
            var codebase = typeof(Dialog).Assembly.CodeBase;
            var uri = new Uri(codebase, UriKind.Absolute);
            return System.IO.Path.GetDirectoryName(uri.LocalPath) + "\\Resources\\config.xml";
        }
    }

    public override void LoadSettingsFromStorage() {

        using (var sr = new StreamReader(Path, Encoding.UTF8)) {
            var xmlSerializer = new System.Xml.Serialization.XmlSerializer(typeof(Store));
            var config = (Store)xmlSerializer.Deserialize(sr);
            this.Num = config.Num;
        }
    }

    public override void SaveSettingsToStorage() {
        var config = new Store {
            Num = this.Num
        };
        using (var sw = new StreamWriter(Path, false, Encoding.UTF8)) {
            var xmlSerializer = new System.Xml.Serialization.XmlSerializer(typeof(Store));
            xmlSerializer.Serialize(sw, config);
        }
    }
}
public class Store {
    public int Num { get; set; }
}

これでxmlに書き出せるようになったのでVSIX中の任意の場所から同じようにxmlにアクセスすれば色々できてうれしい

それでは最後にソースコードをはって終わります

using System;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.OLE.Interop;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.Win32;
using System.ComponentModel;
using System.IO;
using System.Text;

namespace TestDialog {
    /// <summary>
    /// This is the class that implements the package exposed by this assembly.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The minimum requirement for a class to be considered a valid package for Visual Studio
    /// is to implement the IVsPackage interface and register itself with the shell.
    /// This package uses the helper classes defined inside the Managed Package Framework (MPF)
    /// to do it: it derives from the Package class that provides the implementation of the
    /// IVsPackage interface and uses the registration attributes defined in the framework to
    /// register itself and its components with the shell. These attributes tell the pkgdef creation
    /// utility what data to put into .pkgdef file.
    /// </para>
    /// <para>
    /// To get loaded into VS, the package must be referred by &lt;Asset Type="Microsoft.VisualStudio.VsPackage" ...&gt; in .vsixmanifest file.
    /// </para>
    /// </remarks>
    [PackageRegistration(UseManagedResourcesOnly = true)]
    [InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)] // Info on this package for Help/About
    [Guid(Dialog.PackageGuidString)]
    [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "pkgdef, VS and vsixmanifest are valid VS terms")]
    [ProvideOptionPage(typeof(OptionDialog),"Test Dialog","General",0,0,true)]
    public sealed class Dialog : Package {
        /// <summary>
        /// Dialog GUID string.
        /// </summary>
        public const string PackageGuidString = "3e22428e-3305-4a4b-9c3e-11c54c2bef28";

        /// <summary>
        /// Initializes a new instance of the <see cref="Dialog"/> class.
        /// </summary>
        public Dialog() {
            // Inside this method you can place any initialization code that does not require
            // any Visual Studio service because at this point the package object is created but
            // not sited yet inside Visual Studio environment. The place to do all the other
            // initialization is the Initialize method.
        }

       #region Package Members

        /// <summary>
        /// Initialization of the package; this method is called right after the package is sited, so this is the place
        /// where you can put all the initialization code that rely on services provided by VisualStudio.
        /// </summary>
        protected override void Initialize() {
            base.Initialize();
        }

       #endregion
    }

    public class OptionDialog : DialogPage {
        [Category("かてごりー")]
        [DisplayName("ひょうじめい")]
        [Description("せつめい")]
        public int Num { get; set; }

        public string Path {
            get {
                var codebase = typeof(Dialog).Assembly.CodeBase;
                var uri = new Uri(codebase, UriKind.Absolute);
                return System.IO.Path.GetDirectoryName(uri.LocalPath) + "\\Resources\\config.xml";
            }
        }

        public override void LoadSettingsFromStorage() {
            
            using (var sr = new StreamReader(Path, Encoding.UTF8)) {
                var xmlSerializer = new System.Xml.Serialization.XmlSerializer(typeof(Store));
                var config = (Store)xmlSerializer.Deserialize(sr);
                this.Num = config.Num;
            }
        }

        public override void SaveSettingsToStorage() {
            var config = new Store {
                Num = this.Num
            };
            using (var sw = new StreamWriter(Path, false, Encoding.UTF8)) {
                var xmlSerializer = new System.Xml.Serialization.XmlSerializer(typeof(Store));
                xmlSerializer.Serialize(sw, config);
            }
        }
    }
    public class Store {
        public int Num { get; set; }
    }
}