Attached Properties

Bên cạnh property thông thường, XAML còn có thêm một loại property khác – attached properties. Loại property này có thể được áp dụng cho nhiều loại control khác nhau, nhưng lại được định nghĩa bên trong một class khác. Attached property được sử dụng thường xuyên trong WPF để điều khiển các layout.

Sau đây sẽ trình bày về cách hoạt động của attached property. Mỗi control tất nhiên đều có các property của riêng nó (ví dụ, text box sẽ có các property để quy định font chữ (FontFamily), text color (Foreground), nội dung (Text) …). Khi bạn đặt một control vào bên trong một container, nó sẽ có thêm một vài tính năng bổ sung tuỳ vào loại container. (Ví dụ, bạn đặt một text box bên trong một grid, lúc này bạn có thêm lựa chọn sẽ đặt nó trong cell nào của grid) Có một số chi tiết bổ sung khác có thể được thiết đặt thông qua attached property.

Trong ví dụ eight ball window, attached property cho phép đặt mỗi control vào một row (các rows này không được hiển thị ra rõ ràng) bên trong grid:

<TextBox ... Grid.Row="0" >
   [Place question here.]
</TextBox>   <Button ... Grid.Row="1" >
   Ask the Eight Ball
</Button>   <TextBox ... Grid.Row="2" >
   [Anwser will appear here.]
</TextBox> 

Attached property không thực sự là một property thông thường. Thực tế thì nó được chuyển thành các lời gọi phương thức nhất định. Trình phân tích XAML sẽ gọi các phương thức tĩnh và các phương thức này đều có dạng: DefiningType.SetPropertyName(). Ví dụ, trong đoạn mã XAML trên, defining type chính là Grid class, và property là Row, vì vậy nên trình phân tích sẽ gọi phương thức Grid.SetRow().

Khi gọi phương thức SetPropertyName(), XAML parser sẽ truyền vào 2 tham số: đối tượng liên quan và giá trị của property có thể nhận. Ví dụ, khi bạn muốn thiết đặt thuộc tính Grid.Row cho đối tượng là TextBox, XAML parser sẽ thực thi đoạn code sau:

Grid.SetRow(txtQuestion, 0);

Nếu nhìn vào dòng lệnh trên, bạn sẽ nghĩ row number là một field thuộc đối tượng Grid. Tuy nhiên, thực ra thì row number lại là một thành phần của đối tượng sử dụng nó – trong trường hợp này đó là đối tượng TextBox. Nguyên nhân của nó chính là vì TextBox, cũng như tất cả các control trong WPF, đều kế thừa từ base class DependencyObject. Sau này, chúng ta sẽ được tìm hiểu về DependencyObject.

Thực tế, phương thức Grid.SetRow() chỉ là một phiên bản rút gọn, nó tương tự như khi ta gọi phương thức có dạng DependencyObject.SetValue(), như các bạn có thể thấy ở đây:

txtQuestion.SetValue(Grid.RowProperty, 0);

Attached property là một thành phần cốt lõi của WPF. Nó hoạt động như một hệ thống có thể thực hiện rất nhiều các chức năng mở rộng. Ví dụ, nếu ta định nghĩa Row property là một attached property, bạn đảm bảo rằng nó có thể được sử dụng cho bất kì control nào. Trong trường hợp ta đưa nó trở thành một phần thuộc base class, FrameworkElement chẳng hạn, thì không những nó sẽ làm cho interface trở nên phức tạp, rắc rối với rất nhiều các property khác nhau, trong khi các property này chỉ có thể được sử dụng trong một số trường hợp nhất định, ngoài ra nó còn làm cho việc add thêm các kiểu container mới trở thành không thể bởi vì tất nhiên các container này cũng sẽ yêu cầu thêm các property mới.


Note: Attached property cũng tương tự như extender providers trong ứng dụng Windows Form. Cả hai đều cho phép bạn thêm vào các property “ảo” để mở rộng một class khác. Khác biệt ở chỗ bạn cần phải create một thể hiện cho một extender provider trước khi sử dụng nó, và giá trị của các thuộc tính bổ sung này được lưu trữ trong extender provider chứ không phải là extended control. Với attached property, bạn có thêm một lựa chọn tốt hơn cho ứng dụng WPF của mình bởi vì nó giúp bạn tránh khỏi vấn đề quản lí thời gian tồn tại (ví dụ như lúc nào cần phải huỷ extender provider).


Nesting Elements

Như các bạn đã biết, ta có thể tạo ra các elements đặt trong các elements khác trong XAML documents. Như trong ví dụ mà chúng ta đang xét, phần tử Window chứa phần tử Grid, và Grid lại chứa thêm các phần tử khác như TextBox và Button.

XAML cho phép mỗi đối tượng được quyền quyết định cách tương tác với các nested elements khác. Cách tương tác này là một trong ba cơ chế được xét theo thứ tự sau:

  • Nếu parent element có cài đặt IList interface, XAML parser sẽ gọi phương thức IList.Add() tham số được truyền vào chính là child element.
  • Nếu parent element cài đặt IDictionary interface, XAML parser sẽ gọi phương thức IDictionary.Add. Khi sử dụng dictionary collection, đương nhiên bạn cần phải thiết đặt thuộc tính x:Key để xác định key name cho mỗi item trong collection.
  • Nếu parent element có thuộc tính ContentProperty, trình phân tính sẽ sử dụng child element làm giá trị cho thuộc tính đó.

