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.

Nincsenek megjegyzések:

Megjegyzés küldése