最近這一個月事情還真不少,不斷的在嘴砲和務實的角色之間切換,也寫了不少的程式碼,而且為了因應今年 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 比較適合。
這是第一個版本的使用者介面:
但總是覺得看起來沒 fu,所以又換了第二種:
這樣就有 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 內容,而是將它換個方式來顯示,例如:
在表上方的 Configuration 一項,原本是 Base64 編碼的 string,顯示起來就是一堆編碼的字串,但是實在是不好看,而且人家要看的應該是解出來的內容,而不是一串亂碼,所以我們必須要改掉這個預設的顯示方式,而用一個可以檢視正確內容的作法,同時也讓原本顯示的內容換一下,換成我們要的。再舉個例:
在表中的 PrimaryAccessKey 與 SecondaryAccessKey 都是亂碼字串,但是有意義的,只是我們還是不想讓它顯示原本的資料,所以我們仍然要將它改成我們要的顯示格式,而真正的內容,則是按下 "..." 的按鈕才可得到(資料內容因不便顯示,我已抹除):
而這樣的編輯器格式內建並沒有,必須要由開發人員自行設計與寫碼來建置。所以,我們還是捲起袖子來寫些程式吧。
首先,我們要先了解 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,表示我們要顯示對話方塊:
而在 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 物件) 如下:
這個表單的程式很簡單,只是顯示資料而已,這個大家都會,就留給大家自己做了。
有了編輯器還不夠,因為 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; }
}
}
這樣就大功告成了。
Reference:
http://msdn.microsoft.com/en-us/library/ms171840.aspx
http://www.codeproject.com/KB/miscctrl/bending_property.aspx