1. 程式人生 > >[UWP]為附加屬性和依賴屬性自定義程式碼段(相容UWP和WPF)

[UWP]為附加屬性和依賴屬性自定義程式碼段(相容UWP和WPF)

1. 前言

之前介紹過依賴屬性附加屬性的程式碼段,這兩個程式碼段我用了很多年,一直都幫了我很多。不過這兩個程式碼段我也多年沒修改過,Resharper老是提示我生成的程式碼可以修改,它這麼有誠意,這次就只好從了它,順便簡單介紹下怎麼自定義程式碼段。

2. VisualStudio自帶程式碼段的問題

以依賴屬性為例,一個完整的依賴屬性應該包含以下部分:

  1. 註冊依賴屬性並生成依賴屬性識別符號。依賴屬性識別符號為一個public static readonly DependencyProperty欄位。依賴屬性識別符號的名稱必須為“屬性名+Property”。在PropertyMetadata中指定屬性預設值。

  2. 實現屬性包裝器。為屬性提供 get 和 set 訪問器,在Getter和Setter中分別呼叫GetValue和SetValue。Getter和Setter中不應該有其它任何自定義程式碼。

  3. 如果需要監視屬性值變更,可以在PropertyMetadata中定義一個PropertyChangedCallback方法。因為這個方法是靜態的,可以再實現一個同名的例項方法(可以參考ContentControl的OnContentChanged方法)。

public int MyProperty
{
    get { return (int)GetValue(MyPropertyProperty); }
    set { SetValue(MyPropertyProperty, value); }
}

// Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
public static readonly DependencyProperty MyPropertyProperty =
    DependencyProperty.Register("MyProperty", typeof(int), typeof(ownerclass), new PropertyMetadata(0));

如上面程式碼所示,VisualStudio自帶的依賴屬性的程式碼段propdp只實現了最基本的功能,PropertyChangedCallback等函式還得自己實現,而這部分也挺麻煩的。另外,ownerclass基本都是當前類的名字,沒有理由不使用當前類的名字作為預設值。

/// <summary>
/// 獲取或設定MyProperty的值
/// </summary>  
public int MyProperty
{
    get => (int)GetValue(MyPropertyProperty);
    set => SetValue(MyPropertyProperty, value);
}

/// <summary>
/// 標識 MyProperty 依賴屬性。
/// </summary>
public static readonly DependencyProperty MyPropertyProperty =
    DependencyProperty.Register(nameof(MyProperty), typeof(int), typeof(MainPage), new PropertyMetadata(default(int), OnMyPropertyChanged));

private static void OnMyPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{

    var oldValue = (int)args.OldValue;
    var newValue = (int)args.NewValue;
    if (oldValue == newValue)
        return;

    var target = obj as MainPage;
    target?.OnMyPropertyChanged(oldValue, newValue);
}

/// <summary>
/// MyProperty 屬性更改時呼叫此方法。
/// </summary>
/// <param name="oldValue">MyProperty 屬性的舊值。</param>
/// <param name="newValue">MyProperty 屬性的新值。</param>
protected virtual void OnMyPropertyChanged(int oldValue, int newValue)
{
}

上面是我自定義的程式碼段,改進了這些地方:

  • getter和setter使用了表示式主體;
  • DependencyProperty.Register的第一個引數使用了nameof()關鍵字代替了字串;
  • typeof(MainPage)這裡使用了程式碼段函式ClassName()直接獲取當前類的名稱;
  • 依賴屬性的預設值使用了default()關鍵字,因為絕大部分情況下依賴屬性的預設值就是資料型別的預設值,修改預設值的工作交給DefaultStyle的Setter;
  • 添加了相對完成的PropertyChangedCallback函式;

3. 如何自定義程式碼段

基本上,一個程式碼段就是一個XML檔案,

3.1 程式碼段的結構

