[.NET] PropertyGrid 控制項的進階使用:自訂資料檢視與編輯器

最近這一個月事情還真不少,不斷的在嘴砲和務實的角色之間切換,也寫了不少的程式碼,而且為了因應今年 9/13-15 的微軟大拜拜 (Tech.days) 的課程,我還特別寫了支範例程式準備要在課堂上 demo 用,這支範例程式是 Windows Azure Platform 上的服務管理應用程式,核心均來自 Service Management APIs,很快的,就在 Tech.days 2011 Taiwan 研討會中將正式釋出...

最近這一個月事情還真不少,不斷的在嘴砲和務實的角色之間切換,也寫了不少的程式碼,而且為了因應今年 9/13-15 的微軟大拜拜 (Tech.days) 的課程,我還特別寫了支範例程式準備要在課堂上 demo 用,這支範例程式是 Windows Azure Platform 上的服務管理應用程式,核心均來自 Service Management APIs,很快的,就在 Tech.days 2011 Taiwan 研討會中將正式釋出。

不過在開發這支範例程式的同時,我卻有一個困擾,就是怎麼呈現這些資料,如果使用傳統的 Label + TextBox 方式,光是拉控制項和寫 event handler 就真的會累死人,而且 Windows Forms 的編輯器又不是很好用,有時拉一拉都會偏掉,當然這是操作習慣的問題啦。所以在使用者介面上我偷了個懶,用了 PropertyGrid 控制項,這樣我就只需要在物件中加說明即可,不需要再去拉一堆東西出來排列。當然啦,ListView 也許也可以,但因為一個物件只有一筆資料要顯示,所以我就直接使用 PropertyGrid 比較適合。

這是第一個版本的使用者介面:

WALazyboneConsole1

但總是覺得看起來沒 fu,所以又換了第二種:

WALazyboneConsole2

這樣就有 fu 多了吧。

畫面右邊就是一個 PropertyGrid,當我點選的項目中有保存資料物件時,就會將物件的資訊顯示在 PropertyGrid 中,程式碼也只要這樣:

private void tvHostingStorage_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e)
{
    if (e.Node.Tag == null)
    {
      
this.pgObjectProperty.SelectedObject = null;
        return;
    }

   this.pgObjectProperty.SelectedObject = e.Node.Tag;
}

 

而這個物件會自動將物件內的屬性拆解出來,並列舉到 Grid 中,如此一來我就不必再拉一堆東西了。

然而,李組長眉頭一皺,覺得事情並不單純。

如果說物件中的資料是一些經過加密或是轉換的 (ex: Base64 String),那麼 PropertyGrid 一樣會將資料原原本本的顯示出來,可是我們要的是經過處理的,不要直接顯示原本的資料在 Grid 內容,而是將它換個方式來顯示,例如:

image

在表上方的 Configuration 一項,原本是 Base64 編碼的 string,顯示起來就是一堆編碼的字串,但是實在是不好看,而且人家要看的應該是解出來的內容,而不是一串亂碼,所以我們必須要改掉這個預設的顯示方式,而用一個可以檢視正確內容的作法,同時也讓原本顯示的內容換一下,換成我們要的。再舉個例:

image

在表中的 PrimaryAccessKey 與 SecondaryAccessKey 都是亂碼字串,但是有意義的,只是我們還是不想讓它顯示原本的資料,所以我們仍然要將它改成我們要的顯示格式,而真正的內容,則是按下 "..." 的按鈕才可得到(資料內容因不便顯示,我已抹除):

image

 

而這樣的編輯器格式內建並沒有,必須要由開發人員自行設計與寫碼來建置。所以,我們還是捲起袖子來寫些程式吧。

首先,我們要先了解 PropertyGrid 是怎麼處理這個部份的,當 PropertyGrid 在物件中找到屬性有宣告 EditorAttribute 的時候,就會在編輯視窗中看到按鈕,而按鈕按下的反應則是透過 System.Drawing.Design 命名空間的 UITypeEditor 類別處理,因此我們要實作這個類別,告訴 PropertyGrid 編輯器的位置,在本例中,我們實作一個 WAStorageKeyTypeEditor 物件:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Design;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Windows.Forms.Design;

namespace MyEditor.TypeEditors
{
    public class WAStorageKeyTypeEditor : UITypeEditor
    {
        public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
        {
            IWindowsFormsEditorService formService = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService;

            using (WAStorageKeyViewer viewer = new WAStorageKeyViewer(value.ToString()))
            {
                formService.ShowDialog(viewer);
            }

            return value;
        }

