銀の光と碧い空

クラウドなインフラとC#なアプリ開発の狭間にいるエンジニアの日々

Visual Studio SDKで設定ページに任意のユーザーコントロールを配置する

Visual Studio SDKで設定ページを作る場合、Visual Studioのメニューのツール>オプションに自分のカテゴリを追加することができます。この追加したカテゴリよく見ると、デフォルトっぽい画面と自前で作っているっぽい画面の二種類あることがわかります。

デフォルトの例 f:id:tanaka733:20180211003416p:plain

自前の例 f:id:tanaka733:20180211003433p:plain

デフォルトの設定ページの作り方はドキュメントがあるので、自前の画面の作り方を説明します。

オプション ページを作成する | Microsoft Docs

使うクラスはMicrosoft.VisualStudio.Shell.UIElementDialogPageクラスです。このクラスの存在はこのブログを読んで知りました。実際にVS2017で使うにはいくつか足りないところがあったので補足しています。

haacked.com

これを継承した適当なクラスを作ります。

using Microsoft.VisualStudio.Shell;
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Windows;

namespace MyVisualStudio.Vsix.Options
{
    [ClassInterface(ClassInterfaceType.AutoDual)]
    [Guid("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")] //GUIDは適当な値
    public sealed class MySetting : UIElementDialogPage
    {
        private readonly MyViewModel viewModel;
        private readonly MyView view;

        protected override UIElement Child => view;

        public OpenShiftMasterEndpointSetting()
        {
            viewModel = new MyViewModel();
            view = new MyView
            {
                DataContext = viewModel
            };
        }

        public override void LoadSettingsFromStorage()
        {
            //ここを同期的に完了を待ったほうがいいかどうかは謎
            viewModel.LoadAsync().GetAwaiter().GetResult();
            base.LoadSettingsFromStorage();
        }

        protected override void OnClosed(EventArgs e)
        {
            //設定画面でキャンセルが押されたときなど。
            //保存に失敗したとか、未保存状態のときにクローズをキャンセルする手はありそう
            base.OnClosed(e);
        }

        public override void SaveSettingsToStorage()
        {
            //保存処理
            viewModel.SaveAsync().GetAwaiter().GetResult();
            base.SaveSettingsToStorage();
        }

        public override void ResetSettings()
        {
            base.ResetSettings();
        }

        protected override void OnActivate(CancelEventArgs e)
        {
            //設定ダイアログで該当カテゴリに移動したとき
            base.OnActivate(e);
        }

        protected override void OnDeactivate(CancelEventArgs e)
        {
            //設定ダイアログで該当カテゴリから移動するとき
            base.OnDeactivate(e);
        }
    }
}

Childプロパティを継承して自前のユーザーコントロールのインスタンスを返すようにします。この例では、ViewModelもこのクラスが参照を持っていますが、その辺はお好きに。設定データの読み込みと保存はLoadSettingsFromStorageSaveSettingsToStorageメソッドをオーバーライドして行います。なんとなく処理の完了を待機した方がいいのでしていますが、あまり自信はないです。

次にVisual Studio SDKプロジェクトを作ったときのPackageクラスの属性に次のように設定のカテゴリ、ページ名を追加します。

    [ProvideOptionPage(typeof(MySetting ), "MySettingCategory", "MySettingPage", 0, 0, true)]
    public sealed class MyVSPackage : Package

あとは、適当にViewとViewModelを渡せばいいわけですが、保存処理についてです。自前で適当なところに保存してもいいのでしょうが、今回はMicrosoft.VisualStudio.Settings.WritableSettingsStoreクラスを使いました。最初に紹介したブログによるとVSIXをバージョンアップしても設定をきちんと保持できるとあるので、今度検証してみましょう。WritableSettingsStoreはいくつかのプリミティブなオブジェクトしか保存するメソッドを用意していないので、今回は安易にJSONに変換して保存いています。

using Microsoft.VisualStudio.Settings;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Settings;
using Newtonsoft.Json;
using Reactive.Bindings;
using System;
using System.Diagnostics;
using System.Linq;

namespace MyVisualStudio.Vsix.Models
{

    internal class SettingModel
    {
        const string CollectionPath = "MySetting";
        const string PropertyName = "AllMySetting";

        readonly WritableSettingsStore writableSettingsStore;

        internal SettingModel()
        {
            var shellSettingsManager = new ShellSettingsManager(ServiceProvider.GlobalProvider);
            writableSettingsStore = shellSettingsManager.GetWritableSettingsStore(SettingsScope.UserSettings);

            Load();
        }

        internal void Load()
        {
            try
            {
                if (writableSettingsStore.PropertyExists(CollectionPath, PropertyName))
                {
                    var json = writableSettingsStore.GetString(CollectionPath, PropertyName);
                    var entites = JsonConvert.DeserializeObject<MyEntity>(json);
                    //entites を適当に渡す
                }
            }
            catch (Exception ex)
            {
                Debug.Fail(ex.Message);
            }
        }

        internal void Save()
        {
            try
            {
                if (!writableSettingsStore.CollectionExists(CollectionPath))
                {
                    writableSettingsStore.CreateCollection(CollectionPath);
                }

                //entitiesを適当にとってくる
                var jsonString = JsonConvert.SerializeObject(entites);
                writableSettingsStore.SetString(CollectionPath, PropertyName, jsonString);
            }
            catch (Exception ex)
            {
                Debug.Fail(ex.Message);
            }
        }
    }
}