WPF Binding bez codebehind

Celkem zajímavý dotaz padl v konferenci o .net na serveru builder.cz. V krátkosti se jednalo o změnu datového zdroje nabindovaného na ListView při změně vybrané položky jiného ListView.

Varianta codebehind

V dotazu bylo poukazováno na obsluhu události SelectionChanged, ve které chtěl tazatel řešit změnu bindování na jiný deklarovaný zdroj ve Window.Resources, konkrétně různě naplněný XmlDataProvider.

Samozřejmě by toto provázání bylo taktéž možné, ale z vlastní zkušenosti a vývoje wpf aplikací jsem zjistil, že používání codebehind souboru není většinou nutné a je možné vše deklarovat pomocí XAML. Codebehind je tak ve větší míře využíván jen při tvorbě vlastních Control.

Varianta bez codebehind

Tazateli jsem tedy zaslal odkaz na příspěvek [Selecting the Detail Level to View at Runtime in WPF] publikovaný na CodeProject od Josh Smith, který se věnoval bindování různých deklarovaných template dle uživatelského výběru. Principiálně tedy velice podobný problém. Martin to tak neviděl a tak jsem se rozhodl konkrétní případ vytvořit a publikovat. Jelikož je však třeba bližšího komentáře, ponechal jsem si odpověď jako krátké povídání.

Postup vytvoření aplikace splňující zadání

Začal jsem s čistým projektem typu WPF aplikace. První na řadě tak byla deklarace datových zdrojů – XmlDataProviderů a naplnění daty.

Deklarace datových zdrojů

<XmlDataProvider x:Key="products" XPath="root/datas">
            <x:XData>
                <root xmlns="">
                    <datas>
                        <data id="a1">a</data>
                        <data id="a2">b</data>
                        <data id="a3">c</data>
                    </datas>
                </root>
            </x:XData>
        </XmlDataProvider>
        <XmlDataProvider x:Key="a1" XPath="root/datas">
            <x:XData>
                <root xmlns="">
                    <datas>
                    <data>1aa</data>
                    <data>1bb</data>
                    <data>1cc</data>
                    </datas>
                </root>
            </x:XData>
        </XmlDataProvider>
        <XmlDataProvider x:Key="a2" XPath="root/datas">
            <x:XData>
                <root xmlns="">
                    <datas>
                    <data>2aa</data>
                    <data>2bb</data>
                    <data>2cc</data>
                    </datas>
                </root>
            </x:XData>
        </XmlDataProvider>
        <XmlDataProvider x:Key="a3" XPath="root/datas">
            <x:XData>
                <root xmlns="">
                    <datas>
                    <data>3aa</data>
                    <data>3bb</data>
                    <data>3cc</data>
                    </datas>
                </root>
            </x:XData>
        </XmlDataProvider>

Deklaraci datových zdrojů a přiřazení jim příslušných identifíkátorů jsem vložil do Window.Resources elementu tak, aby tyto zdroje byly dostupné v celém objektu okna. Tady musím zmínit, že názvy datových zdrojů, které budou měněny po výběru jsem definoval shodné s hlavním zdrojem dat. Toto samozřejmě není nutné, jde jen o to, mít jasně definovaný převodní můstek pro výběr správného zdroje.

Vzhled aplikace

Následovala deklarace aplikace, zvolil jsem StackPanel, do kterého jsem postupně přidával jednotlivé prvky. Nesmí chybět nějaký nadpisek aplikace, abychom neměli jen holé okno. Následuje pak deklarace prvního ListBoxu – jakožto jednodušší varianty (předka) pro ListView. Tento ListBox je nabindován na hlavní datový zdroj. Zároveň definuje vlastní DataTemplate, který se stará o vizuální zobrazení dat v prvku.

        <ListBox Width="400" Height="200" Background="Honeydew" x:Name="prod">
            <ListBox.ItemsSource>
                <Binding Source="{StaticResource products}" XPath="*" />
            </ListBox.ItemsSource>
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock FontSize="12" Foreground="Red">
                      <TextBlock.Text>
                        <Binding XPath="." />
                      </TextBlock.Text>
                    </TextBlock>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

