2013. április 23., kedd

Wpf #10 Trigger

Egy program fejlesztése során számos alkalommal fordul elő, hogy egy adott tulajdonság megváltoztatása csak bizonyos esemény hatására kell, hogy megtörténjen. Az adott esemény lehet az oldal betöltődése, de lehet akár egy tetszőlegesen tárolt változó egy előre meghatározott értékének felvétele.

A Wpf alatt erre szolgál a trigger, amelyből három féle létezik.
Property trigger: amely akkor aktivizálódik, ha egy DependencyProperty értéke megváltozik.
Data trigger: amely akkor aktivizálódik, ha egy normál .Net tulajdonság értéke megváltozik
Event trigger: amely tetszőleges Routed Event hatására aktivizálódik

A DependencyProperty hasonló a normál tulajdonságokhoz. Egy Wpf elem számos DependencyProperty-vel rendelkezik. Pl. Width, Height.
Mivel egy adott elem megjelenítését számos tulajdonság határozza meg, könnyen belátható, hogy néhány elem kirajzolása szinte  felfalná az összes memóriát.
A DependencyProperty ennek megakadályozására lett kitalálva.
Az adott elem összes tulajdonsága egy alapértékkel rendelkezik, amely csak egyszer kerül eltárolásra egy programban. Minden esetben, amikor egy elem tulajdonsága felülírásra kerül (pl Width="100") akkor  csak ez az érték kerül külön tárolásra.
Számos események látszó tulajdonság is DependencyProperty-ként kerül eltárolásra. Ilyen pl. az IsMouseOver amely a nevéből adódóan a kurzort figyeli, és amennyiben az adott elem fölött található, akkor true értéket vesz fel.

A Routed Event nagyon leegyszerűsítve megfelel egy tetszőleges eseménynek. Pl: Loaded vagy Click.

A triggerek elhelyezése az adott elemhez definiált stíluson belül történik. A beállításokat továbbra is setter elemek végzik, akárcsak egy normál stílus esetén. Mivel az adott elem tulajdonságának triggerel történő beállítása a stíluson keresztül történik, ezért az elem azonos tulajdonságát is kötelezően a stílusban kell megadni.

Egy trigger definíciója az alábbihoz hasonló:
<Grid>
 <Grid.Resources>
  <Style TargetType="{x:Type Button}" x:Key="MyButton">
   <Style.Triggers>
    <trigger>
       trigger által megkívánt teendők
    </trigger>
   </Style.Triggers>
  </Style>
 </Grid.Resources>
 
 <Button Style="{StaticResource MyButton}"/>
 
</Grid>

Egy tetszőleges Resource-ban deklarálásra kerül az adott elemhez tartozó stílus. Jelen esetben egy Button. A stíluson belül deklarálásra kerül a trigger, amely a három különöző trigger esetén más néven kerül bevezetésre.

Az adott elem stílusát beállítva a trigger élesítésre kerül, és a feltételeknek megfelelő pillanatban kifejti a hatását.

Event trigger

Egy Wpf elem számos esemény kezelésére képes. Ilyen pl.: Loaded vagy Click.
Amennyiben egy esemény meghívásra kerül a trigger aktiválódik.  
Az event triggereket animációk irányítására lehet használni.
Az alábbi példa a Button betöltésekor indít egy animációt, amely a Button szélességét változtatja meg 100-ról 200-ra 2 másodperc alatt.

<Grid>
 <Grid.Resources>
  <Style TargetType="{x:Type Button}" x:Key="MyButton">
   <Style.Triggers>
    <EventTrigger RoutedEvent="Loaded">
     <BeginStoryboard>
      <Storyboard>
       <DoubleAnimation From="100" To="200"  Duration="0:0:2" Storyboard.TargetProperty="Width"/>
      </Storyboard>
     </BeginStoryboard>
    </EventTrigger>
   </Style.Triggers>

   <Setter Property="Width" Value="100"/>
   <Setter Property="Height" Value="20"/>
  </Style>
 </Grid.Resources>
 
 <Button Style="{StaticResource MyButton}"  Content="Click"/>
 
</Grid>

Az event trigger a stíluson belül az EventTrigger deklarációval határozható meg. A RoutedEvent attributum segítségével adható meg a figyelni kívánt esemény (Loaded).
Mivel az event trigger animációk irányítására szolgál, ezért egy storyboard megadása szükséges.
Ebben a storyboard-ban kell minden animációt deklarálni amelyet az adott elem meghatározott eseményekor le kell játszani.

Property trigger

A Wpf-ben a property trigger az alapértelmezésű trigger. Bármely DependencyProperty megfigyelhető vele.

Az alábbi példa ezt szemlélteti
<Grid>
 <Grid.Resources>
  <Style x:Key="MyButton" TargetType="{x:Type Button}">
   <Style.Triggers>
    <Trigger Property="IsMouseOver" Value="True">
     <Setter Property="Width" Value="200"/>
    </Trigger>
   </Style.Triggers>
   
   <Setter Property="Width" Value="100"/>
   <Setter Property="Height" Value="20"/>
  </Style>
 </Grid.Resources>
 
 <Button Style="{StaticResource MyButton}" Content="Click"/>
 
</Grid>

Az előző példától eltérően a stíluson belül egy szimpla Trigger kerül deklarálásra.
A Property tulajdonság határozza meg, hogy mely DependencyProperty-t kell figyelni, a Value pedig azt az értéket tartalmazza, amely esetén a triggernek aktiválódnia kell.

Data trigger

Segítségével tetszőleges tulajdonság értéke megfigyelhető. A megfigyelt tulajdonságnak olyan osztályban kell lennie, amely megvalósítja az INotifyPropertyChanged Interface-t, valamint a tulajdonságnak elérhetőnek kell lennie a DataContext-en keresztül.

Az alábbi példa egy boolean típusú tulajdonság felhasználásával változtatja meg a Button szélességét.
Ebben az esetben kódot is kell írni a megjelenítés mögé.

<Grid>
 <Grid.Resources>
  <Style x:Key="MyButton" TargetType="{x:Type Button}">
   <Style.Triggers>
    <DataTrigger Binding="{Binding ChangeWidth}" Value="True">
     <Setter Property="Width" Value="200"></Setter>
    </DataTrigger>
   </Style.Triggers>

   <Setter Property="Width" Value="100"/>
   <Setter Property="Height" Value="20"/>
  </Style>
 </Grid.Resources>

 <Button Style="{StaticResource MyButton}" Content="Just a button"/>

 <Button VerticalAlignment="Bottom" Height="20" Content="Change Width" Click="ButtonBase_OnClick"/>
</Grid>

A trigger-t a stíluson belül a DataTrigger deklarációval kell megkezdeni.  A Binding értékadásával lehet beállítani a figyelni kívánt tulajdonságot, a Value szerepe itt is az itt megadott érték figyelése.

public partial class DataTrigger : Window, INotifyPropertyChanged
{
 private bool _changeWidth;
 public bool ChangeWidth
 {
  get { return _changeWidth; }
  set { _changeWidth = value; OnPropertyChanged("ChangeWidth"); }
 }

 // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

 public event PropertyChangedEventHandler PropertyChanged;
 public void OnPropertyChanged(string propertyName)
 {
  if (PropertyChanged != null)
   PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
 }

 // --------------------------------------------------------------------

 public DataTrigger()
 {
  InitializeComponent();
  ChangeWidth = false;
  DataContext = this;
 }

 private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
 {
  ChangeWidth = !ChangeWidth;
 }
}

Fordítás után az alul található gomb a ChangeWidth tulajdonság értékét változtatja, és a trigger ennek függvényében változtatja a gomb szélességét.

MultiTrigger - VAGY kapcsolat

Egy trigger nem feltétlenül csak egy bizonyos feltétel meglététől aktiválódhat. Előfordulhat, hogy ugyanaz a trigger több más - egymástól teljesen független - feltétel megléte esetén is kell, hogy végrehajtsa a megkívánt beállításokat.

Ezt nevezik MultiTrigger-nek.
MultiTrigger-ből is többféle létezik (MultiTrigger, MultiDataTrigger), azonban a logikai VAGY kapcsolathoz az előbb megismert triggerek bármelyike felhasználható.

Az alábbi példa egy egyszerű VAGY kapcsolatot mutat be, amely során a Button szélessége megváltozik, ha az egérrel a kurzor felé kerül vagy ha a Button magassága 30 lesz.
Fordítás után a Button magasságát az alul található gomb segítségével lehet növelni.

<Grid>
 <Grid.Resources>
  <Style x:Key="MyButton" TargetType="{x:Type Button}">
   <Style.Triggers>
    <Trigger Property="IsMouseOver" Value="True">
     <Setter Property="Width" Value="200"/>
    </Trigger>
    
    <Trigger Property="Height" Value="30">
     <Setter Property="Width" Value="200"/>
    </Trigger>
   </Style.Triggers>

   <Setter Property="Width" Value="100"/>
   <Setter Property="Height" Value="20"/>
  </Style>
 </Grid.Resources>

 <Button Name="Btn" Style="{StaticResource MyButton}" Content="Click"/>

 <Button VerticalAlignment="Bottom" Height="20" Content="Increment Height" Click="ButtonBase_OnClick"/>

</Grid>

