Markup ExtCzęść NR.4Po długiej przerwie zastanawiam się czy uzupełniać ten kurs dalej, zwłaszcza że ma on swoje wady, ale co tam.

W poprzednim wpisie omówiłem właściwości elementów z punktu widzenia języka XAML.

Czyli w wielkim skrócie możemy ustawić właściwości elementów na dwa sposoby.

Przy pomocy atrybutów.

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
        Background="AliceBlue"
        Content="OK">
</Button>

Przy pomocy elementów.

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
    <Button.Background>
        Wheat
    </Button.Background>
</Button>

Warto jednak powiedzieć coś o konwersji, która tutaj zachodzi. W XAML jak widzimy nie musimy pisać całej przestrzeni nazw by uzyskać kolekcję statycznych kolorów dostępnych w “System.Windows.Media.Brushes”.

System.Windows.Controls.Button b = new System.Windows.Controls.Button();
b.Content = "TEXT";
b.Background = System.Windows.Media.Brushes.White;

Kolory w XAML są deklarowane jako napisy stringi, ale czy na pewno tak jest?

Okej głupie pytanie.

Na pewno tak nie jest, ale trzeba przyznać, że jest to wygodne. Zwłaszcza że mamy Intellisense.

Intellisense XAML

W takich wpadkach XAML wyszukuje odpowiednich konwerterów typu, które tłumaczą zapis string na dany typ danych.

WPF posiada wiele domyślnych konwerterów dla typów danych jak: Brush, Color, FontWeight,Point i tak dalej.

Wszystkie te klasy dziedziczą po “TypeConverter” co oznacza, że możesz napisać swój własny konwerter. Konwerter rozumieją zapisy wartości nie zależnie od wielkości znaków. Możesz n.p zapisać tło małymi literami a XAML nadal zrozumie o co ci chodzi.

<Button Background="aliceblue"  
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" />

Bez konwerterów kolor tła musiał być deklarowany w następujący sposób.

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
        Content="OK">
    <Button.Background>
        <SolidColorBrush Color="AliceBlue"/>
    </Button.Background>
</Button>

Z konwerterami możesz zapisać kolor jako wyrażenie hexadecymalne RGB.

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
        Background="#F1FAAA"/>

Bez z nich musiałbyś użyć następującego zapisu.

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
    <Button.Background>
        <SolidColorBrush>
            <SolidColorBrush.Color>
                <Color A="255" R="241" G="250" B="170"/>
            </SolidColorBrush.Color>
        </SolidColorBrush>
    </Button.Background>
</Button>

Nawet ten powyższy zapis jest możliwy, ponieważ istnieje konwerter, który tłumaczy stringi na typ “byte”. Bo takie wartości przyjmuje konstruktor typu “Color”. Będą pozbawieni tego konwertera w sumie nie możemy już w żaden wygodny sposób zadeklarować koloru dla tego przycisku.

Ciekawostka: Używanie konwerterów w kodzie w C#

Alternate TextWszystkie konwertery istnieją jako klasy, a to oznacza, że jesteśmy wstanie ich użyć w normalnym kodzie C#. Niestety nie używa on tego samego typu konwersji co parser XAML. Jest to najbliższa reprezentacja tego co się dzieje w kodzie XAML z punktu widzenia C#.

System.Windows.Controls.Button b = new System.Windows.Controls.Button();
b.Background = (Brush)System.ComponentModel.TypeDescriptor.GetConverter(
typeof(Brush)).ConvertFromInvariantString("AliceBlue");

Oczywiście, jeśli napiszemy kolor z błędem podobnie jak w XAML otrzymamy błąd. W C# oczywiście ten błąd zostanie wyrzucony w trakcie działania aplikacji. W XAML ten błąd zostanie wyrzucony przez parser.

Jak szukanie konwerterów typu działa

Alternate Text Jak kompilator XAML znajduje odpowiednie konwertery i jak on kojarzy je z daną klasą. Klasy .NET używają w tym celu atrybutu “System.ComponentModel.TypeConverterAttribute”.

Przykładowo “BrushConverter” jest używany przez XAML do właściwości Background. Dzieje się tak, ponieważ właściwości ta jest typu Brush, a klasa ta ma właśnie ten atrybut wskazujący na ten konwerter.

[Localizability(LocalizationCategory.None, Readability = Readability.Unreadable)]
    [TypeConverter(typeof(BrushConverter))]
    [ValueSerializer(typeof(BrushValueSerializer))]
    public abstract class Brush : Animatable, IFormattable, DUCE.IResource

A co z właściwościami, które nie są reprezentowane przez klasy tylko przez typy wartościowe jak n.p double? W takim wypadku właściwości przykładowo “FontSize” ma przypisany ten atrybut.

[TypeConverter(typeof(FontSizeConverter))]
public double FontSize
{
    get;
    set;
}

Oczywiście typ double może określać nie tylko rozmiar czcionki dlatego ten konwerter nie jest powiązany z każdą właściwością mającą typ double. W WPF typ double zwykle jest powiązany z konwerterem “LenghtConverter”.

Markup Extensions

Markup Extensions podobnie jak konwertery typów rozszerzają one wyrażenia XAML o kolejny poziom. Markup Extension są to wyrażenia typu string, które są konwertowane na odpowiednie obiekty. Podobnie jak z konwerterami typów w WPF mamy kilka gotowych wyrażeń Markup Extensions, ale możemy także napisać swoje własne.

Podstawowa różnica pomiędzy konwerterami typów, a Markup Extension polega na tym, że Markup Extensions mają swoją określoną składnie. Użycie ich ma więc tylko sens w składni XAML. Powstały one w wyniku pewnych ograniczeń związanymi z konwerterami typów.

