Style
Một Style là một tập hợp các giá trị thuộc tính có thể áp vào một đối tượng đồ hoạ. Style trong WPF đóng vai trò tương tự như CSS trong HTML. Giống như CSS, WPF Style cho phép định nghĩa một tập hợp những định dạng chung để áp dụng trong toàn bộ chương trình để bảo đảm tính nhất quán. WPF Style hoạt động một cách tự động, áp dụng lên kiểu dữ liệu mục tiêu (target type), và có thể áp dụng nối tiếp xuống cây cấu trúc các đối tượng đồ hoạ. Tuy nhiên, WPF Style còn mạnh mẽ hơn CSS khi nó có thể đặt giá trị của bất kì thuộc tính phụ thuộc (dependency property) nào, ví dụ như trạng thái của control. WPF hỗ trợ cả triggers, dùng để thay đổi các thuộc tính của control khi mà một thuộc tính nào đó thay đổi, và hỗ trợ templates để định dạng lại cấu trúc bên dưới của control. Style là một điểm mạnh mà ta có thể mạnh dạn áp dụng trong chương trình.
Cơ bản về Style
Để hiểu cách hoạt động của style, trước tiên hãy đến với một ví dụ. Chẳng hạn ta đang cần quy định chung về font sử dụng trong một cửa sổ. Các đơn giản nghĩ tới ngay là đổi các thuộc tính về font của cửa sổ. Những thuộc tính này có FontFamily, FontSize, FontWeight, FontStyle, FontStretch. Khi đặt giá trị các thuộc tính này ở cửa sổ, những đối tượng nằm trong cửa sổ sẽ thừa kế thuộc tính font của cửa sổ, trừ khi chúng được đặt một giá trị khác.
Bây giờ nghĩ tới một trường hợp khác, khi mà ta không muốn thay đổi font của toàn bộ cửa sổ, mà chỉ là một phần nào đó của giao diện. Nếu như các đối tượng cần thay đổi đều nằm chung trong một container (chẳng hạn Grid, StackPanel) thì có thể dùng cách tương tự như trên. Tuy nhiên, không phải lúc nào cũng vậy. Chẳng hạn ta muốn đặt thuộc tính của tất cả các Button một kiểu font nhất định, độc lập với các thành phần khác.
Style đem lại một giải pháp tuyệt cho việc này. Ta định nghĩa một style gom hết các thuộc tính ta muốn định dạng.
<Window.Resources>
<Style x:Key=”BigFontButtonStyle”>
<Setter Property=”Control.FontFamily” Value=”Times New Roman”/>
<Setter Property=”Control.FontSize” Value=”18″/>
<Setter Property=”Control.FontWeight” Value=”Bold”/>
</Style>
</Window.Resources>
Đoạn mã này tạo một resource là một object kiểu System.Windows.Style. Object style này chứa một collection Setters chứa 3 object Setter, mỗi cái ứng với một thuộc tính ta muốn định dạng. Mỗi object Setter chứa tên thuộc tính nó định dạng và giá trị gán cho thuộc tính đó. Là một resoure, Style có trường Key dùng để lấy Style ra từ collection Resource khi cần. Ở trên key là BigFontButtonStyle. (Thông thường, giá trị key của một style có đuôi là “Style”).
Mỗi element có thể dùng một style. Gán style cho element thông qua thuộc tính Style (được định nghĩa trong class cơ sở FrameworkElement). Ví dụ, để đặt style đã tạo ở trên cho một Button, ta làm như sau:
<Button Padding=”5″ Margin=”5″ Name=”cmd” Style=”{StaticResource BigFontButtonStyle}”>
A Customized Button
</Button>
Ta cũng có thể đặt style bằng code. Lấy style ra khỏi collection Resources bằng phương thức FindResource(). Đây là code dùng cho một Button có giá trị Name là cmd:
cmd.Style = (Style)cmd.FindResource(“BigFontButtonStyle”);
Tính năng style đem lại rất nhiều lợi ích. Nó không chỉ giúp ta tạo dựng một tập hợp những thiết lập cho ứng dụng, mà còn tổ chức hợp lý hoá file markup để dễ dàng hơn khi áp dụng những thiết lập này. Hơn cả, ta có thể áp dụng một style rất linh động, quyết định thiết kế mà không bị lệ thuộc vào chi tiết kĩ thuật. Trong ví dụ trước, thiết lập font đặt trong style BigFontButtonStyle. Nếu sau đó bạn quyết định big-font button cần thêm các thiết lập về Padding và Margin, ta có thể bổ sung các Setter cho các thuộc tính Padding và Margin. Tất cả các button đã sử dụng style này sẽ tự động áp dụng thiết lập mới.
Collection Setters là phần quan trọng nhất của class Style. Nhưng có tới năm thuộc tính quan trọng mà sẽ được đề cập. Bảng sau tóm lược chung về các thuộc tính đó:
Thuộc tính Mô tả Setters Một collection các object Setter hoặc EventSetter để thiết đặt các giá trị thuộc tính hoặc các event handler. Triggers Một collection các object thừa kế từ TriggerBase và cho phép tự động thay đổi các thiết lập của Style. Ví dụ style sẽ thay đổi khi một thuộc tính nào đó thay đổi hoặc khi xảy ra một sự kiện (event). Resources Một collection resource được sử dụng trong style. BasedOn Giúp tạo một style thừa kế các thiết lập của một style khác. TargetType Đặt kiểu element mà style này áp dụng. Tạo một object Style
Trong ví dụ trước, object Style được tạo ở Resources của Window và được sử dụng ở các Button nằm trong Window. Mặc dù đó là cách phổ biến, nhưng không phải là cách duy nhất.
Nếu bạn muốn tạo những style trong những phạm vi cụ thể hơn, bạn có thể đặt style tại collection Resources của container của nó, như StackPanel hoặc Grid. Nếu muốn dùng style trong toàn chương trình thì đặt style tại collection Resources của chương trình. Đó cũng là những phương pháp thông dụng.
Nhưng cần khẳng định, ta không nhất thiết phải sử dụng style và resource kèm với nhau. Chẳng hạn, ta có thể định nghĩa style của của một Button trực tiếp ở thuộc tính Style của nó:
<Button Padding=”5″ Margin=”5″>
<Button.Style>
<Style>
<Setter Property=”Control.FontFamily” Value=”Times New Roman”/>
<Setter Property=”Control.FontSize” Value=”18″/>
<Setter Property=”Control.FontWeight” Value=”Bold”/>
</Style>
</Button.Style>
<Button.Content>A Customized Button</Button.Content>
</Button>
Cách này hoạt động nhưng không có tiện lợi. Bởi vì không thể chia sẻ style này với những element khác.
Các giải quyết này không có giá trị vì phải dùng style để đặt giá trị vài thuộc tính trong khi sẽ dễ hơn khi đặt giá trị thuộc tính trực tiếp. Tuy nhiên cách này cũng hợp lý trong một số trường hợp khi chỉ muốn áp dụng style lên một element riêng lẻ. Chẳng hạn, có thể dùng cách này để thêm trigger vào element.
Thiết lập các thuộc tính
Có thể thấy, mỗi object Style chứa một tập hợp các object Setter. Mỗi object Setter thiết lập một thuộc tính riêng lẻ trong một element. Giới hạn ở đây là Setter chỉ có thể thay đổi dependency property – các property khác không thể thay đổi.
Trong vài trường hợp, ta không thể thiết lập một property bằng chuỗi thông thường. Ví dụ, object ImageBrush không thể thiết lập bằng một chuỗi (string) được. Trong trường hợp này, ta có thể dùng một cách quen thuộc của XAML là thế chỗ attribute bằng một element:
<Style x:Key=”HappyTiledElementStyle”>
<Setter Property=”Control.Background”>
<Setter.Value>
<ImageBrush TileMode=”Tile” ViewportUnits=”Absolute” Viewport=”0 0 32 32″ ImageSource=”happyface.jpg”Opacity=”0.3″> </ImageBrush>
</Setter.Value>
</Setter>
</Style>
Để xác định thuộc tính ta muốn thiết lập, ta cần chỉ ra cả class và tên thuộc tính. Tuy nhiên, class mà ta dùng không nhất thiết phải là class của thuộc tính. Có thể dùng class thừa kế từ class của thuộc tính. Có thể xem một dạng khác của style BigFontButton, reference của Control class được trỏ tới class Button:
<Style x:Key=”BigFontButtonStyle”>
<Setter Property=”Button.FontFamily” Value=”Times New Roman”/>
<Setter Property=”Button.FontSize” Value=”18″/>
<Setter Property=”Button.FontWeight” Value=”Bold”/>
</Style>
Nếu ta thế style này với style ở ví dụ trước, ta sẽ thu được kết quả như nhau. Nhưng sự khác biệt là gì? Trong trường hợp này, sự khác nhau ở việc làm cách nào WPF xử lý với các class khác cũng có các thuộc tính FontFamily, FontSize, FontWeight nhưng không thừa kế từ class Button. Ví dụ, nếu ta áp dụng style này vào một control Label, nó sẽ không có tác dụng. WPF bỏ qua ba thuộc tính đơn giản vì thuộc tính không được áp dụng. Nhưng nếu ta dùng style được định nghĩa theo cách ở ví dụ trước, định dạng font sẽ có tác dụng bởi vị class Label thừa kế từ class Control.
Có những trường hợp mà một thuộc tính được thiết lập ở nhiều nơi trong cây thừa kế element. Ví dụ, FontFamily được thiết lập ở cả class Control và class TextBlock. Nếu ta đang tạo một style áp dụng lên TextBlock và các element thừa kế từ Control, ta phải viết markup như sau:
<Style x:Key=”BigFontButtonStyle”>
<Setter Property=”Button.FontFamily” Value=”Times New Roman”/>
<Setter Property=”Button.FontSize” Value=”18″/>
<Setter Property=”TextBlock.FontFamily” Value=”Arial”/>
<Setter Property=”TextBlock.FontSize” Value=”10″/>
</Style>
Tuy nhiên, nó sẽ không có tác dụng như mong muốn. Vấn đề là mặc dù Button.FontFamily và TextBlock.FontFamily được thiết lập riêng rẽ trong base class, nhưng chúng đều trỏ tới cùng một dependency property. (Có nghĩa rằng TextBlock.FontSizeProperty và Control.FontSizeProperty là các reference cùng trỏ tới một object DependencyProperty). Kết quả là, khi áp dụng style này, WPF thiết lập các thuộc tính FontFamily và FontSize hai lần. Thiết lập cuối cùng (trong trường hợp này là font Arial cỡ 10) sẽ áp dụng với cả object Button và object TextBlock. Mặc dù vấn đề này không thường gặp và không gặp phải với nhiều property, nhưng cũng rất quan trọng để chú ý khi ta tạo các style áp dụng lên các element có kiểu khác nhau.
Có một cách nữa để làm đơn giản cách viết style. Nếu tất cả các thuộc tính đều áp dụng lên cùng một kiểu element, ta có thể đặt thuộc tính TargetType của object Style để chỉ class mà các thuộc tính kia thuộc về. Ví dụ ta đang tạo một style chỉ dành cho Button:
<Style x:Key=”BigFontButtonStyle” TargetType=”Button”>
<Setter Property=”FontFamily” Value=”Times New Roman”/>
<Setter Property=”FontSize” Value=”18″/>
<Setter Property=”FontWeight” Value=”Bold”/>
</Style>
Thuộc tính TargetType sẽ khiến cho style tự động được áp dụng trong cho các Button trong chương trình nếu ta bỏ qua trường Key của Style.
Attach Event Handlers
Setter là phổ biến nhất trong Style, nhưng ta còn có thể tạo collection EventSetter để xác định các event handler. Sau đây là ví dụ cho thấy việc attach event handler cho các sự kiện MouseEnter và MouseLeave:
<Style x:Key=”MouseOverHighlightStyle”>
<EventSetter Event=”TextBlock.MouseEnter” Handler=”element_MouseEnter”/>
<EventSetter Event=”TextBlock.MouseLeave” Handler=”element_MouseLeave”/>
<Setter Property=”TextBlock.Padding” Value=”5″/>
</Style>
Và đây là code event handle:
private void element_MouseEnter(object sender, MouseEventArgs e)
{
((TextBlock)sender).Background = new SolidColorBrush(Colors.LightGoldenrodYellow);
}
private void element_MouseLeave(object sender, MouseEventArgs e)
{
((TextBlock)sender).Background = null;
}
MouseEnter và MouseLeave sử dụng event routing trực tiếp, tức không “nổi lên” (bubble up) hoặc “truyền xuống” (tunnel down) cây element. Nếu ta muốn attach hiệu ứng mouseover cho nhiều element, ta cần thêm các event handler MouseEnter và MouseLeave vào mỗi element. Ta có thể thực hiện đơn giản hơn bằng cách áp dụng style có chứa các property setter và event setter:
<TextBlock Style=”{StaticResource MouseOverHighlightStyle}”>
Hover over me.
</TextBlock>
Event setter là một kĩ thuật ít dùng trong WPF. Để thực hiện điều tương tự như trên ta cũng có thể dùng event trigger (không cần phải dùng tới code behind). Event trigger được thiết kể để hỗ trợ animation.
Event setter không phải là lựa chọn tốt khi muốn handle event dùng cách bubble. Trong trường hợp đó thì tiện lợi hơn là handle event ở một element có level cao hơn. Ví dụ, nếu ta muốn link tất cả button trong toolbar với cùng một event handler cho sự kiện Click, cách tốt nhất là attach một event handler duy nhất cho Toolbar element chứa các button đó. Trong trường hợp này, event setter là rắc rối.
Nhiều lớp Style
Mặc dù ta có thể định nghĩa vô số style ở nhiều level khác nhau, mỗi WPF element chỉ có thể dùng một lúc một style object. Có thể đây là sự giới hạn, tuy nhiên nó được khắc phục khi mà giá trị của property có thể thừa kế và style cũng có tính thừa kế.
Ví dụ, trường hợp ta muốn cho một nhóm control cùng một font mà không phải áp dụng style cho từng element. Ta có thể nhóm chúng vào trong cùng một container rồi đặt style cho container. Những giá trị property sẽ được truyền xuống cho các con của container. Các property có thể áp dụng cách này gồm có IsEnabled, IsVisible, Foreground, và tất cả property về font.
Trong trường hợp khác, ta muốn tạo một style được tạo ra từ một style khác. Ta ứng dụng sự thừa kế bằng cách đặt tham số BasedOn của style. Ví dụ ở 2 style sau:
<Window.Resources>
<Style x:Key=”BigFontButtonStyle”>
<Setter Property=”Control.FontFamily” Value=”Times New Roman”/>
<Setter Property=”Control.FontSize” Value=”18″/>
<Setter Property=”Control.FontWeight” Value=”Bold”/>
</Style>
<Style x:Key=”EmphasizedBigFontButtonStyle” BasedOn=”{StaticResource BigFontButtonStyle}”>
<Setter Property=”Control.Foreground” Value=”White”/>
<Setter Property=”Control.Background” Value=”DarkBlue”/>
</Style>
</Window.Resources>
Style đầu tiên (BigFontButtonStyle) xác định ba property về font. Style thứ hai (EmphasizeBigFontButtonStyle) sẽ chứa ba property về font từ BigFontButtonStyle và thêm vào hai thuộc tính về foreground và background. Việc tách ra hai style như thế này có thể áp dụng style chỉ về font hoặc style cả font và màu sắc. Cách này còn có thể tạo các style kết hợp các thuộc tính về font và thuộc tính về màu sắc.
Tự động áp dụng style theo kiểu (Type)
Ta đã thấy cách tạo style, đặt tên và đưa nó vào markup. Ngoài ra, còn có cách khác. Ta có thể áp dụng một style tự động cho những element thuộc một kiểu nhất định.
Thực hiện điều này khá đơn giản. Ta đặt TargerType để chỉ định kiểu và bỏ qua key name. Khi đó, WPF thực ra sẽ tự đặt name key theo markup extension, như sau: x:Key=”{x:Type Button}”
Khi này, style sẽ được tự động áp dụng cho tất cả button từ trên xuống trong element tree. Ví dụ, nếu ta định nghĩa một style theo cách này trong window, nó sẽ được áp dụng cho tất cả button trong window đó (trừ khi có style ở dưới thay thế nó).
Sau đây là ví dụ đặt style cho button tự động:
<Window.Resources>
<Style TargetType=”Button”>
<Setter Property=”FontFamily” Value=”Times New Roman” />
<Setter Property=”FontSize” Value=”18″ />
<Setter Property=”FontWeight” Value=”Bold” />
</Style>
</Window.Resources>
<StackPanel Margin=”5″>
<Button Padding=”5″ Margin=”5″>A Customized Button</Button>
<TextBlock Margin=”5″>Normal Content.</TextBlock>
<Button Padding=”5″ Margin=”5″ Style=”{x:Null}”> A Normal Button </Button>
<TextBlock Margin=”5″>More normal Content.</TextBlock>
<Button Padding=”5″ Margin=”5″>Another Customized Button</Button>
</StackPanel>
Trong ví dụ này, button ở giữa thay đổi style. Không áp dụng một style riêng cho nó, button được đặt property Style về null, chỉ có tác dụng bỏ style.
Mặc dù đặt style tự động tiện lợi, nó có thể làm phức tạp việc design. Một số lý do:
- Trong một window phức tạp với nhiều style và nhiều layer style, sẽ phức tạp để xác định xem một property sẽ được thừa hưởng từ việc thừa kế property value hay là từ style (và nếu là style thì áp dụng style nào). Kết quả là nếu ta muốn thay đổi một giá trị, ta phải xem lại toàn bộ markup của window.
- Việc định dạng trong một window thường thực hiện ban đầu chung nhất, và càng xuống càng phức tạp hơn và chi tiết hơn. Nếu ta áp dụng style tự động trước đó, và ta sẽ cần phải override ở nhiều nơi trong window. Việc này làm phức tạp toàn bộ quá trình design. Sẽ là tiện lợi đơn giản hơn nếu tạo các style được đặt tên và áp dụng bằng Name của nó.
- Thí dụ, ta tạo style tự động cho element TextBlock, ta sẽ phải chỉnh sửa các control khác sử dụng TextBlock (như ListBox có template).
Để tránh rắc rối, sử dụng style tự động một cách thận trọng. Ví dụ như dùng style tự động cho một padding phù hợp cho các button, hoặc chỉnh margin của các textbox trong một container thay vì trong cả window.
Trigger
Sử dụng trigger, ta có thể thay đổi style với các sự kiện có sẵn. Ví dụ, ta có thể hiện thay đổi khi một property được thay đổi và áp dụng tự động.
Trigger được nối với style trong collection Style.Triggers. Mỗi style có không giới hạn số trigger, mỗi trigger là một class derive từ System.Windows.TriggerBase. WPF cung cấp các loại trigger như sau:
Tên Mô tả Trigger Đây là dạng đơn giản nhất của trigger. Nó chờ một sự thay đổi của dependency property và sau đó dùng setter để thay đổi style. MultiTrigger Tương tự như trigger nhưng kết hợp nhiều điều kiện. Tất cả các điều kiện đều thoả thì trigger mới có hiệu lực. DataTrigger Trigger hoạt động với data binding. Nó tương tự như trigger, nhưng nó chờ sự thay đổi của data đã được thực hiện databinding. MultiDataTrigger Kết hợp nhiều data trigger. EventTrigger Đây là trigger tinh vi nhất. Nó thực hiện animation khi có một event. Ta có thể áp dụng trigger trực tiếp vào element, không cần phải tạo một style, bằng cách dung collection FrameworkElement.Triggers. Tuy nhiên trigger collection này chỉ hỗ trợ event trigger. (không phải vì lý do kĩ thuật, chỉ là chưa được bổ sung).
Trigger đơn giản
Ta có thể attach một trigger đơn giản vào bất kì dependency property nào. Ví dụ, ta có thể tạo hiệu ứng mouseover và focus khi có thay đổi về các thuộc tính IsFocused, IsMouseOver, IsPressed của class Control.
Mỗi trigger đơn giản xác định bởi giá trị ta theo dõi và giá trị ta sẽ cần thay đổi. Khi giá trị theo dõi đạt tới giá trị ta cần, những setter trong Trigger.Setters sẽ có hiệu lực.
Sau đây là một trigger mà khi button được focus thì button sẽ được chuyển màu background thành đỏ sậm:
<Style x:Key=”BigFontButton”>
<Style.Setters>
<Setter Property=”Control.FontFamily” Value=”Times New Roman” />
<Setter Property=”Control.FontSize” Value=”18″ />
</Style.Setters>
<Style.Triggers>
<Trigger Property=”Control.IsFocused” Value=”True”>
<Setter Property=”Control.Foreground” Value=”DarkRed” />
</Trigger>
</Style.Triggers>
</Style>
Điều tiện lợi của trigger là ta không cần phải viết thành phần logic nào để trả về lại giá trị cũ. Khi mà trigger không có hiệu lực, element sẽ tự động trả về thể hiện cũ. Trong ví dụ trên button sẽ trở về màu xám bình thường khi mà user chuyển nhấn tab chuyển focus sang đối tượng khác.
Có thể tạo nhiều trigger áp dụng cùng lúc trên một element. Nếu các trigger thay đổi các property khác nhau, sẽ không có sự nhập nhằng. Ngoài ra, nếu có nhiều trigger cùng thay đổi một property, trigger cuối trong danh sách sẽ có tác dụng.
Ví dụ, các trigger sau có tác dụng với control tuỳ vào việc nó được focus, mousehover, click:
<Style x:Key=”BigFontButton”>
<Style.Setters> … </Style.Setters>
<Style.Triggers>
<Trigger Property=”Control.IsFocused” Value=”True”>
<Setter Property=”Control.Foreground” Value=”DarkRed” />
</Trigger>
<Trigger Property=”Control.IsMouseOver” Value=”True”>
<Setter Property=”Control.Foreground” Value=”LightYellow” />
<Setter Property=”Control.FontWeight” Value=”Bold” />
</Trigger>
<Trigger Property=”Button.IsPressed” Value=”True”>
<Setter Property=”Control.Foreground” Value=”Red” />
</Trigger>
</Style.Triggers>
</Style>