A hozzá tartozó kód, amely az alsó gomb click eseményét kezeli.
public partial class MultiTrigger_Or : Window
{
 public MultiTrigger_Or()
 {
  InitializeComponent();
 }

 private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
 {
  Btn.Height += 10;
 }
}

A gomb megnyomására a középső Button magassága 10-el növekszik. Amint eléri a 30-at, a szélessége a triggerben megadott 200-ra változik. Majd miután 40-re növekszik visszaáll az eredeti értékre.

Itt fontos megjegyezni, hogy az eredeti értékre történő visszaállítással nem kell foglalkozni, azt a keretrendszer automatikusan elvégzi.

Egy trigger logikai VAGY kapcsolata számos további triggert tartalmazhat, ezáltal tetszőleges számú feltételhez köthető a kívánt beállítás aktiválása.

MultiTrigger - ÉS kapcsolat

Előfordulhat, hogy egy adott trigger aktiválódására csak több feltétel együttes megléte esetén van szükség. Erre szolgál a MultiTrigger.
A MutiTrigger csak DependencyProperty vizsgálatára jó.
Amennyiben .Net tulajdonságok vizsgálatára van szükség, úgy a MultiDataTrigger használata kell.

Az alábbi példában a Button szélessége csak abban az esetben változik meg, ha a Button magassága 30 és az egér is a gomb felett van.

<Grid>
 <Grid.Resources>
  <Style x:Key="MyButton" TargetType="{x:Type Button}">
   <Style.Triggers>
    <MultiTrigger>
     <MultiTrigger.Conditions>
      <Condition Property="IsMouseOver" Value="True"/>
      <Condition Property="Height" Value="30"/>
     </MultiTrigger.Conditions>

     <Setter Property="Width" Value="200"/>
    </MultiTrigger>

   </Style.Triggers>

   <Setter Property="Width" Value="100"/>
   <Setter Property="Height" Value="20"/>
  </Style>
 </Grid.Resources>

 <Button Name="Btn" Style="{StaticResource MyButton}" Content="Click"/>

 <Button VerticalAlignment="Bottom" Height="20" Content="Increment Height" Click="ButtonBase_OnClick"/>

</Grid>

A hozzá tartozó kód:
public partial class MultiTrigger_And : Window
{
 public MultiTrigger_And()
 {
  InitializeComponent();
 }

 private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
 {
  Btn.Height += 10;
 }
}

A MultiTrigger feltételeit egyesével meg kell adni a MultiTrigger Conditions tulajdonságában. Amennyiben minden feltétel teljesül a triger aktiválódik.

2013. április 18., csütörtök

Wpf #9 Stílusok

Mint sokmás deklaratív nyelv, az XAML se kerülte el a stílusok létrehozásának lehetőségét. Bár használata hosszútávon felbecsülhetetlen ez nem jelenti azt, hogy kötelező. Egy Wpf program nélküle is fut, segítségével viszont szép és jól elosztott kódot lehet készíteni.

Hagyományos módszer használatával az adott elem egyes tulajdonságai direkt módon kerülnek beállításra. Több azonos tulajdonságú elem esetén ugyanazokat a beállításokat mindenhol újra jelölni kell.

Az alábbi kód 3 gombot tesz egymás mellé.
<StackPanel Orientation="Horizontal">
 <Button Content="Click" FontSize="15" Height="30"/>
 <Button Content="Click" FontSize="15" Height="30"/>
 <Button Content="Click" FontSize="15" Height="30"/>
</StackPanel>

Mindhárom nyomógomb ugyanazt a két beállítást tartalmazza. A betűméret 15  a magasság pedig 30. Amennyiben valamelyik paramétert változtatni kell, akkor mindhárom nyomógomb esetében meg kell ezt tenni. Belátható, hogy minél több helyen szerepelenek ezek a konstans értékek, annál több módosítást kell elvégezni.

Stílusok használatával az egyes tulajdonságok csoportba rendezhetőek amelyet tetszőleges számú elem elérhet. A stílusban történő változtatás az összes olyan elemet érinti, amely használja.

Az elöbbi kód stílusokkal:
<StackPanel Orientation="Horizontal">
 <StackPanel.Resources>
  <Style x:Key="MyButton">
   <Setter Property="Button.FontSize" Value="15"/>
   <Setter Property="Button.Height" Value="30"/>
  </Style>
 </StackPanel.Resources>

 <Button Content="Click" Style="{StaticResource MyButton}"/>
 <Button Content="Click" Style="{StaticResource MyButton}"/>
 <Button Content="Click" Style="{StaticResource MyButton}"/>
</StackPanel>

Az első és legfontosabb, hogy a stílusokat un. Resource-ban kell tárolni. Ez lehet egy külön resource file is. Jelen esetben a StackPanel Resource-ban található.

A stílus hatáskörét megszabja, hogy mely elem Resource-ában lett definiálva. A példában a stílus csak a stackpanel-en belül érvényes. Amennyiben az egész oldalra érvényes stílus deklarálása a cél, úgy azt a Window.Resources alatt érdemes elhelyezni.

A <Style x:Key="MyButton">  létrehoz egy stílust amelyre az x:Key attributumban megadott névvel lehet a továbbiakban hivatkozni. Minden stílus Setter elemekből épül fel. Egy Setter elem két attributummal kell, hogy rendelkezzen.
A Property, amely meghatározza, hogy mely tulajdonság értéket kell beállítani.
A Value, amely magát az értéket tartalmazza.

A stílusra az elem Style attributumán keresztül lehet hivatkozni: 
Style="{StaticResource MyButton}".

Stílusok öröklése

Számos esetben előfordulhat, hogy egy stílus mindössze egy-két tulajdonságában tér el, egy már létező stílustól. Esetleg egy már létező stílust kell kibővíteni. Erre szolgál az öröklés lehetősége.

<StackPanel Orientation="Horizontal">
 <StackPanel.Resources>
  <Style x:Key="MyButton">
   <Setter Property="Button.FontSize" Value="15"/>
   <Setter Property="Button.Height" Value="30"/>
  </Style>
  
  <Style x:Key="MyBoldButton" BasedOn="{StaticResource MyButton}">
   <Setter Property="Button.FontWeight" Value="Bold"/>
  </Style>
 </StackPanel.Resources>

 <Button Content="Click" Style="{StaticResource MyButton}"/>
 <Button Content="Click" Style="{StaticResource MyBoldButton}"/>
 <Button Content="Click" Style="{StaticResource MyButton}"/>
</StackPanel>

Az újonnan létrehozott stílus egyfelől örökli a szülő minden beállítását (BasedOn="{StaticResource MyButton}"), másfelől kiegészíti az egy új beállítással, amely a szöveg vastagságát bold-ra állítja.

A három nyomógomb közül, a középső ezt az új stílust használja. Minden egyéb beállításban egyezik a másik kettővel, kivéve a betűvastagságot.

A Stílus elem megadása

Amennyiben egy stílusról ismert, hogy csak egyféle elem típushoz tartozik, akkor célszerű azt a stílus létrehozásakor jelölni. Az előbbi esetekben a stílusok mind nyomógombok megjelenítéséhez készültek, ezért a Property beállításnál szükség volt egy Button prefixre. Ha már a stílus deklarálásakor jelezzük, hogy az adott stílus mely típusú elemhez tartozik, akkor a prefix elhagyható.

<StackPanel Orientation="Horizontal">
 <StackPanel.Resources>
  <Style x:Key="MyStyle" TargetType="{x:Type Button}">
   <Setter Property="FontSize" Value="15"/>
   <Setter Property="Height" Value="30"/>
   <Setter Property="Background" Value="Yellow"/>
  </Style>
 </StackPanel.Resources>

 <Button Content="Click" Style="{StaticResource MyStyle}"/>
 <Button Content="Click" Style="{StaticResource MyStyle}"/>
</StackPanel>

A TargetType="{x:Type Button}" attributum használtával egyértelműen jelzésre került, hogy az adott stílus csak Button-hoz kapcsolható. Amennyiben valami más elem szeretné használni, akkor futásidejű hiba keletkezik.
Mivel a TargetType egyértelműen megadja annak az elemnek a típusát, amely az adott stílust fel fogja használni, ezért a Setter elem Property attributumában már nem kell külön Button prefixet használni.
<Setter Property="FontSize" Value="15"/>

Stílus név nélkül.

Abban az esetben, amennyiben ismert, hogy egy adott elem mindig ugyanúgy néz ki célszerű implicit stílusokat használni.

<StackPanel Orientation="Horizontal">
 <StackPanel.Resources>
  <Style TargetType="{x:Type Button}">
   <Setter Property="FontSize" Value="15"/>
   <Setter Property="Height" Value="30"/>
   <Setter Property="Background" Value="Yellow"/>
  </Style>
 </StackPanel.Resources>

 <Button Content="Click"/>
 <Button Content="Click"/>
</StackPanel>

Ebben az esetben a a Style nem tartlamaz x:Key attributumot, a TargetType megadása viszont kötelező. Hatására a StackPanelen található összes Button megkapja ezt a stílust. Használatával a Button elemnek nem kell külön jelezni, hogy melyik stílust kell használnia.

Noha egy-egy tulajdonság beállítása miatt a stílus használata eléggé felesleges többletgépelésnek látszik, a végső kódban célszerű a direkt értékadásokat megszüntetni.  A stílusok általában külön Resource file-ban kerülnek tárolásra, nem pedig az adott elemen belül
Ehhez egy új Resource Dictionary-t kell a projekthez adni, amely üresen így néz ki

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    
</ResourceDictionary>

