WPF之屬性


屬性

.NET Framework中的屬性又稱為CLR屬性(CLR,Common Language Runtime),既可以說CLR屬性是private字段的安全訪問包裝(Get/Set方法),也可以說一個private字段在后台支持(back)一個CLR屬性。
C#代碼中的屬性的編譯結果是兩個方法,再多實例方法也只有一個拷貝,CLR屬性並不會增加內存的負擔。屬性僅僅是個語法糖衣(Syntax Sugar)

相關資料參考:屬性(C# 編程指南)

依賴屬性(Dependency Property)

在WPF中,微軟推出了“依賴屬性”這個新概念。依賴屬性就是一種可以自己沒有值,並能通過使用Binding從數據源獲得值(依賴在別人身上)的屬性。擁有依賴屬性的對象被稱為“依賴對象”。

與傳統的CLR屬性和面向對象思想相比依賴屬性有很多新穎之處,其中包括:

  • 節省實例對內存的開銷。
  • 屬性值可以通過Binding依賴在其他對象上。

依賴屬性對內存的使用方式

傳統的.NET開發中,一個對象所占用的內存空間在調用new操作符進行實例化的時候就已經決定了,而WPF允許對象在被創建的時候並不包含用於存儲數據的空間(即字段所占用的空間)、只保留在需要用到數據時能夠獲得默認值、借用其他對象數據或實時分配空間的能力一一這種對象就稱為依賴對象(Dependency Object)而它這種實時獲取數據的能力則依靠依賴屬性(Dependency Property)來實現。

在WPF開發中,必須使用依賴對象作為依賴屬性的宿主,使二者結合起來,才能形成完整的Binding目標被數據所驅動。

在WPF系統中,依賴對象的概念被DependencyObject類所實現,依賴屬性的概念則由DependencyProperty類所實現

DependencyObject具有GetValue和SetValue兩個方法:

public class DependencyObject : DispatcherObject
{
    public void ClearValue(DependencyProperty dp)
    {
        //...
    }
    public void SetValue(DependencyProperty dp, object value);
    {
        //...
    }
}

這兩個方法都以DependencyProperty對象為參數,GetValue方法通過DependencyProperty對象獲取數據;SetValue通過DependencyProperty對象存儲值——正是這兩個方法把DependencyObject和DependencyProperty緊密結合在一起。

DependencyObject是WPF系統中相當底層的一個基類,繼承樹如下所示:

WPF的所有UI控件都是依賴對象,WPF的類庫在設計時充分利用了依賴屬性的優勢,UI控件的絕大多數屬性都已經依賴化了。

聲明和使用依賴屬性

准備好一個界面,如下所示:

<StackPanel>
    <TextBox x:Name="textBox1" BorderBrush="Black" Margin="5"/>
    <TextBox x:Name="textBox2" BorderBrush="Black" Margin="5"/>
    <Button Content="OK" Margin="5" Click="Button_Click"/>
</StackPanel>

聲明依賴屬性

DependencyProperty必須以DependencyObject為宿主、借助它的SetValue和GetValue 方法進行寫入與讀取。因此,**想使用自定義的DependencyProperty,宿主一定是DependencyObject 的派生類。

DependencyProperty實例的聲明特點很鮮明——引用變量由public static readonly三個修飾符修飾,實例並非使用new操作符得到而是使用DependencyProperty.Register方法生成,代碼如下:

public class Student : DependencyObject
{
    public static readonly DependencyProperty NameProperty=DependencyProperty.Register("Name", typeof(string), typeof(Student));
}

命名約定:成員變量的名字需要加上Property后綴以表明它是一個依賴屬性

上面使用的是DependencyProperty.Register方法參數最少、最簡單的一個重載,分析一下這3個參數:

  • 第1個參數為string類型,用這個參數來指明以哪個CLR屬性作為這個依賴屬性的包裝器,或者說此依賴屬性支持(back)的是哪個CLR屬性。將來會使用名為Name的CLR屬性來包裝它,所以這個參數被賦值為Name。
  • 第2個參數用來指明此依賴屬性用來存儲什么類型的值,學生的姓名是string類型,所以這個參數被賦值為typeof(string)。
  • 第3個參數用來指明此依賴屬性的宿主是什么類型,或者說DependencyProperty.Register方法將把這個依賴屬性注冊關聯到哪個類型上。本例中的意圖是為Student類准備一個可依賴的名稱屬性,所以這個參數被賦值為typeof(Student)。

這里有三點需要注意:

  • 依賴屬性的包裝器(Wrapper)是一個CLR屬性,實際上的依賴屬性是那個由public static readonly修體的Dependency Property實例,有沒有包裝器這個依賴屬性都存在
  • 包裝器的作用是以“實例屬性”的形式向外界暴露依賴屬性,這樣一個依賴屬性才能成為數據源的一個Path。
  • 注冊依賴屬性時使用的第二個參數是一個數據類型,這個數據類型也是包裝器的數據類型,它的全稱應該是“依賴屬性的注冊類型”,一般情況下也會把這個類型類型稱為“依賴屬性的類型”。

使用依賴屬性

理解了依賴屬性聲明變量和創建實例的過程,就可以嘗試使用它了。

依賴屬性的“屬性”

依賴屬性首先是屬性,嘗試用依賴屬性來存儲值並把值順利讀取出來
UI中OK按鈕的Click事件處理器代碼如下:

private void Button_Click(object sender, RoutedEventArgs e)
{
    //創建一個Student 實例並使用變量stu引用
    Student stu = new Student(); 
    //調用SetValue方法把textBox1.Text 屬性的值存儲進依賴屬性
    stu.SetValue(Student.NameProperty, this. textBox1.Text); 
    //使用GetValue方法把值讀取出來,注意SetValue的返回值是object類型,要進行適當的類型轉換
    textBox2.Text = (string)stu.GetValue(Student.NameProperty);
}

Student類的SetValue和GetValue方法繼承自DependencyObject類,效果如下所示:

依賴屬性的“依賴”性

讓textBox1作為數據來源,把Student實例作為數據的目標,讓Student實例依賴在textBoxl上(僅僅是為了展示依賴屬性的“依賴”功能,現實工作中幾乎從來不這么做),代碼如下:

Student stu;
public MainWindow()
{
    InitializeComponent();

    stu = new Student();
    //創建一個Binding的實例,讓textBox1作為數據源對象並從其Text屬性中獲取數據
    Binding binding = new Binding("Text") { Source = textBox1 };
    //使用BindingOperations類的SetBinding方法指定將stu對象借助剛剛聲明的Binding實例依賴在textBox1上
    BindingOperations.SetBinding(stu, Student.NameProperty, binding);
}

private void Button_Click(object sender, RoutedEventArgs e)
{
    //彈出對話框顯示依賴屬性的值
    MessageBox.Show(stu.GetValue(Student.NameProperty).ToString());
}

注:依賴屬性即使沒有CLR屬性作為其外包裝也可以很好地工作。

如果想把textBox1和textBox2關聯起來,代碼如下:

Binding binding = new Binding("Text") { Source = textBox1 };
textBox2.SetBinding(TextBox.TextProperty, binding);

DependencyObject類(Student類的基類)沒有SetBinding方法,SetBinding 方法是FrameworkElement類的方法。FrameworkElement是個相當高層的類(比UIElement類還高),這從側面表明微軟希望能夠SetBinding(即作為數據目標)的對象是UI元素。

FrameworkElement類的SetBinding方法僅僅對BindingOperations的SetBinding方法做了一個簡單的封裝,代碼如下:

public class FrameworkElement : UIElement //…
{
    //…
    public BindingExpressionBase SetBinding(DependencyProperty dp, BindingBase binding)
    {
        return BindingOperations.SetBinding(this, dp, binding);
    }
    //…
}

添加CRL屬性外包裝:

現在使用的依賴屬性依靠SetValue和GetValue兩個方法進行對外界的暴露,而且在使用GetValue的時候還需要進行一次數據類型的轉換,所以大多數情況下會為依賴屬性添加一個CRL屬性外包裝,代碼如下:

public class Student : DependencyObject
{
    //CLR屬性包裝器
    public string Name
    {
        get { return (string)GetValue(NameProperty); }
        set { SetValue(NameProperty, value); }
    }
    
    public static readonly DependencyProperty NameProperty=DependencyProperty.Register("Name", typeof(string), typeof(Student));
}

通過CLR屬性包裝訪問依賴屬性,代碼如下:

private void Button_Click(object sender, RoutedEventArgs e)
{
    Student stu = new Student(); 
    stu.Name = this.textBox1.Text; 
    this.textBox2.Text = stu.Name;
}

依賴對象可以通過Binding依賴在其他對象上,即依賴對象是作為數據的目標而存在的。為依賴對象的依賴屬性添加CLR屬性包裝就相當於為依賴對象准備了用於暴露數據的Binding Path,現在的依賴對象已經具備了扮演數據源和數據目標雙重角色的能力。盡管Student類沒有實現INotifyPropertyChanged接口,當屬性的值發生改變時與之關聯的Binding對象依然可以得到通知,依賴屬性默認帶有這樣的功能,天生就是合格的數據源

使用依賴屬性形成Binding鏈

向FrameworkElement類借用一下它的SetBinding方法、升級一下Student類,代碼如下:

public class Student : DependencyObject
{
    //CLR屬性包裝器
    public string Name
    {
        get { return (string)GetValue(NameProperty); }
        set { SetValue(NameProperty, value); }
    }
    
    //依賴屬性
    public static readonly DependencyProperty NameProperty=DependencyProperty.Register("Name", typeof(string), typeof(Student));

    //SetBinding 包裝
    public BindingExpressionBase SetBinding(DependencyProperty dp, BindingBase binding)
    {
        return BindingOperations.SetBinding(this,dp,binding);
    }
}

使用Binidng把Student對象關聯到textBox1上,再把textBox2關聯到Student對象上形成Binding鏈,代碼如下:

Student stu;
public MainWindow()
{
    InitializeComponent();
    stu = new Student(); 
    stu.SetBinding(Student.NameProperty, new Binding("Text") { Source = textBox1 }); 
    textBox2.SetBinding(TextBox.TextProperty, new Binding("Name") { Source = stu });
}
private void Button_Click(object sender, RoutedEventArgs e)
{
    //便於監視依賴屬性的值
    MessageBox.Show(stu.GetValue(Student.NameProperty).ToString());
}

運行程序,當在textBoxl中輸入字符的時候,textBox2就會同步顯示,此時Student對象的Name屬性值也同步變化了。

注:在一個類中聲明依賴屬性時並不需要手動進行聲明、注冊並使用CLR屬性封裝,只需要輸入propdp連按兩次Tab鍵,一個標准的依賴屬性(帶CLR屬性包裝)就聲明好了,繼續按動Tab鍵,可以在提示環境中修改依賴屬性的各個參數。

依賴屬性的DefaultMetadata屬性

在自動生成的代碼中,DependencyProperty.Register使用的是帶4個參數的重載,前3個參數與前面介紹的一致,第4個參數的類型是PropertyMetadata類

第4個參數的作用是給依賴屬性的DefaultMetadata屬性賦值,DefaultMetadata的作用是向依賴屬性的調用者提供一些基本信息,信息包括:

  • CoerceValueCallback:依賴屬性值被強制改變時此委托會被調用,此委托可關聯一個影響函數。
  • DefaultValue:依賴屬性未被顯式賦值時,若讀取之則獲得此默認值,不設此值會拋出異常。
  • IsSealed:控制PropertyMetadata的屬性值是否可以更改,默認值為true
  • PropertyChangedCallback:依賴屬性的值被改變之后此委托會被調用,此委托可關聯一個影響函數。

依賴屬性的DefaultMetadata只能通過Register方法的第4個參數進行賦值,而且一旦賦值就不能改變(DefaultMetadata是個只讀屬性)。如果想用新的PropertyMetadata 替換這個默認的Metadata,需要使用DependencyProperty.OverrideMetadata方法

依賴屬性值存取的秘密

依賴對象的依賴屬性是一個static對象,調用依賴對象的SetValue方法時值不可能是保存在static對象里,重點分析DependencyProperty.Register方法和DependencyObject.SetValue方法和DependencyObject.GetValue方法

DependencyProperty.Register方法

DependencyProperty.Register方法由名稱可知,不僅要創建DependencyProperty實例,還要對它進行“注冊”。

閱讀源碼會發現DependencyProperty類具有這樣一個成員:

private static Hashtable PropertyFromName = new Hashtable();

一旦程序運行,就會有這樣一個全局的Hashtable存在,這個Hashtable就是用來注冊DependencyProperty實例的地方

在源碼中,所有的DependencyProperty.Register方法重載最后都歸結為對DependencyProperty.RegisterCommon方法的調用(可以把RegisterCommon理解為Register方法的“完整版”),RegisterCommon方法的源碼如下:

/*
 * RegisterCommon方法的前4個參數與前面分析過的Register方法一致
 */
private static DependencyProperty RegisterCommon(string name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata, ValidateValueCallback validateValueCallback)
{
    /*
     * FromNameKey 是一個,NET Framework內部數據類型。它的構造器代碼如下:
     * public FromNameKey(string name,Type ownerType) 
     * {
     *     _name = name;
     *     _ownerType = ownerType;
     *     hashCode = _name.GetHashCode()^_ownerType.GetHashCode();
     * }
     * 
     * 並且override有其GetHashCode方法:
     * public override int GetHashCode()
     * {
     *     return _hashCode;
     * }
     * 
     * 
     * 由上面的代碼可知:FromNamekey對象(也就是變量key)的hash code實際上是RegisterCommon第1個參數(CLR屬性名字符串)的hash code與第3個參數(宿主類型)的hash code做異或運算得來的。
     * 
     */
    FromNameKey key = new FromNameKey(name, ownerType);
    lock (Synchronized)
    {
        /*
         * 每對“CLR屬性名一宿主類型”所決定的DependencyProperty實例是唯一的。
         * 如果嘗試使用同一個CLR屬性名字和同一個宿主類型進行注冊,程序會拋出異常。
         */
        if (PropertyFromName.Contains(key))
        {
            throw new ArgumentException(SR.Get(SRID.PropertyAlreadyRegistered, name, ownerType.Name));
        }
    }

    /*
     * RegisterCommon 檢查程序員是否提供了PropertyMetadate,如果沒有提供則為之准備一個默認的PropertyMetadate實例。
     */

    // 為所有類型建立默認metadata(如果未提供)
    if (defaultMetadata == null)
    {
        defaultMetadata = AutoGeneratePropertyMetadata(propertyType, validateValueCallback, name, ownerType);
    }
    else // Metadata對象已經提供
    {
        // 如果未指定defaultValue,則自動生成一個
        if (!defaultMetadata.DefaultValueWasSet())
        {
            defaultMetadata.DefaultValue = AutoGenerateDefaultValue(propertyType);
        }

        ValidateMetadataDefaultValue(defaultMetadata, propertyType, name, validateValueCallback);
    }

    // 創建DependencyProperty的實例
    DependencyProperty dp = new DependencyProperty(name, propertyType, ownerType, defaultMetadata, validateValueCallback);

    // Seal (null means being used for default metadata, calls OnApply)
    defaultMetadata.Seal(dp, null);

    if (defaultMetadata.IsInherited)
    {
        dp._packedData |= Flags.IsPotentiallyInherited;
    }

    if (defaultMetadata.UsingDefaultValueFactory)
    {
        dp._packedData |= Flags.IsPotentiallyUsingDefaultValueFactory;
    }


    // Map owner type to this property
    // Build key
    lock (Synchronized)
    {
        //DependencyProperty實例被注冊進Hashtable中(Hashtable會自動調用key的GetHashcode 方法獲取其hash code):
        PropertyFromName[key] = dp;
    }


    if (TraceDependencyProperty.IsEnabled)
    {
        TraceDependencyProperty.TraceActivityItem(
            TraceDependencyProperty.Register,
            dp,
            dp.OwnerType);
    }

    //生成的DependencyProperty實例被當作返回值交還
    return dp;
}

用一句話概括DependencyProperty對象的創建與注冊,那就是:創建一個DependencyProperty 實例並用它的CLR屬性名和宿主類型名生成hash code,最后把hash code和DependencyProperty 實例作為Key-Value對存入全局的、名為PropertyFromName的Hashtable中。這樣,WFP屬性系統通過CLR屬性名和宿主類型名就可以從這個全局的Hashtable中檢索出對應的DependencyProperty實例。

把DependencyProperty實例注冊進全局Hashtable時使用的key由CLR屬性名哈希值和宿主類型哈希值經過運算得到,但這並不是DependencyProperty實例的哈希值。每個DependencyProperty實例都具有一個名為GilobalIndex的int 類型屬性,GlobalIndex的值是經過一些算法處理得到的,確保了每個DependencyProperty實例的Giloballndex是唯一的

DependencyProperty的GetHashCode方法亦被重寫:

public override int GetHashCode() 
{
    return GlobaIndex;
}

所以,GlobalIndex屬性值也就是DependencyProperty實例的哈希值——這一點非常重要,因為通過這個值就可以直接檢索到某個DependencyProperty實例

DependencyObject.GetValue方法

GetValue方法源碼如下:

public object GetValue(DependencyProperty dp)
{
    this.VerifyAccess();

    if (dp == null)
    {
        throw new ArgumentNullException("dp");
    }
    
    return GetValueEntry(
            LookupEntry(dp.GlobalIndex),
            dp,
            null,
            RequestFlags.FullyResolved).Value;
}

方法的前幾行是為了校驗傳入參數的有效性,只有return一句才是核心內容,return語句展開可以寫成這樣:

Entrylndex entrylndex = LookupEntry(dp.Globallndex);
EffectiveValueEntry vaueEnry = GetValueEntry(entrylndex, dp,null, RequestFlags.FullyResolved);
return valueEntry.Value;

Entry是“入口”的意思,WPF的依賴屬性系統在存放值的時候會把每個有效值存放在EffectiveValueEntryy類的實例里,每個實例都有自己的入口——檢索算法只要找到這個入口、走進入口就能拿到依賴屬性的值。
EffectiveValueEntry的所有構造器都包含一個DependencyProperty類型的參數,每個EectiveValueEntry都關聯着一個DependencyProperty
EffectiveValueEntry類具有一個名為PropertyIndex的屬性,這個屬性的值實際上就是與之關聯的DependencyProperty的GlobalIndex屬性值

在DependencyObject類的源碼中可以找到這樣一個成員變量:

//此DependencyObject的有效值緩存
//這是一個使用DP.GlobalIndex排序的數組,這種排序是通過插入排序算法來維持的
private EffectiveValueEntry[] _effectiveValues;

這個數組向我們提示了依賴屬性存儲值的秘密——每個Dependencyobject實例都自帶一個EffectiveValueEntry類型數組,當某個依賴屬性的值要被讀取時,算法就會從這個數組中去檢索值,如果數組中沒有包含這個值,算法會返回依賴屬性的默認值(這個值由依賴屬性的DefaultMetadata來提供)。

由上可知,被static關鍵字所修飾的依賴屬性對象其作用是用來檢索真正的屬性值而不是存儲值;被用做檢索鍵值的實際上是依賴屬性的GlobalIndex屬性(本質是其hash code,而 hash code又由其CLR包裝器名和宿主類型名共同決定),為了保證GlobalIndex屬性值的穩定性,聲明的時候又使用了readonly關鍵字進行修飾

實際工作中,依賴屬性的值除了可能存儲在EffectiveValueEntry數組或由默認值提供外,還有很多途徑可以獲得,如元素的Style或Theme。WPF對依賴屬性值的讀取優先級由先到后依次是:
(1)WPF屬性系統強制值。
(2)由動畫過程控制的值。
(3)本地變量值(存儲在EffectiveValueEntry數組中)。
(4)由上級元素的Template設置的值。
(5)由隱式樣式(Implicit Style)設置的值。
(6)由樣式之觸發器(Style Trigger)設置的值。
(7)由模板之觸發器(Template Trigger)設置的值。
(8)由樣式之設置器(Style Setter)設置的值。
(9)由默認樣式(Default Style)設置的值,默認模式其實就是由主題(Theme)指定的模式。
(10)由上級元素繼承而來的值。
(11)默認值,來源於依賴屬性的元數據(metadata)。

DependencyObject.SetValue方法

SetValue方法源碼如下:

public void SetValue(DependencyProperty dp, object value)
{
    // 驗證調用線程是否有權訪問此對象,只有dispatcher線程可以訪問DispatcherObject
    this.VerifyAccess();

    // 緩存此方法無論如何需要獲取的metadata對象
    PropertyMetadata metadata = SetupPropertyChange(dp);

    // 進行標准屬性設置
    SetValueCommon(dp, value, metadata, false /* coerceWithDeferredReference */, false /* coerceWithCurrentValue */, OperationType.Unknown, false /* isInternal */);
}

賦值流程主要有這樣幾個操作:

  • 檢查值是不是DependencyProperty.UnsetValue,是則說明調用者的意圖是清空現有的值,程序會調用ClearValueCommon方法來清空現有的值。
  • 檢查EffectiveValueEntry數組中是否已經存在相應依賴屬性的位置,有則把舊值改寫為新值,沒有則新建EffectiveValueEntry對象並存儲新值。只有被用到的值才會被放進這個列表,WPF系統用算法(時間)換取了對內存(空間)的節省
  • 調用UpdateEffectiveValue對新值做一些相應處理。

附加屬性(Attached Properties)

附加屬性的含義

一個屬性本來不屬於某個對象,但由於某種需求而被后來附加上,也就是把對象放入一個特定環境后對象才具有的屬性(表現出來就是被環境賦予的屬性)就稱為附加屬性(Attached Properties)。附加屬性的作用是將屬性與數據類型(宿主)解耦,讓數據類型的設計更加靈活。

在Grid里對一個TextBox定位,XAML代碼如下:

<Grid ShowGridLines="True">
    <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition/>
        <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition/>
        <RowDefinition/>
    </Grid.RowDefinitions>
    <TextBox Background="Lime" Grid.Column="1" Grid.Row="1" />
</Grid>

如果TextBox被放置在Canvas里,XAML代碼如下:

<Canvas Margin="10">
    <TextBox Background="Lime" Width="200" Canvas.Top="0"/>
    <TextBox Background="Lime" Width="200" Canvas.Top="30"/>
    <TextBox Background="Lime" Width="200" Canvas.Top="60"/>
</Canvas>

如果TextBox被放在DockPanel里,XAML代碼如下:

<DockPanel LastChildFill="False">
    <TextBox Background="Orange" DockPanel.Dock="Top"/>
    <TextBox Background="Orange" DockPanel.Dock="Bottom"/>
    <TextBox Background="Green" Width="80" DockPanel.Dock="Left"/>
    <TextBox Background="Green" Width="80" DockPanel.Dock="Right"/>
</DockPanel>

放在StackPanel里最簡單,XAML代碼如下:

<StackPanel Margin="10,5">
    <TextBox Background="LightBlue" Margin="0,5"/>
    <TextBox Background="LightBlue" Margin="0,5"/>
    <TextBox Background="LightBlue" Margin="0,5"/>
</StackPanel>

TextBox控件的設計者不可能知道控件發布后程序員是把它放在Grid里還是Canvas里(甚至是以后版本將推出的新布局里),所以也不可能為TextBox准備諸如Column、Row或者Left、Top這類屬性。直接讓布局來決定一個TextBox用什么屬性來設置它的位置,放在Grid里就讓Grid為它附加上Column和Row屬性,放在Canvas 里就讓Canvas為它附加上Top、Left等屬性,放在DockPanel里就讓DockPanel為它附加Dock屬性。

聲明、注冊和使用

附加屬性的本質就是依賴屬性,二者僅在注冊和包裝器上有一點區別。前面用於快速創建依賴屬性的snippet是propdp,這里用於快速創建附加屬性的snippet是propa

自定義類之間使用附加屬性

以人在學校里會獲得年級和班級兩個屬性為例,人放在學校里會獲得年級和班級兩個屬性說明年級和班級兩個屬性是由學校附加給人的,這兩個屬性的真實所有者(宿主)應該是學校。

准備一個名為School的類,並讓它繼承DependencyObject類,完成附加屬性的框架,代碼如下:

class School : DependencyObject
{
    public static int GetGrade(DependencyObject obj)
    {
        return (int)obj.GetValue(GradeProperty); 
    }

    public static void SetGrade(DependencyObject obj, int value) 
    {
        obj.SetValue(GradeProperty, value);
    }

    public static readonly DependencyProperty GradeProperty = 
        DependencyProperty.RegisterAttached("Grade", typeof(int), typeof(School), new UIPropertyMetadata(0));
} 

可明顯看出,GradeProperty就是一個DependencyProperty類型成員變量,聲明時一樣使用public static readonly 三個關鍵字共同修飾,唯一的不同就是注冊附加屬性使用的是名為RegisterAttached的方法,但參數卻與使用Register方法無異
附加屬性的包裝器也與依賴屬性不同——依賴屬性使用CLR屬性對GetValue和SetValue兩個方法進行包裝,附加屬性則使用兩個方法分別進行包裝(為了在使用的時候保持語句行文上的通暢)。

准備一個派生自DependencyObject、名為Human的類:

class Human:DependencyObject
{
   
}

在UI上准備一個Button,Click事件的處理器代碼如下:

private void Button_Click(object sender, RoutedEventArgs e)
{
    Human human = new Human();
    School.SetGrade(human, 6); 
    int grade = School.GetGrade(human); 
    MessageBox.Show(grade.ToString());
}

點擊按鈕,效果如下:

這一過程與前面依賴屬性保存值的過程別無二致——值仍然被保存在Human實例的EffectiveValueEntry數組里,只是用於在數組里檢索值的依賴屬性(即附加屬性)並不以Human類為宿主而是寄宿在School類里——反正CLR屬性名和宿主類型名只用來生成hash code和Globallndex。

如何在XAML和C#代碼中直接為附加屬性賦值

XAML代碼如下:

<Grid ShowGridLines="True">
    <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition/>
        <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition/>
        <RowDefinition/>
    </Grid.RowDefinitions>
    <Button Content="OK" Grid.Column="1" Grid.Row="1"/>
</Grid>

等效C#代碼如下:

Grid grid = new Grid() { ShowGridLines = true }; 

grid.ColumnDefinitions.Add(new ColumnDefinition()); 
grid.ColumnDefinitions.Add(new ColumnDefinition()); 
grid.ColumnDefinitions.Add(new ColumnDefinition()); 

grid.RowDefinitions.Add(new RowDefinition()); 
grid.RowDefinitions.Add(new RowDefinition()); 
grid.RowDefinitions.Add(new RowDefinition());

Button button = new Button(){ Content = "OK"}; 
Grid.SetColumn(button, 1); 
Grid.SetRow(button, 1); 

grid.Children.Add(button);
this.Content = grid;

使用Binding依賴在其他對象的數據上

附加屬性的本質是依賴屬性一—附加屬性也可以使用Binding依賴在其他對象的數據上

窗體使用Canvas布局,兩個Slider用來控制矩形在Canvas中的橫縱坐標,效果如下:

XAML代碼如下:

<Canvas>
    <Slider x:Name="sliderX" Canvas.Top="10" Canvas.Left="10" Width="260" Minimum="50" Maximum="200"/>
    <Slider x:Name="sliderY" Canvas.Top="40" Canvas.Left="10" Width="260" Minimum="50" Maximum="200"/>
    <Rectangle x:Name="rect" Fill="Blue" Width="30" Height="30" Canvas.Left="{ Binding ElementName=sliderX, Path=Value}" Canvas.Top="{ Binding ElementName=sliderY, Path=Value}"/>
</Canvas>

等效C#代碼(僅Binding部分)如下:

// 設置Binding 
this.rect.SetBinding(Canvas.LeftProperty,new Binding("Value") { Source = sliderX });
this.rect.SetBinding(Canvas.TopProperty, new Binding("Value") { Source = sliderY });

由此可見,在使用Binding時除了宿主類型稍有不同外沒有任何區別


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM