János's profileJános JankaPhotosBlogListsMore Tools Help

Blog


    February 24

    Néhány hasznos eszköz

    Tartottam egy kis gyűjtést, hátha kell valakinek:

    A következő lépés a Microsoft Research-től a bugok feltárásához:
    Pex: Dynamic Analysis and Test Generation for .NET

    A Win32 függvények meghívásának megkönnyítése végett:
    P/Invoke Signature Generator

    Egy ingyenes kis segédeszköz fájlátvitelhez (FTP/S, SFTP, WebDAV/S):
    The Free No-Install FTP, FTPS, SFTP and WebDAV Client

    Már lerágott csont, de azért kiteszem:
    101 LINQ Samples

    Ezt csak érdekességként, hogy mik nem vannak :):
    blogtalkradio

    Mi van vajon az ASP.NET alatt, amit mi nem látunk?
    A low-level Look at the ASP.NET Architecture

    February 15

    MVC (Model View Controller) tervezési minta

    Tegyünk egy kis kiruccanást - a WPF miatt is többek között - mert nagyon hasznos dolog lehet a jövőben amiről itt most beszélni fogok. Jegyzem meg, nekem is új ez a téma, de szerintem ezzel nemcsak én vagyok így (legalábbis a prog.hu alapján), viszont javunkra legyen írva a dolog, hogy csak az utóbbi időkben kezdett ez a szemléletmód a fejlesztő eszközökbe is egyre jobban beívódni, úgyhogy sose késő, vágjunk is bele! (A tanulást nem lehet elég korán elkezdeni, de hogy mennyire nem? A válasz: itt).

    Kezdjük egy kis rövid felvezetővel:

    Először is mondjuk ki az igazat, azt amit mindannyian gondolunk: manapság különböző programozási nyelvek ismerete édes kevés még az éhhalálhoz is. Ezzel szerintem mindenki egyetért ebben a formában. Hiába ismerem elég jól az Object Pascal, C/C++, C#, Visual Basic, Java, Ruby, stb. nyelveket, ha fogalmam nincs arról a platformról amire fejlesztek, illetve azokról a technológiákról, amik a rendelkezésemre állnak általa. Nyilván ez elég sokmindent magába foglal, tehát röviden fogalmazzunk úgy, hogy szükséges egy átfogóbb ismeret arról amire fejleszteni szeretnénk. Tehát pl. Win32 fejlesztőként illik ismerni a Windows API-t annyira mélyen amennyire csak lehet, beleásni a lelkivilágába, s ez vonatkozik ugyanúgy Delphi/C++Builder fejlesztőkre egyaránt, RAD ide, vagy oda. Ha .NET-re fejlesztünk szintén van rengeteg technológia amit el kell sajátítani mihamarabb, mint pl. ADO.NET, ASP.NET, WPF, WCF, WF, LINQ..., s majd csak most eztán mondhatjuk azt, hogy igen, ezzel már tudok mit kezdeni, már képes vagyok professzionális alkalmazásokat fejleszteni önállóan, józan paraszti ésszel. De, most jön az a dolog, amiről a legtöbben megfeledkeznek, az alapos tervezés és rendszerezés. Ma már nem lehet csak úgy nekiállni valamit megcsinálni és ömlesztve ahogy esik úgy puffan írni a kódot - mégha helyes is - mert rengeteg fejfájásunk lehet belőle később, ismétlem rengeteg, sőt ezt már sajnos személyesen is megtapasztaltam elég sokszor egy 6 éven keresztül fejlesztett projektemnél (hibáiból tanul az ember alapon), de épp ezért szeretném függetlenül attól, hogy most milyen fejlesztőeszközt használunk, hogy gondolkodjunk el az alábbiakon. Először is, tegyük fel, hogy megvannak a kellő technológiai ismereteink, kezdjünk el rendet rakni a fejünkben, de mindent csak az elejétől:

    Mik azok a design patternek?

    A design pattern, mint ahogy neve is sugalja, tervezési minta. A design patternek objektumközpontú megoldásokat nyújtanak több számtalan létező problémára, amelyekkel már más fejlesztők életük során többször is szembekerültek. Tulajdonképp arról van szó, hogy a saját tapasztalataik alapján készítettek az adott feladat megoldására egy modellt, ezek persze általában alapos tesztelésen esnek át, s ezeket követve sok fejfájástól kímélheti meg magát az ember. Ez a feladat-megoldási modell általában kiterjeszthető és rugalmasan testreszabható. Létezik már jónéhány minta (több könyv is született már erről a témáról), melyek kategória szerint vannak besorolva: létrehozási minták, szerkezeti minták, viselkedési minták... Minél több ilyen patternt ismerünk, annál általánosabb rálátásunk lehet egy adott feladatra és annál rugalmasabban és későbbi fejfájásoktól megkímélve bővíthetjük a projektünket. Pl. ami VCL esetében tipikus, biztos volt már úgy sok ember, hogy újra fel kellett volna használni egy kódot, ami valamilyen feladatot hajt végre (lehet ez akár egy komplex gyártási folyamat is), s mivel szépen bedrótozta ezt a form kódjába, már nem tudta máshonnan újrahasználni azt. Mi az oka mindennek? A helytelen/átgondolatlan tervezés. Na pl. ilyen és sok más dologra adnak a tervezési minták egyfajta megoldási útmutatót.

    Mik azok a módszerek, melyek rossz tervezésre vallanak?

    Első alapszabály VCL és WinForms programozóknak, hogy a vizuális vezérlőkbe történő üzleti logika beágyazását el kell kerülni. Rengeteg olyan volt a prog.hu-n is, hogy pl. egy threadet készített az egyik kolléga, a thread pedig közvetlenül egy controlon (ListBox v. ComboBox) operált, ami akár volt rá precedens, hogy valamilyen üzleti folyamat részét képezte, ráadásul még amennyire lehet be is volt drótozva, totál újrahasználhatatlan volt a kód. Ez többek között köszönhető a VCL szemléletmódjának is, ami ugyan nem kézteti ki ezt a viselkedést, csak hát az ember általában szeret lusta lenni és inkább összecsapja gyorsan a dolgokat, akár időszűkében is, hisz úgyis csak klikkelgetni, meg legózni kell tudni. Még véletlenül se, mostantól nem! Ezen minták nyelvtől és fejlesztőkörnyezettől függetlenek, így nemcsak .NET nyelvek esetén érvényesek! Akár Visual C#, Visual Basic, Visual C++, Delphi, Delphi for .NET, Delphi for PHP, C++Builder, JBuilder-t használunk, ezen minták mindenhol alkalmazhatóak, viszont most már vannak olyan technológiák, amelyek egy-egy modellhez egyre jobban próbálnak közelíteni, mint pl. a Windows Presentation Foundation az MVC modellhez, de ez nem kizáró ok a máshol történő felhasználására sem.

    Mi is az MVC?

    Mint látható, három szóból tevődik össze a dolog, melynek mindnek önálló jelentése van: Model, View, Controller. Ez egy architektúrális minta, elég régen megszületett már a gondolata (1979) és azóta számtalan teszten is átment, sok területen alkalmazták már sikerrel, tehát érdemes időt szánni rá. Arról szól tulajdonképp, amit fentebb is írtam, mégpedig sokszor van úgy, hogy változtatni szeretnénk az adatkezelésen, újraszervezni az adatokat és nem szeretnénk, hogy mindez érintse a felhasználói felületet, vagyis ahhoz nekünk még véletlenül se kelljen ezen dolgok módosítása miatt hozzányulnunk. Az adathozzáférés és az ún. üzleti logika elválik az adat prezentációjától, melyet egy köztes komponens bevezetésével érünk el, ezt hívjuk Controller-nek. Menjünk akkor sorban, hogy melyik micsoda a modellben:

    Model

    Ebben a részben fogalmazódik meg az üzleti logika. Ezen réteg feladata leírni az adatszerkezeteket (melyeket használni fogunk az alkalmazásban) és definiálni azokat a szabályokat, melyek alapján elérhetjük azokat. Amennyiben az alkalmazásunk valamilyen adatbázisból szedi elő az adatokat (az esetek többségében) és ezt változtatjuk szerkezetileg, vagy netán kicseréljük (pl. Oracle helyett legyen MS-SQL), akkor jobb esetben csak ezt a réteget kell módosítanunk, s ami mégfontosabb, nem vonja magával a user interface változtatásának szükségét. Pontosan ezért régebben, már az ORM korszak előtt is tervezéskor célszerű volt az adatbázis minden táblájához egy-egy külön osztályt gyártani és ezáltal a modell újrahasználhatóvá vált a későbbiekben és egyben függetlenné is az alatta lévő adatforrástól. Manapság az új ORM (Object Relational Mapping) eszközöknek köszönhetően ez a feladat jóval egyszerűbbé válik, mint pl. Microsoft LINQ, Borland/CodeGear ECO, Hibernate (Java), NHibernate (.NET)... A modell megvalósítása történhet újrahasználhatósági okokból COM osztályokban, vagy sima webservicekben, illetve WCF szolgáltatásokban is akár.

    View

    Ez itt a megjelenítéssel/prezentációval kapcsolatos osztályokat írja le, s itt mutatkozik meg az MVC igazi ereje, vagyis szeparálódik a prezentáció az adatoktól. A View dolga a Model teljes tartalmának vagy egy részének prezentálása. A legfontosabb alapszabály: az adatokat csak a Model osztályainak példányain keresztül érhetjük el és módosíthatjuk! Ezt az elvet a helyes tervezés érdekében muszály követni, s a lényeg még csak most jön, ha idáig követtük ezt a gondolatmenetet, akkor az eredményt bármilyen felületen keresztül, legyen az böngésző, mobil eszközök, VCL/WinForms/WPF alkalmazás, bármilyen tartalmat előállíthatunk a modell alapján. A feladat mindössze annyi, hogy egy-egy View-ot készítünk mindhez, melyek ugyanazzal a modellel fognak dolgozni. Ez a terület az amire a WPF is ki lett hegyezve, s a WPF adatkötési támogatása sem elhanyagolandó a téma szempontjából (aminek az erejét már a korábban bemutatott kis WPF-es példáimban is láthattatok).

    Controller

    Itt a lényege az egésznek. Ok, hogy van Model és vannak View-k, de ezeket össze kellene kötni valahogy. Ezért felel a Controller. Az eseményeket, mint pl. kattintás egy gombra, bill. leütés... átfordítja egy a modell által végrehajtandó akcióra, illetve az akció lefutásának eredményétől függően megjeleníti a megfelelő View-t. A Controller nem mindig van külön megvalósítva, előfordul néha a gazdag kliens alkalmazások esetében, hogy összeolvad a View-al.

    Röviden és tömören ennyit az MVC-ről. Rengeteg előnye van a dolognak, mint pl. az üzleti logika elválik az interakciós logikától (lsd. WPF codebehind fájl első comment). Az alkalmazás egyes részei jól elhatárolódnak egymástól, ezáltal újrafelhasználhatóvá válik a kód, s nem utolsó sorban mindez a csapatmunkát is elősegíti, mindenki a saját feladatára összpontosíthat, a másikra történő túlságosan erős ráutaltság elkerülhető vele. Egyetlen hátránya, hogy jól meg kell tervezni egy nagy adag osztályt, amik ezt az elhatárolódást szavatolják és nyilván sokkal több időt igényel, de ez a végén busásan megtérül, arról nem is beszélve, hogy szépen mindennek meglesz a helye, nem kell elveszni a nagy összevisszaság áradatában.

    A további hozzászólásokat várom a témával kapcsolatban, ha esetleg valaki még tud hozzátenni valamit, bár szerintem a fő elvi irányokat sikerült itt megfogalmaznom. A WPF esetében mindenképp ezt a szemléletet érdemes követni, mégha az elején szokatlan is lesz, meg többletmunkával is jár. Íme néhány link az információgyűjtéshez:

    Model-View-Controller
    A practical use of the MVC pattern
    ASP.NET MVC Framework
    Introduction to Model View Control (MVC) Pattern using C#
    Implementing MVC Design Pattern in .NET

    Jó tanulást mindenkinek!

    February 07

    WPF sablonok használata IV. (ListView vs DataGrid - Rendezés)

    Már szinte mindent tud a ListView-unk, amit egy alap gridtől el lehet várni, de még rendezni szegény nem nagyon képes. Hogyan tudunk ezen segíteni?

    Megint csupa jó híreim vannak, ez szintén alapból támogatott. Az ItemCollection osztály, amilyen a ListView Items tulajdonsága is, rendelkezik egy SortDescriptions SortDescriptionCollection típusú tulajdonsággal, melyhez egyszerűen hozzá lehet adni rendezés leírási objektumokat. Ez támogatja az előre-visszafele rendezést és az összetett rendezést egyaránt. Az SDK alapból egy egyszerű rendezést valósít meg annak mintájára, ahogy alapból a ListView működik pl. a Windows-ban is. Na most pl. nekem spec. pont nem elég ennyi soha, tehát mi itt összetett rendezést fogunk csinálni a ListView-ban.

    Menjünk sorjában akkor, hogy mikre lesz itt szükségünk. Először is van egy olyan tulajdonsága a GridViewColum-nak, hogy HeaderTemplate. Ez idáig ok, nyilván ez alapján lecserélhetjük bármi másra az oszlopok fejlécét, akárcsak az item-ek esetében tettük ezt. Na most vegyük szemügyre mi kell a SortDescription-nek (a konstruktor paraméterei alapján):

    public SortDescription(string propertyName, ListSortDirection direction)

    Első a tulajdonság neve, amire rendezni akarunk (pl. ID, Name, Town, stb.), második pedig az irány (Asc, Desc). Nyilván amilyen sorrendbe bekerülnek ezek az objektumok majd a kollekcióba (SortDescriptions), olyan sorrendben történik a rendezés is. Tehát szükségünk lesz a tulajdonság nevére a ListView GridViewColumnHeader.Click eseményének kezelése során. Nem jó megközelítés a GridViewColumn Header tulajdonságára hagyatkozni, mert azt én most fogom és lokalizálom (szí'p magyarosan):

    <ListView.View>                   
        <
    GridViewAllowsColumnReorder="true">
          
    <!-- ID -->
           <GridViewColumn                       
              
    Header="Azonosító"
              
    Width="150"                                  
              
    CellTemplate="{StaticResourceIDTemplate}"/>                           

          
    <!-- Name -->
           <GridViewColumn
              
    Header="Név"
              
    Width="100"
              
    CellTemplate="{StaticResourceNameTemplate}"/>
           
          
    <!-- Town -->
           <GridViewColumnHeader="Város"
              
    Width="100"
              
    CellTemplate="{StaticResourceTownTemplate}"/>

          
    <!-- Phone -->
           <GridViewColumn                           
              
    Header="Telefon"
              
    Width="110"
              
    CellTemplate="{StaticResourcePhoneTemplate}"/>

          
    <!-- Picture -->
           <GridViewColumn                           
              
    Header="Kép"
              
    Width="150"
              
    CellTemplateSelector="{StaticResourcePictureTemplateSelector}"/>
        </
    GridView>
    </
    ListView.View>

    Puff neki, már be is krepálna az egész. Postosan ezért, hogy ez ne függjön a Header-től, most csinálunk egy saját GridViewColumn származékot és abba teszünk egy string típusú tulajdonságot, amibe tároljuk, hogy ez az oszlop tulajdonképp az objektum (User) mely tulajdonságához köt. Ezt a tulajdonságot célszerű dependency propertyként definiálni, hogy köthető legyen máshoz. Ez így fest:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Windows.Controls;
    using System.Windows;
    using System.ComponentModel;
    
    namespace WPFTemplates
    {
        public class SortableGridViewColumn : GridViewColumn
        {        
            /// <summary>
            /// A célobjektum azon tulajdonsága, ami ehhez az oszlophoz tartozik
            /// </summary>
            public string PropertyName
            {
                get { return (string)GetValue(PropertyNameProperty); }
                set { SetValue(PropertyNameProperty, value); }
            }
    
            public static readonly DependencyProperty PropertyNameProperty =
                DependencyProperty.Register("PropertyName",
                typeof(string), typeof(SortableGridViewColumn));
        }
    }

    Még mielőtt bárki kiakadna, ezt a sok "szörnyűséget" nem kell kézzel megírni. Elég bepötyögni, hogy propd és egy TAB-ot nyomni, s szépen lehet tabokkal ugrálva kitöltögetni a megfelelő dolgokat. Na most következik, hogy ezt használni is kellene valahogy. Szépen lecseréljük a fenti GridViewColumn-okat erre:

    <ListView.View>                    
        <GridView AllowsColumnReorder="true">
            <!-- ID -->
            <WPFTemplates:SortableGridViewColumn                            
                Header="Azonosító"
                PropertyName="ID"
                Width="150"                                   
                CellTemplate="{StaticResource IDTemplate}"/>                            
    
            <!-- Name -->
            <WPFTemplates:SortableGridViewColumn 
                Header="Név"
                PropertyName="Name"
                Width="100"
                CellTemplate="{StaticResource NameTemplate}"/>
            
            <!-- Town -->
            <WPFTemplates:SortableGridViewColumn                            
                Header="Város"
                PropertyName="Town"
                Width="100"
                CellTemplate="{StaticResource TownTemplate}"/>
            
            <!-- Phone -->
            <WPFTemplates:SortableGridViewColumn                            
                Header="Telefon"
                PropertyName="Phone"
                Width="110"
                CellTemplate="{StaticResource PhoneTemplate}"/>
    
            <!-- Picture -->
            <WPFTemplates:SortableGridViewColumn                            
                Header="Kép"
                PropertyName="Picture"
                Width="150"
                CellTemplateSelector="{StaticResource PictureTemplateSelector}"/>
        </GridView>
    </ListView.View>

    That's all. Senki sem mondhatja, hogy nagyon meg kellett erőltetni magunkat. Következő lépés, hogy kellene kezelni a fejlécen történő klikkelést. Egészítsük ki a ListView-ot egy kicsit (közbe mutatom, hogy működik az IntelliSense is):

     XAML IntelliSense

    Most már mindössze annyi dolgunk maradt, hogy megírjuk a rendezéshez szükséges kódot. Mondjuk működjön úgy, hogy ha a felhasználó nyomja a CTRL-t, akkor lehessen több oszlopot is hozzáadni. Valahogy ezen a ponton meg kellene jegyezni, hogy mi hogy van rendezve. Első ötlet erre, hogy a saját GridViewColumn származékunkat egészítsük ki egy ListSortDirection? típusú tulajdonsággal és ebbe tároljuk, hogy az adott oszlop éppen merre van rendezve, vagy éppen nincs rendezve (null). Szép is lenne, csak nem működik, mivel az oszlopfejlécek mindig újraépülnek. Ehelyett én most egy Dictionary segítségével oldottam ezt meg, nyilván lehet ezt cicomázni, szétszedni, stb., sőt célszerű saját ListView származékot készíteni (pl. SortableListView), de gyorsan összeütöttem egy működő megoldást, íme:

    Először is kell egy struktúra, amibe az aktuális állapotot tároljuk el:

    struct SortedColumn
    {
        public string PropertyName { get; set; }        
        public ListSortDirection SortDirection { get; set; }        
    }

    Majd jöhet a többi, a fejlécklikk kezelése is:

    private Dictionary<GridViewColumnHeader, SortedColumn> sortedColumns =
        new Dictionary<GridViewColumnHeader, SortedColumn>();
    
    /// <summary>
    /// Fejléc klikk
    /// </summary>
    private void lvUsersColumnHeader_Click(object sender, RoutedEventArgs e)
    {
        // oszlopfejléc
        GridViewColumnHeader columnHeader = e.OriginalSource as GridViewColumnHeader;
        if (columnHeader == null)
            return;
    
        // SortableGridViewColumn-ról van szó ?
        SortableGridViewColumn sortableColumn = columnHeader.Column as SortableGridViewColumn;
        if (sortableColumn == null)
            return;
    
        // ha nincs megadva tulajdonságnév az oszlophoz, akkor nem lehet rá rendezni
        if (string.IsNullOrEmpty(sortableColumn.PropertyName))
            return;
    
        ListView listView = e.Source as ListView;
    
        SortedColumn sortedColumn;
    
        // ha már benne van a szótárban...
        if (sortedColumns.ContainsKey(columnHeader))
        {
            sortedColumn = sortedColumns[columnHeader];
            // rendezés megfordítása
            sortedColumn.SortDirection =
                sortedColumn.SortDirection == ListSortDirection.Ascending ?
                ListSortDirection.Descending : ListSortDirection.Ascending;
        }
        else
        {
            sortedColumn =
                new SortedColumn
                {
                    PropertyName = sortableColumn.PropertyName,
                    SortDirection = ListSortDirection.Ascending
                };
        }
    
        // ha a CTRL le van nyomva...
        if (Keyboard.Modifiers == ModifierKeys.Control)
        {
            sortedColumns[columnHeader] = sortedColumn;
        }
        else
        {
            sortedColumns.Clear();
    
            // az alapértelmezett fejlécsablonok visszaállítása
            GridView gridView = listView.View as GridView;
            if (gridView != null)
            {
                foreach (GridViewColumn gvColumn in gridView.Columns)
                    gvColumn.HeaderTemplate = null;
            }
    
            sortedColumns.Add(columnHeader, sortedColumn);
        }
    
        // a megfelelő template kiválasztása
        if (sortedColumn.SortDirection == ListSortDirection.Ascending)
            columnHeader.Column.HeaderTemplate =
                listView.Resources["HeaderTemplateArrowUp"] as DataTemplate;
        else
            columnHeader.Column.HeaderTemplate =
                listView.Resources["HeaderTemplateArrowDown"] as DataTemplate;
    
        // tényleges rendezés...
        listView.Items.SortDescriptions.Clear();
        foreach (SortedColumn sc in sortedColumns.Values)
        {
            try
            {
                listView.Items.SortDescriptions.Add(
                    new SortDescription(sc.PropertyName, sc.SortDirection)
                );
            }
            catch
            {
                // elkapunk minden kivételt
                // mert pl. a képet nem lehet rendezni
            }
        }
    
        // frissítés
        listView.Items.Refresh();
    }
    Ez persze elég csúnya így, ráadásul manuálisan nem is tudjuk befolyásolni a rendezést külön metódusokkal, mármint olyan értelembe ami frissítené a fejlécsablonokat, de most ettől a kis affértól eltekintve működik. A teljes ListView kódot is beillesztem, mert kell kettő darab DataTemplate a nyilacskákhoz is:
    <!-- felhasználók listája -->
    <ListView
        x:Name="lvUsers"                                                    
        IsSynchronizedWithCurrentItem="True"                                                
        ItemsSource="{Binding Source={StaticResource UserManagerDS}}"                
        GridViewColumnHeader.Click="lvUsersColumnHeader_Click">
                        
        <!-- ListView erőforrások -->
        <ListView.Resources>
            <!-- fejlécsablon (nyíl felfele) -->
            <DataTemplate x:Key="HeaderTemplateArrowUp">
                <DockPanel>
                    <TextBlock HorizontalAlignment="Center" Text="{Binding}"/>
                    <Path x:Name="ArrowUp"
                          StrokeThickness="1"                                      
                          Fill="Black"
                          Data="M 5,10 L 15,10 L 10,5 L 5,10"/>
                </DockPanel>
            </DataTemplate>
    
            <!-- fejlécsablon (nyíl lefele) -->
            <DataTemplate x:Key="HeaderTemplateArrowDown">
                <DockPanel>
                    <TextBlock HorizontalAlignment="Center" Text="{Binding}"/>
                    <Path x:Name="ArrowDown"
                          StrokeThickness="1"                                   
                          Fill="Black"
                          Data="M 5,5 L 10,10 L 15,5 L 5,5"/>
                </DockPanel>
            </DataTemplate>
    
            <!-- "nincs adat" sablon -->
            <DataTemplate x:Key="EmptyTemplate">
                <TextBlock
                    Text="Nincs megjeleníthető arcképe a felhasználónak"
                    Foreground="Red"
                    TextAlignment="Center"
                    TextWrapping="Wrap"/>
            </DataTemplate>                    
            
            <!-- ID sablon -->
            <DataTemplate x:Key="IDTemplate">
                <TextBox Text="{Binding Path=ID,
                                        Mode=TwoWay,
                                        UpdateSourceTrigger=PropertyChanged}"/>
            </DataTemplate>
            
            <!-- Name sablon -->
            <DataTemplate x:Key="NameTemplate">
                <TextBox Text="{Binding Path=Name,
                                        Mode=TwoWay,
                                        UpdateSourceTrigger=PropertyChanged}"/>
            </DataTemplate>
            
            <!-- Town sablon -->
            <DataTemplate x:Key="TownTemplate">
                <ComboBox Text="{Binding Path=Town,
                                     Mode=TwoWay,
                                     UpdateSourceTrigger=PropertyChanged}"
                      IsEditable="True">
                    <ComboBox.Items>
                        <ComboBoxItem Content="Debrecen"/>
                        <ComboBoxItem Content="Budapest"/>
                        <ComboBoxItem Content="Szeged"/>
                        <ComboBoxItem Content="Miskolc"/>
                    </ComboBox.Items>
                </ComboBox>
            </DataTemplate>
    
            <!-- Phone sablon -->
            <DataTemplate x:Key="PhoneTemplate">
                <TextBox Text="{Binding Path=Phone,
                                        Mode=TwoWay,
                                        UpdateSourceTrigger=PropertyChanged}"/>
            </DataTemplate>
    
            <!-- Picture sablon -->
            <DataTemplate x:Key="PictureTemplate">
                <Image Source="{Binding Path=Picture,
                                    Mode=TwoWay,
                                    UpdateSourceTrigger=PropertyChanged}"
                   Height="75" Width="100"
                   Stretch="Uniform"/>
            </DataTemplate>
           
            
            <!-- sablon választó -->
            <WPFTemplates:UserTemplateSelector
                x:Key="PictureTemplateSelector"
                DefaultTemplate="{StaticResource PictureTemplate}"
                NullTemplate="{StaticResource EmptyTemplate}"
                PropertyName="Picture"/>
    
        </ListView.Resources>
    
        <!-- nézet leírása -->
        <ListView.View>                    
            <GridView AllowsColumnReorder="true">
                <!-- ID -->
                <WPFTemplates:SortableGridViewColumn                            
                    Header="Azonosító"
                    PropertyName="ID"
                    Width="150"                                   
                    CellTemplate="{StaticResource IDTemplate}"/>                            
    
                <!-- Name -->
                <WPFTemplates:SortableGridViewColumn 
                    Header="Név"
                    PropertyName="Name"
                    Width="100"
                    CellTemplate="{StaticResource NameTemplate}"/>
                                        
                <!-- Town -->
                <WPFTemplates:SortableGridViewColumn                            
                    Header="Város"
                    PropertyName="Town"
                    Width="100"
                    CellTemplate="{StaticResource TownTemplate}"/>
                
                <!-- Phone -->
                <WPFTemplates:SortableGridViewColumn                            
                    Header="Telefon"
                    PropertyName="Phone"
                    Width="110"
                    CellTemplate="{StaticResource PhoneTemplate}"/>
    
                <!-- Picture -->
                <WPFTemplates:SortableGridViewColumn                            
                    Header="Kép"
                    PropertyName="Picture"
                    Width="150"
                    CellTemplateSelector="{StaticResource PictureTemplateSelector}"/>
            </GridView>
        </ListView.View>
    
    </ListView>

    A két fejlécsablont (ArrowUp, ArrowDown) szépen cserélgetjük a rendezés során, éppen ahogy rendezve van az oszlop. A kis háromszögek egy-egy Path objektum segítségével vannak leírva, innen látszik, hogy bármit betehetnénk oda a rendezési irány jelzésére akár, pl. egy képet is.

    A végeredmény egy összetett rendezésre képes ListView, mint látható a képen is először rendeztem városra növekvő, majd névre csökkenő sorrendbe:

    SortableListView

    Nyilván lassan elékerzünk egy olyan pontra, hogy mindezeket érdemes lenne egyetlen egy saját ListView-ba implementálni, rugalmasabban, bárhol újra használhatóan. Majd erre is sort kerítünk, de elsőre gyakorlásképp ez is megteszi.

    Ennyit a rendezésről és akkor legközelebb innen folytatjuk...

    February 04

    WPF sablonok használata III. (ListView vs DataGrid)

    Az előzőeket folytatván, röviden összefoglalva mit csináltunk eddig: megismerkedtünk egy újfajta kódolási szemlélettel, új osztályokkal, interfészekkel (pl. ObservableCollection, INotifyCollectionChanged), új nyelvi C# 3.0 elemekkel (pl. Linq To Xml), adatsablonokkal (DataTemplate), ListView controllal, illetve pl. olyan kis finomságokkal, mint a szűrés. Most viszont tovább finomítjuk a dolgokat. Egyrészt nem felhasználóbarát a kezelése néhány műveletnek (mint pl. a törlés), másrészt rendezni, csoportosítani sem lehet az adatokat, harmadrészt pedig ez a külső sem éppen a "user experience" szellemében megfelelő hatást éri el.

    Még mielőtt ebbe belevágnánk, Socó Zsolti kolléga kérdését kielégítve szeretnék egy-két szóban válaszolni arra a kérdésre, hogy mely névtér miért létezik itt jelen esetben:

    <Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2006"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:WPFTemplates="clr-namespace:WPFTemplates"
        xmlns:system="clr-namespace:System;assembly=mscorlib"
        mc:Ignorable="d"
        x:Class="WPFTemplates.WinMain" ...

    Az első: http://schemas.microsoft.com/winfx/2006/xaml/presentation névtér deklaráció a teljes WPF névteret térképezi. Ez az alapértelmezett névtér, látszik is, hogy nincs külön névtér prefix (lsd. SDK), pl. xlmns:wpf=...  előtte. Ezen keresztül látszik az összes control, ablak, stb. Ennek kizárásos alapon mindenképp maradnia kell, ha szeretnénk bármit is csinálni.

    Következő az xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml, mely általában szükséges úgyszint minden WPF alkalmazáshoz, pl. tartalmaz XamlReader osztályt is. Jelen esetben a lényege, hogy összetársítja a XAML-t a codebehind fájlal (lsd. x:Class="WPFTemplates.WinMain"), ahol a WinMain nem más, mint egy partial class.

    A következők, úgy mint:
    xmlns:d=http://schemas.microsoft.com/expression/blend/2006
    xmlns:mc=http://schemas.openxmlformats.org/markup-compatibility/2006
    mc:Ignorable="d"
    mind a Blend toldaléka. Egy konkrét példa arra, hogy mire is jó ez? Például a Blend tud automatikusan tesztadatokat generálni egy listának, akkor amikor a megjeleníteni kívánt típushoz a blend designerébe elkészítünk egy DataTemplate-t, van ott alul egy kis checkbox, hogy "Generate sample data". Így már tudjuk tesztelni az alkalmazást néhány auto generált adattal. Na most pl. a fentiek segítségével ezt kikapcsolhatjuk, ha a ListBox-ot kiegészítjük pl. így:

    <ListBox d:UseSampleData="false" ...

    Megjegyzendő, hogy a Cider-be nem jelennek meg ezek a tesztadatok, csak a Blend miatt van értelme használni ezt.

    Következő névtér az xmlns:WPFTemplates="clr-namespace:WPFTemplates", ami nem más, mint a saját alkalmazásunk névtere. Minden, amit én ebben a névtérben hozok létre, az XAML-ben így érhető el pl.: <WPFTemplates:User/>. Ez persze igaz a többire is <x:XamlReader/>, stb. Az IntelliSense automatikusan detektálja és a kurzor helyén felkínálja az ott definiálható tagok listáját. Pl. egy felhasználó létrehozása XAML-ben:

    <Window.Resources>
        <WPFTemplates:User x:Key="XamlUser">
            <WPFTemplates:User.ID>kpista@msn.com</WPFTemplates:User.ID>
            <WPFTemplates:User.Name>Kiss Pista</WPFTemplates:User.Name>
            <WPFTemplates:User.Phone>06 00 111-2222</WPFTemplates:User.Phone>
            <WPFTemplates:User.Town>Debrecen</WPFTemplates:User.Town>
            <WPFTemplates:User.Picture>
                <BitmapImage UriSource="KissPista.jpg"/>
            </WPFTemplates:User.Picture>
        </WPFTemplates:User>    
    </Window.Resources>
    
    <Grid>   
        <Button DataContext="{StaticResource XamlUser}" Content="{Binding Path=Name}"/>
        <Button DataContext="{StaticResource XamlUser}">            
           <Label Content="{Binding Path=Name}"/>
        </Button>
    </Grid>

    Az adatkontextus (DataContext) használatával pedig a típus összes tulajdonsága könnyedén megadhatóvá válik anélkül, hogy a forrást a kötésben állandóan fel kellene tüntetni {Binding Source=...}. Direkt két gombot csináltam, hogy lássuk, a kontextus kiterjed az alsóbb színtű elemekre is. Majd erről később...

    Nem maradt más hátra, mint a xmlns:system="clr-namespace:System;assembly=mscorlib". Ez pedig mint korábban elmítettem a CLR alaptípusok használatához volt szükséges felvegyem (lsd. ObjectDataProvider). Minden névtér deklaráció látványosan egyébként két részre oszlik, első a clr-namespace, az a névtér, ahol a nyilvánosnak deklarált típusok látszanak és elérhetőek, a másik pedig az assembly neve útvonal nélkül. A clr-namespace elválasztása az értéktől : jellel, míg az assembly elválasztása az értéktől = jellel történik, pl.: xmlns:system="clr-namespace:System;assembly=mscorlib".

    Ennyit a névtérdeklarációkról és térjünk vissza a mi kis ListView példánkhoz.

    Kezdjük akkor a törlés gomb példával. Mi történik, ha pl. kitörlünk a forrásból (ObservableCollection) egy felhasználót? Válasz: a kurzor eltűnik, nem képes megjegyezni, hogy mi állt előtte és ott maradni. Na ez így már nem túl elegáns, ráadásul mi több, egyenesen idegesítő. A megoldás nem is gondolnánk milyen egyszerű, úgy hívják, hogy IsSynchronizedWithCurrentItem. Ez egy nullázható boolean típusú tulajdonság, melyet igazra állítva a SelectedItem szinkronba fog maradni az aktuális tétellel az ItemCollection-ben. Ezt ki is próbálhatjuk például úgy, hogy leteszünk pl. egy listbox-ot a ListView mellé a következő módon (ez még mindig az előző bejegyzésekben szereplő kód kiegészítése, ezért is csak egy részletet szúrok ide be):

    <StackPanel Margin="0,50,0,0">
        <!-- név lista -->
        <ListBox
            IsSynchronizedWithCurrentItem="True"
            ItemsSource="{Binding Source={StaticResource UserManagerDS}}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Label Content="{Binding Path=Name}"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    
        <!-- felhasználók listája -->
        <ListView
            x:Name="lvUsers"                                    
            IsSynchronizedWithCurrentItem="True"
            ItemsSource="{Binding Source={StaticResource UserManagerDS}}">
    ...
    </StackPanel>

    Most ha lefutattjuk így az alkalmazásunkat, ami mindössze egy stackpanel-el, egy listbox-al, illetve az IsSynchronizedWithCurrentItem-ek beállításával bővült láthatjuk, hogy mindkét lista szinkronban marad:

    image

    Na vajon hogyan tudnánk azt megcsinálni, hogy értéktől függően cserélődjenek a sablonok. Pl. ha nincs képe a felhasználónak, akkor jelenjen meg helyette egy piros szöveg, hogy nincs kép. Többféle megoldás is létezik, most még maradjunk a GridViewColumn-nál. A megoldás a CellTemplateSelector tulajdonság. Mindössze készítenünk kell egy saját DataTemplateSelector származékot és felül írnunk a SelectTemplate tagfüggvényét. Most így néz ki a ListView-unk:

    <!-- felhasználók listája -->
    <ListView
        x:Name="lvUsers"                                                    
        IsSynchronizedWithCurrentItem="True"                                                
        ItemsSource="{Binding Source={StaticResource UserManagerDS}}">
    
        <!-- ListView erőforrások -->
        <ListView.Resources>     
            <!-- "nincs adat" sablon -->
            <DataTemplate x:Key="EmptyTemplate">
                <TextBlock
                    Text="Nincs megjeleníthető arcképe a felhasználónak"
                    Foreground="Red"
                    TextAlignment="Center"
                    TextWrapping="Wrap"/>
            </DataTemplate>                    
            
            <!-- ID sablon -->
            <DataTemplate x:Key="IDTemplate">
                <TextBox Text="{Binding Path=ID,
                                        Mode=TwoWay,
                                        UpdateSourceTrigger=PropertyChanged}"/>
            </DataTemplate>
            
            <!-- Name sablon -->
            <DataTemplate x:Key="NameTemplate">
                <TextBox Text="{Binding Path=Name,
                                        Mode=TwoWay,
                                        UpdateSourceTrigger=PropertyChanged}"/>
            </DataTemplate>
            
            <!-- Town sablon -->
            <DataTemplate x:Key="TownTemplate">
                <ComboBox Text="{Binding Path=Town,
                                         Mode=TwoWay,
                                         UpdateSourceTrigger=PropertyChanged}"
                          IsEditable="True">
                    <ComboBox.Items>
                        <ComboBoxItem Content="Debrecen"/>
                        <ComboBoxItem Content="Budapest"/>
                        <ComboBoxItem Content="Szeged"/>
                        <ComboBoxItem Content="Miskolc"/>
                    </ComboBox.Items>
                </ComboBox>
            </DataTemplate>
    
            <!-- Phone sablon -->
            <DataTemplate x:Key="PhoneTemplate">
                <TextBox Text="{Binding Path=Phone,
                                        Mode=TwoWay,
                                        UpdateSourceTrigger=PropertyChanged}"/>
            </DataTemplate>
    
            <!-- Picture sablon -->
            <DataTemplate x:Key="PictureTemplate">
                <Image Source="{Binding Path=Picture,
                                    Mode=TwoWay,
                                    UpdateSourceTrigger=PropertyChanged}"
                   Height="75" Width="100"
                   Stretch="Uniform"/>
            </DataTemplate>
           
            
            <!-- sablon választó -->
            <WPFTemplates:UserPictureTemplateSelector
                x:Key="PictureTemplateSelector"
                PictureTemplate="{StaticResource PictureTemplate}"
                EmptyTemplate="{StaticResource EmptyTemplate}"/>
    
        </ListView.Resources>
    
        <!-- nézet leírása -->
        <ListView.View>                    
            <GridView AllowsColumnReorder="true">
                <!-- ID -->
                <GridViewColumn                        
                    Header="ID"
                    Width="150"
                    CellTemplate="{StaticResource IDTemplate}"/>                            
    
                <!-- Name -->
                <GridViewColumn 
                    Header="Name"
                    Width="100"
                    CellTemplate="{StaticResource NameTemplate}"/>
                
                <!-- Town -->
                <GridViewColumn                            
                    Header="Town"
                    Width="100"
                    CellTemplate="{StaticResource TownTemplate}"/>
    
                <!-- Phone -->
                <GridViewColumn                            
                    Header="Phone"
                    Width="110"
                    CellTemplate="{StaticResource PhoneTemplate}"/>
    
                <!-- Picture -->
                <GridViewColumn                            
                    Header="Picture"
                    Width="150"
                    CellTemplateSelector="{StaticResource PictureTemplateSelector}"/>
            </GridView>
        </ListView.View>
    
    </ListView>
    

    A template selector kódja pedig ennyi:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    
    namespace WPFTemplates
    {
        public class UserPictureTemplateSelector : DataTemplateSelector
        {
            public DataTemplate PictureTemplate { get; set; }
            public DataTemplate EmptyTemplate { get; set; }
            
            public override DataTemplate SelectTemplate(object item,
    DependencyObject container) { User user = item as User; if (item == null) return base.SelectTemplate(item, container); return user.Picture == null ? EmptyTemplate : PictureTemplate; } } }

    Az eredmény szintén okés:

    image

    Ha azt szeretnénk, még egy picit általánosabbá is tehetjük:

    <!-- sablon választó -->
    <WPFTemplates:UserTemplateSelector
        x:Key="PictureTemplateSelector"
        DefaultTemplate="{StaticResource PictureTemplate}"
        NullTemplate="{StaticResource NullTemplate}"
        PropertyName="Picture"/>

    C#

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    
    namespace WPFTemplates
    {
        public class UserTemplateSelector : DataTemplateSelector
        {
            public DataTemplate DefaultTemplate { get; set; }
            public DataTemplate NullTemplate { get; set; }
            public string PropertyName { get; set; }
            
            public override DataTemplate SelectTemplate(object item,
    DependencyObject container) { User user = item as User; if (item == null) return base.SelectTemplate(item, container); object value = null; switch (PropertyName) { case "ID": value = user.ID; break; case "Name": value = user.Name; break; case "Town": value = user.Town; break; case "Phone": value = user.Phone; break; case "Picture": value = user.Picture; break; } return value == null ? NullTemplate : DefaultTemplate; } } }

    Én most ennyi tulajdonság miatt nem használtam reflection-t, se más megközelítést. Lehetne ezt tovább cicomázni, de a lényeg ugyanaz.

    Innen folytatjuk tovább legközelebb...

    February 03

    WPF sablonok használata II. (ListView vs DataGrid)

    Folytatva az előző bejegyzést, mert sajnos nem fért ki egybe, a következőket tesszük:

    Kénytelenek leszünk saját sablonokat definiálni. A GridViewColumn DisplayMemberBinding tulajdonsága helyett a CellTemplate -et fogjuk használni. Ha az adott sablonra szükségünk van máshol is alkalmazás szinten, akkor meg mehet az App.xaml -be is akár. Most a ListView -ba teszek mindent, hogy látszódjon a dolog egyben ahogy van szépen kikommentezve, illetve ezzel kapcsolatban is két módszert mutatok:

    <!-- felhasználók listája -->
    <ListView x:Name="lvUsers"
              ItemsSource="{Binding Source={StaticResource UserManagerDS}}">
    
        <!-- ListView erőforrások -->
        <ListView.Resources>                
            <!-- Town -->
            <DataTemplate x:Key="TownTemplate">
                <ComboBox Text="{Binding Path=Town}" IsEditable="True">
                    <ComboBox.Items>
                        <ComboBoxItem Content="Debrecen"/>
                        <ComboBoxItem Content="Budapest"/>
                        <ComboBoxItem Content="Szeged"/>
                        <ComboBoxItem Content="Miskolc"/>
                    </ComboBox.Items>
                </ComboBox>
            </DataTemplate>
    
            <!-- Picture -->
            <DataTemplate x:Key="PictureTemplate">
                <Image Source="{Binding Path=Picture}"
                           Height="75" Width="100"
                           Stretch="Uniform"/>
            </DataTemplate>
        </ListView.Resources>
    
        <!-- nézet leírása -->
        <ListView.View>
            <GridView AllowsColumnReorder="true">
                <!-- ID -->
                <GridViewColumn                        
                        Header="ID"
                        Width="150">
                    <GridViewColumn.CellTemplate>
                        <DataTemplate>
                            <TextBox Text="{Binding Path=ID}"/>
                        </DataTemplate>
                    </GridViewColumn.CellTemplate>
                </GridViewColumn>
    
                <!-- Name -->
                <GridViewColumn 
                        Header="Name"
                        Width="100">
                    <GridViewColumn.CellTemplate>
                        <DataTemplate>
                            <TextBox Text="{Binding Path=Name}"/>                                
                        </DataTemplate>
                    </GridViewColumn.CellTemplate>
                </GridViewColumn>
    
                <!-- Town -->
                <GridViewColumn
                        CellTemplate="{StaticResource TownTemplate}"
                        Header="Town"
                        Width="100"/>
    
                <!-- Phone -->
                <GridViewColumn                            
                        Header="Phone"
                        Width="110">
                    <GridViewColumn.CellTemplate>
                        <DataTemplate>
                            <TextBox Text="{Binding Path=Phone}"/>
                        </DataTemplate>
                    </GridViewColumn.CellTemplate>
                </GridViewColumn>
    
                <!-- Picture -->
                <GridViewColumn
                        CellTemplate="{StaticResource PictureTemplate}"
                        Header="Picture"
                        Width="150"/>
            </GridView>
        </ListView.View>
        
    </ListView>

    A két módszer közötti különbség mindössze csak annyi, hogy a Town és Picture esetében a DataTemplate-t mint erőforrást definiáltam és a CellTemplate segítségével megadtam ezt a megfelelő GridViewColumn-nak.

    Az eredmény magáért beszél, a ListView most már így néz ki:

     Pic3

    Király! Majd a végén jöhet a stilizálás (szerintem ez egy új bejegyzés lesz majd a jövőhéten), bemutatom azt is hogy ajánlott, ugyanis nem tanácsos teleszemetelni a kódot, mert így is elég néha átlátni. De haladjunk tovább. Sokszor előfordul, hogy szűrnünk kell az adatokat, mert túl sok jelenik meg egyszerre. Jó hírem vannak, a WPF helyből támogatja ezt. Mindössze hozzáadunk egy-két új controlt. Ide másolom újra a teljes kódot:

    <Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2006"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:WPFTemplates="clr-namespace:WPFTemplates"
        xmlns:system="clr-namespace:System;assembly=mscorlib"
        mc:Ignorable="d"
        x:Class="WPFTemplates.WinMain"
        Title="WPF Templates" Height="600" Width="800"
        WindowStartupLocation="CenterScreen">
    
        <!-- ablak szintű erőforrások -->
        <Window.Resources>
            <ObjectDataProvider 
                x:Key="UserManagerDS"
                d:IsDataSource="True"
                ObjectType="{x:Type WPFTemplates:UserManager}"
                MethodName="GetUsersByTown">
                <ObjectDataProvider.MethodParameters>
                    <x:Null/>
                </ObjectDataProvider.MethodParameters>
            </ObjectDataProvider>
        </Window.Resources>
    
        <Grid>
            <!-- szűrés -->
            <StackPanel Orientation="Horizontal"
                        VerticalAlignment="Top"                    
                        Margin="10,10,10,10">
                <Label Content="Szűrés:"/>
    
                <TextBox Name="tbFilter"
                         Margin="15,0,0,0" Width="300" Height="22"
                         TextChanged="tbFilter_TextChanged"/>
            </StackPanel>
    
            <!-- felhasználók listája -->
            <ListView x:Name="lvUsers"
                      Margin="0,50,0,0"
                      ItemsSource="{Binding Source={StaticResource UserManagerDS}}">
    
                <!-- ListView erőforrások -->
                <ListView.Resources>                
                    <!-- Town -->
                    <DataTemplate x:Key="TownTemplate">
                        <ComboBox Text="{Binding Path=Town}" IsEditable="True">
                            <ComboBox.Items>
                                <ComboBoxItem Content="Debrecen"/>
                                <ComboBoxItem Content="Budapest"/>
                                <ComboBoxItem Content="Szeged"/>
                                <ComboBoxItem Content="Miskolc"/>
                            </ComboBox.Items>
                        </ComboBox>
                    </DataTemplate>
    
                    <!-- Picture -->
                    <DataTemplate x:Key="PictureTemplate">
                        <Image Source="{Binding Path=Picture}"
                                   Height="75" Width="100"
                                   Stretch="Uniform"/>
                    </DataTemplate>
                </ListView.Resources>
    
                <!-- nézet leírása -->
                <ListView.View>
                    <GridView AllowsColumnReorder="true">
                        <!-- ID -->
                        <GridViewColumn                        
                                Header="ID"
                                Width="150">
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <TextBox Text="{Binding Path=ID}"/>
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
    
                        <!-- Name -->
                        <GridViewColumn 
                                Header="Name"
                                Width="100">
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <TextBox Text="{Binding Path=Name}"/>                                
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
    
                        <!-- Town -->
                        <GridViewColumn
                                CellTemplate="{StaticResource TownTemplate}"
                                Header="Town"
                                Width="100"/>
    
                        <!-- Phone -->
                        <GridViewColumn                            
                                Header="Phone"
                                Width="110">
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <TextBox Text="{Binding Path=Phone}"/>
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
    
                        <!-- Picture -->
                        <GridViewColumn
                                CellTemplate="{StaticResource PictureTemplate}"
                                Header="Picture"
                                Width="150"/>
                    </GridView>
                </ListView.View>
                
            </ListView>
        </Grid>
    </Window>
    

    A codebehind fájlba (WinMain.xaml.cs) mindössze a TextChanged eseményt kezeljük, de ezt az eseménykezelőt automatikusan megírja nekünk a VS (mégha nincs is a Ciderben ehhez külön designeri támogatás, mint a VCL/WinForms-ban, de a Blendben van, csak ott meg a szűrő nem működik az események neveire, úgyhogy lehet bogarászni):

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
    
    namespace WPFTemplates
    {
        public partial class WinMain : Window
        {
            public WinMain()
            {
                InitializeComponent();
            }
    
            /// <summary>
            /// A szöveg megváltozott
            /// </summary>
            private void tbFilter_TextChanged(object sender, TextChangedEventArgs e)
            {
                if (string.IsNullOrEmpty(tbFilter.Text))
                {
                    lvUsers.Items.Filter = null;
                }
                else
                {
                    lvUsers.Items.Filter = new Predicate<object>(Filter);
                }
            }
            /// <summary>
            /// Szűrés
            /// </summary>
            public bool Filter(object item)
            {
                User user = item as User;
                if (item == null)
                    return false;
    
                string text = tbFilter.Text.ToLower();
    
                bool found = false;
    
                if (!string.IsNullOrEmpty(user.ID))
                    found |= user.ID.ToLower().Contains(text);
                if (!string.IsNullOrEmpty(user.Name))
                    found |= user.Name.ToLower().Contains(text);
                if (!string.IsNullOrEmpty(user.Town))
                    found |= user.Town.ToLower().Contains(text);
                if (!string.IsNullOrEmpty(user.Phone))
                    found |= user.Phone.ToLower().Contains(text);
    
                return found;
            }
    } }

    S lám, már működik is a szűrés:

    Pic4

    Ez idáig szép, de most lehessen új felhasználót is felvinni. Adjunk a szűrő textbox után még két gombot, legyen az egyik neve "Új felhasználó", a másiké pedig "Töröl". Most már csak kódrészeket másolok és inkább magyarázom, hogy mi micsoda. Tehát a keresős StackPanel-en módosítunk egy kicsit, így:

    <!-- szűrés -->
    <StackPanel Orientation="Horizontal"
                VerticalAlignment="Top"                    
                Margin="10,10,10,10">
        <Label Content="Szűrés:"/>
    
        <TextBox Name="tbFilter"
                 Margin="15,0,0,0" Width="300" Height="22"
                 TextChanged="tbFilter_TextChanged"/>
    
        <Button Name="btNew"
                Content="Új felhasználó"
                Margin="30,0,0,0"
                Width="100"
                Click="btNew_Click"/>
    
        <Button Name="btDelete"
                Content="Töröl"
                Margin="5,0,0,0"
                Width="100"
                Click="btDelete_Click"/>
    </StackPanel>

    WinMain.xaml.cs (itt is kétféle módszert mutatok, amin keresztül el lehet érni az adatforrást):

    /// <summary>
    /// Új felhasználó
    /// </summary>
    private void btNew_Click(object sender, RoutedEventArgs e)
    {
        ObjectDataProvider objectDP = Resources["UserManagerDS"] as ObjectDataProvider;
        if (objectDP == null)
            return;
    
        ObservableCollection<User> users = objectDP.Data as ObservableCollection<User>;
        if (users == null)
            return;
    
        users.Add(new User());
    }
    
    /// <summary>
    /// Felhasználó törlése
    /// </summary>
    private void btDelete_Click(object sender, RoutedEventArgs e)
    {            
        ObservableCollection<User> users = lvUsers.ItemsSource as ObservableCollection<User>;
        if (users == null)
            return;
        
        users.Remove(lvUsers.SelectedValue as User);
    }

    Láthatjuk, hogy az ObservableCollection-nek köszönhetően minden úgy történik, ahogy kell, de ezt ki is próbálhatjuk, mégpedig ha mindenhol lecseréljük ezt List<User>-ra, már nem lesz ilyen frankó a dolog. De még most sem az, hozzáteszem. Van megintcsak egy kis problémánk. Tegyünk le mégegy gombot, legyen az a feladata, hogy "törli" az összes telefonszámot:

    <!-- szűrés -->
    <StackPanel Orientation="Horizontal"
                VerticalAlignment="Top"                    
                Margin="10,10,10,10">
        <Label Content="Szűrés:"/>
    
        <TextBox Name="tbFilter"
                 Margin="15,0,0,0" Width="300" Height="22"
                 TextChanged="tbFilter_TextChanged"/>
    
        <Button Name="btNew"
                Content="Új felhasználó"
                Margin="30,0,0,0"
                Width="100"
                Click="btNew_Click"/>
    
        <Button Name="btDelete"
                Content="Töröl"
                Margin="5,0,0,0"
                Width="100"
                Click="btDelete_Click"/>
        
        <Button Name="btDeletePhone"
                Content="Telefon törlése"
                Margin="5,0,0,0"
                Width="100"
                Click="btDeletePhone_Click"/>
    </StackPanel>

    WinMain.xaml.cs -be:

    /// <summary>
    /// Telefon törlése
    /// </summary>
    private void btDeletePhone_Click(object sender, RoutedEventArgs e)
    {
        ObservableCollection<User> users = lvUsers.ItemsSource as ObservableCollection<User>;
        if (users == null)
            return;
    
        foreach (User user in users)
            user.Phone = null;
    }

    Hopp! Láss csodát, nyomogatunk a gombra és semmi sem történik gondolnánk, pedig igen, csak az UI nem frissül. Az odáig ok, hogy az INotifyCollectionChanged-et implementálja az ObservableCollection, de a User osztálynak továbbra is szükséges implementálnia az INotifyPropertyChanged interfészt ahhoz, hogy a tulajdonságainak változására is lehessen reagálni (ami jelen esetben az adatforrás elemeinek módosítását jelenti). Egészítsük ki egy kicsit a User osztályunkat, hogy működjön a dolog:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Windows.Media.Imaging;
    using System.ComponentModel;
    
    namespace WPFTemplates
    {
        public class User : INotifyPropertyChanged
        {
            private string _id;
            public string ID
            {
                get { return _id; }
    
                set
                {
                    if (_id != value)
                    {
                        _id = value;
                        NotifyPropertyChanged("ID");
                    }
                }
            }
    
            private string _name;
            public string Name
            {
                get { return _name; }
                set 
                {
                    if (_name != value)
                    {
                        _name = value;
                        NotifyPropertyChanged("Name");
                    }
                }
            }
    
            private string _town;
            public string Town
            {
                get { return _town; }
                set
                {
                    if (_town != value)
                    {
                        _town = value;
                        NotifyPropertyChanged("Town");
                    }
                }
            }
    
            private string _phone;
            public string Phone
            {
                get { return _phone; }
                set
                {
                    if (_phone != value)
                    {
                        _phone = value;
                        NotifyPropertyChanged("Phone");
                    }
                }
            }
    
            private BitmapImage _picture;
            public BitmapImage Picture
            {
                get { return _picture; }
                set
                {
                    if (_picture != value)
                    {
                        _picture = value;
                        NotifyPropertyChanged("Picture");
                    }
                }
            }
    
            #region INotifyPropertyChanged Members
    
            public event PropertyChangedEventHandler PropertyChanged;
    
            private void NotifyPropertyChanged(string name)
            {
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs(name));
            }
    
            #endregion
        }
    }

    Remek, most már működik a telefontörlő gombunk is. Tehát az UI válaszol az adatforrás változásaira. De vajon ez fordítva is igaz? Ha én valamit beleírok egy controlba a ListView-ban, teszem fel a nevet átírom, akkor az megváltoztatja a forrás objektum tulajdonságát? Jelenleg nem. Megoldás: az adatkötés módosítása. Minden DataTemplate esetében, amit használtunk, ott használjuk a TwoWay módot és kész. Ide másolom megint a teljesen kódot:

    <Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2006"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:WPFTemplates="clr-namespace:WPFTemplates"
        xmlns:system="clr-namespace:System;assembly=mscorlib"
        mc:Ignorable="d"
        x:Class="WPFTemplates.WinMain"
        Title="WPF Templates" Height="600" Width="800"
        WindowStartupLocation="CenterScreen">
    
        <!-- ablak szintű erőforrások -->
        <Window.Resources>
            <ObjectDataProvider 
                x:Key="UserManagerDS"
                d:IsDataSource="True"
                ObjectType="{x:Type WPFTemplates:UserManager}"
                MethodName="GetUsersByTown">
                <ObjectDataProvider.MethodParameters>
                    <x:Null/>
                </ObjectDataProvider.MethodParameters>
            </ObjectDataProvider>
        </Window.Resources>
    
        <Grid>
            <!-- szűrés -->
            <StackPanel Orientation="Horizontal"
                        VerticalAlignment="Top"                    
                        Margin="10,10,10,10">
                <Label Content="Szűrés:"/>
    
                <TextBox Name="tbFilter"
                         Margin="15,0,0,0" Width="300" Height="22"
                         TextChanged="tbFilter_TextChanged"/>
    
                <Button Name="btNew"
                        Content="Új felhasználó"
                        Margin="30,0,0,0"
                        Width="100"
                        Click="btNew_Click"/>
    
                <Button Name="btDelete"
                        Content="Töröl"
                        Margin="5,0,0,0"
                        Width="100"
                        Click="btDelete_Click"/>
    
                <Button Name="btDeletePhone"
                        Content="Telefon törlése"
                        Margin="5,0,0,0"
                        Width="100"
                        Click="btDeletePhone_Click"/>
            </StackPanel>
    
            <!-- felhasználók listája -->
            <ListView x:Name="lvUsers"
                      Margin="0,50,0,0"
                      ItemsSource="{Binding Source={StaticResource UserManagerDS}}">
    
                <!-- ListView erőforrások -->
                <ListView.Resources>                
                    <!-- Town -->
                    <DataTemplate x:Key="TownTemplate">
                        <ComboBox Text="{Binding Path=Town,
                                                 Mode=TwoWay,
                                                 UpdateSourceTrigger=PropertyChanged}"
                                  IsEditable="True">
                            <ComboBox.Items>
                                <ComboBoxItem Content="Debrecen"/>
                                <ComboBoxItem Content="Budapest"/>
                                <ComboBoxItem Content="Szeged"/>
                                <ComboBoxItem Content="Miskolc"/>
                            </ComboBox.Items>
                        </ComboBox>
                    </DataTemplate>
    
                    <!-- Picture -->
                    <DataTemplate x:Key="PictureTemplate">
                        <Image Source="{Binding Path=Picture,
                                                Mode=TwoWay,
                                                UpdateSourceTrigger=PropertyChanged}"
                               Height="75" Width="100"
                               Stretch="Uniform"/>
                    </DataTemplate>
                </ListView.Resources>
    
                <!-- nézet leírása -->
                <ListView.View>
                    <GridView AllowsColumnReorder="true">
                        <!-- ID -->
                        <GridViewColumn                        
                                Header="ID"
                                Width="150">
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <TextBox Text="{Binding Path=ID,
                                                            Mode=TwoWay,
                                                            UpdateSourceTrigger=PropertyChanged}"/>
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
    
                        <!-- Name -->
                        <GridViewColumn 
                                Header="Name"
                                Width="100">
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <TextBox Text="{Binding Path=Name,
                                                            Mode=TwoWay,
                                                            UpdateSourceTrigger=PropertyChanged}"/>                                
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
    
                        <!-- Town -->
                        <GridViewColumn
                                CellTemplate="{StaticResource TownTemplate}"
                                Header="Town"
                                Width="100"/>
    
                        <!-- Phone -->
                        <GridViewColumn                            
                                Header="Phone"
                                Width="110">
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <TextBox Text="{Binding Path=Phone,
                                                            Mode=TwoWay,
                                                            UpdateSourceTrigger=PropertyChanged}"/>
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
    
                        <!-- Picture -->
                        <GridViewColumn
                                CellTemplate="{StaticResource PictureTemplate}"
                                Header="Picture"
                                Width="150"/>
                    </GridView>
                </ListView.View>
                
            </ListView>
        </Grid>
    </Window>

    Ezt lehet cicomázni tovább, meg még fogjuk is, de csak sorjában. Innen folytatjuk...

    WPF sablonok használata I. (ListView vs DataGrid)

    Ezen a ponton szerintem összefogja csapni mindenki a két kezét, aki nem hallott még erről, főleg WinForms és VCL fejlesztők. Aki került már többször olyan helyzetbe, hogy pl. egy listbox-ban, ami saját egyéni objektumokat tárolt (pl. felhasználókat névvel, címmel, telefonszámmal és egy képpel) megjelenítsen stílusos formában, hát nem éppen trivi dolog volt GDI, esetleg GDI+-t használva. WinForms és VCL-ben lehetett az ősi kőkori módszereket használva kezelni a különböző erre szakosodott eseménykezelőket, utána pedig leásni akár API szinten a GDI objektumok kezelésének fortélyaiba. Nem is lehet igazán elcsodálkozni azon, hogy az eddigi alkalmazások java része egyáltalán nem "felhasználóbarát", értem ezalatt, hogy még mindig a Win95-ös szürke és számomra teljesen kiábrándító megjelenési stílust használja. Ennek oka abban rejlik, hogy nincs egyszerűen idő rá, ami persze érthető. De Hölgyeim és Uraim, itt vége ennek a korszaknak! A user interface teljesen deklaratív módon leírható XAML-ben és testreszabható. Ha a korábbi technológiák felől nézzük ezt azt jelenti például, hogy egy gomb (Button) csupán csak számunkra néz ki gombnak a WPF-ben, de szó nincs arról, hogy ez oda lenne beleégetve a kódba. Ez egy vizuális elem, aminek van alap megjelenése és viselkedése, de sablonokkal teljesen testreszabható.

    De akkor haladjunk sorjában. Ezt az egészet szintúgy egy alkalmazáson keresztül szeretném prezentálni. Láttam a múltkor a channel9-en egy kis videót, amin volt egy-két érdekes dolog, mint pl. kézírás felismerés, stilizálás, stb. Ezek közül itt a saját blogomban is bemutatnék egyet-kettőt majd később, mert megtetszett. De még előtte szükséges egy kis bevezető ismeretterjesztés is :):

    Kezdjünk egy egyszerű kis programocskával első körben. Indítsuk a VS-t és hozzunk létre egy új WPF alkalmazást. A feladat: az első példánál maradva készítsünk egy listát, amibe felsoroljuk a felhasználókat tokkal, vonóval. Ehhez most használjunk egy egyszerű XML adatbázist és nevezzük el Users.xml -nek:

    <?xml version="1.0" encoding="utf-8" ?>
    <Users>
      <User ID="szarka@freemail.hu"
            Name="Szarka Gyula"
            Town="Téglás"
            Phone="06 (52) 000-0000"
            Picture="Szarka_Gyula.jpg"/>
      
      <User ID="kissk@freemail.hu"
            Name="Kiss Kornél"
            Town="Debrecen"
            Phone="06 (52) 111-1111"
            Picture="Kiss_Kornel.bmp"/>
      
      <User ID="jjanos@hotmail.com"
            Name="Janka János"
            Town="Debrecen"  
            Phone="06 (30) 222-2222"
            Picture="Janka_Janos.jpg"/>
      
      <User ID="chikk@freemail.hu"
            Name="Unbornchikken"
            Town="Debrecen"
            Phone="06 (30) 333-3333"
            Picture="Unbornchikken.jpg"/>
      
      <User ID="kpista@msn.com"
            Name="Kovács Judit"
            Town="Miskolc"
            Phone="06 (20) 444-4444"
            Picture="Kovacs_Judit.jpg"/>
    </Users>
    

    Ez idáig ok. Semmi különleges egetrengető dolog nincs benne. Egy ListView-ot fogunk használni most. A Window1.xaml-t nevezzük el mondjuk WinMain.xaml -re. Az egységesség kedvéért mondom sorban mit kell tenni ehhez, mert még véletlenül sem ennyire egyszerű:

    1. Window1.xaml átnevezése a Solution Explorer-ben MainWindow.xaml -re
    2. A Window1.xaml.cs -ben az osztály átnevezése WinMain -re
    3. WinMain.xaml -ben az első sor cseréje erre: <Window x:Class="WPFTemplates.WinMain"
    4. App.xaml StartupUri beállítása WinMain.xaml -re

    Most ha ezzel megvagyunk, akkor készítsünk egy User osztályt, ami ezekből a nyers adatokból majd összeáll:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Windows.Media.Imaging;
    
    namespace WPFTemplates
    {
        public class User
        {
            public string ID { get; set; }
            public string Name { get; set; }
            public string Town { get; set; }
            public string Phone { get; set; }
            public BitmapImage Picture { get; set; }
        }
    }

    Most jön a következő érdekesség. Eddig megvan az adatbázis a nyers adatokkal és van egy osztályunk, ami reprezentál egy-egy entitást. Kellene nekünk egy valamilyen kollekció osztály, amely tárolja ezeket az entitásokat, helyben a memóriában, illetve abban módosítanánk, törölnénk, vennénk fel újabbakat, s majd a végén egy mentés gombbal perzisztálnánk vissza az XML-be ezeket az adatokat. Most gondolhatná  mindenki, hogy kicseszve magammal XML-t kezdtem el ehhez az egyszerű kis demonstrációhoz használni, egy MS-SQL adatbázissal és Linq To Sql-el 1000x egyszerűbb lenne, de szó nincs erről, hiszen van itt ilyen nekünk, hogy Linq To XML. Lehetne többféleképp is XML-t kezelni (pl. XmlDataProvider), de most pont ezt a módszert akarom megmutatni (főleg Delphi fejlesztőknek), ezért most egy kicsit szerencsétlen is ez az XML választás, de most képzeljük el úgy, hogy teljesen transzparens az egész, tehát ki lehet cserélni alatta az adatforrást (magyarul lehetne alatta egy Access adatbázis is akár), ez az objektum-modellen nem nagyon változtatna jobb esetben.

    Jöjjön hát a nagy kérdés: Milyen tároló osztályt válasszunk mindehhez?
    Elsőre lehetne ez egy egyszerű List<User> is, ezzel viszont az a gond, hogy ebben az esetben nekünk kellene gondoskodni a ListView (UI) frissítéséről. Amint hozzáadnánk egy új felhasználót, vagy akár törölnénk egyet a listából, nem tükröződne automatikusan a ListView controlban. Ez a régebbi VCL-es technológiáknál nem is volt másképp, ugye rosszabb esetben vagy újra feltöltöttük az egész lista controlt (ezzel elveszítve a kurzor pozíciót is), vagy valamilyen szinkronizációs logikát bele kellett fogalmazni a kódba. A WinForms esetében már sokkal jobb volt a helyzet az újszerű adatkötésnek köszönhetően, mint a VCL esetében, de a WPF esetében most tényleg totál rugalmas lett a rendszer. A lényeg az új INotifyCollectionChanged interfészben rejlik. Ezt használva, implementálva már válaszképessé tehető a felhasználói felület, viszont szerencsére előre gondolkozva a Microsoft már létre is hozott egy ilyen kollekciós osztályt, amelynek találóan ObservableCollection lett a neve. Ez egy sima generikus kollekció osztály, amely implementálja többek között ezt az interfészt is.

    Következő lépésben hozzunk létre egy új osztályt (UserManager), ami manageli ezt az egészet. Ez így fest nálam (próbálom most már követni a C# elnevezési szabályait, pl. mezőnevek esetében: _fieldName, stb. a Delphis korszakból kilépve):

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Xml.Linq;
    using System.Collections.ObjectModel;
    using System.Windows.Media.Imaging;
    using System.IO;
    
    namespace WPFTemplates
    {
        public class UserManager
        {
            public string XmlDataSource { get; set; }
    
            /// <summary>
            /// A felhasználó manager osztály inicializálása
            /// </summary>
            public UserManager()
            {
                XmlDataSource = @"Database\Users.xml";
            }
    
            /// <summary>
            /// A felhasználó manager osztály inicializálása
            /// </summary>
            /// <param name="xmlDataSource">XML adatforrás</param>
            public UserManager(string xmlDataSource)
            {
                XmlDataSource = xmlDataSource;
            }
                   
            /// <summary>
            /// Bitkép kinyerése fájlból
            /// </summary>
            /// <param name="FileName">Képfájl</param>
            /// <returns>Visszatér egy bitkép objektummal ha sikerült megnyitni a fájlt, ellenkező esetben, ha kivétel történik null-al</returns>
            public BitmapImage GetBitmapFromFile(string fileName)
            {
                BitmapImage bitmap = null;
                
                fileName = Path.Combine(Path.Combine(
                    AppDomain.CurrentDomain.BaseDirectory, "Database"), fileName);
    
                // létezik/elérhető a kép
                if (File.Exists(fileName))
                {
                    try
                    {
                        bitmap = new BitmapImage(new Uri(fileName));
                    }
                    catch
                    {
                        return null;
                    }
                }
    
                return bitmap;
            }
    
            /// <summary>
            /// Felhasználók kinyerése a város alapján
            /// </summary>
            /// <returns>Visszaadja a felhasználok listáját</returns>
            public ObservableCollection<User> GetUsersByTown(string town)
            {            
                ObservableCollection<User> userList = new ObservableCollection<User>();
    
                // létezik/elérhető az adatforrás fájl
                if (File.Exists(XmlDataSource))
                {
                    var users =
    
                        from u in XElement.Load(XmlDataSource).Elements()
    
                        let attrID = (string)u.Attribute("ID")
                        let attrName = (string)u.Attribute("Name")
                        let attrTown = (string)u.Attribute("Town")
                        let attrPhone = (string)u.Attribute("Phone")
                        let attrPicture = (string)u.Attribute("Picture")
    
                        where !string.IsNullOrEmpty(attrID) &&
                              (town == null || attrTown == town)
    
                        select new User
                        {
                            ID = attrID,
                            Name = attrName,
                            Town = attrTown,
                            Phone = attrPhone,
                            Picture = GetBitmapFromFile(attrPicture)
                        };
    
    
                    // userList feltöltése (users = IEnumerable<User> !!!)
                    foreach (var user in users)
                        userList.Add(user);
                }
    
                return userList;
            }
        }
    }
    

    Próbáltam úgy tördelni a kódot, hogy elférjen meg olvasható is legyen. A lényeg itt a GetUsersByTown metódus, amely szűr a paraméterben megadott városokra, vagy ha null, akkor hoz minden felhasználót, feltéve, hogy van rendes ID-je. Ebben a kis példában látható milyen egyszerű a LINQ-et alkalmazni XML dokumentumokra úgy, hogy az atombiztos is legyen! Figyelni kell, hogy az XML tag létezik e, különben elszáll az egész alkalmazás, ha egy ablak erőforrásként deklarált ObjectDataProvider-nek megadjuk majd ezt és közben az XML fájlban valahol valami hiányosan lett megadva, vagy lemaradt. Az "idóta biztosságot" LINQ ide, vagy oda, garantálni kell. Ezért van egy külön GetBitmapFromFile metódus is, ami elkap minden kivételt és ebben az esetben null-t ad vissza, tehát nem jelenítjük meg a felhasználó arcképét, ha valami gáz van vele. Annyit még hozzáteszek, hogy a képeket ráérne utólag betöltögetni, tehát nem célszerű egyszerre a többi adattal betölteni, sőt cachelni is lehet, mert 2000 évig kell így várni sok kép esetén (ez egyébként Linq To Sql esetén külön támogatott is), de itt most korántsem a tökéletességre törekedtem, csak az elv bemutatására.

    OK, haladjunk tovább. Most már ki tudjuk nyerni a felhasználókat az alapján, hogy melyik városban laknak. Hogyan tudjuk mindezt megjeleníteni?

    Akkor most első lépésben navigáljunk át a WinMain.xaml -re (lehet használni a Blendet is, sőt kelleni is fog, mert a Cider összefog omlani majd). Tegyünk le egy ListView controlt, adjunk neki valami nevet, mondjuk legyen lvUsers. Amíg nincs DataGrid a WPF-be, addig lehet alternatívaként használni a ListView-ot, vagy esetleg az Xceed-es DataGrid-et is. Tehát eddig ennyi, ami előttünk kell lebegjen:

    <Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2006"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:WPFTemplates="clr-namespace:WPFTemplates"
        xmlns:system="clr-namespace:System;assembly=mscorlib"
        mc:Ignorable="d"
        x:Class="WPFTemplates.WinMain"
        Title="WPF Templates" Height="600" Width="800"
        WindowStartupLocation="CenterScreen">   
        
        <Grid>
            <ListView Name="lvUsers">
                
            </ListView>
        </Grid>
    </Window>

    S most sehova tovább! Következő kérdés, hogyan tudunk mi kapcsolatot teremteni egy objektum alapú adatforrással? Itt jön a képbe az ObjectDataProvider osztály. Ez elég rugalmasan testreszabható úgyszint, mindjárt látni fogjuk mire gondolok, viszont itt már lesznek gondok és elő kell venni a blendet, mert a Cider teljesen "kész", megadja magát a null paramétertől. Tehát első körben adjunk meg egy ObjectDataProvider-t:

    <Window.Resources>
        <ObjectDataProvider 
            x:Key="UserManagerDS"
            d:IsDataSource="True"
            ObjectType="{x:Type WPFTemplates:UserManager}"
            MethodName="GetUsersByTown">
            <ObjectDataProvider.MethodParameters>
                <x:Null/>
            </ObjectDataProvider.MethodParameters>            
        </ObjectDataProvider>        
    </Window.Resources>

    Itt a lényeg az ObjectType. Ebből fogja tudni, milyen típust kell pédányosítani, majd megadjuk a metódus nevét (MethodName), amit használni szeretnénk, jelen esetben a GetUsersByTown-t. A paramétereket pedig szépen felsorolhatjuk az <ObjectDataProvider.MethodParameters> között. Ahhoz, hogy stringet tudjunk megadni, fel kell venni a következő névteret:

    <Window
        xmlns:system="clr-namespace:System;assembly=mscorlib"
    ...
    <Window.Resources>
        <ObjectDataProvider 
            x:Key="UserManagerDS"
            d:IsDataSource="True"
            ObjectType="{x:Type WPFTemplates:UserManager}"
            MethodName="GetUsersByTown">
            <ObjectDataProvider.MethodParameters>
                <system:String>Debrecen</system:String>
            </ObjectDataProvider.MethodParameters>            
        </ObjectDataProvider>        
    </Window.Resources>

    Ez utóbbi XAML kód tulajdonképpen átfordítva C#-ba ezt jelenti: GetUsersByTown("Debrecen").

    Sőt, jobbat is mondok, akár a UserManager osztály konstruktorának paramétereit is be lehet állítani XAML-be:

    <Window.Resources>
        <ObjectDataProvider 
            x:Key="UserManagerDS"
            d:IsDataSource="True"
            ObjectType="{x:Type WPFTemplates:UserManager}"
            MethodName="GetUsersByTown">
    <
    ObjectDataProvider.ConstructorParameters> <system:String>Database\Users.xml</system:String> </ObjectDataProvider.ConstructorParameters>
    <
    ObjectDataProvider.MethodParameters> <system:String>Debrecen</system:String> </ObjectDataProvider.MethodParameters> </ObjectDataProvider> </Window.Resources>

    Kicsit komoly, mi? Persze a Cider nem bír el a nullal (a null XAML reprezentációja a <x:Null/> vagy <x:NullExtension/>), de a Blendnek szerencsére nincs vele gondja. Következő lépés az, hogy valahogy ezt a ListView-hoz kellene kötni. Ez mindössze ennyi (most már bemásolom ide a teljes kódot, hogy összeálljon a dolog):

    <Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2006"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:WPFTemplates="clr-namespace:WPFTemplates"
        xmlns:system="clr-namespace:System;assembly=mscorlib"
        mc:Ignorable="d"
        x:Class="WPFTemplates.WinMain"
        Title="WPF Templates" Height="600" Width="800"
        WindowStartupLocation="CenterScreen">
        
        <Window.Resources>
            <ObjectDataProvider 
                x:Key="UserManagerDS"
                d:IsDataSource="True"
                ObjectType="{x:Type WPFTemplates:UserManager}"
                MethodName="GetUsersByTown">            
                <ObjectDataProvider.MethodParameters>
                    <x:Null/>
                </ObjectDataProvider.MethodParameters>            
            </ObjectDataProvider>        
        </Window.Resources>
        
        <Grid>
            
            <ListView
                x:Name="lvUsers"
                ItemsSource="{Binding Source={StaticResource UserManagerDS}}">
                
            </ListView>
    
        </Grid>
    </Window>

    Ha most lefordítjuk és futtatjuk az alkalmazást, akkor valami ilyesmit kell kapjunk:

    Pic1

    Nem túl barátságos, igaz? Akkor most alakítsuk át egy kicsit a dolgot és most rátérhetünk arra, amiről tulajdonképpen ez az egész is szól. Hogyan tudunk ennek a ListView-nek valamilyen sablont definiálni, hogy ki is nézzen valahogy? A kulcs jelen esetben a DataTemplate (adatsablon) lesz. Na most azt elárulom, hogy a Blend adatkötési ablakában van egy olyan gomb alul, hogy "Define DataTemplate", ha valaki nincs oda az XML szadomazóért, sőt previewot is megjelenít arról a sablonról, amit készít a típushoz, meg még lehetőség van ott is testreszabni egy-két dolgot.

    Nézzük akkor, hogy fest a ListView controlunk mindezek után:

    <ListView
        x:Name="lvUsers"
        ItemsSource="{Binding Source={StaticResource UserManagerDS}}">
    
        <ListView.View>
            <GridView AllowsColumnReorder="true">
    
                <GridViewColumn
                        DisplayMemberBinding="{Binding Path=ID}"
                        Header="ID"
                        Width="250"/>
                <GridViewColumn                                
                        DisplayMemberBinding="{Binding Path=Name}"
                        Header="Name"
                        Width="150"/>
                <GridViewColumn
                        DisplayMemberBinding="{Binding Path=Town}"
                        Header="Town"
                        Width="150"/>
                <GridViewColumn
                        DisplayMemberBinding="{Binding Path=Phone}"
                        Header="Phone"
                        Width="120"/>
                <GridViewColumn
                        DisplayMemberBinding="{Binding Path=Picture}"
                        Header="Picture"
                        Width="120"/>
    
            </GridView>
        </ListView.View>
        
    </ListView>

    A GridView AllowsColumnReorder tulajdonságával tudjuk engedélyezni az oszlopok egérrel történő újrarendezését, hasonlóan mint a régi DataGrid esetében volt. A GridView-on belül pedig definiáljuk az oszlopokat és a DisplayMemberBinding segítségével odakötjük a User objektum megfelelő tulajdonságához őket. Ha most futtatjuk az alkalmazást, már láthatjuk, hogy egész szép dolgot kapunk:

    Pic2

    Na most ez nagyon szép, de először is:

    1. A képet meg kellene jeleníteni 
    2. Hogyan tudom megváltoztatni az egyes értékeket közvetlenül a táblázatban?

    Ezekre a válasz a következő bejegyzésben megtalálható...