Minden stílus deklarációt ezen belül kell elhelyezni.

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <Style x:Key="MyStyle" TargetType="{x:Type Button}">
        <Setter Property="FontSize" Value="15"/>
        <Setter Property="Height" Value="30"/>
        <Setter Property="Background" Value="Yellow"/>
    </Style>

</ResourceDictionary>

Legvégül a XAML kódban csak a Resource File-ra kell hivatkozni.

<StackPanel Orientation="Horizontal">
 <StackPanel.Resources>
  <ResourceDictionary Source="Dictionary1.xaml"/>
 </StackPanel.Resources>

 <Button Content="Click" Style="{StaticResource MyStyle}"/>
 <Button Content="Click" Style="{StaticResource MyStyle}"/>
</StackPanel>

Magát a hivatkozást szintén az adott elem Resource  tulajdonságában kell elhelyezni. Azonban itt már csak egy hivatkozás kell a külső file nevére.

A stílusok megfontolt használatával a leíró kód rövidebbé és átláthatóbbá válik, hosszútávon rengeteg felesleges és redundáns adat begépelése kerülhető el vele.

2013. április 15., hétfő

HLSL #2 Textura

Folytatva az előzőekben megkezdett HLSL ismertetőt, ebben a részben a texturák használatával még látványosabb lesz mindaz amit a képernyőn meg kívánunk jeleníteni.
Továbbra is az FxComposer használatával, egy új üres effect-re lesz szükség.

Egy tetszőleges kép megjelenítéséhez az alábbiakra van szükség:
- model
- kép
- UV koordináták
- Sampler

A model továbbra is az előző részben beillesztett négyzet marad.
A kép tetszőleges lehet, akár saját vagy akár az internetről letöltött
Az UV koordinátákat jelen esetben a program adja a modellhez

A sampler definiáláshoz az alábbi kódsorok szükségeltetnek:
texture diffuseMap
<
    string UIName = "Diffuse Texture";
>;

sampler2D diffuseMapSampler = sampler_state
{
 Texture = <diffuseMap>;
};

Első lépésben egy texture kerül bevezetésre. Ez látható a properties panelen "Diffuse Texture" néven.

A sampler2D definiál minden egyéb paramétert a shader számára, hogy az adott texturát milyen módon kell megjeleníteni.(pl hogyan kell sokszorosítani, stb)

Egyenlőre egyetlen hivatkozás bőven elég, amely meghatározza, hogy az adott sampler melyik képhez tartozik: Texture = <diffuseMap>;
Ahol a diffuseMap az előbb létrehozott texture neve

Fontos megjegyezni, hogy ahány texture kerül bevezetésre, pontosan annyi sampler kell.

A kép megjelenítéséhez szükség van UV koordinátákra, ezért a vertexshader bemenete megváltozik.
struct app2vertex {
 float4 position  : POSITION;
 float2 texCoord  : TEXCOORD0;
};

A vertexsahder minden esetben elvárja a vertex pozícióját: float4 position : POSITION;

A textura koordináták a struktura texCoord elemén keresztül érhetőek el:
float2 texCoord : TEXCOORD0;
A TEXCOORD0 SEMANTICS utasítja az FXComposert, hogy a model első UV koordinátáit adja át.

Amennyiben egy modellhez több UV koordináta is tartozik, úgy azokat a sorszámok növelésével lehet elérni. (TEXCOORD1, TEXCOORD2...)

Mivel az UV koordinátákra a pixelshader-ben is szükség lesz, ezért annak a bemenetét is változtatni kell.
struct vertex2pixel {
  float4 position  : POSITION;
  float2 texCoord  : TEXCOORD0;  
};

Noha jelen esetben a két struktúra megegyezik, ez a legritkább esetben fordul csak elő.

Általánosságban elmondható, hogy mindig vagy egy struktúra amely a programtól a vertexshader felé tartalmaz bemenetet (app2vertex) és van egy struktúra amely a vertexshadertól a pixelshader felé tartalmazza a szükséges adatokat (vertex2pixel)

Mivel a pixelshader a vertexshadertól várja a már előkészített paramétereket, ezért logikus, hogy a vertexshader kimenete a vertex2pixel struktúra.
Bemenete pedig az app2vertex struktúra.

A végső vertexshader az alábbi:
vertex2pixel mainVS(app2vertex In)
{
  vertex2pixel Out; 
  Out.position = mul(In.position, WorldViewProj); 
  Out.texCoord = In.texCoord;
  return Out;
}

A pozíció a már megismert módon kiszámításra kerül.
A textura UV koordináta pedig minden  változtatás nélkül továbbadásra kerül a pixelshader felé.

Mivel a vertexshader kimenete egy vertex2pixel struktúra, ebből következőleg a pixelshader bemenetének is muszáj annak lennie.

float4 mainPS(vertex2pixel In) : COLOR
{
 float4 diffuseTexture = tex2D(diffuseMapSampler, In.texCoord);
 return diffuseTexture;
}

A tex2D függvény készíti el a végső képet és 2 paramétert vár.
Az első a sampler neve, a második a modellhez tartozó UV koordináta, amely immáron a vertexshader kimeneteként jön mint paraméter: vertex2pixel In

A végső shader az alábbi:
/*****************************************************/
// **                AUTO GENERATED
/*****************************************************/

float4x4 WorldViewProj : WorldViewProjection;


/*****************************************************/
// **                TEXTURES
/*****************************************************/

texture diffuseMap
<
    string UIName = "Diffuse Texture";
>;

sampler2D diffuseMapSampler = sampler_state
{
 Texture = <diffuseMap>;
};

/*****************************************************/
// **                IO STRUCTURES
/*****************************************************/

// input from application
struct app2vertex {
 float4 position  : POSITION;
 float2 texCoord  : TEXCOORD0;
};

// output to pixelshader
struct vertex2pixel {
 float4 position  : POSITION;
 float2 texCoord  : TEXCOORD0;  
};


/*****************************************************/
// **                PROGRAMS
/*****************************************************/

vertex2pixel mainVS(app2vertex In)
{
 vertex2pixel Out; 

 Out.position = mul(In.position, WorldViewProj);    
 Out.texCoord = In.texCoord;

 return Out;
}


float4 mainPS(vertex2pixel In) : COLOR
{
 float4 diffuseTexture = tex2D(diffuseMapSampler, In.texCoord);
 return diffuseTexture;
}

/*****************************************************/
// **
// **                TECNIQUES
// **
/*****************************************************/

technique technique0 {
 pass p0 {
  CullMode = none;
  VertexShader = compile vs_3_0 mainVS();
  PixelShader = compile ps_3_0 mainPS();
 }
}



2013. április 12., péntek

Wpf #8 A konstruktor elött

Egy tetszőleges Wpf alkalmazás fejlesztésekor hamar felmerülhet a kérdés, vajon a konstruktor hol hívódik meg? Átnézve a file-okat, amelyeket a VisualStudio hozott létre, az alkalmazáshoz sehol se lehet a konstruktor meghívására utaló kódot találni.

A VisualStudio által készített projektben ilyen nincs is. Ez persze nem azt jelenti, hogy nem lehet.

A projektet közelebbről megvizsgálva található benne egy App.xaml én annak a mögöttes kódja.
Az XAML file tartalma hasonló az alábbihoz (leszámítva az osztály elnevezést)
<Application x:Class="Wpf08_BeforeConstructor.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
         
    </Application.Resources>
</Application>


A program ablak megjelenítés céljából a fontos  értékadás a StartupUri attributumban találaható.
A megatott érték annak a Window-nak a neve amely a program indulásakor látható lesz.

Amennyiben a projekthez hozzáadásra kerül egy új Window (add/new item/Window(WPF)), akkor annak nevét megadva immáron az lesz a kezdőablak.

A konstruktor meghívását a keretrendszer a háttérben elvégzi.

A legritkább  esetek közé tartozik, hogy egy komplett rendszer rögtön az UI megmutatásával indul.
Valójában előtte számos inicializálás és vizsgálat lezajlik, sőt jónéhány paramétert is átadásra kerülhet.

Ahhoz, hogy a konstuktor meghívása már az általunk felügyelt kódból történjen néhány apró változtatásra van szükség.
A legelső, hogy az előbbi StartupUri="MainWindow.xaml" attributumot ki kell törölni és helyette az alábbi attributum kell: Startup="ApplicationStartup"

Az App.xaml új tartalma:
<Application x:Class="Wpf08_BeforeConstructor.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             Startup="ApplicationStartup">
    <Application.Resources>
         
    </Application.Resources>
</Application>

A Startup attributum meghatározza az induláskor lefuttatandó metódus nevét. 
Ebből következőleg valahol kell egy ApplicationStartup nevű metódus.

Az App.xaml.cs file-t megnyitva egy teljesen üres osztály látható. A számunkra szükséges kódot itt kell elhelyezni.

public partial class App : Application
{
 public void ApplicationStartup(Object sender, StartupEventArgs e)
 {
 }
}

A program indításakor immáron ez az eljárás hívódik meg. A Wpf ablak konstruktorának a meghívását is ebből (vagy valamely ebből meghívott eljárásból) célszerű végrehajtani.

