Markup Znaczniki rozszerzeniowe! Bardzo ważny aspekt w XAML-u. Znaczniki są krótkimi napisami zazwyczaj zamkniętymi w nawiasach klamrowych, które dodają nową funkcjonalność w XAML-u. W języku XAML stosujemy różne znaczniki takie jak “{StaticResource}”, “{TemplateBinding}”, i “{Binding}” oraz wiele innych. Używam ich przez cały czas.
Ale co z tego skoro ktoś wymyślił składnie do uzupełniania atrybutów XAML w taki prosty sposób bez użycia kodu pobocznego (code-behind). Ten fakt we własnej osobie jest interesujący. Jednak co jest jeszcze bardziej interesujące, to fakt , że możemy stworzyć swoje znaczniki. Żadnego kodu pobocznego
Tworzenie własnych znaczników jest popularne w różnych wzorcach jak MVVM. Obecnie WPF oraz Silverlight 5 pozwala na stworzenie własnego znacznika. Chociaż w Silverlight 5 beta robi się to zupełnie inaczej niż w WPF. Aplikacja, którą napisałem w WPF nie może być tak po prostu przeniesiona za pomocą “copy/paste” do Silverlight.
Jakie to proste
Nie ma tutaj żadnych ceregieli. Nasza nowa klasa musi dziedziczyć po klasie abstrakcyjnej “MarkupExtension” i posiadać nadpisaną metodę “ProvideValue”. W tej właśnie metodzie piszemy jaka konkretna wartość ma być zwrócona. No i klasa musi zawsze posiadać konstruktor bezparametrowy. Zawsze. Nawet jeśli dodamy inne konstruktory z parametrami. Inaczej XAML sobie nie poradzi.
class RandomNumExtension : MarkupExtension
{
public RandomNumExtension() { }
Random random = new Random();
public override object ProvideValue
(IServiceProvider serviceProvider)
{
return random.Next(int.MinValue, int.MaxValue).ToString();
}
}
Aby użyć znacznika trzeba tylko dodać do XAML przestrzeń nazw, w której klasa się znajduje. Można śledzić konwencje nazywania znaczników bez słowa Extension. W ten sposób w XAML mam RandomNum , a nie RandomNumExtension. Konwencja jest pomocna, ale nieobowiązkowa.
xmlns:extension="clr-namespace:ExtensionMarkupWpf.Markup" ... <TextBlock Text="{extension:RandomNum}" />
Z parametrami
Co to byłby za wpis, gdybym nie objaśnił większych bajerów związanych z tworzeniem własnych znaczników. Jeśli chodzi o tworzenie znaczników rozszerzeniowych z parametrami, to jest dobra wiadomość i zła wiadomość. Dobrą wiadomością jest fakt, że XAML sam konwertuje wartości string i double więc stosowanie ich w swoich znacznikach wydaje się dobry pomysłem.
W klasie zostały dodane paramenty oraz drugi konstruktor parametrowy.
class ParamExtension : MarkupExtension
{
public ParamExtension() {}
public string Note {get;set;}
public double number {get;set;}
public ParamExtension(string _Note,double _number)
{
Note = _Note;
number = _number;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
string s = Note + ": " + number.ToString();
return s;
}
}
Zastosowanie znacznika w XAML wygląda tak. Tak jak mówiłem wcześniej dla typów double i string nie trzeba dokonywać żadnych konwersji.
<TextBlock Text="{extension:Param Note=Jeden,number=1}" />
Wróćmy jednak do mojego poprzedniego przykładu, który losował liczby za pomocą klasy Random. Klasa ta wymaga zmiennej int. Zauważ, że tym razem parametry zostały oznaczone aby łatwiej dało się je mapować. Używa go XAML do serializacji i nie jest on wymagany.
class RandomNum2Extension : MarkupExtension
{
public RandomNum2Extension() {}
public RandomNum2Extension(object minvalue,object maxvalue)
{
Minvalue = minvalue;
Maxvalue = maxvalue;
}
[ConstructorArgument("minvalue")]
public object Minvalue { get; set; }
[ConstructorArgument("maxvalue")]
public object Maxvalue { get; set; }
Random random = new Random();
public override object ProvideValue
(IServiceProvider serviceProvider)
{
int min;
int max;
if ((int.TryParse(Minvalue.ToString(),out min))
&& (int.TryParse(Maxvalue.ToString(),out max)))
{
return random.Next(min,max).ToString();
}
else {
return string.Empty;
}
}
}
Powyższe kody nie są dobre. Zwracają one wartość string więc znacznik nadaje się tylko dla tych właściwości gdzie może być wartość string wyświetlona. Czyli znacznik jest bezużyteczny dla kontrolek np. progressbar, slider . Przynajmniej dzięki temu przykłady są proste.
xmlns:sys="clr-namespace:System;assembly=mscorlib"...
<TextBlock Grid.Row="1">
<TextBlock.Text>
<extension:RandomNum2>
<extension:RandomNum2Extension.Minvalue>
<sys:Int32>10</sys:Int32>
</extension:RandomNum2Extension.Minvalue>
<extension:RandomNum2Extension.Maxvalue>
<sys:Int32>20</sys:Int32>
</extension:RandomNum2Extension.Maxvalue>
</extension:RandomNum2>
</TextBlock.Text>
</TextBlock>
Wyświetlenie w Textblock wartości “int” wymaga dodania przestrzeni nazw System oraz rozłożenie kodu na części. Zauważ, że kiedy referujemy się do zmiennych musimy stosować pełną nazwę znacznika. Mogło wydać się to dziwne, ale mogę nazwać gałęzie z wartościami i zmieniać ich parametry w kodzie pobocznym.
Automatyczne rzutowanie typów
Poprzednie przykłady wykazały dwa problemy. Pierwszy problem to rzutowanie typów parametrów w XAML a drugi problem to wartość zwracana, która najlepiej aby sama się dostawała do oczekującego typu właściwości kontrolki. Nie będę kłamał tutaj potrzebowałem małej pomocy Internetu.
Czas użyć parametru “IServiceProvider” w funkcji “ProvidedValue”. Ten obiekt przetrzymuje wiele interesujących informacji o obiekcie docelowym. Interface zawiera w sobie funkcje “GetService”, która zwraca kolejny interface “IProvideValueTarget”. Używając tego interface mamy dostęp do obiektu docelowego i jego właściwości, jak i jakiego są typu oraz jak ustawić wartości bezpośrednio.
W tym wypadku chcemy by nasz znacznik wylosował liczbę z przedziału określonego w dwóch “int”-ach i wyświetlił ją w TextBlock. Oczekując, że rezultat int będzie skonwertowany do string, ale i nie tylko.
[MarkupExtensionReturnType(typeof(object))]
class RandomNum3Extension : MarkupExtension
{
public RandomNum3Extension() { }
public RandomNum3Extension(object minvalue, object maxvalue)
{
Minvalue = minvalue;
Maxvalue = maxvalue;
}
[ConstructorArgument("minvalue")]
public object Minvalue { get; set; }
[ConstructorArgument("maxvalue")]
public object Maxvalue { get; set; }
Random random = new Random();
public object GetRandomNumber()
{
int min;
int max;
if ((int.TryParse(Minvalue.ToString(), out min))
&& (int.TryParse(Maxvalue.ToString(), out max)))
{
return random.Next(min, max) as object;
}
else {
return null;
}
}
public override object ProvideValue
(IServiceProvider serviceProvider)
{
if (serviceProvider == null)
return null;
IProvideValueTarget ipvt =
(IProvideValueTarget)serviceProvider.GetService
(typeof(IProvideValueTarget));
DependencyObject targetObject = ipvt.TargetObject
as DependencyObject;
object val = GetRandomNumber();
if (ipvt.TargetProperty is DependencyProperty)
{
DependencyProperty dp = ipvt.TargetProperty
as DependencyProperty;
if (val is IConvertible)
{
val = Convert.ChangeType(val, dp.PropertyType);
}
}
else {
PropertyInfo info = ipvt.TargetProperty
as PropertyInfo;
if (val is IConvertible)
{
val = Convert.ChangeType(val, info.PropertyType);
}
}
return val;
}
}
Ten przykład jest bardziej skomplikowany, ale jeśli przyjrzeć się bardziej kodowi nie dzieje się tutaj nic magicznego. Wewnątrz “ProvideValue” sprawdzam czy właściwość jest typu “DependencyProperty”, czy jest też zwykłą właściwością. Bazując, na jakim typie właściwości operujemy sprawdzam, czy dany typ może być konwertowany. Potem wywołuje metodę Converter.ChangeType, która przekształca wartość zwracaną na odpowiedni typ.
Teraz kod XAML wygląda normalnie.
<TextBlock Text="{extension:RandomNum3 Minvalue=-10,Maxvalue=10}"/>
Bonus : Tworzenie listy losowych liczb
Bazując na poprzednich przykładach stworzyłem szybko rozszerzenie, które tworzy listę wylosowanych liczb. W tym przykładzie postanowiłem zająć się wychwyceniem możliwych wyjątków, które mogą się pojawić przy użyciu tego znacznika w Visual Studio 2010.
Aby nie powielać kolejnych parametrów w kolejnych konstruktorach jeden z konstruktorów dziedziczy po swoich poprzednikach.
[MarkupExtensionReturnType(typeof(List<int>))]
class ListRanNumExtension : MarkupExtension{
public ListRanNumExtension() { }
public ListRanNumExtension(object itemscount)
{
ItemsCount = itemscount;
}
public ListRanNumExtension(object minvalue, object maxvalue):this()
{
Minvalue = minvalue;
Maxvalue = maxvalue;
}
private object _MinValue = int.MinValue;
private object _MaxValue = int.MaxValue;
[ConstructorArgument("minvalue")]
public object Minvalue
{
get { return _MinValue; }
set { _MinValue = value; }
}
[ConstructorArgument("maxvalue")]
public object Maxvalue
{
get { return _MaxValue; }
set { _MaxValue = value; }
}
[ConstructorArgument("ItemsCount")]
public object ItemsCount
{
get; set;
}
Random random = new Random();
public override object ProvideValue(IServiceProvider serviceProvider)
{
int itemsCount;
if (ItemsCount == null)
throw new InvalidOperationException("ItemsCount mus be specifed");
if (int.TryParse(ItemsCount.ToString(), out itemsCount) == false)
throw new InvalidCastException("ItemsCount can't be converted to int type");
int min;
int max;
if ((int.TryParse(Minvalue.ToString(),out min)) &&
(int.TryParse(Maxvalue.ToString(),out max )) )
{
List<int> list = new List<int>();
for (int i = 0; i < itemsCount; i++)
{
list.Add(random.Next(min, max));
}
return list;
}
else {
throw new InvalidCastException("MaxValue and MinValue can't be converted to int type");
}
}
Użycie w XAML
<ListBoxItemsSource="{extension:ListRanNum ItemsCount=10}" />
<ListBox ItemsSource="{extension:ListRanNum ItemsCount=2,Minvalue=1,Maxvalue=5}" />