みなさん PropertyGrid 使ってますか?プロパティ名と値を一覧にして閲覧/入力させてくれるコントロールです。Visual Studio のプロパティでも使われているアイツですね。Windows Forms と言えば DataGridView がすべて!みたいな風潮も多少なり (謎) ありますが、私はこの PropertyGrid の方が知ったときの衝撃は大きかったです。とてつもなく便利で、今も業務でよく利用しています。
PropertyGrid における読み取り専用
PropertyGrid を読み取り専用にする方法はいくつかあります。私が把握している範囲では以下の 3 つです。
- Setter プロパティのアクセシビリティを private にする
- プロパティに ReadOnlyAttribute を付ける
- クラスに ReadOnlyAttribute を付ける
ここでちょっと不思議なのが PropertyGrid 自体に ReadOnly プロパティのような読み取り専用化するための機能がないということです。TextBox や ComboBox、DataGridView などの入力可能なコントロールには何らか読み取り専用化するプロパティがあり、アプリケーション実行時に動的に入力可否を切り替えることが可能です。ですが、上記の通り PropertyGrid にはその機能がありませんし、先に挙げた 3 つの方法はすべてコンパイル時に状態が決まっているので動的な挙動の変更はできません。稀にこれができなくてとても困ります。
PropertyGrid を動的に読み取り専用にすることを考えた場合、最も単純な方法は ReadOnly 属性が付いたクラスと付いていないクラスのふたつを用意し、必要に応じて SelectedObject で切り替えることでしょう。以下のような感じです。
class Person { public string Name { get; set; } public int Age { get; set; } } [ReadOnly(true)] class ReadOnlyPerson { public string Name { get; set; } public int Age { get; set; } }
var person = (何かの条件) ? new Person() : new ReadOnlyPerson(); this.propertyGrid.SelectedObject = person;
...確かにできますが、完全に同じプロパティを持つクラスをふたつ作るなんて保守性もヘッタクレもありません。流石にコレは NG。なので動的に属性を付与してしまえばいいじゃないか!と考えるワケです。
動的な属性付与
特定のインスタンスや、クラスに対して動的に属性を付ける場合は TypeDescriptor クラスを利用します。ボタンクリック時に動的に読み取り専用属性を追加/削除をする場合のサンプルは、以下のような感じになります。(すでに PropertyGrid にインスタンスが設定されているものとする)
private TypeDescriptionProvider provider; //--- 状態記憶 private void ReadOnlyButton_Click(object sender, EventArgs e) { //--- インスタンスに動的に読み取り専用属性を付与 this.provider = TypeDescriptor.AddAttributes(this.propertyGrid.SelectedObject, new ReadOnlyAttribute(true)); this.propertyGrid.Refresh(); //--- 再読み込みさせる } private void EditableButton_Click(object sender, EventArgs e) { //--- インスタンスに動的に付与した属性を削除 TypeDescriptor.RemoveProvider(this.provider, this.propertyGrid.SelectedObject); this.provider = null; this.propertyGrid.Refresh(); }
案外簡単です。
汎用化
しかしこんな実装を毎度するわけにはいきません。なので ReadOnlyPropertyGrid として汎用化してしまいましょう。
public class ReadOnlyPropertyGrid : PropertyGrid { //--- インスタンスと属性適用状態のペア private class Pair { public TypeDescriptionProvider Provider { get; set; } public object Instance { get; set; } } //--- ペアの保持 private IReadOnlyList<Pair> Pairs { get; set; } //--- 読み取り専用にするかどうか public bool IsReadOnly { get { return this.isReadOnly; } set { if (this.isReadOnly == value) return; if (value) this.EnableReadOnly(); else this.DisableReadOnly(); this.Refresh(); this.isReadOnly = value; } } private bool isReadOnly = false; //--- インスタンスの変更時 protected override void OnSelectedObjectsChanged(EventArgs e) { base.OnSelectedObjectsChanged(e); if (this.IsReadOnly) { this.DisableReadOnly(); this.EnableReadOnly(); this.Refresh(); } } //--- 読み取り専用化 private void EnableReadOnly() { if (this.Pairs != null) return; var attribute = new ReadOnlyAttribute(true); this.Pairs = this.SelectedObjects .Select(x => new Pair { Provider = TypeDescriptor.AddAttributes(x, attribute), Instance = x, }) .ToArray(); } //--- 読み取り専用解除 private void DisableReadOnly() { if (this.Pairs == null) return; foreach (var pair in this.Pairs) TypeDescriptor.RemoveProvider(pair.Provider, pair.Instance); this.Pairs = null; } }
このようにしておけば、あとは好きなタイミングで IsReadOnly プロパティを設定するだけです。とても簡単ですね!
//--- 読み取り専用にする this.propertyGrid.SelectedObject = new Person(){ Name = "xin9le", Age = 30 }; this.propertyGrid.IsReadOnly = true; //--- 途中のインスタンス変更もOK this.propertyGrid.SelectedObject = new Person(){ Name = "Anders Hejlsberg", Age = 54 };