A konstruktor jelenleg az alábbi kinézetű
public partial class MainWindow : Window
{
 public MainWindow()
 {
  InitializeComponent();
 }
}

Mivel a konstrukor meghívása immáron kódból történik, ezért tetszőleges paraméter is átadható.
Tételezzük fel, hogy egy string paraméter átadása szükséges.
Ebben az esetben a konstrukor: public MainWindow(string someString)

A meghívása az App.xaml.cs ApplicationStratup-ból történik
public partial class App : Application
{
 public void ApplicationStartup(Object sender, StartupEventArgs e)
 {
  var window = new MainWindow("hello world");
  // itt még tetszőleges, a Window-ban található általad megírt metódus meghívható
  // pl Init()
  window.Show();
 }
}

Ezzel a módszerrel immáron számos ellenőrzés elvégezhető mielőtt az UI meghívásra kerülne.
Számos szerviz injektálható a konstruktoron keresztül (pl Log), amelyek létrehozásához a Wpf ablaknak semmi köze nincs, és illik már készen átadni.

2013. április 9., kedd

Wpf #7 MultiBinding, MultiConverter

Az adatkötés használata során elég gyakran kell egy tetszőleges elem értékét több - egymástól különböző típusú, vagy különböző helyen deklarált - tulajdonság értéke alapján megállapítani.
Noha erre lehet mögöttes kódot írni, ami mindig figyeli mi történik éppen a felhasználói felületen, sokkal egyszerűbb megoldás is létezik a probléma megoldására.

Legyen adott egy cég különböző termékekkel és azok éves bevételivel.
A program ezen adatokat egy listában jelenítse meg.
Kérje be továbbá az éppen aktuális Euro árfolyamot és az összbevételt számítsa ki Euroban.

A felhasznált Model tartalmazza a termék nevét és az éves bevételt.
class Model : INotifyPropertyChanged
{
 private string _productName;
 public string ProductName
 {
  get { return _productName; }
  set { _productName = value; OnPropertyChanged("ProductName"); }
 }

 private decimal _income;
 public decimal Income
 {
  get { return _income; }
  set { _income = value; OnPropertyChanged("Income"); }
 }

 // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

 public event PropertyChangedEventHandler PropertyChanged;
 public void OnPropertyChanged(string propertyName)
 {
  if (PropertyChanged != null)
   PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
 }

 // ---------------------------------------------------------------------

 public Model(string productName, decimal income)
 {
  ProductName = productName;
  Income = income;
 }

}

A kapcsolódó ViewModel egy gyűjteményben tárolja a cég termékeit, valamint a ViewModel-ben kerül tárolásra az éppen aktuális Euro értéke.
A gyűjtemény 3 termékkel kerül feltöltésre a konstruktorban:
class ViewModel : INotifyPropertyChanged
{
 private ObservableCollection<Model> _myModel;
 public ObservableCollection<Model> MyModel
 {
  get { return _myModel; }
  set { _myModel = value; OnPropertyChanged("MyModel"); }
 }

 private decimal _euroValue;
 public decimal EuroValue
 {
  get { return _euroValue; }
  set { _euroValue = value; OnPropertyChanged("EuroValue"); }
 }

 public event PropertyChangedEventHandler PropertyChanged;
 public void OnPropertyChanged(string propertyName)
 {
  if (PropertyChanged != null)
   PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
 }

 public ViewModel()
 {
  MyModel = new ObservableCollection<Model>
   {
    new Model("Product1", 10000),
    new Model("Product2", 55891),
    new Model("Product3", 158689)
   };

  EuroValue = 305.23m;
 }
}

A View kódja a már megszokott egyszerűséget hozza
public partial class MainWindow : Window
{
 private readonly ViewModel _viewModel;

 public MainWindow()
 {
  InitializeComponent();
  _viewModel = new ViewModel();
  DataContext = _viewModel;
 }
}

A View-ban megjlenítésre kerül egy lista, amiben az egyes termékek nevei illetve az éves bevételük látható. A Lista megjelenítése a Grid.Resource-ba lévő DataTemplate-ben található.

A View-ban található továbbá az éppen aktuális Euro bevitelére szolgáló input mező, valamint legvégül az átváltott összeg, ami jelen esetben egyenlőre egy nagy kérdőjel.

<Grid>
 <Grid.Resources>
  <DataTemplate x:Key="MyListTemplate">
   <StackPanel Orientation="Horizontal">
    <TextBlock Width="100" Text="{Binding ProductName}"/>
    <TextBlock Width="100" Text="{Binding Income}"/>
   </StackPanel>
  </DataTemplate>
 </Grid.Resources>
 
 <StackPanel Orientation="Vertical">
 
  <ListBox ItemsSource="{Binding MyModel}" ItemTemplate="{StaticResource MyListTemplate}"/>
  
  <StackPanel Orientation="Horizontal">
   <TextBlock Text="Euro: "/>
   <TextBox Text="{Binding EuroValue}" Width="100"/>
  </StackPanel>

  <StackPanel Orientation="Horizontal">
   <TextBlock Text="Teljes bevétel Euroban: "/>
   <TextBlock Text="?"/>
  </StackPanel>

 </StackPanel>
</Grid>

A nyilvánvaló  nagy kérdés az, hogy mi kerül a kérdőjel helyére? A feladat egyértlemű. Add össze a bevételeket majd a kapott értéket az aktuális Euro értékével osztani kell.

Ehhez két input paraméter kell, amelyek a ViewModel osztályban találhatóak.
A gyűjtemény, amely tartalmazza az összes terméket (MyModel)
Valamint az Euro aktuális értéke. (EuroValue)

A fenti számítás egy külön osztályban kerül kiértékelésre.
class EuroConverter : IMultiValueConverter
{
 public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
 {
  if (values != null && values.Length == 2)
  {
   var collection = values[0] as ObservableCollection<Model>;
   var euroValue = (decimal)values[1];

   var totalIncome = collection.Sum(model => model.Income);
   var incomeInEuro = totalIncome/euroValue;
   return incomeInEuro.ToString();
  }
  return Binding.DoNothing;
 }

 // ********************************************************************

 public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
 {
  return null;
 }

}

Az osztálynak muszáj megvalósítania az IMultiValueConverter interface-t.
A számításhoz fontos műveletek a Convert metódusban találhatóak.
A bejövő pareméter immáron egy tömb (object[] values) amely tartalmazza a gyűjteményt, valamint az Euro értékét.
A kódot közelebbről megvizsgálva látható, hogy a tömb eleminek feltöltésekor ügyelni kell a sorrendre. Jelen esetben  a program elvárja, hogy a gyűjtemény legyen a tömb első és az Euro értéke a tömb második eleme.

Az összeg kiszámítása után azt stringként vissza kell adni a TextBlock részére.

A View több ponton is változik.
Be kell vezetni egy új namespace-t, ami attól függ, hogy a projekt (de leginkább az EuroConverter osztály) milyen namespace alatt található.
az új sor így kell, hogy kinézzen:
xmlns:multiConverter="clr-namespace:A TE NAMESPACE ELNEVEZÉSED"

A Grid.Resource is bővítésre szorul, végső formája az alábbiakban található
<Grid.Resources>
 <DataTemplate x:Key="MyListTemplate">
  <StackPanel Orientation="Horizontal">
   <TextBlock Width="100" Text="{Binding ProductName}"/>
   <TextBlock Width="100" Text="{Binding Income}"/>
  </StackPanel>
 </DataTemplate>
 
 <multiConverter:EuroConverter x:Key="EuroMultiConverter"/>
</Grid.Resources>

A Konverterre a Grid-en belül a továbbiakban az EuroMultiConverter névvel lehet hivatkozni.

Legvégül maga az adatkötés:
<StackPanel Orientation="Horizontal">
 <TextBlock Text="Teljes bevétel Euroban: "/>
 <TextBlock Width="100">
  <TextBlock.Text>
   <MultiBinding Converter="{StaticResource EuroMultiConverter}">
    <Binding Path="MyModel"/>
    <Binding Path="EuroValue"/>
   </MultiBinding>
  </TextBlock.Text>
 </TextBlock>
</StackPanel>

Mivel a végső érték több bemeneti érték kiszámításával kerül kiértékelésre, ezért MultiBinding adatkapcsolást kell végrehajtani. A konverter az előbb meghatározott  EuroMultiConverter.

A paraméterek pedig abban a sorrendben kerülnek átadásra, ahogy azt az EuroConverter osztály elvárja. Az első a gyűjtemény, a második az Euro értéke.

A végső View az alábbi:
<Window x:Class="Wpf07_MuliBinding.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:multiConverter="clr-namespace:Wpf07_MuliBinding"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.Resources>
            <DataTemplate x:Key="MyListTemplate">
                <StackPanel Orientation="Horizontal">
                    <TextBlock Width="100" Text="{Binding ProductName}"/>
                    <TextBlock Width="100" Text="{Binding Income}"/>
                </StackPanel>
            </DataTemplate>
            
            <multiConverter:EuroConverter x:Key="EuroMultiConverter"/>
        </Grid.Resources>
        
        <StackPanel Orientation="Vertical">
            <ListBox ItemsSource="{Binding MyModel}" ItemTemplate="{StaticResource MyListTemplate}"/>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="Euro: "/>
                <TextBox Text="{Binding EuroValue, UpdateSourceTrigger=PropertyChanged}" Width="100"/>
            </StackPanel>

            <StackPanel Orientation="Horizontal">
                <TextBlock Text="Teljes bevétel Euroban: "/>
                <TextBlock Width="100">
                    <TextBlock.Text>
                        <MultiBinding Converter="{StaticResource EuroMultiConverter}">
          <Binding Path="MyModel"/>
          <Binding Path="EuroValue"/>
         </MultiBinding>
                    </TextBlock.Text>
                </TextBlock>
            </StackPanel>

        </StackPanel>
    </Grid>