<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
    <CodeSnippet Format="1.0.0">
        <Header>
            <Keywords>
                <Keyword>dp</Keyword>
            </Keywords>
            <SnippetTypes>
                <SnippetType>SurroundsWith</SnippetType>
            </SnippetTypes>
            <Title>Dependency Property</Title>
            <Author>dino.c</Author>
            <Description>For Dependency Property</Description>
            <HelpUrl>
            </HelpUrl>
            <Shortcut>dp</Shortcut>
        </Header>
        <Snippet>
            <References>
                <Reference>
                    <Assembly>
                    </Assembly>
                </Reference>
            </References>
            <Declarations>
                <Literal Editable="true">
                    <ID>PropertyType</ID>
                    <ToolTip>屬性型別</ToolTip>
                    <Default>int</Default>
                    <Function>
                    </Function>
                </Literal>
                ...
            </Declarations>
            <Code Language="csharp" Kind="method body">
                <![CDATA[     ...        ]]>
            </Code>
        </Snippet>
    </CodeSnippet>
</CodeSnippets>

如上所示,程式碼段定義XML中主要分成以下幾個部分:

  1. Header:包括Keyword、Shortcut等資訊。Author和Description等可有可無;
  2. Declarations:程式碼段中的變數;
  3. Code:程式碼段的程式碼;

3.2 程式碼段中的變數

在我定義的依賴屬性程式碼段中包含了三個變數:

<Literal Editable="true">
    <ID>PropertyType</ID>
    <ToolTip>屬性型別</ToolTip>
    <Default>int</Default>
    <Function>
    </Function>
</Literal>
<Literal Editable="true">
    <ID>MyProperty</ID>
    <ToolTip>屬性名</ToolTip>
    <Default>MyProperty</Default>
    <Function>
    </Function>
</Literal>
<Literal Editable="false">
    <ID>classname</ID>
    <ToolTip>類名</ToolTip>
    <Function>ClassName()</Function>
    <Default>ClassNamePlaceholder</Default>
</Literal>

其中classname不可編輯,它使用了ClassName()這個程式碼段函式,返回包含已插入程式碼段的類的名稱。其它可用的程式碼段函式可以參考這個頁面:程式碼段函式

引用變數的語法是$變數名$,如下所示:

public static readonly DependencyProperty $MyProperty$Property =
    DependencyProperty.Register(nameof($MyProperty$), typeof($PropertyType$), typeof($classname$), new PropertyMetadata(default($PropertyType$), On$MyProperty$Changed));

3.3 匯入程式碼段

在選單上選擇“工具->程式碼片段管理器”:

在“程式碼片段管理器”視窗中點選“匯入”,選中需要匯入的檔案後開啟“匯入程式碼片段”,選擇位置後點擊“完成”即可完成程式碼段匯入:

3.4 最終成果

依賴屬性的程式碼段:

<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
    <CodeSnippet Format="1.0.0">
        <Header>
            <Keywords>
                <Keyword>dp</Keyword>
            </Keywords>
            <SnippetTypes>
                <SnippetType>SurroundsWith</SnippetType>
            </SnippetTypes>
            <Title>Dependency Property</Title>
            <Author>dino.c</Author>
            <Description>For Dependency Property</Description>
            <HelpUrl>
            </HelpUrl>
            <Shortcut>dp</Shortcut>
        </Header>
        <Snippet>
            <References>
                <Reference>
                    <Assembly>
                    </Assembly>
                </Reference>
            </References>
            <Declarations>
                <Literal Editable="true">
                    <ID>PropertyType</ID>
                    <ToolTip>屬性型別</ToolTip>
                    <Default>int</Default>
                    <Function>
                    </Function>
                </Literal>
                <Literal Editable="true">
                    <ID>MyProperty</ID>
                    <ToolTip>屬性名</ToolTip>
                    <Default>MyProperty</Default>
                    <Function>
                    </Function>
                </Literal>
                <Literal Editable="false">
                    <ID>classname</ID>
                    <ToolTip>類名</ToolTip>
                    <Function>ClassName()</Function>
                    <Default>ClassNamePlaceholder</Default>
                </Literal>
            </Declarations>
            <Code Language="csharp" Kind="method body">
                <![CDATA[        
        /// <summary>
        /// 獲取或設定$MyProperty$的值
        /// </summary>  
        public $PropertyType$ $MyProperty$
        {
            get => ($PropertyType$)GetValue($MyProperty$Property);
            set => SetValue($MyProperty$Property, value);
        }

        /// <summary>
        /// 標識 $MyProperty$ 依賴屬性。
        /// </summary>
        public static readonly DependencyProperty $MyProperty$Property =
            DependencyProperty.Register(nameof($MyProperty$), typeof($PropertyType$), typeof($classname$), new PropertyMetadata(default($PropertyType$), On$MyProperty$Changed));

        private static void On$MyProperty$Changed(DependencyObject obj,DependencyPropertyChangedEventArgs args)
        {
            var oldValue = ($PropertyType$)args.OldValue;
            var newValue = ($PropertyType$)args.NewValue;
            if (oldValue == newValue)
              return;
            
            var target= obj as $classname$;
            target?.On$MyProperty$Changed(oldValue, newValue);
        }

        /// <summary>
        /// $MyProperty$ 屬性更改時呼叫此方法。
        /// </summary>
        /// <param name="oldValue">$MyProperty$ 屬性的舊值。</param>
        /// <param name="newValue">$MyProperty$ 屬性的新值。</param>
        protected virtual void On$MyProperty$Changed($PropertyType$ oldValue,$PropertyType$ newValue)
        {
        }]]>
            </Code>
        </Snippet>
    </CodeSnippet>