V tomto případě jsem použil expandovaný zápis pro binding i když by se samozřejmě dal použít i inline styl. Druhý deklarovaný ListBox je jen o něco málo složitější, ale ukrývá v sobě hlavní sílu WPF a to MultiBinding a využití IValueConverter resp. IMultiValueConverter.

IMultiValueConverter – srdce bindingu pro řešení zadání

Dříve než sem napíšu i deklaraci druhého ListBoxu, musím se zmínit o srdci celého řešení a to je vytvoření konverteru, který zajistí vyhledání datového zdroje v Resources a jeho navrácení. Pro vyhledání jakéhokoliv pojmenovaného zdroje je možné použít metodu FindResource, případně její rozšířenou variantu TryFindResource lišící se pouze v tom, že nevyvolá Exception v případě, že daný klíč není nalezen v Resources. Pro získání daného zdroje pak potřebujeme získat objekt, který tento zdroj vlastní v tomto případě objekt Window. Zároveň potřebujeme mít k dispozici i vybranou hodnotu prvního ListBoxu.

Jelikož potřebujeme dvě hodnoty, není možné využít jednoduchý IValueConverter, ale musíme použít IMultiValueConverter, který se liší pouze tím, že přebírá do metody Convert pole hodnot.

Jak tedy vypadá kód našeho konverteru:

    public class ResourceKeyConverter: IMultiValueConverter {

        #region Implementation of IMultiValueConverter

        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
            var fe = values[1] as FrameworkElement;
            var val = values[0] as XmlNode;
            if ((fe != null) && (val != null)) {
                var resource = fe.TryFindResource(val.Attributes["id"].Value) as XmlDataProvider;
                return resource;
            }
            return null;
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) {
            throw new NotImplementedException();
        }

        #endregion
    }

Jak je vidět, je to velice jednoduché, samozřejmě je třeba si domyslet ještě i další kontroly, které by bylo vhodné provést.

Binding ListBoxu dle výběru

Nyní se tedy můžeme dostat k deklaraci ListBoxu, který bude obsahovat měněná data z různých datových zdrojů. Jedná se o stejný ListBox, který je definován v prvním případě, rozdílem je pouze zmíněný MultiBinding, který předává konverteru vybranou hodnotu prvního ListBoxu a zároveň i objekt, který drží Resources, tedy v tomto případě pojmenované hlavní okno.

        <ListBox Width="400" Background="AntiqueWhite" ItemsSource="{Binding XPath=*}">
            <ListBox.DataContext>
                <MultiBinding Converter="{StaticResource rsKey}">
                    <MultiBinding.Bindings>
                        <Binding ElementName="prod" Path="SelectedItem" />
                        <Binding RelativeSource="{RelativeSource  AncestorType={x:Type Window}}" />
                    </MultiBinding.Bindings>
                </MultiBinding>
            </ListBox.DataContext>
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock FontSize="12" Foreground="Red">
                      <TextBlock.Text>
                        <Binding XPath="." />
                      </TextBlock.Text>
                    </TextBlock>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

Důležitým krokem v tomto případě je, deklarace a registrace našeho konverteru do zdrojů tak, aby byl k dispozici pro použití v deklaracích u bindingu. To provedeme rovněž v elementu Window.Resources a specifikujeme pro něj klíč. Taktéž je nutné deklarovat namespace, ve kterém se náš konverter nachází.

<WpfBinding:ResourceKeyConverter x:Key="rsKey" />

Stačí jen spustit

Jsme u posledního kroku a to je spuštění aplikace, pokud jsme postupovali správně, dostaneme požadovaný výsledek. Z předchozího povídání je vidět, že pro toto zadání není nutné používat codebehind a je možné téměř všechnu logiku deklarovat za pomoci XAML a vhodně napsaného konverteru, který může být znovupoužit.

Navržené řešení nemusí být optimální a pouze reflektuje dané zadání. Osobně bych se k danému problému snažil postavit v jiném duchu a využil v hojné míře návrhového vzoru Model-View-ViewModel, který používám při tvorbě WPF aplikací. Na druhou stranu se mi toto řešení jeví jako vhodnější, než obsluhovat události jednotlivých prvků v codebehing a snažit se na ně reagovat.

Projekt ukázkové aplikace ke stažení:

1 Comment

Add a Comment