</Window>


Az Euro érték megadására szolgáló TextBox adatkapcsolásának frissítését átállítottam (UpdateSourceTrigger=PropertyChanged), ezáltal abban a pillanatban kiszámításra kerül az új érték, ahogy egy billentyű lenyomásra kerül.

2013. április 8., hétfő

HLSL #1 Alapok

Egy tetszőleges grafika megjelenítése a képernyőn a mai programokkal szinte gyerekjáték. Legyen szó egy képről, vektorgrafikáról vagy 3D modellről, keretrendszerek tucatjai állnak rendelkezésre, amelyek a probléma nagy részének a megoldását a háttérben elvégzik.

Mindazonáltal, ha valami speciálisat - és főleg egyedit - kell a képernyőre kivarázsolni, akkor a legtöbb esetben még mindig a hardver alapszintjére kell visszanyúlni. A shader programok segítségével, és némi fantáziával egyszerű elemekből felépített grafikát is fantasztikusan fel lehet turbózni. Ehhez persze türelem, és rengeteg tapasztalat kell, mint minden máshoz.

A blogban található shader-ek elkészítéséhez az nvdia ingyenesen letölthető fxcomposer programjának 2.5-ös verziója ajánlott, de bármely más shader editor is tökéletesen megfelel. A Composer használatához érdemes rögtön a manual-t is letölteni.

Installálás után első lépésben hozz létre egy új projektet (File/New).
A jelenleg teljesen üres projekthez minimum kell egy shader és egy modell.
Az első shader létrehozása: Create/Effect
A megjelenő menüben válaszd a HLSL FX-et, majd a Next gomb megnyomásával már előre beállított effektek közül lehet válogatni.
Egyenlőre válaszd a "empty" lehetőséget. File névnek tetszőlegesen találj ki valamit, pl: "01_basic.fx", majd kattints a next gombra.
A megjelenő panelen láthatod a létrehozásra kerülő effect nevét. Ezzel el is készült a shader alapja.

Alapbeállítás esetén a bal felső "Materials" ablakban egy fehér kör látható. Ha mégsem, akkor a View/Materials ki/be kapcsolásával ez tetszőlegesen állíthatod.

A modell hozzáadás lényegesen egyszerűbb. Create/Plane
A jobb alsó sarokban, a "Render" ablakban láthatónak kell lennie az imént hozzáadott plane.

A fehér kört drag-drop használatával húzd rá a plane-re, aminek hatására az fehérre változik. Ezzel a plane mostantól ezt a shadert használja, bármi változtatás esetén a plane követni fogja azt.

A shader kód középen látható, a kommentek kiszedése után így kell kinéznie:
float4x4 WorldViewProj : WorldViewProjection;

float4 mainVS(float3 pos : POSITION) : POSITION{
 return mul(float4(pos.xyz, 1.0), WorldViewProj);
}

float4 mainPS() : COLOR {
 return float4(1.0, 1.0, 1.0, 1.0);
}

technique technique0 {
 pass p0 {
  CullMode = None;
  VertexShader = compile vs_3_0 mainVS();
  PixelShader = compile ps_3_0 mainPS();
 }
}

A fenti kód tartalmaz mindent, ami egy shader minimális működéséhez kell.

A modell térbeli elhelyezését, transzformációit pontosan tudnia kell a programnak, erre szolgál a
WorldViewProjection. Ez tulajdonképp 3 mátrix szorzata.
WorldViewProjection = World * View * Projection
A World határozza meg a modell eltolását, elforgatását, nagyítását.
A View mondja meg, hogy kamera hol található a térben és épp merre néz
A Projection határozza meg a látószöget, a torzítás valamint a legközelebbi és a legtávolabbi látható távolságot.

Ezt a paramétert az FXComposer automatikusan adja a shader részére. A Saját programodban ezt neked kell kiszámolnod. Ahhoz, hogy az FXComposer tudja, hogy neki ezt a paramétert adni kell, a programban ez muszáj jelezni.
float4x4 WorldViewProj : WorldViewProjection;
A float 4x4 egy mátrix-ot definiál, a WoldViewProj pedig tetszőleges változó elnevezés
Az ezt követő kettőspont és a WolrdViewProjection úgynevezett SEMANTICS.
Ez határozza meg, hogy egy adott változó/paraméter milyen koncepció alapján kerül felhasználásra.
Jelen esetben a composer ebből tudja, hogy ennek a változónak kell átadni a már előre kiszámított mátrixot.
Fontos megjegyezni, hogy ez a SEMANTICS csak az fxcomposer számára érthető.
Ha ez most elsőre nem világos, ne ragadj le itt, a legtöbb példában  elegendő lesz, hogy ott van. Ettől függetlenül érdemes a neten pontosan utánaolvasni.

A shader két függvényből áll.
VertexShader: amely minden számítást egyszer végez el a modell összes vertexén.
PixelShader: amely minden pixelen, ahol  a model látható lefut.
Ebből következik, hogy mindaz, amit ki lehet számolni a VertexShader-ben, célszerű ott megtenni, hiszen az jóval kevesebbszer fog valószínűleg lefutni, mint a pixelshader.

A VertexShader a modell pozícióját várja input paraméterként, amit a POSTION SEMATICS jelöl.
float3 pos : POSITION
És a shader kimenete a model egy vertexének a pozíciója. Ezt szintén a POSTION SEMATICS jelöli.
float4 mainVS(float3 pos : POSITION) : POSITION
A függvény kiszámítja a vertex pontos helyzetét, majd átadja PixelShader-nek.

A PixelShader visszatérési értéke az a szín, amit a képernyőn meg kell jeleníteni. Ez a COLOR SEMATICS jelöli
float4 mainPS() : COLOR  
A függvény pedig jelen esetben egy teljesen fehér értékkel tér vissza.
return float4(1.0, 1.0, 1.0, 1.0);
Az RGB értékek 0-1 között tetszőlegesek lehetnek. Az utolsó paraméter az Alpha áttetszőség értéke.
Ezek a paraméter értékek tetszőlegesen változtathatóak.
pl: return float4(1.0, 0.0, 0.0, 1.0);
Ebben az esetben a plane piros színű lesz.
Minden változtatás után a programot le kell fordítani. Addig nem látható a hatása. Erre szolgál a Compille button vagy a Build/Compille utasítás.

Megjegyzés: Az utolsó paraméter (Alpha) értéke szintén tetszőleges lehet, de ez jelen esetben nem befolyásolja a plane áttetszőségét. Ahhoz, hogy ez megtörténjen további beállításokra van szükség.

A shader következő eleme a technique.
Egy shader tetszőleges számú technique-t tartalmazhat. Ez határozza meg a shader lefutáshoz szükséges opciókat. Pl. az áttetszőséget is itt lehet beállítani.

Minden techniqe tetszőleges számú pass elemet tartalmaz.
A pass határozza meg, hogy melyik VertexShader és melyik PixelShader függvényeket kell lefuttatni.

A CullMode = None; hatására a kirajzolandó plane minden irányból látható.
A plane kirajzolása vagy óramutató járásával megegyezik vagy ellentétes.
A render ablakban az ALT+bal egérgomb használatával lehet az aktuális nézetet változtatni.
Jelen esetben a plane mindkét oldala látható (no culling)
A CullMode felveheti a "CW" és a "CCW" értékeket.
CullMode = CW; (ne felejtsd el lefordítani a shadert)
Ebben az esetben a plane egyik irányból nem látható.
Valós helyzetben célszerű CW vagy CCW értékre állítani, mert egy adott polygon nagy valószínűséggel úgyis csak az egyik irányból lesz látható.

A megjelenítendő szín kódból történő változtatása elég lassú folyamat. Amennyiben a plane-t zöld színnel szeretnénk kirajzolni, a megfelelő sort át kéne írni, majd újrafordítani az egész kódot.
Ennek egyszerűsítésére az alábbi változtatások szükségesek.
Add hozzá a következő sorokat a program elejéhez.
float4 customColor :Color
<
 string UIName = "Diffuse Color";
> = {1.0, 1.0, 0.0, 1.0};

Illetve a PixelShader visszatérési értékét írd át az alábbira, majd fordítsd le:
return customColor;

A Jobb felső sarokban (default nézet) látható a properties ablak.
Az ablakban immáron látható egy Diffuse Color elem, ami egy színkiválasztót is tartalmaz. Válassz ki egy tetszőleges színt. Látható, hogy a render ablakban a plane realtime követi.

A Color SEMATICS jelzi az fxcomposer számára, hogy ez a változó egy színt tartalmaz.
A kacsacsőrökben elhelyezett string UIName = "Diffuse Color";  úgynevezett Annotation.
Ez szintén csak az fxComposer számára fontos. Jelen esetben megmondja, hogy a User Interface Panelen milyen névvel szerepeljen ez a változó.
A későbbiekben még számos Annotation használatára lesz példa, egyenlőre bőven elég, hogy a változót értelmesen olvasható névvel el lehet látni.