</CodeSnippets>

附加屬性的程式碼段:

<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
    <CodeSnippet Format="1.0.0">
        <Header>
            <Keywords>
                <Keyword>ap</Keyword>
            </Keywords>
            <SnippetTypes>
                <SnippetType>Expansion</SnippetType>
            </SnippetTypes>
            <Title>Attached Property</Title>
            <Author>dino.c</Author>
            <Description>For Attached Property</Description>
            <HelpUrl>
            </HelpUrl>
            <Shortcut>ap</Shortcut>
        </Header>
        <Snippet>
            <References>
                <Reference>
                    <Assembly>
                    </Assembly>
                </Reference>
            </References>
            <Declarations>
                <Literal Editable="true">
                    <ID>PropertyType</ID>
                    <ToolTip>屬性型別</ToolTip>
                    <Default>int</Default>
                    <Function>
                    </Function>
                </Literal>
                <Literal Editable="true">
                    <ID>MyProperty</ID>
                    <ToolTip>屬性名</ToolTip>
                    <Default>MyProperty</Default>
                    <Function>
                    </Function>
                </Literal>
                <Literal Editable="false">
                    <ID>classname</ID>
                    <ToolTip>類名</ToolTip>
                    <Function>ClassName()</Function>
                    <Default>ClassNamePlaceholder</Default>
                </Literal>
            </Declarations>
            <Code Language="csharp">
                <![CDATA[
        /// <summary>
        /// 從指定元素獲取 $MyProperty$ 依賴項屬性的值。
        /// </summary>
        /// <param name="obj">從中讀取屬性值的元素。</param>
        /// <returns>從屬性儲存獲取的屬性值。</returns>
        public static $PropertyType$ Get$MyProperty$(DependencyObject obj) => ($PropertyType$)obj.GetValue($MyProperty$Property);

        /// <summary>
        /// 將 $MyProperty$ 依賴項屬性的值設定為指定元素。
        /// </summary>
        /// <param name="obj">對其設定屬性值的元素。</param>
        /// <param name="value">要設定的值。</param>
        public static void Set$MyProperty$(DependencyObject obj, $PropertyType$ value) => obj.SetValue($MyProperty$Property, value);

        /// <summary>
        /// 標識 $MyProperty$ 依賴項屬性。
        /// </summary>
        public static readonly DependencyProperty $MyProperty$Property =
            DependencyProperty.RegisterAttached("$MyProperty$", typeof($PropertyType$), typeof($classname$), new PropertyMetadata(default($PropertyType$), On$MyProperty$Changed));


        private static void On$MyProperty$Changed(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            var oldValue = ($PropertyType$)args.OldValue;
            var newValue = ($PropertyType$)args.NewValue;
            if (oldValue == newValue)
              return;
              
            var target = obj as $classname$;
        }

        ]]>
            </Code>
        </Snippet>
    </CodeSnippet>
</CodeSnippets>

4. 結語

雖然這兩個程式碼段比較複雜,並不是每次建立依賴屬性都需要這麼完整,但刪除程式碼總比增加程式碼簡單得多,所以我多年來每次建立依賴屬性和附加屬性都是使用這兩個程式碼段。

WPF的依賴屬性可以十分複雜,但平時用不到這麼多功能,所以和UWP使用相同的程式碼段就夠了。

完整的程式碼段已上傳到 Github

5. 參考

程式碼段