自訂 [屬性] 視窗
您可以在 Visual Studio 中,自訂特定領域語言 (DSL) 中屬性視窗的外觀和行為。 在 DSL 定義中,您會在每個網域類別上定義網域屬性。 根據預設,當您在圖表或模型總管中選取類別的執行個體時,每個網域屬性都會列在屬性視窗中。 這可讓您查看和編輯網域屬性的值,即使您尚未將其對應至圖表上的圖形欄位也一樣。
名稱、描述和類別
名稱和顯示名稱。 在網域屬性的定義中,屬性的 [顯示名稱] 是出現在屬性視窗中執行階段的名稱。 相較之下,當您撰寫程式碼來更新屬性時,會使用「名稱」。 「名稱」必須是正確的 CLR 英數字元名稱,但「顯示名稱」可以包含空格。
當您在 DSL 定義中設定屬性的「名稱」時,其「顯示名稱」會自動設定為「名稱」的複本。 如果您撰寫 Pascal 大小寫的名稱,例如 「FuelGauge」,「顯示名稱」會自動包含空格:「Fuel Gauge」。 不過,您可以將「顯示名稱」明確設定為另一個值。
描述。 網域屬性的描述會出現在兩個地方:
當使用者選取屬性時,在屬性視窗底部。 您可以使用該描述向使用者說明屬性所代表的內容。
在產生的程式碼中。 如果您使用文件設備來擷取 API 文件,則會在 API 中顯示為此屬性的描述。
類別。 類別是 [屬性] 視窗中的標題。
公開樣式功能
圖形元素的某些動態功能可以表示或公開為網域屬性。 使用者可以更新以這種方式公開的功能,而且程式碼可以更輕鬆地更新。
以滑鼠右鍵按一下 DSL 定義中的圖形類別,指向 [新增公開],然後選擇功能。
在圖形上,您可以公開 FillColor、OutlineColor、TextColor、OutlineDashStyle、OutlineThickness 和 FillGradientMode 屬性。 在連接器上,您可以公開 Color,
TextColor、DashStyle 和 Thickness 屬性。 在圖表上,您可以公開 FillColor 和 TextColor 屬性。
轉送:顯示相關元素的屬性
當您的 DSL 使用者選取模型中的元素時,該元素的屬性會顯示在屬性視窗中。 不過,您也可以顯示指定相關元素的屬性。 如果您已定義一組一起運作的元素,這會很有用。 例如,您可以定義主要元素和選擇性外掛程式元素。 如果主要元素對應至圖形,而另一個元素未對應,則看到其所有屬性就如同在一個元素上一樣,很有用。
此效果稱為屬性轉送,而且會在數種情況下自動發生。 在其他情況下,您可以藉由定義網域類型描述元來達成屬性轉送。
預設屬性轉送案例
當使用者選取圖形或連接器,或 Explorer 中的元素時,下列屬性會顯示在 [屬性] 視窗中:
在模型元素的網域類別上定義的網域屬性,包括基底類別中定義的網域屬性。 例外狀況是您已將 Is Browsable 設定為
False
的網域屬性。透過多重性為 0..1 之關聯性連結的元素名稱。 即使您尚未定義關聯性的連接器對應,這也會提供方便的方法,讓您查看選擇性連結的元素。
以元素為目標之內嵌關聯性的網域屬性。 因為內嵌關聯性通常不會明確顯示,這可讓使用者查看其屬性。
在選取的圖形或連接器上定義的網域屬性。
新增屬性轉送
若要轉送屬性,您可以定義網域類型描述元。 如果您在兩個網域類別之間有網域關聯性,您可以使用網域類型描述元,將第一個類別中的網域屬性設定為第二個網域類別中網域屬性的值。 例如,如果您在 Book 網域類別與 Author 網域類別之間有關聯性,您可以使用網域類型描述元,在使用者選取 Book 時,將 Book Author 的 Name 屬性顯示在 [屬性] 視窗中。
注意
屬性轉送只會影響使用者編輯模型時的 [屬性] 視窗。 它不會在接收類別上定義網域屬性。 如果您想要存取 DSL 定義或程式碼其他部分的轉送網域屬性,您必須存取轉送元素。
下列程序假設您已建立 DSL。 前幾個步驟摘要說明必要條件。
從另一個元素轉送屬性
建立包含至少兩個類別的特定領域語言工具解決方案,在此範例中稱為 Book 和 Author。 Book 與 Author 之間應該有某種關聯性。
來源角色的多重性 (Book 端的角色) 應該是 0..1 或 1..1,因此每個 Book 都有一個 Author。
在 DSL Explorer 中,以滑鼠右鍵按一下 [Book] 網域類別,然後按一下 [新增 DomainTypeDescriptor]。
名稱為 [自訂屬性描述元的路徑] 的節點會出現在 [自訂類型描述元] 節點下方。
以滑鼠右鍵按一下 [自訂類型描述元] 節點,然後按一下 [新增 PropertyPath]。
新的屬性路徑會出現在 [自訂屬性描述元的路徑] 節點下。
選取新的屬性路徑,然後在 [屬性] 視窗中,將 [屬性路徑] 設定為適當模型元素的路徑。
您可以按一下該屬性右邊的向下箭號,以編輯樹狀檢視中的路徑。 如需網域路徑的詳細資訊,請參閱網域路徑語法。 當您編輯它時,路徑應該類似於 BookReferencesAuthor.Author/!Author。
將 [屬性] 設定為 Author 的 Name 網域屬性。
將 [顯示名稱] 設定為作者名稱。
轉換所有範本,建置並執行 DSL。
在模型圖表中,建立書籍、作者,並使用參考關聯性連結它們。 選取書籍元素,並在 [屬性] 視窗中,除了書籍屬性以外,您應該看到作者名稱。 變更連結作者的名稱,或將書籍連結至不同的作者,並觀察書籍的作者名稱變更。
自訂屬性編輯器
屬性視窗會為每個網域屬性的類型提供適當的預設編輯體驗。 例如,針對列舉類型,使用者會看到下拉式清單,而針對數值屬性,使用者可以輸入數字。 這只適用於內建類型。 如果您指定外部類型,使用者將可以看到屬性的值,但無法編輯它。
不過,您可以指定下列編輯器和類型:
另一個搭配標準類型使用的編輯器。 例如,您可以指定字串屬性的檔案路徑編輯器。
網域屬性的外部類型,及其編輯器。
.NET 編輯器,例如檔案路徑編輯器,或者您可以建立自己的自訂屬性編輯器。
外部類型與字串等類型的轉換,其具有預設編輯器。
在 DSL 中,外部類型是任何非簡單類型 (例如布林值或 Int32) 或 String 的類型。
定義具有外部類型的網域屬性
在 [方案總管] 中,在 Dsl 專案中,新增包含外部類型的組件 (DLL) 參考。
組件可以是 .NET 元件,或您提供的組件。
除非您已經這麼做,否則請將類型新增至 [網域類型] 清單。
開啟 DslDefinition.dsl,然後在 [DSL 總管] 中,以滑鼠右鍵按一下根節點,然後按一下 [加入新的外部類型]。
新的項目會出現在 [網域類型] 節點下。
警告
功能表項目位於 DSL 根節點上,而非 [網域類型] 節點上。
在 [屬性] 視窗中,設定新類型的名稱和命名空間。
以一般方式將網域屬性新增至網域類別。
在 [屬性] 視窗中,從 [類型] 欄位中的下拉式清單中選取外部類型。
在這個階段,使用者可以檢視屬性的值,但無法加以編輯。 顯示的值是從
ToString()
函式取得。 您可以撰寫程式碼來設定屬性的值,例如在命令或規則中。
設定屬性編輯器
將 CLR 屬性新增至網域屬性,格式如下:
[System.ComponentModel.Editor (
typeof(AnEditor),
typeof(System.Drawing.Design.UITypeEditor))]
您可以使用 [屬性] 視窗中的 [自訂屬性] 項目,在屬性 (property) 上設定屬性 (attribute)。
AnEditor
的類型必須衍生自第二個參數中指定的類型。 第二個參數應該是 UITypeEditor 或 ComponentEditor。 如需詳細資訊,請參閱EditorAttribute。
您可以指定自己的編輯器或 .NET 編輯器,例如 FileNameEditor 或 ImageEditor。 例如,使用下列程序,讓使用者可以在其中輸入檔案名稱的屬性。
定義檔案名稱網域屬性
使用您的 DSL 定義將網域屬性新增至網域類別。
選取新屬性。 在 [屬性] 視窗中的 [自訂屬性] 欄位中,輸入下列屬性。 若要輸入此屬性,請按一下省略符號 [...],然後分別輸入屬性名稱和參數:
[System.ComponentModel.Editor ( typeof(System.Windows.Forms.Design.FileNameEditor) , typeof(System.Drawing.Design.UITypeEditor))]
將網域屬性的類別保留為字串的預設設定。
若要測試編輯器,請確認使用者可以開啟檔案名稱編輯器來編輯您的網域屬性。
按 CTRL+F5 或 F5。 在偵錯解決方案中,開啟測試檔案。 建立網域類別的元素,並加以選取。
在 [屬性] 視窗中,選取網域屬性。 值欄位會顯示省略符號 [...]。
按一下省略符號。 檔案對話方塊隨即出現。 選取檔案,並關閉對話方塊。 檔案路徑現在是網域屬性的值。
定義您自己的屬性編輯器
您可以定義自己的編輯器。 您可以這麼做,讓使用者編輯您已定義的類型,或以特殊方式編輯標準類型。 例如,您可以允許使用者輸入代表公式的字串。
您可以撰寫衍生自 UITypeEditor 的類別來定義編輯器。 您的類別必須覆寫:
EditValue,以與使用者互動,並更新屬性值。
GetEditStyle,以指定您的編輯器是否會開啟對話方塊或提供下拉式功能表。
您也可以提供將顯示在屬性方格中屬性值的圖形表示。 若要這樣做,請覆寫 GetPaintValueSupported
和 PaintValue
。 如需詳細資訊,請參閱UITypeEditor。
注意
在 Dsl 專案的個別程式碼檔案中,新增程式碼。
例如:
internal class TextFileNameEditor : System.Windows.Forms.Design.FileNameEditor
{
protected override void InitializeDialog(System.Windows.Forms.OpenFileDialog openFileDialog)
{
base.InitializeDialog(openFileDialog);
openFileDialog.Filter = "Text files(*.txt)|*.txt|All files (*.*)|*.*";
openFileDialog.Title = "Select a text file";
}
}
若要使用此編輯器,請將網域屬性的自訂屬性設定為:
[System.ComponentModel.Editor (
typeof(MyNamespace.TextFileNameEditor)
, typeof(System.Drawing.Design.UITypeEditor))]
如需詳細資訊,請參閱UITypeEditor。
提供值的下拉式清單
您可以為使用者提供值的下拉式清單以供選擇。
注意
這項技術提供可在執行階段變更的值清單。 如果您想要提供不會變更的清單,請考慮改用列舉類型作為網域屬性的類型。
若要定義標準值清單,可在網域屬性中新增 CLR 屬性,其格式如下:
[System.ComponentModel.TypeConverter
(typeof(MyTypeConverter))]
定義衍生自 TypeConverter 的類別。 在 Dsl 專案的個別檔案中,新增程式碼。 例如:
/// <summary>
/// Type converter that provides a list of values
/// to be displayed in the property grid.
/// </summary>
/// <remarks>This type converter returns a list
/// of the names of all "ExampleElements" in the
/// current store.</remarks>
public class MyTypeConverter : System.ComponentModel.TypeConverter
{
/// <summary>
/// Return true to indicate that we return a list of values to choose from
/// </summary>
/// <param name="context"></param>
public override bool GetStandardValuesSupported
(System.ComponentModel.ITypeDescriptorContext context)
{
return true;
}
/// <summary>
/// Returns true to indicate that the user has
/// to select a value from the list
/// </summary>
/// <param name="context"></param>
/// <returns>If we returned false, the user would
/// be able to either select a value from
/// the list or type in a value that is not in the list.</returns>
public override bool GetStandardValuesExclusive
(System.ComponentModel.ITypeDescriptorContext context)
{
return true;
}
/// <summary>
/// Return a list of the values to display in the grid
/// </summary>
/// <param name="context"></param>
/// <returns>A list of values the user can choose from</returns>
public override StandardValuesCollection GetStandardValues
(System.ComponentModel.ITypeDescriptorContext context)
{
// Try to get a store from the current context
// "context.Instance" returns the element(s) that
// are currently selected i.e. whose values are being
// shown in the property grid.
// Note that the user could have selected multiple objects,
// in which case context.Instance will be an array.
Store store = GetStore(context.Instance);
List<string> values = new List<string>();
if (store != null)
{
values.AddRange(store.ElementDirectory
.FindElements<ExampleElement>()
.Select<ExampleElement, string>(e =>
{
return e.Name;
}));
}
return new StandardValuesCollection(values);
}
/// <summary>
/// Attempts to get to a store from the currently selected object(s)
/// in the property grid.
/// </summary>
private Store GetStore(object gridSelection)
{
// We assume that "instance" will either be a single model element, or
// an array of model elements (if multiple items are selected).
ModelElement currentElement = null;
object[] objects = gridSelection as object[];
if (objects != null && objects.Length > 0)
{
currentElement = objects[0] as ModelElement;
}
else
{
currentElement = gridSelection as ModelElement;
}
return (currentElement == null) ? null : currentElement.Store;
}
}