János's profileJános JankaPhotosBlogListsMore ![]() | Help |
|
March 28 WPF alkalmazások lokalizációja I.
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ésLehető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:
Í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: <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: <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:
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: <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: Á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" Illetve ezután még egy dolgot meg kell tegyünk, az AssemblyInfo.cs fájlba kikommentezzük a következő sort: [assembly: 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: Innen folytatjuk legközelebb a következő megoldással... March 16 Windows Presentation Foundation + MVC (2. rész)
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: 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,
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 { 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): 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> 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(); } Az eredmény pedig magáért beszélt: 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)
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:
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: 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: 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= További részletek az SDK-ban, illetve a Windows Presentation Foundation SDK blogban. March 02 MVC - Delphi Win32 vs. .NETTá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:
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 !!! |
|
|