1. 程式人生 > >XAML屬性賦值轉換之謎(WPF XAML語法解密)

XAML屬性賦值轉換之謎(WPF XAML語法解密)

XAML與XML類似,就是XML延伸過來的。為了更好的表達一些功能,WPF對XML做了擴充套件,有些功能是WPF在後臺悄悄的替你做了。有時候,雖然實現了某個功能,但是對實現原理還是很茫然。今天就講講XAML中賦值操作。

1 通過型別轉換賦值

賦值是最簡單最常見的操作,舉例:

 <Button  Width="200" Height="100">
 </Button>

這裡把Width值賦值為200;用程式碼實現賦值,則為Button.With = 200; 這種賦值操作很直接,大家都能理解。但是仔細想想,感覺有點不對勁。XAML表示式Width="200",這裡200是字串,Width型別是double。字串200怎麼就轉換成double了!你會說,200很明顯可以轉換為double型別,有什麼大驚小怪的!

有時,程式實現的邏輯操作很傻瓜,人很容易理解的事,程式並不一定能理解。需要你告訴XAML編譯器,怎麼把字串型轉換成double型。確實有 一個轉換類悄悄的把字串型轉換成了double型。

通過元檔案,可以查到Width屬性定義。

//
        // 摘要:
        //     獲取或設定元素的寬度。
        //
        // 返回結果:
        //     元素的寬度,單位是與裝置無關的單位(每個單位 1/96 英寸)。預設值為 System.Double.NaN。此值必須大於等於 0.0。有關上限資訊,請參見“備註”。
        [Localizability(LocalizationCategory.None, Readability = Readability.Unreadable)]
        [TypeConverter(
typeof(LengthConverter))] public double Width { get; set; }
Width屬性定義[TypeConverter(typeof(LengthConverter))]。這句話就表明width轉換型別是LengthConverter。當XAML編譯器看到Width賦值操作,就會呼叫LengthConverter。輸入是字串,返回就是double。
你可能感覺到,對這個屬性講解有點囉嗦。我這裡是想告訴你:幾乎所有的賦值操作,都需要這種轉換。
引申: 更深一步講,如果我們定義了一個屬性,這個屬性是一個複雜的型別。在XAML如何賦值? 比如自己定義了型別如下:
public class MyPointItem
 {
        public double Latitude { get; set; }
        public double Longitude { get; set; }
 }

 有一個類包含此屬性:

  public class MyClass
 {
  public MyPointItem Item { get; set; }
 }

在XAML語法中如何對Item賦值,XAML語法只認識字串型。這時需要參考上文Width處理方式。需要自己定義些轉換類。定義一個型別繼承TypeConverter,實現裡面的函式。

比如這樣賦值MyClass.Item = "123,456";你需要告訴編譯器,如何將"123,456"轉化成型別MyPointItem。這裡字串用逗號分隔,你可以用別的符號分隔;比如“#”,只要你的轉換函式能處理就行。完整的處理函式如下:

//定義轉換型別
    public class MyPointItemConverter : TypeConverter
    {
        public override bool CanConvertFrom( ITypeDescriptorContext context, Type sourceType)
        {
            if (sourceType is string)
                return true;
            return base.CanConvertFrom(context, sourceType);
        }

        public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
        {
            if (destinationType is MyPointItem)
                return true;

            return base.CanConvertTo(context, destinationType);
        }

        public override object ConvertFrom(ITypeDescriptorContext context,
                    CultureInfo culture, object value)
        {
            if (value is string)
            {
                try
                {
                    return MyPointItem.Parse(value as string);
                }
                catch (Exception ex)
                {
                    throw new Exception(string.Format("Cannot convert '{0}' ({1}) because {2}", value, value.GetType(), ex.Message), ex);
                }
            }

            return base.ConvertFrom(context, culture, value);
        }


        public override object ConvertTo(ITypeDescriptorContext context,
            CultureInfo culture, object value, Type destinationType)
        {
            if (destinationType == null)
                throw new ArgumentNullException("destinationType");

            MyPointItem gpoint = value as MyPointItem;

            if (gpoint != null)
                if (this.CanConvertTo(context, destinationType))
                    return gpoint.ToString();

            return base.ConvertTo(context, culture, value, destinationType);
        }
    }

    //自定義型別
    [TypeConverter(typeof(MyPointItemConverter))]
    public class MyPointItem
    {
        public double Latitude { get; set; }
        public double Longitude { get; set; }

        internal static MyPointItem Parse(string data)
        {
            if (string.IsNullOrEmpty(data))
                return new MyPointItem();

            string[] items = data.Split(','); //用逗號分隔,和XAML賦值中字串分隔符保持一致
            if (items.Count() != 2)
                throw new FormatException("should have both latitude and longitude");

            double lat, lon;
            try
            {
                lat = Convert.ToDouble(items[0]);
            }
            catch (Exception ex)
            {
                throw new FormatException("Latitude value cannot be converted", ex);
            }

            try
            {
                lon = Convert.ToDouble(items[1]);
            }
            catch (Exception ex)
            {
                throw new FormatException("Longitude value cannot be converted", ex);
            }

            return new MyPointItem() { Latitude=lat, Longitude=lon };
        }
    }