A leírásban található idegen szavak - főleg a sematics és az annotation - bővebb leírására érdemes a neten rákeresni. Gyakorlásképpen hozz létre még egy Color változót és a PixelShader visszatérési értéke legyen a kettőnek az összege.

Az alábbiakban látható a megoldás:
float4x4 WorldViewProj : WorldViewProjection;

float4 customColor :Color
<
 string UIName = "Diffuse Color";
> = {1.0, 1.0, 0.0, 1.0};

float4 newColor :Color
<
 string UIName = "Another Color";
> = {1.0, 1.0, 0.0, 1.0};

float4 mainVS(float3 pos : POSITION) : POSITION{
 return mul(float4(pos.xyz, 1.0), WorldViewProj);
}

float4 mainPS() : COLOR {
 return customColor + newColor;
}

technique technique0 {
 pass p0 {
  CullMode = none;
  VertexShader = compile vs_3_0 mainVS();
  PixelShader = compile ps_3_0 mainPS();
 }
}

2013. április 4., csütörtök

Wpf #6 Konverter

Az eddig megismertek felhasználásával már számos Model osztályt fel lehet építeni, ám rengeteg olyan WPF tulajdonság van, amit nem biztos, hogy célszerű az osztályban ugyanolyan típusúra deklarálni.

A legegyszerűbb talán egy adott elem Visibility tulajdonsága.
pl.: <Rectangle Visibility="Hidden"/>
A Visibility 3 értéket vehet fel: Hidden, Visible, Collapsed

Amennyiben  kódból kell az értékét változtatni úgy az alábbiak szerint kell eljárni:
MyRectangle.Visibility = Visibility.Hidden;

A Most következő példában a "Collapsed" érték mellőzésre kerül. A láthatóságot egy bool tulajdonság segítségével lehet szabályozni. (true=Visible, false=Hidden)

Ehhez egy nagyon egyszerű Model osztályra lesz szükség:
class Model : INotifyPropertyChanged
{

 private bool _rectVisibility;
 public bool RectVisibility
 {
  get { return _rectVisibility; }
  set { _rectVisibility = value; OnPropertyChanged("RectVisibility"); }
 }

 // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

 public event PropertyChangedEventHandler PropertyChanged;
 public void OnPropertyChanged(string propertyName)
 {
  if (PropertyChanged != null)
   PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
 }

 // ---------------------------------------------------------------------

 public Model(bool visibility)
 {
  RectVisibility = visibility;
 }


}

A Láthatóságot a konstruktoron keresztül célszerű alaphelyzetbe hozni.

A hozzá tartozó ViewModel is pont ugyanennyire egyszerű
class ViewModel : INotifyPropertyChanged
{
 private Model _myModel;
 public Model MyModel
 {
  get { return _myModel; }
  set { _myModel = value; OnPropertyChanged("MyModel"); }
 }

 public event PropertyChangedEventHandler PropertyChanged;
 public void OnPropertyChanged(string propertyName)
 {
  if (PropertyChanged != null)
   PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
 }

 public ViewModel()
 {
  MyModel = new Model(true);
 }
}

A View kód:
public partial class MainWindow : Window
{
 private readonly ViewModel _viewModel;
 public MainWindow()
 {
  InitializeComponent();
  _viewModel = new ViewModel();
  DataContext = _viewModel;
 }
}

Legvégül az alap XAML
<Window x:Class="Wpf06_Converter.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <StackPanel Orientation="Horizontal" Height="30">
            <Rectangle  Width="100" Height="30" Fill="Red"/>
        </StackPanel>
    </Grid>
</Window>


Az egyenlőre egyetlen darab piros négyzet kirajzolására szolágló kód hamarosan  kibővül számos új elemmel, amelyek mind-mind a négyzet láthatóságát fogják szabályozni.

Jelen pillanatban van egy Model osztály, amelynek egyetlen bool típusú publikus tulajdonsága van (RectVisibility). Ezt kell valamilyen módon összepárosítani a Rectangle Visibility tulajdonságával.

Ahhoz, hogy mindezt elérjük, egy konverter osztályra lesz szükség. Ez az osztályt egy külön DLL-ben lesz elhelyezve (ez nem szükséges).

Hozzunk létre új Class Library project-et. A neve legyen: Converter.
Töröld ki az automatikusan létrehozott class file-t, és adj a projekt-hez egy új osztályt VisibilityConverter néven.
A Library-hez az alábbi referenciákat kell hozzáadni (jobb klikk a References lenyíló menün, Add refference): PresentationCore, PresentationFrameWork

Az osztály kódja:
namespace Converter
{
 public class VisibilityConverter : IValueConverter
 {
  // ** Model konvertálása az UI felé
  public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
  {
   if ((bool)value)
    return Visibility.Visible;
   else
    return Visibility.Hidden;
  }

  // ** UI konvertálása a Model felé
  public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
  {
   return value;
  }
 }
}

A fenti namespace és osztály elnevezések pontosan egyezzenek! Természetesen a későbbiekben tetszőleges elnevezést lehet használni, de a példa futtatásához ez most nélkülözhetetlen.

Mivel az osztály másik szerelvényben található ezért az osztály kötelezően publikus, hogy a későbbiekben is elérhető maradjon.
Az osztály használatához szükség van az IValueConverter interface implementálására. Ehhez szükség van a System.Windows.Data namespace-re.
using System.Windows.Data;

Az interface két metódus meglétét követeli meg. A tényleges konvertálás ezeken keresztül történik.
public object Convert
public object ConvertBack

A Convert metódus konvertál minden a Model osztályban deklarált tulajdonságot olyan formára, ahogy azt az UI elvárja.
Jelen esetben egy bool tulajdonság a bement, és az alapján dől el a visszatérési érték, amely már feldolgozható a Rectangle Visibility tulajdonsága számára.

A WPF projekthez referenciaként hozzá kell adni az előbb létrehozott Converter szerelvényt.
A hozzáadás után érdemes egy fordítást végezni a kódon, mivel a Wpf számtalanszor nem találja meg elsőre a beimportált elemeket.

Az XAML kód pedig az alábbiak szerint változik.
Egy új xml namespace deklarációval a konvertert deklarálni kell.
xmlns:converter="clr-namespace:Converter;assembly=Converter"
Ezért volt fontos, hogy az namespace és osztály nevek egyezzenek a példában leírtakkal.
Ez a sor megmondja, hogy melyik DLL és melyik namespace kell a kód működéséhez a továbbiakban.

Következő lépésként a Resource-ban definiálni kell a konvertert.
<Grid.Resources>
            <converter:VisibilityConverter x:Key="RectVisibilityConverter"/>
</Grid.Resources>

A converter(az elöbb lett definiálva) a Converter namespace alatt található VisibilityConverter osztályra mutat, az x:Key pedig egy tetszőlegesen elnevezett hivatkozási nevet definiál.

A Rectangle  az alábbiak szerint változik.
<Rectangle  Width="100" Height="30" Fill="Red" Visibility="{Binding MyModel.RectVisibility, Mode=OneWay, Converter={StaticResource RectVisibilityConverter}}"/>

Az adatkapcsolás a Model osztály RectVisibility tulajdonságát használja.

A Mode=OneWay utasítja a keretrendszert, hogy az adatkapcsolás csak egyirányú, azaz csak a Model osztály irányából a Rectangle felé. (az ezt megelőzően egy textboxba írt szöveg tartalmát a framework bemásolta a Model osztály tulajdonságába)

A Converter a Grid.Resource-ban definiált konverterre mutat, ami a bool értéket alakítja át olyan típusra,  amit a Rectangle Visibility tulajdonsága értelmezni tud.

Az XAML jelenleg itt tart:
<Window x:Class="Wpf06_Converter.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:converter="clr-namespace:Converter;assembly=Converter"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.Resources>
            <converter:VisibilityConverter x:Key="RectVisibilityConverter"/>
        </Grid.Resources>
        
        <StackPanel Orientation="Horizontal" Height="30">
            <Rectangle  Width="100" Height="30" Fill="Red" Visibility="{Binding MyModel.RectVisibility, Mode=OneWay, Converter={StaticResource RectVisibilityConverter}}"/>
        </StackPanel>
    </Grid>
</Window>


Lefordítva és lefuttatva a négyzet továbbra is látható.  A ViewModel osztály konstruktorában egyetlen sor található: MyModel = new Model(true);
Amennyiben a true értékét false-ra cseréljük a program futtatásakor a négyzet láthatatlan lesz.

Egészítsük ki a XAML kódot egy CheckBox-al.
<CheckBox  Content="Rect visibility" IsChecked="{Binding MyModel.RectVisibility, Mode=TwoWay}"/>
Mivel az IsChecked bool típust vár ezért nincs szükség konverter használatára.
A Mode=TwoWay (default) miatt a CheckBox változtatásával a Model osztály RectVisibility tulajdonsága is változik.

