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.

1 megjegyzés:

  1. Szuper jó cikkeket írsz, sokat segítenek ezek az apróságok. Sokkal szebb és felhasználóbarátabb megoldást mutattál be, mint amit eddig használtam! Köszönöm :)

    VálaszTörlés