轉換型別不是萬能的: 只有型別轉換,也會遇到難以處理的情況。比如MyClass.Item = "null"。我的意思是將Item賦值為null。但是編譯不會這麼處理,仍然會呼叫轉換型別MyPointItemConverter,結果就會丟擲異常!WPF為此又引入了擴充套件識別符號的概念。

2 擴充套件識別符號

擴充套件識別符號有特殊的語法,如果屬性賦值為null,語法如下:
MyClass.Item ="{x:Null}"; 這裡的Null其實是一個型別,繼承自MarkupExtension;
//
    // 摘要:
    //     實現 XAML 標記以返回 null 物件,可使用該物件在 XAML 中將值顯式設定為 null。
    [MarkupExtensionReturnType(typeof(object))]
    [TypeForwardedFrom("PresentationFramework, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")]
    public class NullExtension : MarkupExtension
    {
        //
        // 摘要:
        //     初始化 System.Windows.Markup.NullExtension 類的新例項。
        public NullExtension();

        //
        // 摘要:
        //     提供要用作值的 null 作為此標記擴充套件的輸出。
        //
        // 引數:
        //   serviceProvider:
        //     可為標記擴充套件實現提供服務的物件。
        //
        // 返回結果:
        //     空引用。
        public override object ProvideValue(IServiceProvider serviceProvider);
    }
MyClass.Item ="{x:Null}"這句話的意思就是:編譯器生成型別NullExtension,呼叫函式ProvideValue,將此返回值賦值給MyClass.Item;

再舉個例子:
Height="{x:Static SystemParameters.IconHeight}”;
編譯器處理邏輯是:生成型別StaticExtension,將字串“SystemParameters.IconHeight”傳給建構函式,呼叫函式ProvideValue,返回double型別。

其實StaticExtension會將字串“SystemParameters.IconHeight”認為一個靜態變數。XAML眼裡只有字串!

繫結 -- 一種很常用的擴充套件識別符號型別
看如下語法:
 <Button  Width="200" Height="200"
                 Content="{Binding Height,RelativeSource={RelativeSource Self}}">
  </Button>

對content的賦值,是不是感到一頭霧水! binding其實也是擴充套件標識,最終繼承自MarkupExtension;

  Binding : BindingBase --> BindingBase : MarkupExtension;

所以binding的作用也是將字串轉換成我們需要的型別。不過binding的引數比較多,有時候需要轉好幾個彎,才能找到真的源頭!

對於上面的賦值,咱做個分析,來看看編譯器處理的步驟:

  1)生成Binding型別,建構函式傳入“Height”,

   2)Binding有一個屬性為RelativeSource,參見元檔案

   

 //
        // 摘要:
        //     通過指定繫結源相對於繫結目標的位置,獲取或設定繫結源。
        //
        // 返回結果:
        //     一個 System.Windows.Data.RelativeSource 物件,該物件指定要使用的繫結源的相對位置。預設值為 null。
        [DefaultValue(null)]
        public RelativeSource RelativeSource { get; set; }

仔細看看程式碼,屬性型別和變數名稱都是RelativeSource,這是c#語法允許的。當然,這樣做會使人困惑!

  RelativeSource={RelativeSource Self},第一個RelativeSource其實是Binding的屬性名稱,第二個是型別名。Self是一個列舉值。

這句話的意思就是,生成一個型別RelativeSource,建構函式是列舉值Self;將這個變數賦值給屬性RelativeSource。

3) 當Content需要值時,就會呼叫Binding的ProvideValue。這個函式就會把Button的屬性Height返回!

當然這裡繞了很大一圈,只實現了一個簡單的操作:將Button的高度顯示出來!感覺好費勁!
但是:繫結有一個特點,可以感知“源”變數的變化!舉例如下
 <StackPanel>
            <Button x:Name="btnTest" Width="200" Height="30"
                 Content="{Binding Height,RelativeSource={RelativeSource Self}}">
            </Button>
            <Button Margin="10" Width="200" Height="30" Click="Button_Click">增加高度</Button>
        </StackPanel>
 
Button_Click函式:
 private void Button_Click(object sender, RoutedEventArgs e)
        {
            btnTest.Height += 10;
        }

當執行Button_Click時,btnTest的高度增加10,顯示內容也隨之變化。是不是很神奇!為什麼會變化?這裡需要了解WPF特殊的屬性“依賴屬性”。這裡就不深入講解了!

當然繫結的優點不僅僅是這些,WPF會用到大量繫結,如果這些繫結都用程式碼來實現,太繁瑣,也不易理解。

總結:微軟為了讓XAML好用,費了很多心思。為了XAML能做更多的工作,編譯器會替你做很多事情!簡單的一個賦值操作,背後門道很多!初學者如果不瞭解這些門道,就感到一片茫然!本文初步揭示了賦值操作背後的一些內幕,希望你讀後,有豁然開朗的感覺!