Ví dụ, trong phần trước, chúng ta thấy LinearGradientBrush có thể chứa một tập hợp các đối tượng có kiểu GradientStop bằng cứ pháp như sau:

<LinearGradientBrush>
   <LinearGradientBrush.GradientStops>
      <GradientStop Offset="0.00" Color="Red" />
      <GradientStop Offset="0.05" Color="Indigo" />
      <GradientStop Offset="1.00" Color="Violet" />
   </LinearGradientBrush.GradientStops>
</LinearGradientBrush>  

Trong trường hợp này, trình phân tích XAML nhận ra phần tử LinearGradientBrush.GradientStops là một complex property bởi vì nó có dấu . ở giữa. Vì vậy, nó cần phải duyệt qua các tag bên trong hơi khác so với bình thường. Ở đây, trình phân tích phát hiện GradientStops property trả về một đối tượng là GradientStopCollection, và GradientStopCollection có cài đặt IList interface. Do đó, nó sẽ nhận ra là mỗi GradientStop cần phải được add vào collection bằng phương thức IList.Add():

GradientStop gradientStop1 = new GradientStop();
gradientStop1.Offset = 0;
gradientStop1.Color = Colors.Red;
IList list = brush.GradientStops;
list.Add(gradientStop1);

Một vài property có thể hỗ trở nhiều hơn một kiểu collection. Trong trường hợp này, bạn cần phải thêm một thẻ để xác định loại collection class nào:

<LinearGradientBrush>
   <LinearGradientBrush.GradientStops>
      <GradientStopCollection>
         <GradientStop Offset="0.00" Color="Red" />
         <GradientStop Offset="0.50" Color="Indigo" />
         <GradientStop Offset="1.00" Color="Violet" />
      </GradientStopCollection>
   </LinearGradientBrush.GradientStops>
</LinearGradientBrush>  

 


Note: nếu mặc định của collection được khởi gán là null, bạn cần phải đính kèm một thẻ dùng để khai báo collection class, như vậy thì collection object mới được khởi tạo. Nếu collection đó có một default instance rồi thì bạn chỉ cần add phần tử cho nó và bỏ qua việc khởi tạo.


Không phải bao giờ các thẻ con đều là một collection. Ta xét qua ví dụ về các phần tử trong Grid để chứng mình điều này:

<Grid Name="grid1">
   ...
   <TextBox Name="txtQuestion" ... >
      ...
   </TextBox>
   <Button Name="cmdAnswer" ... >
      ...
   </Button>
   <TextBox Name="txtAnswer" ... >
      ...
   </TextBox>
</Grid>  

Các thẻ con này không phải là complex property bởi vì nó không có dấu . nào cả. Hơn nữa, Grid control không phải là một collection, nghĩa là nó không cài đặt các interface như IList hoặc IDictionary. Cái mà Grid có là ContentProperty attribute. Tổng quát, ContentProperty attribute được áp dụng cho Panel class, chính là lớp mà class Grid kế thừa:

[ContentPropertyAttribute("Children")]
public abstract class Panel

Điều này chỉ ra rằng bất kì thẻ nào được đặt trong Grid đều được dùng để gán giá trị cho Children property. XAML parser sẽ “đối xử”với các content property khác nhau theo những cách khác nhau, tuỳ thuộc vào việc nó có cài đặt IList hoặc IDictionary interface hay không. Bởi vì property Panel.Children sẽ trả về một đối tượng UIElementCollection, và UIElementCollection lại cài đặt IList interface, do đó trình phân tích sẽ dùng phương thức IList.Add để thêm các phần tử con vào Grid.

Như trong ví dụ, XAML parser sẽ tạo ra một instance của mỗi element con, sau đó thực hiện việc add các thành phần này vào Grid bằng cách gọi phương thức Grid.Children.Add():

txtQuestion = new TextBox();
...
grid1.Children.Add(txtQuestion);
cmdAnswer = new Button();
...
grid1.Children.Add(cmdAnswer);
txtAnswer = new TextBox();
...
grid1.Children.Add(txtAnswer);

Những gì xảy ra tiếp theo phụ thuộc hoàn toàn vào loại cài đặt các thuộc tính của control. Grid sẽ hiển thị tất cả các control mà nó chứa theo từng hàng, từng cột nhất định.

ContentProperty attribute được sử dụng khá thường xuyên trong WPF. Nó không chỉ được sử dụng trong các container controls (ví dụ như Grid) và các control được xếp vào loại collection (như ListBox và TreeView), nó còn được dùng cho các control chỉ chứa một nội dung duy nhất. Ví dụ như TextBox và Button chỉ có thể chứa duy nhất một thành phần hoặc một đoạn text, nhưng cả hai đều sử dụng content property trong việc thao tác với các phần tử con:

<TextBox Name="txtQuestion" ... >
[Place question here.]
</TextBox>
<Button Name="cmdAnswer" ... >
Ask the Eight Ball
</Button>
<TextBox Name="txtAnswer" ... >
[Answer will appear here.]
</TextBox>

TextBox class sử dụng thuộc tính ContentProperty để đánh dấu thuộc tính TextBox.Text, tương tự class Button cũng sử dụng attribute này để đánh dấu cho thuộc tính Button.Content. XAML parser sử dụng nội dung được truyền vào làm giá trị cho các thuộc tính trên.

[To be continued]