        public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
        {
            return UITypeEditorEditStyle.Modal;
        }
    }
}

在 WAStorageKeyTypeEditor 中,我們實作了 EditValue() 以及 GetEditStyle() 兩種方法,EditValue() 會回傳由開發人員定義的編輯器所傳回的結果,而 GetEditStyle() 則是決定 PropertyGrid 怎麼顯示編輯器,可用的類型如下圖表格所示,本例是使用 UITypeEditorEditStyle.Modal,表示我們要顯示對話方塊:

image

而在 EditValue() 中,我們要驅動我們的對話盒來給使用者編輯資料用,因此我們在這裡要利用 System.Windows.Forms.Design 命名空間內的 IWindowsFormsEditoService (其實作為 System.Windows.Forms.PropertyGridInternal 命名空間的 PropertyGridView 類別,這是一個內部實作) 來操作使用者介面:

IWindowsFormsEditorService formService = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService;

using (WAStorageKeyViewer viewer = new WAStorageKeyViewer(value.ToString()))
{
   
formService.ShowDialog(viewer);
}

return value;

接著,我們設計一個對話盒 (程式中的 WAStorageKeyViewer 物件) 如下:

image

這個表單的程式很簡單,只是顯示資料而已,這個大家都會,就留給大家自己做了。

有了編輯器還不夠,因為 PropertyGrid 還是會顯示資料的值,我們並不想讓它顯示,而要改成我們自己的值,所以我們還要改寫一個地方:讓 PropertyGrid 讀由我們決定的值,這個功能需要透過實作 TypeConverter 基底類別來做,不過我們不需要從頭到實作到尾,而只要改繼承自 ExpandableObjectConverter,並覆寫必要的方法即可,所以程式碼並不難:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;

namespace MyEditor.Converters
{
    public class WAStorageKeyDisplayConverter : ExpandableObjectConverter
    {
        public override object ConvertTo(
            ITypeDescriptorContext context,
            System.Globalization.CultureInfo culture, object value, Type destinationType)
        {
            return "(Storage Key)";
        }
    }
}

有了這些類別後,我們就可以將它套用到要顯示到 PropertyGrid 的類別物件了:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Design;
using System.Linq;
using System.Text;

namespace MyEditor.Entity
{
    [DefaultProperty("ServiceName"), Description("Windows Azure Storage Service Profile")]
    public class StorageService
    {
        [Category("Profile"), Description("Service name of storage service."), Bindable(BindableSupport.No),
         DesignOnly(false)]
        public string ServiceName { get; set; }
        [Category("Profile"), Description("Service URL of storage service."), Bindable(BindableSupport.No),
         DesignOnly(false)]
        public string Url { get; set; }
        [Category("Property"), Description("Label of storage service."), Bindable(BindableSupport.No),
         DesignOnly(false)]
        public string Label { get; set; }
        [Category("Property"), Description("Description of storage service."), Bindable(BindableSupport.No),
         DesignOnly(false)]
        public string Description { get; set; }
        [Category("Property"), Description("Affinity Group of storage service."), Bindable(BindableSupport.No),
         DesignOnly(false)]
        public string AffinityGroup { get; set; }
        [Category("Property"), Description("Service Location of storage service."), Bindable(BindableSupport.No),
         DesignOnly(false)]
        public string Location { get; set; }
        [Category("Key"), Description("Primary access key of storage service."), Bindable(BindableSupport.No),
         DesignOnly(false),
Editor(typeof(TypeEditors.WAStorageKeyTypeEditor), typeof(UITypeEditor)),
         TypeConverter(typeof(Converters.WAStorageKeyDisplayConverter)),
         EditorBrowsable(EditorBrowsableState.Always)]
        public string PrimaryAccessKey { get; set; }
        [Category("Key"), Description("Secondary access key of storage service."), Bindable(BindableSupport.No),
         DesignOnly(false), Editor(typeof(TypeEditors.WAStorageKeyTypeEditor), typeof(UITypeEditor)),
         TypeConverter(typeof(Converters.WAStorageKeyDisplayConverter)),
         EditorBrowsable(EditorBrowsableState.Always)]
        public string SecondaryAccessKey { get; set; }
    }
}

這樣就大功告成了。

image

 

 

 

Reference:

http://msdn.microsoft.com/en-us/library/ms171840.aspx

http://www.codeproject.com/KB/miscctrl/bending_property.aspx