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能做更多的工作,編譯器會替你做很多事情!簡單的一個賦值操作,背後門道很多!初學者如果不瞭解這些門道,就感到一片茫然!本文初步揭示了賦值操作背後的一些內幕,希望你讀後,有豁然開朗的感覺!