János's profileJános JankaPhotosBlogListsMore Tools Help

Blog


    March 28

    WPF alkalmazások lokalizációja I.


    Ezt a témát most külön veszem az MVC résztől, mert nem feltétele annak, csupán csak egy lehetőség, viszont előre kigondolandó és betervezendő lehetőség az UI tervezése szempontjából.

    Bevezető

    Volt szó az alkalmazások lokalizációjáról, mint lehetőségről az alkalmazásunk üzleti értékének növelése érdekében. Ezt már a tervezési fázisban számításba kell vennünk, mert az UI-t úgy kell kialakítanunk, hogy zökkenőmentesen működjön. Még véletlenül se gondoljuk azt, hogy ez pár "klikk és kész" a WPF-ben (mint anno a Delphi esetén), viszont automatikus a megfelelő nyelvi erőforrások betöltése a Windows területi és nyelvi beállításaitól függően, nem kell külön ezzel is foglalkoznunk, feltéve, ha az SDK-s módszert követjük. De még mielőtt nekiállnánk, mindenképp van egy-két irányelv, amit érdemes követni a lokalizáció megvalósíthatósága érdekében az UI tervezésekor. A következőkben ezekről ejtek néhány szót felsorolás-szerűen.

    Tervezés

    Lehetőleg ne fixáljuk a controlok méretét, ne használjunk abszolút pozíciókat! Helyette használjunk automatikus layoutokat. Ez annyit tesz pl. egy gomb esetében, hogy a szélességét nem állítjuk be, hanem hagyjuk, hogy az tartalomtól függően dinamikusan változzon:

    Button 1 Button 2
    <Button
        Height="25"
        Content="It is growing dark"/>
    <Button
        Height="25"
        Content="Sötétedik"/>
    

    Így nyelvtől függetlenül fog automatikusan a szöveghossznak megfelelően méreteződni a control bármiféle extra kód írása nélkül a betöltést követően. Na persze ez így korántsem annyira szép néha, úgyhogy más alternatív lehetőségként, adjunk meg nagyobb méretet a vezérlőnek, amibe elképzelésünk szerint bele fog férni minden nyelv szövege.

    Használjuk a Grid-et automatikus layoutok létrehozásához. Megoldódnak a problémái az újraméretezésnek és pozícionálásnak egyaránt. A Grid-nek van egy olyan jó tulajdonsága, hogy lehetőség van további oszlopokra és sorokra felosztani azt, majd ezután minden control, ami a griden belül helyezkedik el hozzákapcsolható egy-egy ilyen cellához attach propertyken keresztül. Erről még nem volt szó, majd később kitérek ezekre is, hogy mi célt szolgálnak, de pont ilyeneket és ehhez hasonlókat, mint most ez is. Ha pl. visszaemlékszünk a DockPanel.Dock tulajdonságára, na az is egy ilyen tulajdonság volt. Itt egy példa a Grid használatára is:

    Grid autolayout

    <Window x:Class="GridDemo.WinMain"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Grid" Height="600" Width="800"
        WindowStartupLocation="CenterScreen">
        
        <Grid ShowGridLines="True">
            
            <!-- columns -->
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            
            <!-- rows -->
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>        
            </Grid.RowDefinitions>
            
            <!-- col: 0, row: 0 -->
            <TextBlock
                Grid.Column="0" Grid.Row="0"
                Margin="5,5"
                VerticalAlignment="Center"
                Text="Automatikus layout"/>
                    
            <!-- col: 1, row: 0 -->
            <Button
                Grid.Column="1" Grid.Row="0"            
                Margin="5,5"
                FontSize="30"
                Content="WPF"/>
            
            <!-- col: 0, row: 1 -->
            <Button
                Grid.Column="0" Grid.Row="1"            
                Margin="5,5"
                FontSize="30"
                Content="OK"/>        
            
            <!-- col: 1, row: 1 -->
            <TextBlock
                Grid.Column="1" Grid.Row="1"
                Margin="5,5"
                VerticalAlignment="Center"            
                Text="Tetszik érteni?"/>
            
        </Grid>
    </Window>
    

    Sőt a span-t is meglehet határozni, így akár átcsúszhat az egyik cellából a másikba a control:

    Grid.Span property

    <Button
        Grid.Column="0" Grid.RowSpan="2"
        Margin="5,5"
        FontSize="30"
        Content="OK"/> 

    Erről a Grid-ről még lesz szó később, mert van egy-két tulajdonsága, ami jól jöhet, mint pl. IsSharedSizeScope a későbbiekben. A kis szaggatott vonal azért látszik csak, mert beállítottam a Grid ShowGridLines tulajdonságát true-ra szemléltesképp.

    Akkor röviden összefoglalva, hogy mikre figyeljünk oda, ha lokalizálni szeretnénk alkalmazásunkat:

    • Lehetőleg ne használjunk abszolút pozíciókat
      Ebből következik, hogy a Canvas-t el kell felejteni, mivel az ilyenekkel dolgozik.
      Használjuk a szokásos layout controlokat, úgy mint DockPanel, StackPanel, Grid.
    • Ne állítsuk az ablakokat fix méretűre
      Használjuk az ablakok SizeToContent képességet, pl. így:
      <Window x:Class="Localization.WinMain"
          xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
          Title="Grid"
          SizeToContent="WidthAndHeight"    
          WindowStartupLocation="CenterScreen">
    • FlowDirection
      A WPF szintúgy, akárcsak a Delphi VCL, támogatja a különböző latin, kelet ázsiai, arab, héber, stb. írásmódokat. Ugyebár balról-jobbra, illetve jobbról-balra történő írás:

      VCL WPF
      image FlowDirection

    • Használjunk kompozit fontokat fizikai fontok helyett
      A WPF-ben, ellenben korábbi kliensoldali prezentációs technológiákkal (VCL, WinForms), lehetőség van több betűtípust is meghatározni, akárcsak CSS-el. Pl.:

      <TextBlock
          Grid.Column="0" Grid.Row="0"
          Margin="5,5"
          VerticalAlignment="Center"
          FontFamily="Segoe UI, Arial"
          Text="Automatikus layout"/>
      Ebben az esetben, ha van olyan font, hogy Segoe UI (Windows Vista), akkor azt használja, ha nincs, akkor Arial-t. A következő composite fontokkal ez még rugalmasabbá tehető, ha pl. használjuk a GlobalUserInterface-t, akkor Windows Vista-n Segoe UI lesz a betűtípus, XP-n meg tahoma:

      GlobalMonospace.CompositeFont
      GlobalSanSerif.CompositeFont
      GlobalSerif.CompositeFont
      GlobalUserInterface.CompositeFont

      Pl.:
      <TextBlock
          Grid.Column="0" Grid.Row="0"
          Margin="5,5"
          VerticalAlignment="Center"
          FontFamily="Global User Interface"
          Text="Automatikus layout"/>
    • Használjuk az xml:lang tulajdonságot
      A kompozit fontok ezt pl. használják is és "nem kell kakast áldozni" (chikk) a használatukhoz:
      <TextBlock
          xml:lang="HU"
          Grid.Column="0" Grid.Row="0"
          Margin="5,5"
          FontFamily="Global User Interface"
          VerticalAlignment="Center"            
          Text="Windows Presentation Foundation"/>

    Na és most végre elérkeztünk arra a pontra, hogy már nagyjából képben vagyunk, hogy mikre is érdemes odafigyelni egy UI megtervezése során, ha szeretnénk azt a későbbiekben lokalizálni. Akkor térjünk is át a tárgyra:

    Lokalizáció

    Több módszer is létezik a feladat megoldására, nézzünk egyet-kettőt. Első körben hozzunk létre egy új WPF alkalmazást, például így:

    Step 1

    <Window x:Class="Localization.WinMain"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:Localization="clr-namespace:Localization"
        Title="WPF localization"
        SizeToContent="WidthAndHeight"
        WindowStartupLocation="CenterScreen">
    
        <!-- command bindings -->
        <Window.CommandBindings>
            <CommandBinding
                x:Name="Cmd_New"
                Command="New"
                Executed="Cmd_New_Executed"/>
            <CommandBinding
                x:Name="Cmd_Exit"
                Command="{x:Static Localization:LocCommands.Exit}"
                Executed="Cmd_Exit_Executed"/>
        </Window.CommandBindings>
    
        <DockPanel>
            <!-- main menu -->
            <Menu DockPanel.Dock="Top">
                <MenuItem Header="_File">
                    <MenuItem Command="New"/>
                    <Separator/>
                    <MenuItem Command="{x:Static Localization:LocCommands.Exit}"/>
                </MenuItem>
            </Menu>
    
            <Grid>
                <!-- textblock -->
                <TextBlock
                    Margin="50,50"
                    VerticalAlignment="Center"
                    HorizontalAlignment="Center"                
                    FontWeight="Bold"
                    FontSize="36"
                    Text="It is growing dark"/>
            </Grid>
        </DockPanel>
    </Window>
    

    Ebbe idáig semmi új nincs. Szokásos parancskötések, stb., stb. A LocCommands osztály ennyi ehhez:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Windows.Input;
    
    namespace Localization
    {
        public static class LocCommands
        {
            static LocCommands() { }
                 
            /// <summary>
            /// Exit
            /// </summary>
            public static RoutedUICommand Exit =
                new RoutedUICommand(
                    "Exit", 
                    "Exit", 
                    typeof(LocCommands), 
                    new InputGestureCollection 
                    { 
                        new KeyGesture(Key.Q, ModifierKeys.Control) 
                    }
                );
        }
    }

    Első megoldás *.resx fájlok használata/készítése. Először is a Project menü <Projektnév> Properties... menüjére kattintva hozzuk létre a string erőforrásokat, amiket lokalizáltan szeretnénk látni:

    Resources

    Állítsuk át az Access Modifiert public-ra, hogy elérjük az erőforrásokat azon a statikus osztályon keresztül amit a VS generál. Annyit hozzáteszek, hogy érdemes valamilyen konvenciót követni az elnevezésekben, mert sok string esetén már elég lesz kikeresni a megfelelőt, ha módosítani szeretnénk, meg azért valamilyen logikai szervezést sem árt bevezetni. Na jó, nem ragozom ezt tovább, ebben a kis példában ez teljesen lényegtelen, csak nem árt, ha erre is gondolunk majd később.

    Ezután a XAML-t módosítsuk a következőképp:

    <Window x:Class="Localization.WinMain"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:Localization="clr-namespace:Localization"        
        xmlns:Properties="clr-namespace:Localization.Properties"
        Title="{x:Static Properties:Resources.StrTitle}"
        SizeToContent="WidthAndHeight"
        WindowStartupLocation="CenterScreen">
    
        <!-- command bindings -->
        <Window.CommandBindings>
            <CommandBinding
                x:Name="Cmd_New"
                Command="New"
                Executed="Cmd_New_Executed"/>
            <CommandBinding
                x:Name="Cmd_Exit"
                Command="{x:Static Localization:LocCommands.Exit}"
                Executed="Cmd_Exit_Executed"/>
        </Window.CommandBindings>
    
        <DockPanel>
            <!-- main menu -->
            <Menu DockPanel.Dock="Top">
                <MenuItem Header="{x:Static Properties:Resources.StrFile}">
                    <MenuItem Command="New"/>
                    <Separator/>
                    <MenuItem Command="{x:Static Localization:LocCommands.Exit}"
                              Header="{x:Static Properties:Resources.StrExit}"/>
                </MenuItem>
            </Menu>
    
            <Grid>            
                <!-- textblock -->
                <TextBlock
                    Margin="50,50"
                    VerticalAlignment="Center"
                    HorizontalAlignment="Center"                
                    FontWeight="Bold"
                    FontSize="36"
                    Text="{x:Static Properties:Resources.StrContent}"/>
            </Grid>
        </DockPanel>
    </Window>
    

    Felvettem a Properties névteret, mint láthatjuk, majd mindent, amit lokalizálni kell, hozzákötöttem a statikus erőforrás osztály tagjain keresztül a megfelelő tulajdonsághoz. Figyeljük meg, hogy a menu itemek, amik command bindinget használnak is lokalizálhatók, mivel előbb a command binding kötései lépnek érvénybe, ezért ha mi felülírjuk a header-t, akkor hiába Exit van default beállítva, az felül fog írodni, szerencsére :). A másik pedig, a belső parancsokhoz kötött elemeket nem szükséges lokalizálnunk, mint fent is látjuk a New esetén, mivel ezek a .NET aktuálisan telepített nyelvi változatának megfelelő értékeit fogják felvenni (nem az OS-ét!), ami persze lehet kényelmetlen is, ha nincs telepítve a nyelvi csomag a keretrendszerhez. Végül is, nem árt egyébként, ha azt is lokalizáljuk.

    Következő lépés: másoljuk le az Resources.resx fájlt és nevezzük át Resources.HU.resx -re. Ezt is adjuk hozzá a projekthez, majd fordítsuk le a szövegeket hasonló módon, mint fent is láttuk, majd buildeljük újra az egészet. Kész is volnánk. Létrejött egy HU mappa a Bin\Debug könyvtárba, ami tartalmaz egy <Projektnév>.resources.dll -t. Ez egy satellite assembly, a runtime gondoskodik ennek a betöltéséről a windows területi beállításaitól függően automatikusan. Ilyenkor az angol nyelvű cucc, ami továbbra is az Resources.resx-ben van bele van fordítva a main assemblybe, azaz az exe-be, csak a magyar cuccok vannak külön satellite assemblybe csoportosítva. Ennek előnye, hogy alapból nem fog megborulni a program, ha nincs ott a lokalizált erőforrás assembly, ill. akkor se, ha valamelyik erőforrást nem lokalizáltuk, pl. az StrFile-t lehagyjuk a Resources.HU.resx-ből, akkor auto az angol nyelvű változat, ami a main assemblyben van fog betöltődni. De kiszervezhetjük külön az angol nyelvet is, mindössze annyi a teendő, a *.csproj fájlba beletesszük az UICulture tagot:

    <?xml version="1.0" encoding="utf-8"?>
    <Project ToolsVersion="3.5"
    DefaultTargets="Build"
    xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <UICulture>en-US</UICulture>
    ...

    Illetve ezután még egy dolgot meg kell tegyünk, az AssemblyInfo.cs fájlba kikommentezzük a következő sort:

    [assembly:
    NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]

    Kész is volnánk, láthatjuk, hogy automatikusan a magyar szövegek töltődnek be, ha a területi és nyelvi beállítások között ez szerepel:

    Result

    Innen folytatjuk legközelebb a következő megoldással...

    March 16

    Windows Presentation Foundation + MVC (2. rész)


    Úgy döntöttem, hogy még mielőtt ajtóstól rontanánk a házba, muszály vagyok az előző részben felsorolt pontokat egy kicsit részletesebben taglalni. Ugye kezdtem azzal, hogy lehetőleg amit csak tudunk deklaratív módon célszerű leírni, s közben erősen az adatkötési modellre támaszkodni. Ezzel idáig túl sok újat nem mondtam, de fontos. Utána szó volt a parancsalapú vezérlésről, s akkor itt álljunk is meg egy pillanatra:

    Parancsok a WPF-ben

    Alapvetően a koncepció nem új a Delphi után, de a megvalósítás rugalmasabb mint ott, s az MVC megközelítését is alapvetően befolyásolja. Miről is van szó? Egy egyszerű példán keresztül szemléltetve: tegyük fel, hogy van egy alkalmazásunk és ennek az alkalmazásnak vannak bizonyos műveletei, mint pl. új projekt létrehozása, megnyitás, mentés, nyomtatás, kilépés, visszavonás, kivágás, másolás, beillesztés, törlés, kijelölés, keresés, stb. Na most ezek tipikusan olyan akciók, amiket egyszerre több helyen is szeretnénk elérhetővé tenni a felhasználói felületen. Pl. ha megnézünk bármilyen szövegszerkesztő programot, akkor láthatjuk, hogy a beillesztés (CTRL+V) funkció a fő menü / szerkesztés menüjébe is megtalálható, illetve az adott szerkesztő control (textbox, memo, stb.) kontext menüjében is:

    image   image

    A WPF parancsokat használva a parancs szövege, a hozzá rendelt shortcut és a teljes viselkedés testreszabható (pl. inaktív beillesztés gomb, ha nincs a vágólapon semmi). Ez idáig ugyanaz, mint amit a Delphi TAction osztályai is támogatnak, mindkét control ugyanabban az állapotban lesz, hála a commandoknak, amelyek a viselkedést az UI-tól függetlenül definiálják. Viszont mindjárt mutatom azokat a dolgokat, amikkel még érdekesebbé tehető a dolog a WPF-ben. Itt az ideje az öveket becsatolni, mert vége a kínlódós, gányolós, 66x megírok mindent előről időszaknak!

    Mielőtt tovább mennék, mellékesen azt még megjegyzem, hogy a WPF-ben a felhasználói bemeneti műveleteket bemeneti gesztusoknak (InputGestures) nevezik. Ezentúl én is így fogok ezekre hivatkozni.

    Kezdjük az elején akkor:

    Az egész alapja az ICommand interfészből indul ki:

    public interface ICommand
    {
        event EventHandler CanExecuteChanged;
    
        bool CanExecute(object parameter);
        void Execute(object parameter);
    }

    Az Execute metódus fogja tartalmazni a parancsfuttatási logikát. Rögtön fel is tűnhet, hogy lehetőség van egy paramétert is átadni neki, pl. így:

    <Button Command="..." CommandParameter="10"/>

    Az ICommand interfész meghatároz egy CanExecute metódust is, amivel ellenőrizhető, hogy az adott művelet jelen körülmények között végrehajtható-e. Értelemszerűen false-t adunk vissza, ha nem, true-t, ha igen, s persze ez dönt az esemény kiváltásának sorsáról is. Még annyit, hogy ennek a CanExecute metódusnak szintén szükséges megkapnia a CommandParameter-t, hiszen ez alapján is dönthetünk a végrehajtás sorsáról. Na most ez idáig ok, de ez még nagyon hurka így, mivel az input gesztusok és a többi finomság ebbe nincs belefogalmazva. 

    Itt jön képbe a RoutedCommand osztály:

    A WPF-ben ez az egyetlen osztály, amely implementálja az ICommand interfészt és kibővíti azt néhány dologgal, úgy mint:

    public class RoutedCommand : ICommand
    {
        public RoutedCommand();
        public RoutedCommand(string name, Type ownerType);
        public RoutedCommand(string name, Type ownerType,
    InputGestureCollection inputGestures); public InputGestureCollection InputGestures { get; }
    public string Name { get; } public Type OwnerType { get; }
    public event EventHandler CanExecuteChanged;

    [SecurityCritical] public bool CanExecute(object parameter, IInputElement target); [SecurityCritical] public void Execute(object parameter, IInputElement target); }
    • Bemeneti gesztusok kezelése
    • Parancsnév meghatározása (mint pl. Paste)
    • Tulajdonos meghatározása

    Na most amit belinkeltem ezzel kapcsolatban az előző alkalommal egy kicsit sántít, mivel az még a 2005. novemberi CTP-vel készült. A végleges megvalósítás így fest, ahogy fent is látszik. Erről az osztályról ezen kívül túl sok minden mást nem is kell tudni, hacsak azt nem, hogy a két metódus meg lett jelölve a SecurityCritical attribútummal, mely megvédi és biztonságosabbá teszi ezen műveletek végrehajtását.

    Egyetlen osztály van még, a RoutedUICommand, mely a RoutedCommand-ból származik, s mindössze egyetlen propertyvel bővíti ki az egészet:

    public class RoutedUICommand : RoutedCommand
    {
    public RoutedUICommand(); public RoutedUICommand(string text, string name, Type ownerType); public RoutedUICommand(string text, string name, Type ownerType,
    InputGestureCollection inputGestures); public string Text { get; set; } }

    Ez a Text tulajdonság hivatott lokalizáltan leírni a parancs funkcióját részletesebben (Delphi: Hint property). Ezt a felhasználók fogják olvasni valamilyen formában, pl. tooltip, ha felé állnak az egérrel, vagy egy statusbaron esetleg, vagy egy menuitem headerjeként.

    Először nézzünk egy példát a beépített parancsokra. Ezzel kapcsolatban már írtam egyet egyébként, de azt esetleg még hozzá lehet tenni, hogy a belső parancsok esetében nem kötelező leírni az osztály nevét, tehát az alábbiak közül mindkettő helyes (spórolunk a kézimunkán):

    <Menu VerticalAlignment="Top">    
        <MenuItem Header="_Edit">
            <MenuItem Command="ApplicationCommands.Copy"/>
            <MenuItem Command="Copy"/>
        </MenuItem>
    </Menu>
    

    Hogyan készíthetünk egyéni parancsokat?

    Praktikusan célszerű az UI-tól elválasztva, logikailag valamilyen csoportba szervezve definiálni ezeket a commandokat, ami most nem jelent mást, mint egy sima statikus osztályt. Felhasználva az alapötletet, ahogy a Microsoft is implementálta ezeket az osztályokat, hasonlóan teszek én is. Legyen az a feladat, hogy csináljunk egy kilépés és egy támogatás parancsot, mivel ilyenek alapból nem léteznek még. Ezt pl. a következőképp lehetne megtenni:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Windows.Input;
    
    namespace ClientApp.AppLogic
    {
        public static class MyCommands
        {
            // parancs azonosítók
            private enum CommandId : byte { Exit, Support, Last }
    
            // UI parancsok tömbje
            private static RoutedUICommand[] _internalCommands =
                new RoutedUICommand[2];
            
            /// <summary>
            /// A paraméterben megadott parancs létrehozása
            /// </summary>
            private static RoutedUICommand _EnsureCommand(CommandId idCommand)
            {
                if (idCommand < CommandId.Exit || idCommand >= CommandId.Last)
                    return null;
                
                lock (_internalCommands.SyncRoot)
                {
                    if (_internalCommands[(int)idCommand] == null)
                    {   
                        RoutedUICommand command = new RoutedUICommand(
                            GetUIText(idCommand),
                            GetPropertyName(idCommand),
                            typeof(MyCommands),
                            GetInputGestures(idCommand)
                        );
    
                        _internalCommands[(int)idCommand] = command;
                    }
                }
    
                return _internalCommands[(int)idCommand];
            }
            
            /// <summary>
            /// Tulajdonságnevek kinyerése a parancs alapján
            /// </summary>
            private static string GetPropertyName(CommandId commandId)        
            {
                string str = string.Empty;
                switch (commandId)
                {
                    case CommandId.Exit:
                        return "Exit";
                    case CommandId.Support:
                        return "Support";
                }        
                
                return str;
            }
            
            /// <summary>
            /// UI szöveg
            /// </summary>
            private static string GetUIText(CommandId commandId)
            {
                string str = string.Empty;
                switch (commandId)
                {
                    case CommandId.Exit:
                        return "Exit";
                    case CommandId.Support:
                        return "Support";
                }
    
                return str;
            }
    
            /// <summary>
            /// Input gesztusok a parancs alapján
            /// </summary>
            private static InputGestureCollection GetInputGestures(CommandId commandId)
            {
                InputGestureCollection gestures = new InputGestureCollection();
    
                switch (commandId)
                {
                    case CommandId.Exit:
                        gestures.Add(new KeyGesture(Key.Q, ModifierKeys.Control));                    
                        break;
                    case CommandId.Support:
                        gestures.Add(new KeyGesture(Key.F1, ModifierKeys.Control));
                        break;
                }
    
                return gestures;
            }
    
            // Kilépés parancs (Exit)
            public static RoutedUICommand Exit
            {
                get { return _EnsureCommand(CommandId.Exit); }
            }
    
            // Támogatás (Support)
            public static RoutedUICommand Support
            {
                get { return _EnsureCommand(CommandId.Support); }
            }
        }
    }
    

    Nem olyan ördöngős ez, mint ahogy elsőre kinéz. Azért, hogy rugalmasan lehessen kezelni a parancsokat egy tömbben tároljuk őket, így bármikor egyszerűbben adhatunk újabbakat hozzá, illetve adott esetben ha éppen szükség van egyre, akkor létrehozzuk azt (Singleton pattern). Ezután már mindössze csak annyi dolgunk maradt, hogy az ablakunk kódjába felhasználjuk ezeket a commandokat:

    Illetve mégse! Sajnos van egy kis bug is, mégpedig, hogy ha nincs fókuszban egy elem se az ablakban, addig nem fog normálisan működni a command binding a ContextMenu esetében. Mindjárt mutatom miről is van szó, normális esetben ezt írnánk, ha szeretnénk az ablakhoz egy konextus menüt készíteni parancskötésekkel:

    <Window.ContextMenu>
        <ContextMenu>
            <MenuItem Command="{x:Static AppLogic:MyCommands.Support}"/>
            <Separator/>
            <MenuItem Command="{x:Static AppLogic:MyCommands.Exit}"/>
        </ContextMenu>
    </Window.ContextMenu>

    De sajnos, amíg nem kerül fókuszba egy elem se az ablakban, addig ez így fog működni (jelentem, ezen a ponton sikerült elrántanom az egész VS RTM-et is):

    image

    Workaround:

    <Window.ContextMenu>
      <ContextMenu>
          <MenuItem                
              Command="{x:Static AppLogic:MyCommands.Support}"
              CommandTarget=
                  "{Binding
                      Path=PlacementTarget,
                      RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
          <Separator/>            
          <MenuItem                
              Command="{x:Static AppLogic:MyCommands.Exit}"
              CommandTarget=
                  "{Binding
                      Path=PlacementTarget,
                      RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
      </ContextMenu>
    </Window.ContextMenu>

    image

    Egy kis trükközéssel megkerülhető szerencsére ez a dolog (persze csak a ContextMenu esetében van szükség erre). Na és akkor jöjjön a teljes kód (bugmentesen), hogyan is tudjuk ezt az egészet használni, amit az előbb csináltunk. Először beillesztem ide az egészet, utána magyarázom:

    <Window x:Class="ClientApp.WinMain"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:AppLogic="clr-namespace:ClientApp.AppLogic"
        Title="WPF Commands"
        Height="300" Width="400"
        WindowStartupLocation="CenterScreen">
        
        <!-- parancs kötések -->
        <Window.CommandBindings>
            <CommandBinding
                x:Name="CB_Support"
                Command="{x:Static AppLogic:MyCommands.Support}"
                CanExecute="CB_Support_CanExecute"
                Executed="CB_Support_Executed"/>        
            <CommandBinding
                x:Name="CB_Exit"
                Command="{x:Static AppLogic:MyCommands.Exit}"            
                Executed="CB_Exit_Executed"/>
        </Window.CommandBindings>
    
        <!-- kontextus menü -->
        <Window.ContextMenu>
            <ContextMenu>
                <MenuItem                
                    Command="{x:Static AppLogic:MyCommands.Support}"
                    CommandTarget=
                        "{Binding
                            Path=PlacementTarget,
                            RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
                <Separator/>            
                <MenuItem                
                    Command="{x:Static AppLogic:MyCommands.Exit}"
                    CommandTarget=
                        "{Binding
                            Path=PlacementTarget,
                            RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
            </ContextMenu>
        </Window.ContextMenu>
        
        <Grid>                        
            <!-- főmenü -->
            <Menu VerticalAlignment="Top">
                <MenuItem Header="_File">
                    <MenuItem Command="{x:Static AppLogic:MyCommands.Support}"/>
                    <Separator/>
                    <MenuItem Command="{x:Static AppLogic:MyCommands.Exit}"/>
                </MenuItem>
            </Menu>
            
            <!-- támogatás -->
            <CheckBox 
                Name="chSupport"            
                VerticalAlignment="Bottom"            
                Margin="10,10"
                Content="Support"/>        
        </Grid>
    </Window>
    

    Először is létrehozzuk a főmenüt, megadjuk, hogy a MenuItem-ek mely parancshoz kötnek. Ezt csinálja a <MenuItem Command={x:Static ...} rész. Az x:static markup kiterjesztés egy statikus taghoz való kötést tesz lehetővé. A szintaxisa: <object property="{x:Static prefix:typeName.staticMemberName}" .../>. Ezelőtt még felvettem az AppLogic névteret, hogy elérhető legyen a saját command (MyCommands) osztályunk. Utána hozzáadtam a ContextMenu-t az ablakhoz (Windows.ContextMenu), szintúgy két MenuItem-el, amiket ezekhez a parancsokhoz kötöttem. Már a fenti képeken is látszódhatott, hogy semmi extrát nem írtam, a menü tételeinek szövegét, gesztusait mind a saját command osztályunk írja le. Utána már nem is marad más dolgunk, mint a CommandBinding segítségével hozzákötni ezeket a commandokat a megfelelő eseménykezelőkhöz. Ez most ablakszinten van definiálva, lsd. <Windows.CommandBindings>. Azt, hogy éppen az adott command az adott környezetben, jelen esetben ablakban mit jelent tulajdonképp a CommandBinding határozza meg. Egy másik ablakba én már rendelhetek az Exit parancshoz teljesen más funkcionalitást is. Ez már javarészt a codebehind fájl dolga:

    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.Shapes;
    
    namespace ClientApp
    {
        public partial class WinMain : Window
        {
            public WinMain()
            {
                InitializeComponent();            
            }
    /// <summary> /// Lehetséges támogatást adni? /// </summary> private void CB_Support_CanExecute(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = chSupport.IsChecked ?? false; } /// <summary> /// Támogatás /// </summary> private void CB_Support_Executed(object sender, ExecutedRoutedEventArgs e) { MessageBox.Show("Support"); } /// <summary> /// Alkalmazás bezárása /// </summary> private void CB_Exit_Executed(object sender, ExecutedRoutedEventArgs e) { Close(); }
    } }

    Az eredmény pedig magáért beszélt:

    image

    Ezzel a módszerrel nagyon szépen lehet dolgozni anélkül, hogy folyamatosan oda kellene figyelni minden egyes kis mozzanatra, ami a felhasználói felület frissítését igényelné valamely ponton.

    Hogyan tudunk új egyéni gesztusokat rendelni egy controlhoz?

    Példaként, ha azt szeretnénk, hogy egy textbox SelectAll parancsa működjön ne csak a CTRL+A, hanem a CTRL+K bill. kombinációval is, akkor mindössze belefogalmazzuk ezt az InputBindings közé:

    <TextBox Height="200" Width="300"
             TextWrapping="Wrap"
             AcceptsReturn="True">
        <TextBox.InputBindings>
            <KeyBinding Key="K" Modifiers="Control" Command="SelectAll"/>
        </TextBox.InputBindings>
    </TextBox>

    Innentől kezdve már működik ez is.

    Mindettől függetlenül lehetőség van futtatni egy parancsot a codebehind-ból is, pl. így:

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        MyCommands.Exit.Execute(null, (IInputElement)sender);
    }

    Illetve a másik módszer, amikor az input elem nincs meghatározva:

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        ICommand command = MyCommands.Exit;
        command.Execute(null);
    }

    Azt hiszem egyenlőre ennyi elég is a WPF parancskezelési megoldásáról, s akkor innen folytatjuk tovább az MVC-hez vezető úton...

    March 15

    Windows Presentation Foundation + MVC (1. rész)


    Emlékeztetőül:

    Mint korábban azt már említettem, az MVC pattern lényege, hogy az alkalmazás egyes részei jól elhatárolódnak egymástól külön modell és view részre. Az ASP.NET-hez már születőben van egy kis MVC pattern implementálását megkönnyítő VS 2008 kiegészítés: ASP.NET MVC Preview 2, illetve WPF-hez is, melyet Acropolis-nak hívnak. Az Acropolissal és a Composite Application Modellel kapcsolatban további részletek itt találhatók: WPF Composite Client. Egyenlőre az Acropolis CTP állapotban van, illetve már nem is jelent meg belőle nagyon rég óta új CTP, úgyhogy ezt a suffniba dugom amíg nem lesznek életképes fejlemények felőle.

    Felmerülhet egy egyszerű kérdés mindenkiben:

    Milyen alapelveket szükséges a fejlesztés során követni a WPF-ben, hogy logikailag jól elhatárolódjanak az egyes részei az alkalmazásnak, illetve a designerek is könnyen testreszabhassák az alkalmazás arculatát?

    Néhány irányelvet kigyűjtöttem, ami most így gyorsban eszembe jutott, de majd folyamatosan kiegészítem újabb dolgokkal a későbbiekben:

     

    • Amit csak lehet deklaratív módon kell leírni (XAML). Ez az egyik legfontosabb alapszabály. A megjelenítés testreszabásához használjunk sablonokat (DataTemplate, ControlTemplate, etc.) és kerüljük a codebehind fájlba történő interakciós kód beágyazását. Ezt követve nemhogy átláthatóbbá válik a kód, de a designer is azt kezd az UI-val, amit csak akar. Mondhatjuk úgy is, hogy a Blend teljesen a keze alá fog dolgozni.

    • Használjuk a WPF új adatkötési modelljét. Az alábbi módszereket lehetőleg kerüljük, vagy inkább felejtsük is el teljesen:
      lbTitle.Content = "This is a bad practice";
      lbTitle.Content = this.Title;
    • Ahol lehet használjunk relatív kötéseket. Például az előző példában az lbTitle (Label) contentjének beállítjuk az ablakfejléc szövegét a codebehind-ban. Ehelyett írhattuk volna ezt is XAML-be:
      <Window x:Class="ClientApp.WinMain"
          xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
          Title="Client Application"
      DataContext
      ="{Binding RelativeSource={RelativeSource Self}}"> <Grid> <Label Content="{Binding Path=Title}"/> </Grid> </Window>
    • Command Design Pattern. Megj.: Delphi fejlesztők emlékezzenek csak vissza az StdActns unitra, illetve a TAction osztályra és származékaira (pl. TEditCut, TEditCopy, TWindowClose, TFileOpen..). A WPF szintén definiál előre jónéhány ilyen commandot, illetve lehetővé teszi szintúgy újak írását. A már elődefinált parancsok 5 különböző osztályba vannak csoportosítva:

      ApplicationCommands
      ComponentCommands
      MediaCommands
      NavigationCommands
      EditingCommands


      Ez szerintem egy új bejegyzés tárgya lesz. Addig itt egy rövid kis példa egy teljes szerkesztés menü készítésére (mely definiálja a viselkedést is, akárcsak a Delphi TAction osztályai):
      <Menu VerticalAlignment="Top">    
          <MenuItem Header="_Edit">
              <MenuItem Command="ApplicationCommands.Undo"/>
              <MenuItem Command="ApplicationCommands.Redo"/>
              <Separator/>
              <MenuItem Command="ApplicationCommands.Cut"/>
              <MenuItem Command="ApplicationCommands.Copy"/>
              <MenuItem Command="ApplicationCommands.Paste"/>
              <MenuItem Command="ApplicationCommands.Delete"/>
              <Separator/>
              <MenuItem Command="ApplicationCommands.SelectAll"/>
              <Separator/>
              <MenuItem Command="ApplicationCommands.Find"/>
          </MenuItem>
      </Menu>
    • A WPF közvetlen támogatja az alkalmazások lokalizációját. Néhány értéknövelő szolgáltatás a kliens oldali alkalmazásfejlesztéshez: WPF Globalization and Localization Overview

      S egy példa: WPF Runtime Localization
      Ezzel is érdemes számolni fejlesztés során.
    • Prezentációhoz nagyon jól jöhet a Frame osztály használata is, ami nem keverendő össze a VCL-es frame-l! A Frame képes megjeleníteni különböző tartalmat, mint pl. XAML, ill. HTML. Ezt beágyazva egy ablakba, ahol pl. a Products.xaml egy:
      <Page x:Class="ClientApp.Views.Products.Products"
          xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
          xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml>
          <Grid>
      </Grid> </Page>
      
      

      Beágyazható a tartalom (sőt akár lapozható is):

      <Frame Source="Products.xaml"/>
    • Az erőforrásokat, mint pl. stílusokat lehetőleg ne közvetlen ezekbe a XAML fájlokba fogalmazzuk bele és ne is az App.xaml-be. Használjunk erőforrás szótárakat (ResourceDictionary) és ezeket egyesítsük az App.xaml-be (ne szemeteljük tele a kódot!):
      <Application x:Class="ClientApp.App"
          xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
          StartupUri="WinMain.xaml">
          <Application.Resources>
               
              <ResourceDictionary>
                  <ResourceDictionary.MergedDictionaries>
                      <ResourceDictionary Source="Styles/Button.xaml"/>                
                      <ResourceDictionary Source="Styles/TextBox.xaml"/>                
                  </ResourceDictionary.MergedDictionaries>
              </ResourceDictionary>
              
          </Application.Resources>
      </Application>


      Az ilyenektől lehetőleg kíméljük meg magunkat/környezetünket:

      <Button Content="Have you tried the whisky yet?"
              Width="170" Height="25">
          <Button.Triggers>                
              <EventTrigger RoutedEvent="Mouse.MouseEnter">
                  <EventTrigger.Actions>
                      <BeginStoryboard>
                          <Storyboard>
                              <DoubleAnimation
                                  Duration="0:0:0.5"
                                  Storyboard.TargetProperty="Opacity"
                                  To="0"/>
                          </Storyboard>
                      </BeginStoryboard>
                  </EventTrigger.Actions>
              </EventTrigger>
              <EventTrigger RoutedEvent="Mouse.MouseLeave">
                  <EventTrigger.Actions>
                      <BeginStoryboard>
                          <Storyboard>
                              <DoubleAnimation
                                  Duration="0:0:0.5"
                                  Storyboard.TargetProperty="Opacity"/>
                          </Storyboard>
                      </BeginStoryboard>
                  </EventTrigger.Actions>
              </EventTrigger>
          </Button.Triggers>
      </Button>


      Külön stílust ajánlott létrehozni és szépen külön ResourceDictionarykben elhelyezni azokat. Arról nem is beszélve, hogy ezzel a módszerrel akár futásidőben cserélgethetem az erőforrásokat (lsd. WPF SDK LogonScreen példa):

      ResourceDictionary xBoxTheme = new Resources_XBox();
      Application.Current.Resources = xBoxTheme;

       

      1 2
      3 4

    • Használjuk az INotifyCollectionChanged (pl. ObservableCollection) és INotifyPropertyChanged felületeket az UI szinkronba tartása érdekében.

    • Lehetőleg ne vigyük túlzásba a BitmapEffectek alkalmazását, mivel ezek nem támogatják a hardveres gyorsítást. Ezekre vigyázzunk, mert könnyű átesni a ló túloldalára.

    • Ügyeljünk arra, hogy a üzleti logika kódjába ne kerüljön megjelenítéssel kapcsolatos, UI controlokhoz kötödő kód, s persze ugyanez fordítva is igaz.

    Az eddig felsoroltak mind olyan szempontok voltak, amelyeket érdemes fontolóra venni tervezés során, s nyilván menet közben jönnek be újabbak is, igyekszem ezeket folyamatosan publikálni. Most kapásból ennyi jutott az eszembe. A következőkben már megpróbálom egy példán keresztül demonstrálni azt, hogy mire is gondoltam.

    March 08

    WPF validáció

    A Windows Presentation Foundation kétfajta validációját támogatja az adatoknak. Egyik az egyéni validációs szabályok (ValidationRules) definiálása vagy a beépített ExceptionValidationRule használata, mely már a .NET 3.0 része is volt, másik pedig a .NET 3.5-ben megjelent új IDataErrorInfo interfész által implementált ellenőrzés. Mindkét módszernek nagy szerepe van az adatok validációjának megvalósítása terén. Míg az első külön fogalmazódik meg az adatforrástól, újrafelhasználhatóvá téve a validációs szabályokat más vezérlőkön is, addig az utóbbi (IDataErrorInfo) az üzleti rétegbe, modelbe, adatforrásba fogalmazódik bele. Míg az előbbivel x különböző alkalmazás, amelyek ugyanazzal az adatforrással dolgoznak teljesen másképp validálhatják ugyanazokat az adatokat (pl. az egyik WPF alkalmazás engedi 1900-2100-ig az évszámokat, a másik pedig egy XBAP alkalmazás, ami meg csak 2000-2050-ig /ugyan arról az adatbázisról, tábláról, mezőről van szó/) , addig az IDataErrorInfo esetén a validációt megvalósító kód közös lesz, tehát amit ezzel írok az szentírás lesz mindkettőnek.

    Ezzel kapcsolatban jónéhány példa van már a neten, nem is akarom túlragozni ezt, íme egy rövid kis példa arra, hogyan is lehet használni ezt:

    ObservableObject.cs

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.ComponentModel;
    
    namespace Validation
    {
        public class ObservableObject : INotifyPropertyChanged
        {
            #region INotifyPropertyChanged Members
    
            public event PropertyChangedEventHandler PropertyChanged;
    
            /// <summary>
            /// SetPropertyValue generikus metódus a tulajdonságok beállításához
            /// </summary>
            protected void SetPropertyValue<T>(string name, ref T field, T value)
            {
                if (!object.Equals(field, value))
                {
                    field = value;
                    OnNotifyPropertyChanged(new PropertyChangedEventArgs(name));
                }
            }
    
            /// <summary>
            /// OnNotifyPropertyChanged eseménykezelő metódus
            /// </summary>
            protected virtual void OnNotifyPropertyChanged(PropertyChangedEventArgs e)
            {
                PropertyChangedEventHandler handler = PropertyChanged;
    
                if (handler != null)
                    handler(this, e);
            }
    
            #endregion
        }
    }

    Az ObservableObject csupán implementálja az INotifyPropertyChanged interfészt, ami tartalmaz egy generikus metódust (SetPropertyValue - chikk által), amit a származtatott osztályokban is fel lehet használni majd később a mezők beállításához. S itt jön a lényeg, az IDataErrorInfo implementálása:

    Person.cs

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.ComponentModel;
    
    namespace Validation
    {
        public class Person : ObservableObject, IDataErrorInfo
        {        
            private string _name;
            /// <summary>
            /// Név
            /// </summary>
            public string Name
            {
                get { return _name; }
                set { SetPropertyValue<string>("Name", ref _name, value); }
            }
    
            private int _age;
            /// <summary>
            /// Kor
            /// </summary>        
            public int Age
            {
                get { return _age; }
                set { SetPropertyValue<int>("Age", ref _age, value); }
            }
    
            #region IDataErrorInfo Members
    
            /// <summary>
            /// Hiba szöveg
            /// </summary>
            public virtual string Error
            {
                get { return null; }
            }
    
            /// <summary>
            /// Indexer a validációhoz
            /// </summary>
            public virtual string this[string columnName]
            {
                get
                {
                    string result = null;
    
                    if (columnName == "Age" && (_age < 0 || _age > 150))
                        result = "A kor nem lehet kisebb mint 0, vagy nagyobb mint 150.";
                    
                    return result;
                }
            }
    
            #endregion
        }
    }
    

    Egy kis egyszerű UserControl az adatok megjelenítéséhez:

    LabeledTextBox.xaml.cs

    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 Validation
    { 
        public partial class LabeledTextBox : UserControl
        {
            public LabeledTextBox()
            {
                InitializeComponent();
    
                SetValue(StackPanelOrientationProperty, Orientation.Horizontal);
                SetValue(LabelWidthProperty, 50);
                SetValue(TextBoxWidthProperty, 150);
            }
    
            /// <summary>
            /// Az orientációja a stackpanel-nek
            /// </summary>
            public Orientation StackPanelOrientation
            {
                get { return (Orientation)GetValue(StackPanelOrientationProperty); }
                set { SetValue(StackPanelOrientationProperty, value); }
            }
                    
            public static readonly DependencyProperty StackPanelOrientationProperty =
                DependencyProperty.Register("StackPanelOrientation",
                typeof(Orientation), typeof(LabeledTextBox));
            
            /// <summary>
            /// A szövege a label-nek
            /// </summary>        
            public string LabelText
            {
                get { return (string)GetValue(LabelTextProperty); }
                set { SetValue(LabelTextProperty, value); }
            }
                    
            public static readonly DependencyProperty LabelTextProperty =
                DependencyProperty.Register("LabelText",
                typeof(string), typeof(LabeledTextBox));
            
            /// <summary>
            /// A szövege a textbox-nak
            /// </summary>
            public string TextBoxText
            {
                get { return (string)GetValue(TextBoxTextProperty); }
                set { SetValue(TextBoxTextProperty, value); }
            }
                   
            public static readonly DependencyProperty TextBoxTextProperty =
                DependencyProperty.Register("TextBoxText",
                typeof(string), typeof(LabeledTextBox));
            
            /// <summary>
            /// A label szélessége
            /// </summary>
            public int LabelWidth
            {
                get { return (int)GetValue(LabelWidthProperty); }
                set { SetValue(LabelWidthProperty, value); }
            }
                    
            public static readonly DependencyProperty LabelWidthProperty =
                DependencyProperty.Register("LabelWidth",            
                typeof(int), typeof(LabeledTextBox), new UIPropertyMetadata(0));
    
            /// <summary>
            /// A textbox szélessége
            /// </summary>
            public int TextBoxWidth
            {
                get { return (int)GetValue(TextBoxWidthProperty); }
                set { SetValue(TextBoxWidthProperty, value); }
            }
                    
            public static readonly DependencyProperty TextBoxWidthProperty =
                DependencyProperty.Register("TextBoxWidth",
                typeof(int), typeof(LabeledTextBox), new UIPropertyMetadata(0));
        }
    }
    

    Ja igen, ha már itt tartunk, pl. ami kimaradt az előző cikkekből, hogy default értéket egy dependency property-nek a SetValue metódussal tudunk adni. Ezt láthatjuk a konstruktorban. De honnan jön a SetValue? A válasz a DependencyObject osztály, mely mindent implementál, ami szükséges ennek a kezeléséhez. Például az UI controlok is ilyenek. A XAML markup pedig csak ennyi:

    LabeledTextBox.xaml

    <UserControl        
        x:Class="Validation.LabeledTextBox"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Name="lt">
        <StackPanel        
            Orientation="{Binding Path=StackPanelOrientation, ElementName=lt}">
            
            <Label Name="lbValue"
                   HorizontalAlignment="Left"
                   Content="{Binding Path=LabelText, ElementName=lt}"
                   Width="{Binding Path=LabelWidth, ElementName=lt}"/>
            
            <TextBox Name="tbValue"
                     HorizontalAlignment="Left"
                     Text="{Binding Path=TextBoxText,
                                    ElementName=lt,
                                    Mode=TwoWay,
                                    UpdateSourceTrigger=PropertyChanged}"
                     Width="{Binding Path=TextBoxWidth, ElementName=lt}"/>
            
        </StackPanel>
    </UserControl>
    

    Akkor egy kis tesztprojekt, ami ezt használja:

    WinMain.xaml

    <Window x:Class="Validation.WinMain"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:System="clr-namespace:System;assembly=mscorlib"
        xmlns:Local="clr-namespace:Validation"
        Title="Validáció" Height="600" Width="800"
        WindowStartupLocation="CenterScreen">
        
        <Window.Resources>
            <Local:Person
                x:Key="PersonObject"
                Name="Herczeg Adrienn"
                Age="27"/>
        </Window.Resources>
        
        <Grid>
    
            <StackPanel
                Orientation="Vertical"
                VerticalAlignment="Center"
                HorizontalAlignment="Center"
                DataContext="{StaticResource PersonObject}">
    
                <StackPanel.Resources>                
                    <ControlTemplate x:Key="ErrTemplate">
                        <DockPanel>
                            <TextBlock                             
                                DockPanel.Dock="Right"             
                                VerticalAlignment="Center"
                                Margin="10,0"
                                Foreground="Red"
                                Text="Helytelen érték!"/>
                            
                            <AdornedElementPlaceholder/>
                        </DockPanel>
                    </ControlTemplate>                
                </StackPanel.Resources>
    
                <Local:LabeledTextBox
                     LabelText="Name" 
                     TextBoxText="{Binding Path=Name,
                                           Mode=TwoWay,
                                           UpdateSourceTrigger=PropertyChanged}"/>                       
                <Local:LabeledTextBox                
                     LabelText="Age"
                     Margin="0,10"                
                     Validation.ErrorTemplate="{StaticResource ErrTemplate}"
                     TextBoxText="{Binding Path=Age,
                                           Mode=TwoWay,
                                           UpdateSourceTrigger=PropertyChanged,
                                           ValidatesOnDataErrors=true}"/>
                            
            </StackPanel>
    
        </Grid>
    </Window>
    

    Ez nagyon egyszerű, definiálunk egy személyt az ablak erőforrások között, utána a stackpanel DataContext-nek megadjuk, hogy milyen adatforrásal fogunk dolgozni, jelent esetben ezzel a PersonObject-el. S végül pedig szépen hozzákötögetjük a megfelelő tulajdonságait a LabeledTextBoxoknak az objektum megfelelő tulajdonságaihoz. A Validation.ErrorTemplate-el megadunk egy saját controltemplate-t, ami akkor jelenik meg, ha helytelen értéket adtunk meg, a ValidatesOnDataErrors értékét pedig true-ra állítjuk. Az eredmény:

    image

    Vagy B. megoldásként, pl. egy stílust és a triggereket felhasználva akár a tooltipnek is megadhatjuk az eredeti szöveget:

    <Window x:Class="Validation.WinMain"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:System="clr-namespace:System;assembly=mscorlib"
        xmlns:Local="clr-namespace:Validation"
        Title="Validáció" Height="600" Width="800"
        WindowStartupLocation="CenterScreen">
    
        <Window.Resources>
            <Local:Person
                x:Key="PersonObject"
                Name="Herczeg Adrienn"
                Age="27"/>
        
            <Style x:Key="LabeledTextBoxStyle" TargetType="Local:LabeledTextBox">
                <Style.Triggers>
                    <Trigger Property="Validation.HasError" Value="true">
                        <Setter
                            Property="ToolTip"
                            Value="{Binding RelativeSource=
    {
    x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}"/> </Trigger> </Style.Triggers> </Style> </Window.Resources> <Grid> <StackPanel Orientation="Vertical" VerticalAlignment="Center" HorizontalAlignment="Center" DataContext="{StaticResource PersonObject}"> <Local:LabeledTextBox LabelText="Name" TextBoxText="{Binding Path=Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> <Local:LabeledTextBox Style="{StaticResource LabeledTextBoxStyle}" LabelText="Age" Margin="0,10" TextBoxText="{Binding Path=Age, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=true}"/> </StackPanel> </Grid> </Window>

    image

    További részletek az SDK-ban, illetve a Windows Presentation Foundation SDK blogban.

    March 02

    MVC - Delphi Win32 vs. .NET

    Támadt a hétvégén egy elvetemült ötletem, hogy letesztelem hogyan lehet az MVC patternt implementálni Delphiben. Scott Guthrie blogját alapul véve megpróbáltam valami hasonlót elkészíteni. Na most azt tudjuk, hogy .NET-ben ez csak kézgyakorlat, meg némi tervezés kérdése, mindjárt elmondom miért is. Körülbelül 3-4 órás kínszenvedés után ott tartottam, hogy fejbelövöm magam, de akkor sorolom a problémás okokat:

     

    • Kódújrahasznosítás: Az MVC modell alap pilléreit szükséges lenne absztrakt módon, interfészekkel definiálni, hogy ezt később más projektek esetében is újrahasználhassuk. Na ja, de hogyan? A natúr DLL-ek kilőve, mivel objektumorientált megközelítést akarunk alkalmazni. Két választási lehetőség van: COM könyvtárak létrehozása, vagy BPL (Borland Package Library)-k létrehozása, illetve van mégegy, .NET assemblyk létrehozása, amiket COM interopon keresztül lehetne használni, de az már tényleg nem lenne egészséges. Ó, ha ilyen egyszerű lenne, mint ahogy kijelentettem, de pl. a BPL-el ott vagyunk meglőve, hogy csak Borland fejlesztőeszközök lennének képesek vele dolgozni (persze ez most nem gond), másrészt a linkernek kellenek a DCU fájlok is (ez szintén nem probléma jelen esetben), viszont ami már gond, hogy a BPL-el hozzá is kötöttük az MVC View részét a VCL-hez. Puff neki, ezt a featuret már el is felejthetjük, persze ha csak VCL-ben gondolkozunk, akkor nem gond, ez még nem teszi teljesen használhatatlanná az MVC-t. Ezt a problémát .NET-ben az assemblyk segítségével simán át lehetett volna hidalni.

    • Delphi Win32-ben nincs semmilyen ORM eszköz, az ECO csak a Delphi .NET-ben érhető el. Ok, legyártottam a szükséges osztályokat a táblákhoz a tulajdonságokkal együtt. Persze nincsenek se attribútumok, se nullázható típusok, úgyhogy hatványozódott a munka vele. Ezt a szenvedést a Linq 2 SQL segítségével át lehetett volna hidalni.

    • Nincsenek generikus típusok. Na jó, kit érdekel, csináltam mindenhez saját lista típust, amelyek mindegyikéhez csináltam egy-egy enumerator osztályt, hogy a for ... in működjön rajtuk. Mindezt a bevált copy-paste módszerrel és újrafaktorizálással gyorsan meglehetett csinálni viszonylag. Ezt a kis mókát a C# 2.0/3.0, ill. Delphi .NET 2.0 segítségével simán át lehetett volna hidalni.

    • Nincs adatkötés. Egye fene, de azért valamilyen szinten csak jó lenne tudni, hogy mi történik, ezért megcsináltam az INotifyPropertyChanged interfészt Delphibe és implementáltam azt minden osztályban, ahol szükséges volt (TCustomer, TOrder, TProduct). Így már mindenkiről lehet tudni, hogy mikor mi változott benne. Most már valamivel jobb a helyzet mint azelőtt. Következőkben csináltam egy TObservableCollection osztályt, ami meg implementálja az INotifyCollectionChanged interfészt, ebből tudom, mikor ki kerül be/ki, stb. Utána generikusok híján a TCustomer, TOrder, TProduct-hoz is készítettem egy-egy TObservableCollection származékot. Ezt Windows Formsal, vagy méginkább a WPF-el át lehetett volna hidalni.

    • Attribútumok, bővíthető metaadatok: Első ötletem az volt, hogy az RTTI információkat felhasználva fogom a dolgot megoldani. Persze, hogyne!? Ugye a TPersistent osztály megvan jelölve az {$M+} direktívával, ami azt jelzi a fordítónak, hogy a published láthatósági tagokat lássa el RTTI információkkal. Jó is lenne, mivel a TPersistent osztály támogatja az Assign műveletet is (pluszba), csak épp akkor meg az interfészeket nem tudom implementálni, mivel a TInterfacedObject osztályból kell származzon az osztály, hogy implementálni tudja az interfészt. Ok, akkor TPersistent osztály CTRL+C, CTRL+V, majd megcsinálom én magamnak, hogy jó legyen, annyi, hogy ellátom én ezekkel a direktívákkal az osztályt: {$M+} ... {$M-}. Ekkor abból a feltételezésből indultam ki, hogy az osztály tulajdonságai mind published-ként lesznek deklarálva, a TypInfo segítségével pedig majd kiolvasom és be is állítom ezeket a tulajdonságokat. Mivel nincsenek attribútumok, ezért ezeket az információkat egy másik típusnak kell tárolnia (TDataField), az értékkel együtt. Ez még így működőképes is lett volna, bár jóval körülményesebb, mint a .NET attribútumokkal dolgozni.

    • Memória magament: Ahogy bekapcsoltam a ReportMemoryLeaksOnShutdown-t, mindjárt megkaptam kilépéskor a szép listát, hogy memóriaszivárgás történt, pedig amennyire lehet próbáltam odafigyelni erre. Nesze neked, kezdjünk el bóklászni, hogy mi nem akar megsemmisülni és kb. fél óra alatt meg is találtam a gond egy részét. GC híján az ilyen dolgokra odafigyelni kissé elég nehézkes tud lenni, ennyi időt elpazarolni erre az értékes munka helyett.

    • Prezentáció: adatkötés híján kín keserves kínlódás. Ezen a ponton kezdett el kerülgetni az öngyilkosság gondolata. Minden apró dolgot lekódolni és kezelni - ráadásul simán elnézhető a dolog - öngyilkosság. A megoldás, egyetlen dolgot mondok: Windows Presentation Foundation.

    Ennyit az MVC-ről Delphiben. Azt hiszem egyértelműen kijelenthetem, hogy ez nem a Delphinek való. Annyira távol áll tőle, mint én Bucsaröcsögétől. Talán a Delphi.NET megoldás lenne? Az ECO már kapásból megold néhány problémát a model résszel kapcsolatban, viszont a Delphi .NET a VCL.NET-re épít, mint prezentációs technológia, aminek az adatkötési támogatása szintén = 0. Ez így, ilyen formában semmire nem használható. Itt az ideje mindenhol bevezetni az új kort:

    Windows Presentation Foundation Everywhere 

    Ég veled VCL, ég veled WinForms, hajrá WPF !!!