<Window x:Class="Wpf06_Converter.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:converter="clr-namespace:Converter;assembly=Converter"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.Resources>
            <converter:VisibilityConverter x:Key="RectVisibilityConverter"/>
        </Grid.Resources>
        
        <StackPanel Orientation="Horizontal" Height="30">
            <Rectangle  Width="100" Height="30" Fill="Red" Visibility="{Binding MyModel.RectVisibility, Mode=OneWay, Converter={StaticResource RectVisibilityConverter}}"/>
            <CheckBox Content="Rect visibility" IsChecked="{Binding MyModel.RectVisibility, Mode=TwoWay}"/>
        </StackPanel>
    </Grid>
</Window>


Az eddig megismertek segítségével tetszőleges tulajdonságot lehet bármilyen más típusra alakítani.
A következőkben a User Interface-n történt változtatást fogjuk a Model osztály számára feldolgozni.

A VisibilityConverter osztály ConvertBack metódusát az alábbiak szerint kell megváltoztatni.
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
 var sValue = value as string;
 if (sValue != null)
 {
  if (sValue == "igen")
   return true;

  if (sValue == "nem")
   return false;
 }
 return value;
}

Az XAML-ben pedig egy TextBox-ot kell elhelyezni, amely bemenetként az igen vagy a nem szavakat várja.
<TextBox Text="{Binding MyModel.RectVisibility, Mode=OneWayToSource, Converter={StaticResource RectVisibilityConverter}}" Width="50"/>

A TextBox szintén a Model osztály RectVisibility tulajdonságát használja adatkötésre.

A Mode=OneWayToSource beállítással elérhető, hogy a TextBox tartalmának változásával a Model osztály RectVisibility tulajdonsága megváltozzon, de ha a tulajdonság bárhol máshol megváltozna, azt a TextBox nem követi.
Fontos, hogy ez a VisulStudio szerkesztőjében NullReferenceException-t okozhat, de lefuttatva működni fog. Ha zavaró a hibaüzenet, a beállítás nyugodtan törlésre kerülhet.

A TextBox konvertálásra ugyanazt az osztályt használja mint a Rectangle, csak most visszafelé kell konvertálni.

A végleges XAML kód:
<Window x:Class="Wpf06_Converter.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:converter="clr-namespace:Converter;assembly=Converter"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.Resources>
            <converter:VisibilityConverter x:Key="RectVisibilityConverter"/>
        </Grid.Resources>


        <StackPanel Orientation="Horizontal" Height="30">
            <Rectangle  Width="100" Height="30" Fill="Red" Visibility="{Binding MyModel.RectVisibility, Mode=OneWay, Converter={StaticResource RectVisibilityConverter}}"/>
            <CheckBox Content="Rect visibility" IsChecked="{Binding MyModel.RectVisibility, Mode=TwoWay}"/>

            <TextBlock Margin="20,0,0,0" Text="Látható(ird be igen vagy nem):"/>
            <TextBox Text="{Binding MyModel.RectVisibility, Mode=OneWayToSource, Converter={StaticResource RectVisibilityConverter}}" Width="50"/>
            <Button Content="click"/>
        </StackPanel>
    </Grid>
</Window>


A szövegmező után elhelyezett gomb azért kell, mert a tulajdonság frissítése csak a fókusz elvesztése után történik meg.

Ebben a példában egyetlen tulajdonságot 3 különböző elemhez kapcsoltunk hozzá különböző módon, követve, hogy az adatkötés ténylegesen csak abba az irányba történjen,amelyre feltétlenül szükség van.
A koverter természetesen számos más célra is felhasználható, de a lényege ugyanaz marad. Két tulajdonság között értelmezhető kapcsolat  keletkezzen.

2013. április 3., szerda

Wpf #5 Adatellenőrzés

Az előző blogbejegyzéseimet követve már könnyedén felépíthető egy alap MVVM struktúra, amely segítségével egyszerű adatkapcsolással könnyíthető meg a mindennapi programfejlesztés.

A Model-ben tárolt publikus tulajdonságok könnyedén jeleníthetőek meg a View XAML-ben, illetve a felhasználó által megkövetelt változtatások a háttérben automatikusan megtörténnek.

Egy TextBox-ba begépelt tetszőleges szöveg azonnal elérhető a hozzá kapcsolt tulajdonságban.

Mivel az adatkapcsolás publikus tulajdonságokon keresztül történik, ezért annak a "Set" kódjában az értékét ellenőrizni is lehet, majd annak függvényében beállítani az új értéket. Pl. tételezzük fel, hogy az általunk elvárt érték nem lehet 18-nál kissebb.

private int _someValue;
public int SomeValue
{
 get { return _someValue; }
 set
 {
  _someValue = value < 18 ? 18 : value;
  OnPropertyChanged("SomeValue");
 }
} 

Noha a fenti kód lefut, elég otromba módon próbálja a felhasználó figyelmét felhívni, hogy az általa megadott érték nem megfelelő. Mindez kibővíthető egy MessageBox használatával, ami figyelmezteti, hogy rossz értéket adott meg, de  hosszútávon a felugráló üzenetek elég bosszantóak

Mindezen problémák egy új Interface bevezetésével megoldhatóak.
Ennek neve: IDataErrorInfo

Legyen adott egy Model osztály, amely egy életkort(Age) és egy becenevet(NickName) tartalmaz.
Az életkor nem lehet 18-nál kisebb, a becenév pedig nem lehet üres. 
Az osztály ezek alapján a következőképp néz ki:
class Model : INotifyPropertyChanged, IDataErrorInfo
{

 private int _age;
 public int Age
 {
  get { return _age; }
  set { _age = value; OnPropertyChanged("Age"); }
 }

 private string _nickName;
 public string NickName
 {
  get { return _nickName; }
  set { _nickName = value; OnPropertyChanged("NickName"); }
 }



 // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

 public event PropertyChangedEventHandler PropertyChanged;
 public void OnPropertyChanged(string propertyName)
 {
  if (PropertyChanged != null)
   PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
 }

 // ---------------------------------------------------------------------

 public Model()
 {
  Age = 0;
  NickName = "noName";
 }

 public string this[string columnName]
 {
  get
  {
   string error = string.Empty;

   if (columnName == "Age" && Age <18)
     error = "Csak 18 felett";

   if (columnName == "NickName" && NickName == string.Empty)
    error = "Nem adtál meg nevet";

   return error;
  }
 }

 // *******************************************************************


 public string Error
 {
  get { return null; }
 }
}


Az IDataErrorInfo is a System.ComponentModel namespace alatt található.
Az első és legfontosabb része a kódnak, hogy ún. Class Indexer-t használ: public string this[string columnName]
Az indexer használatáról bővebben a Microsoft oldalán olvashatsz.

A rendszer bejövő paraméterként a felhasználó által épp módosított tulajdonság nevét adja át (columnName). Mivel az osztályban csak 2 tulajdonság van ezért ez vagy az Age vagy a NickName.
Ez egy egyszerű vizsgálattal eldönthető, és a további vizsgálatok már ennek függvényében könnyen elvégezhetőek. Amennyiben a visszatérési érték nem üres string, a Wpf tudni fogja, hogy valami hiba keletkezett.

A ViewModel ez esetben elég egyszerűen néz ki, mindössze egy referenciát tartalmaz a Model-re:
class ViewModel : INotifyPropertyChanged
{
 private Model _myModel;
 public Model MyModel
 {
  get { return _myModel; }
  set { _myModel = value; OnPropertyChanged("MyModel"); }
 }

 public event PropertyChangedEventHandler PropertyChanged;
 public void OnPropertyChanged(string propertyName)
 {
  if (PropertyChanged != null)
   PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
 }

 public ViewModel()
 {
  MyModel = new Model();
 }
}

A View kód se bonyolultabb, egy referenciával a ViewModel-re:
public partial class MainWindow : Window
{
 private ViewModel _viewModel;

 public MainWindow()
 {
  _viewModel = new ViewModel();
  InitializeComponent();

  DataContext = _viewModel;
 }
}

Az XAML alap adatkötése pedig így néz ki:
<Grid>
 <StackPanel Orientation="Vertical">
  <StackPanel Orientation="Horizontal">
   <TextBlock Text="Age:"/>
   <TextBox Width="100" Text="{Binding MyModel.Age}"/>
  </StackPanel>

  <StackPanel Orientation="Horizontal">
   <TextBlock Text="NickName:"/>
   <TextBox Width="100" Text="{Binding MyModel.NickName}"/>
  </StackPanel>
 </StackPanel>
</Grid>

Egyszerű adatkötéssel megjelenítésre kerül az Age és a NickName tulajdonságok értéke, ami a Model osztály konstruktorában  kapott értéket. Az adatok értéke már szabadon változtatható, ám az ellenőrzés még nem történik meg. Ahhoz, hogy az ellenőrző kód lefusson további paraméterek megadása szükséges.

Jelen esetben az adatkötés így néz ki: Text="{Binding MyModel.Age}"
Ezt kell kiegészíteni: Text="{Binding MyModel.Age, UpdateSourceTrigger=LostFocus, ValidatesOnDataErrors=true, NotifyOnValidationError=true}"