Powiedzmy, że chcesz stworzyć tło przycisku jako gradient. Normalnie nie jest to możliwe używając prostego wyrażenia string. Gdybyś jednak stworzył swój własny Markup Extension, który zwracałby taki złożony typ byłoby to możliwe.

Za każdy razem, gdy wartość właściwości jest pokryta nawiasami klamrowymi, parser XAML tratuje to wrażenie jako Marku Extension niż jako zapis string, który być może musi zostać skonwertowany .

<Button 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Background="{x:Null}"
    Height="{x:Static SystemParameters.IconGridHeight}"
    Content="{Binding Path=Height, RelativeSource={RelativeSource Self}}" />

Pierwsze dwa wyrażenie odwołują się do klas z przestrzeni nazw “System.Windows.Markup” przy użyciu prefiksu “X”. Pierwsze wyrażenie korzystające z klasy “NullExtension” zwróci wartości “null” drugie odwołuje się do statycznych wartości dostępnych z klasy “StaticExtension

Ostatnie wyrażenie wykonuje powiązanie (Binding). Nie ma ono prefiksu “x”, gdyż nie korzysta z przestrzeni nazwy “System.Windows.Markup” tylko z “System.Windows.Data”, które jest dostępne w domyślnej przestrzeni nazw języka XAML.

Co dokładnie się dzieje w dwóch pierwszych wrażeniach.

Na podstawie wyrażeń string, które korzystają z parametrów oddzielonymi kropkami XAML zwraca odpowiednie wartość. Parametry oddzielone kropkami mogą zachowywać się jak właściwości rzeczywistych klas w C#. W rzeczywistości w tych wrażeniach odwołujesz się konstruktorów, które przy odpowiednich parametrach zwracają odpowiednie obiekty.

[TypeForwardedFrom("PresentationFramework, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")]
public class StaticExtension : MarkupExtension
{
    [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
    public StaticExtension();

    public StaticExtension(string member);

    [ConstructorArgument("member")]
    public string Member { get; set; }

    [DefaultValue("")]
    public Type MemberType { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider);
}

Konstruktor StaticExtension

Konwertery typów nie wspierają wartości null, ale przy użyciu Markup Extension jesteśmy wstanie taki typ zwrócić. Puste tło nie jest użyteczne, ale jest dobre jako przykład.

StaticExtension pozwala na odwoływanie się do statycznych pól, właściwości, stałych i typów wyliczeniowych dostępnych w frameworku.

Przykładowo statyczna właściwość “IconGridHeight” zwraca wysokość siatki, w którym mieszczą się duże ikony.

Binding to bardziej zaawansowany topiki, na który trzeba poświęci więcej czasu.

Ale ja chce zapisać nawiasy klamrowe

Alternate TextNawiasy klamrowe są specjalny wrażeniem jak więc zapisać nawiasy klamrowe jako czysty string.

Na wszystko jest sposób. Używając nie uzupełnionych nawiasów klamrowych jako prefiks jesteś wstanie napisać wyrażenie posiadające nawiasy klamrowe jako zwykły napis.

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Content="{}{Działa}"/>

Alternatywnie możesz użyć uzupełnienia przy pomocy elementów. One domyślnie nie korzystają z markup extensions w taki sposób jak wyrażenia atrybutowe.

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
    <Button.Content>
        {Działa}
    </Button.Content>
</Button>

Wyrażenie markup extension są tylko klasami z domyślnymi konstruktorami. Mogą być używane w wyrażeniu elementowym w następujący sposób.

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Button.Background>
        <x:Null/>
    </Button.Background>
    <Button.Height>
        <x:Static Member="SystemParameters.IconGridHeight"/>
    </Button.Height>
    <Button.Content>
        <Binding Path="Height">
            <Binding.RelativeSource>
                <RelativeSource Mode="Self"/>
            </Binding.RelativeSource>
        </Binding>
    </Button.Content>
</Button>

Jak to wyrażenie działa? Klasy Markup Extension mają właściwości przypisane parametrom swojego konstruktora. Przykładowo klasa “StaticExtension” ma właściwość “Member”, która działa tak samo, jak argument przekazywany w konstruktorze. Mechanizm ten jest bardziej jasny, gdy spojrzysz na ciekawostkę poniżej i kod klasy “StaticExtension” pokazanej wyżej.

Właściwości StaticExtension

Zapis taki nie jest jednak wspierany przez Intellisense. Oznacza to, że brak pomocy w uzupełnianiu wyrażeń.

image

Ciekawostka: Jak to by wyglądało w C#

Alternate Text

Spróbujmy stworzyć podobny zapis w C#, który właśnie zrobiliśmy w języku XAML.

 

System.Windows.Controls.Button b2 = new System.Windows.Controls.Button();
// Ustawienie tła
b2.Background = null;
// Ustawienie wysokości
b2.Height = System.Windows.SystemParameters.IconGridHeight
// Ustawienie zawartości
System.Windows.Data.Binding binding = new System.Windows.Data.Binding();
binding.Path = new System.Windows.PropertyPath("Height");
binding.RelativeSource = System.Windows.Data.RelativeSource.Self;
b2.SetBinding(System.Windows.Controls.Button.ContentProperty, binding);

Ten kod jednak nie używa tego samego mechanizmu co kompilator XAML, który ustawia w trakcie działania programu wartości wywołując metodę “ProvideValue”. W C# nie można skorzystać z klasy “StaticExtension”.

(przynajmniej Intellisense mi blokuje dostęp)

Dlatego używam klasy i jej statycznej właściwości bezpośrednio “System.Windows.SystemParameters.IconGridHeight”.

Kod proceduralny korzystający z tych mechanizmu jest bardziej złożony. Binding w kodzie proceduralny czasami może mieć zastosowanie, ale na dłuższą metę komplikuje to działania aplikacji.