Windows Presentation Foundation (WPF) je novi napredni grafički podsistem u .NET Frameworku. Pokazao se kao vrlo inovativna tehnologija za izradu grafički bogatih sučelja za web i desktop rješenja, s očitim naglaskom na odvajanje dizajna grafičkog sučelja od logike programskog rješenja.
Trenutno dostupna implementacija WPF-a omogućava mnogo interesatnih novih koncepata poput...
- izrade korisničkog sučelja u vektorskoj, rasterskoj ili kombiniranoj grafici
- definiranje trodimenzionalnih objekata i sučelja za web i desktop rješenja
- korištenje jedinstvenog deklarativnog jezika za stvaranje sučelja - XAML-a
- naprednije deklarativno vezanje (binding) podataka na svojstva (property) objekata
- deklarativno animiranje svojstava objekata u sučelju
- usmjerene događaje (routed events) bazirane na vizalnoj strukturi sučelja
- itd...
Unatoč inovativnosti i dugotrajnom razvoju koncepta i same tehnologije, katkad nam se može dogoditi da naletimo na poneki zid kojeg ne možemo tako lako prijeći. Jedan od tih zidova je nasljeđivanje kontrola koje su bazirane na XAML-u.
Da pojasnim. Korisničke kontrole, i to velika većina njih, koje ćete izrađivati biti će najvjerojatnije proširenje nekih od postojećih (native) kontrola koje WPF donosi. Među njima možemo nabrojati one osnovne - UserControl, Canvas, te i neke koje imaju specifičnu funkcionalnost - npr. StackPanel, Grid, Border, i slične.
Kad napravite svoju vlastitu korisničku kontrolu, ona će se vjerojatno sastojati od XAML-a i koda s implementacijom njene funkcionalnosti.
Čemu XAML?
Alternativni način definiranja korisničkih kontrola jest kreiranje kontrole isključivo izradom koda - klase koja ju definira vizualno i funkcionalno. U tom slučaju, potrebno je cjelokupni dizajn kontrole izvesti u kodu, što ponekad može biti zamoran posao koji rezultira vrlo nepreglednim kodom.
Usporedimo ta dva načina na jednom jednostavnom primjeru:
| XAML | KOD |
| <Label Content="Pozdrav!"/> | Label label1 = new Label(); label1.Content = "Pozdrav!"; labelParent.Children.Add(label1); |
Kao što je vidljivo iz primjera - vrlo jednostavan zadatak kreiranja jednog područja s tekstom i definiranja njegovog sadržaja je mnogo jednostavnije i brže koristeći XAML.
Kontrole koje obično izrađujemo sadrže na desetke objekata, a njihovo definiranje putem XAML-a je spas za svakog programera i dizajnera.
Problem
Napravimo li korisničku kontrolu (nazovimo ju BaznaKontrola) na klasičan način - pomoću XAML-a i koda, i želimo li napraviti drugu kontrolu (nazovimo ju NovaKontrola) koja mora nasljediti kontrolu BaznaKontrola, potrebno je napraviti sljedeće korake:
- U datoteci NovaKontrola.xaml.cs u potpisu klase potrebno je zamijeniti nasljeđivanje UserControl klase s našom klasom BaznaKontrola:
public partial class NovaKontrola : BaznaKontrola
{ ... }
- U datoteci NovaKontrola.xaml u root elementu XAML-a, potrebno je dodati novi namespace atribut xmlns:b koji će predstavljati namespace u kojem je definirana naša bazna kontrola. Također, potrebno je promijeniti i sam root element b:BaznaKontrola kako bi kompajler i Visual Studio mogli znati koji tip kontrole se nasljeđuje i definira u XAML-u
<b:BaznaKontrola
...
xmlns:b="clr-namespace:MojNamespace">
Pokušamo li buildati projekt s našim kontrolama dobivamo sljedeću grešku:
Error MC6017: 'NazivKontrole' cannot be the root of a XAML file because it was defined using XAML.
Što to sad znači?
To znači da moramo ili a) redefinirati kontrolu tako da ne koristi XAML već da cijeli dizajn bude reprezentiran kroz kod, ili b) u potpunosti izbjeći arihetkturu projekta koja zahtjeva nasljeđivanje korisničkih kontrola ili pak c) pronaći neko drugo, zaobilazno, ali funkcionalno rješenje.
Pošto su rješenja a) i b) u danom trenutku najčešće neprihvatljiva, developer koji se našao u ovoj nesretnoj MC6017 situaciji postavlja pitanje - pa postoji li neko zaobilazno rješenje? Na njegovu sreću - postoji!
Zaobilazno rješenje
Za njega koji sad vjerojatno misli da je u teškoj gabuli, te mu je vjerojatno tlak pao/narastao van normalnih granica, objasnit ću postupak potreban za pretvaranje postojeće XAML-om definirane kontrole u istu koju može nasljediti bez problema.
- U Solution Exploreru je potrebno pronaći XAML datoteku bazne kontrole, te u Properties prozoru podesiti sljedeće:
| Postavka | Originalna vrijednost | Nova vrijednost |
| Build action | Page | Resource |
| Custom Tool | MSBuild:Compile | (ostaviti prazno) |
- Otvoriti istu tu XAML datoteku, te iz root elementa izbaciti atribut x:Class i njegov sadržaj (znači obrisati "x:Class="....."")
- Otvoriti codebehind datoteku te XAML datoteke. U njoj će među ostalim biti definiran osnovni konstruktor klase. On inicijalno poziva InitializeComponent metodu koja nije definirana u toj datoteci, već u kompajliranom XAML-u. Pošto smo u koraku 1 podesili da se XAML više ne kompajlira - te metode više nema. No, ne treba odmah izbrisati njen poziv. Naime, ukoliko postoji više konstruktora postoje šanse da i oni zovu istu tu metodu. Ukoliko pak ne postoje drugi konstruktori, svejedno ima šanse da će se u budućnosti pojaviti potreba za njima. Stoga, radi čiste preglednosti i reusabilitya koda, napravimo tu metodu. U nju ćemo staviti tri "čarobne" linije koje će nam omogućiti korištenje XAML-a.
private void InitializeComponent()
{
Uri xamlUri = new Uri("pack://application:,,,/MojNamespace;component/BaznaKontrola.xaml");
Stream stream = Application.GetResourceStream(xamlUri).Stream;
this.Content = XamlReader.Load(stream);
}
I to je u biti to. E sad, za one koji imaju vremena proučavati što smo upravo napravili, idemo objasniti korake.
U prvom koraku smo "ugasili" kompajliranje XAML datoteke i podesili ju da se ponaša kao resurs u projektu. To znači da će datoteka u svakom trenutku biti dostupna aplikaciji, no samo kao obična gomila teksta. Ukoliko ikad bude potrebno vratiti kompajliranje tog XAML-a, sve što je potrebno jest napraviti obrnuti proces od ovog u prvom koraku.
U drugom koraku smo pak napravili metodu koja će učitati taj resurs u jedan Stream, te njega učitati kao XAML u Content svojstvo kontrole. Tu je potrebno naglasiti, da ako kontrola nasljeđuje Panel ili neku derivaciju klase Panel, onda Content svojstvo neće biti dostupno. U tom slučaju je potrebno koristiti alternativu - svojstvo Children, i onda će InitializeComponent izgledati otprilike ovako:
private void InitializeComponent()
{
Uri xamlUri = new Uri("pack://application:,,,/MojNamespace;component/BaznaKontrola.xaml");
Stream stream = Application.GetResourceStream(xamlUri).Stream;
this.Children.Add((UIElement)XamlReader.Load(stream));
}
U jednom od sljedećih postova ćemo pogledati prednosti, nedostatke i ograničenja pri ovakvom učitavanju XAML-a.
Zahvaljujem na strpljenju, i nadam se da je onaj gore developer barem malo odahnuo.
28. travanj 2008.