Az UpdateSourceTrigger határozza meg, hogy az ellenőrzés mikor történjen meg. A LostFocus érték azt jelenti, hogy az adat ellenőrzés mindaddig ne történjen meg, amíg a felhasználó a fókuszt nem helyezi máshová (pl. kattint egy másik textbox-ba)
Ha az értéke PropertyChanged, akkor minden egyes billentyű lenyomáskor vizsgálni fogja, hogy a felhasználó által megadott érték megfelelő-e.
Módosítsuk a XAML kódot ennek ismeretében. Az Age csak akkor kerüljön vizsgálat alá, ha a felhasználó elhagyja a textbox-ot, míg a NickName értéke minden billentyű lenyomásakor kerüljön ellenőrzésre.
<Grid>
 <StackPanel Orientation="Vertical">
  <StackPanel Orientation="Horizontal">
   <TextBlock Text="Age:"/>
   <TextBox Width="100" Text="{Binding MyModel.Age, UpdateSourceTrigger=LostFocus, ValidatesOnDataErrors=true, NotifyOnValidationError=true}"/>
  </StackPanel>

  <StackPanel Orientation="Horizontal">
   <TextBlock Text="NickName:"/>
   <TextBox Width="100" Text="{Binding MyModel.NickName, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=true, NotifyOnValidationError=true}"/>
  </StackPanel>
 </StackPanel>
</Grid>

Írjunk be az életkornak 18-nál kisebb értéket, illetve a becenevet töröljük ki teljesen. A TextBox körvonala pirosra vált, jelezve, hogy hiba van. Sajnos a felhasználó ebből nem fogja tudni, hogy a program mit is vár tőle pontosan. Valamilyen módon figyelmeztetni kell, hogy mi a hiba oka pontosan.

A most következő kód részt megpróbáltam a lehető legegyszerűbbre venni, mert stílusokat és triggereket tartalmaz. Első lépésként egy rövid bevezetést mutatnék a stílusok világába.
A stílusokat az adott elem Resource elemében kell elhelyezni. Ebből következik, hogy hatásköre csak az adott objektum hatásköréig terjed.

Mivel az adott példában minden elem egy Grid-ben helyezkedik el, azért az általunk megkívánt stílusokat helyezzük a Grid.Resource-ba.
A program futtatásakor a két TextBox nincs egymás alatt, ami elég zavaró, ezért az őket megelőző TextBlock-ok szélességének az értéke legyen 100px.
Ehhez az alábbi stílust kell létrehozni:
<Style TargetType="{x:Type TextBlock}" x:Key="CustomTextBlock">
 <Setter Property="Width" Value="100"/>
</Style>

A TargetType meghatározza, hogy a stílust milyen elemhez köthetjük. Jelen esetben TextBlock. A Key adja a stílus nevét, erre kell majd hivatkozni.
Egy stíluson belül tetszőleges számú setter helyezhető el. Itt kell meghatározni, hogy az adott elem melyik tulajdonságának milyen értéket kell adni. 
A fenti stílus egyenértékű ezzel: <TextBlock Width="100"/>

A stílus definiálása után jelezni kell, hogy az adott TextBlock használni is kívánja azt.
<TextBlock Text="Age:" Style="{StaticResource CustomTextBlock}"/>
A StatickResource segítségével tudja a keretrendszer, hogy egy általunk Resource-ban definiát sítlust keressen. A CustomTextBlock a stílus neve, ami definiálva lett.

Ennek ismeretében a XAML kód az alábbi:
<Grid>
 <Grid.Resources>
  <Style TargetType="{x:Type TextBlock}" x:Key="CustomTextBlock">
   <Setter Property="Width" Value="100"/>
  </Style>
 </Grid.Resources>

 <StackPanel Orientation="Vertical">
  <StackPanel Orientation="Horizontal">
   <TextBlock Text="Age:" Style="{StaticResource CustomTextBlock}"/>
   <TextBox Width="100" Text="{Binding MyModel.Age, UpdateSourceTrigger=LostFocus, ValidatesOnDataErrors=true, NotifyOnValidationError=true}"/>
  </StackPanel>

  <StackPanel Orientation="Horizontal">
   <TextBlock Text="NickName:" Style="{StaticResource CustomTextBlock}"/>
   <TextBox Width="100" Text="{Binding MyModel.NickName, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=true, NotifyOnValidationError=true}"/>
  </StackPanel>

 </StackPanel>
</Grid>

Következő lépésként a TextBox ToolTip attributumát kell beállítani a keletkezett hiba szövegére.
Ezáltal ha a TextBox kerete pirosra vált, a felhasználó egyszerűen felé viszi az egeret és máris láthatja a hiba okát.

Ehez a következő stílusra van szükség:
<Style TargetType="{x:Type TextBox}" x:Key="TextBoxValidationStyle">
 <Style.Triggers>
  <Trigger Property="Validation.HasError" Value="true">
   <Setter Property="ToolTip"
    Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
  </Trigger>
 </Style.Triggers>
</Style>

A stílusban található Trigger csak akkor állítja be a ToolTip értékét, ha hiba keletkezik. Egyébként törli(default).

A végső XAML kód:
<Grid>
 <Grid.Resources>
  <Style TargetType="{x:Type TextBlock}" x:Key="CustomTextBlock">
   <Setter Property="Width" Value="100"/>
  </Style>

  <Style TargetType="{x:Type TextBox}" x:Key="TextBoxValidationStyle">
   <Style.Triggers>
    <Trigger Property="Validation.HasError" Value="true">
     <Setter Property="ToolTip"
      Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
    </Trigger>
   </Style.Triggers>
  </Style>
 </Grid.Resources>

 <StackPanel Orientation="Vertical">
  <StackPanel Orientation="Horizontal">
   <TextBlock Text="Age:" Style="{StaticResource CustomTextBlock}"/>
   <TextBox Width="100" 
     Style="{StaticResource TextBoxValidationStyle}" 
     Text="{Binding MyModel.Age, UpdateSourceTrigger=LostFocus, ValidatesOnDataErrors=true, NotifyOnValidationError=true}"/>
  </StackPanel>

  <StackPanel Orientation="Horizontal">
   <TextBlock Text="NickName:" Style="{StaticResource CustomTextBlock}"/>
   <TextBox Width="100" 
     Style="{StaticResource TextBoxValidationStyle}" 
     Text="{Binding MyModel.NickName, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=true, NotifyOnValidationError=true}"/>
  </StackPanel>

 </StackPanel>
</Grid>

Lefuttatva az Age mezőbe amennyiben 18-nál kissebb érték kerül, akkor a Model-ben megadott szöveg kerül a ToolTip-be (az egérrel menj az Input fölé)
Ugyanígy a NickName mező esetében, amennyiben üres szintén láthatóvá válik a hiba oka.

2013. április 1., hétfő

Wpf #4 Command

Mivel az Mvvm felépítése megköveteli, hogy a View csak a ViewModell-re tartalmazzon referenciát ezért ha View-ból bármit műveletet kell a ViewModel-en végezni, akkor annak a referenciáját fel kell használni.

ViewModel _viewModel;
_viewModel.DoSomething();

Amennyiben egy a Model-ben levő metódust kell elérni, azt csak a ViewModel-en keresztül lehet megtenni. Mindez az alábbi folyamatot eredményezi

1. Az XAML-ban deklarált Button Click eseményét feldolgozza a View-ban egy eljárás.
2. A metódusból meghívásra kerül a ViewModel DoSomething() metódusa
3. Amely meghívja a Model DoSomething() metódusát

A WPF Command az első lépés teljes mértékű kihagyásával segít egyszerűsíteni ezt a folyamatot. 
Az WPF #4 bejegyzésben bemutatott kódot az alábbi osztállyal kell kiegészíteni.


A Command megköveteli, hogy az ICommand Interface-ból származtassuk.
Az Interface az alábbiakat követeli meg
- EventHandler CanExecuteChanged
- bool CanExecute
- void Execute

Az osztályban további tetszőleges metódusok, tagváltozók és tulajdonságok deklarálhatók.
Mivel a Button megnyomásakor valamilyen - a ViewModelben elhelyezett - eljárás lefutása megkívánt ezért egy CallBack Action magáért beszél, amelynek az értéke a Konstruktor meghívásakor kerül beállításra.
private readonly Action _executeMethod;

A CanExecute eljárás dönti el, hogy az adott Command lefuttatható-e. Amennyiben false értékkel tér vissza, a View-ban lévő Button inaktív állapotba kerül. (Ez könnyen kipróbálható, a true értéket false-ra kell cserélni)

Az Execute lefuttatja a Command-nak átadott CallBack metódust


A ViewModel az alábbiak szerint változik:

A ViewModel-ben deklarálásra kerül egy ICommand típusú tulajdonság, amelynek a konstruktorban - a létrhozásakor - átadásra kerül a CallBack függvény.

A gomb lenyomásakor Command osztály Execute metódusa ezt a függvényt hívja meg. Mivel ez a ViewModel-ben található, ezért annak minden elemét eléri.

A View-hoz egy Button kerül hozzáadásra. 
<Grid>
    <Grid.Resources>
        <DataTemplate x:Key="MyListTemplate" >
            <TextBlock Text="{Binding TextValue}" Height="15"/>
        </DataTemplate>
    </Grid.Resources>

    <ListBox  Height="150"
              ItemsSource="{Binding MyModelCollection}"
              ItemTemplate="{StaticResource MyListTemplate}">
    </ListBox>
  
    <button VerticalAlignment="Top" Width="100" Height="20" Content="Add new item" Command="{Binding Incrementcommand}"/>
</Grid>

A Button Command attributumához adatkötéssel csatolásra kerül a ViewModel-ben található publikus Incrementcommand tulajdonság.

A View kódja nem változik