Dynamicky generované komponenty v Silverlightu 2.0

V článku si na několika jednoduchých příkladech ukážeme, jak lze dynamicky vytvářet komponenty za běhu aplikace, a jak můžete s takovými komponentami pracovat. V jednotlivých příkladech si postup porovnáme s jejich tvorbu pomocí XAML.
Seriál: Praktické užití Silverlight 2.0 (12 dílů)
- Praktické užití Silverlight 2.0: Data Binding 15. 12. 2008
- Praktické užití Silverlight 2.0: DataGrid 22. 12. 2008
- Praktické užití Silverlight 2.0: UserControl 29. 12. 2008
- Co zajímavého přínáší Silverlight toolkit 5. 1. 2009
- Silverlight toolkit a vizualizace dat 12. 1. 2009
- Jak na komponenty AutoCompleteBox a TreeView ze Silverlight toolkitu 19. 1. 2009
- Nástroje pro tvorbu layoutu v Silverlightu 2.0 a Silverlight toolkitu 26. 1. 2009
- Design se styly a šablonami v Silverlightu 2.0 2. 2. 2009
- Základy 2D grafiky v Silverlightu 2.0 16. 2. 2009
- Dynamicky generované komponenty v Silverlightu 2.0 3. 3. 2009
- Úvod do streamování médií v Silverlightu 2.0 16. 3. 2009
- Práce s videem v Silverlightu 2.0 1. 6. 2009
Při tvorbě jakýchkoli aplikací narazíte čas od času na problém, že zobrazení určitých komponent či grafického prvku je známo až za běhu aplikace. V praxi tato situace může například nastat, když tvoříte anketu a máte otázku: Kouříte? Jako odpověď máte dva RadioButtony a po zvolení „Ano“ bude chtít zobrazit textové pole s popiskem: Jakou značku cigaret?
Pokud chcete vytvořit v Silverlightu jakoukoli komponentu, máte dvě možnosti. První možností je definování v XAMLu a druhou definování v kódu aplikace (Visual Basic, C#). Pokud se podíváme do knihovny MSDN na jakoukoli komponentu (např. CheckBox
), vždy nalezneme ukázku jak komponentu vytvořit v obou případech.
Jak na to?
Pojďme si na jednoduchém příkladu ukázat, jakou strukturu má zápis komponenty v C#. Vytvoříme si CheckBox
, který bychom jinak zapsali v XAMLu následovně:
<CheckBox x:Name="cb"
Content="Označ mě!"
FontFamily="Times New Roman"
FontSize="14"/>
Kód v C# bude vypadat takto:
CheckBox cb = new CheckBox();
cb.Content = "Označ mě!";
cb.FontFamily = new FontFamily("Times New Roman");
cb.FontSize = 14;
Struktura zápisu je jednoduchá. Nejprve vytvoříme instanci třídy CheckBox
a následně definujeme její atributy. Je nutné dávat pozor na typy atributů, jelikož některé z nich jsou definované pomocí jiného objektu nebo výčtu (enumeration). V ukázce je to vidět například u definice fontu. Dále se s tím setkáte například u definice barvy ( Colors.Orange
) nebo u definování stylu písma ( FontStyles.Italic
).
Zařazení
Dalším krokem při definování komponent je jejich zařazení. Když tvoříte komponentu v XAMLu a chcete aby byla zařazena v nějaké tabulce, Canvasu
nebo StackPanelu
, vytvoříte jí prostě uvnitř daného prvku.
Pokud však tvoříte komponenty z logiky musíte nějak říci dané komponentě do jakého „kontejneru“ patří. To uděláme pomocí zápisu:
LayoutRoot.Children.Add(cb);
Definování cizích (nevlastních) atributů
Stejně tak jako v XAMLu definujeme například zařazení do tabulky pomocí atributů GridRow a GridColumn přímo v těle komponenty, musíme i v C# tyto atributy definovat. Zápis se trochu liší, jelikož atributy GridRow a GridColumn nejsou vlastními atributy komponenty. Definujeme je tedy pomocí metody SetValue()
.
XAML:
<CheckBox x:Name="cb"
Content="Označ mě!"
FontFamily="Times New Roman"
FontSize="14"
Grid.Column="1"
Grid.Row="1"/>
C#:
CheckBox cb = new CheckBox();
cb.Content = "Označ mě!";
cb.FontFamily = new FontFamily("Times New Roman");
cb.FontSize = 16;
cb.FontStyle = FontStyles.Italic;
cb.SetValue(Grid.RowProperty, 1);
cb.SetValue(Grid.ColumnProperty, 1);
Podobně by to vypadalo i v případě Canvasu:
CheckBox cb = new CheckBox();
cb.Content = "Označ mě!";
cb.FontFamily = new FontFamily("Times New Roman");
cb.FontSize = 16;
cb.FontStyle = FontStyles.Italic;
cb.SetValue(Canvas.TopProperty, 10);
cb.SetValue(Canvas.LeftProperty, 10);
Příklad
Pokud budeme chtít, můžeme si dynamicky vytvořit i celý formulář společně s tabulkou, kterou použijeme pro rozložení formuláře:
//tabulka
Grid gr = new Grid();
gr.Height = 300;
gr.Width = 600;
//definice radku
RowDefinition rd1 = new RowDefinition();
rd1.Height = new GridLength(30);
RowDefinition rd2 = new RowDefinition();
rd2.Height = new GridLength(30);
//prirazeni definice
gr.RowDefinitions.Add(rd1);
gr.RowDefinitions.Add(rd2);
//definice sloupcu
ColumnDefinition cd1 = new ColumnDefinition();
cd1.Width = new GridLength(100);
ColumnDefinition cd2 = new ColumnDefinition();
cd2.Width = new GridLength(500);
//prirazeni definice
gr.ColumnDefinitions.Add(cd1);
gr.ColumnDefinitions.Add(cd2);
//vutvoreni a vlozeni komponent
//Jmeno
TextBlock tb_jmeno = new TextBlock();
tb_jmeno.Text = "Jméno a Příjmení:";
tb_jmeno.HorizontalAlignment = HorizontalAlignment.Right;
tb_jmeno.SetValue(Grid.ColumnProperty, 0);
tb_jmeno.SetValue(Grid.RowProperty, 0);
TextBox tbx_jmeno = new TextBox();
tbx_jmeno.SetValue(Grid.ColumnProperty, 1);
tbx_jmeno.SetValue(Grid.RowProperty, 0);
//Pohlavi
TextBlock tb_pohlavi = new TextBlock();
tb_pohlavi.Text = "Pohlaví:";
tb_pohlavi.HorizontalAlignment = HorizontalAlignment.Right;
tb_pohlavi.SetValue(Grid.ColumnProperty, 0);
tb_pohlavi.SetValue(Grid.RowProperty, 1);
//vytvoreni StackPanelu pro razeni
StackPanel sp = new StackPanel();
sp.Orientation = Orientation.Horizontal;
sp.SetValue(Grid.ColumnProperty, 1);
sp.SetValue(Grid.RowProperty, 1);
RadioButton rb_muz = new RadioButton();
rb_muz.Content = "muž";
rb_muz.GroupName = "pohlavi";
sp.Children.Add(rb_muz);
RadioButton rb_zena = new RadioButton();
rb_zena.Content = "žena";
rb_zena.GroupName = "pohlavi";
sp.Children.Add(rb_zena);
//prirazeni vsech komponent do tabulky
gr.Children.Add(tb_jmeno);
gr.Children.Add(tbx_jmeno);
gr.Children.Add(tb_pohlavi);
gr.Children.Add(sp);
Ve zdrojovém kódu si můžeme všimnout způsobu tvorby řádků a sloupců v tabulce. Nejprve si vytvoříme tabulku a následně si vytvoříme objekty definice řádků a sloupců, které následně přiřadíme k tabulce.
Události
Ukázali jsme si, jak dynamicky vytvářet komponenty a jak je vkládat do „kontejnerů“. Ale nastanou i případy, kdy po dané vygenerované komponentě budeme chtít, aby něco vykonávala. Pojďme si to ukázat na jednoduchém příkladu, kdy si vytvoříme v XAMLu tlačítko, při jehož stisknutí se vygeneruje další tlačítko, které na stisknutí vygeneruje text.
XAML:
<StackPanel x:Name="LayoutRoot"
Background="White"
Orientation="Vertical">
<Button x:Name="bt_xaml"
Content="Vygeneruj tlačítko"
Width="150"
Height="25"
Click="bt_xaml_Click"/>
</StackPanel>
C#:
private void bt_xaml_Click(object sender, RoutedEventArgs e)
{
//tlacitko pro vygenerovani textu
Button bt = new Button();
bt.Content = "Vygeneruj text";
bt.Width = 150;
bt.Height = 25;
bt.Click += new RoutedEventHandler(bt_Click);
LayoutRoot.Children.Add(bt);
}
void bt_Click(object sender, RoutedEventArgs e)
{
TextBlock tb = new TextBlock();
tb.Text = "Toto je vygenerovaný text.";
tb.Width = 150;
tb.TextWrapping = TextWrapping.Wrap;
tb.FontFamily = new FontFamily("Times New Roman");
tb.Foreground = new SolidColorBrush(Colors.Orange);
LayoutRoot.Children.Add(tb);
}
Principem je přiřazení metody k události na komponentě ( bt.Click += new RoutedEventHandler(bt_Click);
).
Závěrem
Pokud budete chtít tvořit aplikace v Silverlightu, této problematice se dozajista nevyhnete. Zvláště pokud se pustíte do tvorby her, kde je podmíněné generování komponent zcela běžné.
Faktem je, že tvorba komponent z logiky aplikace v C# nebo Visual Basicu není tak pohodlná, jako když je píšeme v XAMLu. Je tomu tak především ze dvou důvodů:
- musíme pamatovat na to, jaké atributy jsou jakého typu (jestli jsou objekt, výčet či text)
- musíme každou komponentu přiřazovat do „kontejneru“ namísto přímého vnoření jako v XAMLu
Pokračování přístě
V příštím článku se blíže podíváme na práci s médii, konkrétně pak na práci s videem.
Další možnost, jak dynamicky generovat uživatelské rozhraní, je použít třídu XamlReader ze System.Windows.Markup (http://msdn.microsoft.com/en-us/library/system.windows.markup.xamlreader.aspx). Není pak problém na straně serveru vygenerovat XAML a "podstrčit" ho do již běžící aplikace.
jj… XamlReader je naprosto zásadní například v případě, že použijeme MVC pattern a Model bude na straně serveru. (s tímto se teď peru ve své bakalářce:-)
Děkuji za zajímavý článek. Přál bych si na zdrojáku víc takových.
Pěkný článek, Když se tu bavíme o dynamickém generování UI, tak bych chtěl upozornit na jednu nepřijšmnou vlastnost-bug v Silverlightu.
Jestliže máte vlastni User Control, ve kterém je Popup a ztento Popup neobsahuje ListBox (a možná další prvky), je možné Popup zobrazit a používat, aniž by byl přidán do kolekce Children. Takto definovaný POPUP funguje bez problémů.
<pexeso:popupbase x:class="RStein.Pexeso.SaveFile" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:pexeso="clr-namespace:RStein.Pexeso">
<grid x:name="LayoutRoot" background="Black">
<popup name="filesPopup">
<popup.child>
<stackpanel orientation="Vertical" background="Red">
<textblock text="Název souboru s uloženou hrou" fontsize="15" margin="5,5,5,0" horizontalalignment="Left" foreground="White" textdecorations="Underline"></textblock>
<stackpanel orientation="Horizontal">
<textbox name="txtFile" margin="5" minwidth="200"></textbox>
<textblock foreground="Yellow" visibility="Collapsed" name="txtError" text="Musíte zadat platný název souboru!" horizontalalignment="Left" verticalalignment="Center" fontweight="Bold" fontsize="10"></textblock>
</stackpanel>
<stackpanel orientation="Horizontal" margin="5">
<button style="{StaticResource DialogButton}" content="Uložit" name="btnSelect" click="btnSelect_Click"></button>
<button style="{StaticResource DialogButton}" content="Zpět" name="btnBack" click="btnCancel_Click"></button>
</stackpanel>
</stackpanel>
</popup.child>
</popup>
</grid>
</pexeso:popupbase>
Jestliže ale Popup obsahuje Listbox (a pravděpodobně i jiné prvky), Popup se zuobrazí, ale při vybrání libovolné položky v ListBoxu celý plugin do obsluhy události UnhandledException a napíše jen něco o interní fatální chybě. Mimochodem, Bety a RC Silverlightu tohle podle mě nedělaly.
Tento popup způsobí pád Silverlightu, jestliže Popup není přidán do kolekce Children.
<pexeso:popupbase x:class="RStein.Pexeso.SelectFile" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:pexeso="clr-namespace:RStein.Pexeso">
<grid x:name="LayoutRoot" background="Black">
<popup name="filesPopup">
<popup.child>
<stackpanel orientation="Vertical" background="Red">
<textblock text="Vyberte uloženou hru" fontsize="15" margin="5,5,5,0" horizontalalignment="Left" foreground="White" textdecorations="Underline"></textblock>
<textblock foreground="Yellow" visibility="Collapsed" fontsize="10" name="txtError"></textblock>
<border cornerradius="20" background="White">
<listbox name="lstFiles" background="Orange" height="200">
<listbox.itemtemplate>
<datatemplate>
<textblock text="{Binding Mode=OneWay}" foreground="White"></textblock>
</datatemplate>
</listbox.itemtemplate>
</listbox>
</border>
<stackpanel orientation="Horizontal">
<button style="{StaticResource DialogButton}" content="Vybrat soubor" name="btnSelect" click="btnSelect_Click"></button>
<button style="{StaticResource DialogButton}" content="Zpět" name="btnBack" click="btnCancel_Click"></button>
</stackpanel>
</stackpanel>
</popup.child>
</popup>
</grid>
</pexeso:popupbase>
Před zobrazením Popupu tedy musíme vždy přidat Popu do kolekce Children a po uzavřeni Popupu jej případně odebrat.
rivate void btnLoad_Click(object sender, RoutedEventArgs e)
{
SelectFile file = new SelectFile();
var files = FileAccessComponent.Instance.GetRootFiles();
if (files.Length == 1)
{
return;
}
file.FileListBox.ItemsSource = files;
LayoutRoot.Children.Add(file);
file.DialogClosed += file_DialogClosed;
showPopup(file.FilesPopup);
file.FileListBox.Focus();
}
DialogClosed je moje vlastní událost v předkovi pro všechny dialogy.
{
SelectFile sfDialog = sender as SelectFile;
try
{
if (sfDialog.LastResult == DialogResult.OK && sfDialog.FileListBox.SelectedItem != null)
{
m_currentGame = PexesoGame.Load(sfDialog.FileListBox.SelectedItem.ToString());
removeButtons();
rebindGameData();
}
}
catch (Exception e1)
{
Console.WriteLine(e1);
}
finally
{
LayoutRoot.Children.Remove(sfDialog); //Pridat do kolekce
sfDialog.DialogClosed -= saveFileDialog_DialogClosed;
hidePopup(sfDialog.FilesPopup);
}
}
Komponenty z toolkitu ani tak v popup nefungují. A bohežel další dost
omezující chyba. Při smázání záznamu z gridu se grid nepřekreslí a
zůstane v něm prázdný řádek po smazaném záznamu. Pokud uživatel
smázne záznamy v gridu všechny, poslední dva řádky se nesmažou a
zůstanou prázdné.