Tehdään WPF-sovellus - 22 - Kantaluokka ViewModeleille
- 5 minsKantaluokka vastaa päivitysten ilmoittamisesta näkymälle
Näkymämme ei osaa päivittyä automaattisesti, ellemme kerro sille eksplisiittisesti, että “nyt tämä ja tämä tieto pitäisi päivittää”.
WPF:n näkymän bindaukset osaavat kuunnella INotifyPropertyChanged
-rajapinnan ilmoituksia propertyjen muutoksista. Emme halua kuitenkaan implementoida kyseistä rajapintaa jokaisella ViewModelillamme erikseen, joten luodaan tätä varten oma kantaluokka, josta muut ViewModelimme voivat periä rajapinnan toteutuksen automaattisesti.
Lisätään Solution-tasolle uusi kansio “MVVM” ja luodaan tänne uusi tiedosto/luokka ViewModel
namespace Kettunen.BMICalculator.WPFClient.MVVM
{
public abstract class ViewModel
{
}
}
Tehdään luokasta public ja abstrakti kantaluokka, koska haluamme aina käyttää ViewModelista jotain konkreettista toteutusta.
AViewModel
, vaikka tämä seuraisikin rajapintojen IRajapinta
-nimeämistyyliä. Jos projektissasi on muita tekijöitä mukana, niin on aina hyvä sopia yhteisistä pelisäännöistä tämmöisten asioiden suhteen. Lisätään INotifyPropertyChanged
-rajapinnan toteutus ViewModel
-luokallemme:
-public abstract class ViewModel
+public abstract class ViewModel : INotifyPropertyChanged
Hoveroimalla aaltoviivalla alleviivatun “INotifyPropertyChanged”-sanan alla VS tarjoaa kätevän hehkulamppupainikkeen, jolla voi nopeasti ottaa käyttöön tarvitsemansa nimiavaruuden.
Rajapinnan alla on edelleen aaltoviivaa, joten voimme klikata kursorin sen kohdalle, painaa Ctrl
+ .
ja valita “Implement interface”, jolloin VS lisää rajapinnan toteutuksen vaatimat funktiot ja/tai propertyt luokalle automaattisesti.
using System.ComponentModel;
namespace Kettunen.BMICalculator.WPFClient.MVVM
{
public abstract class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
}
}
INotifyPropertyChanged
-rajapinnan voi toteuttaa muutamalla hieman toisistaan eroavalla tavalla. Näkymä osaa kuunnella juuri lisäämäämme PropertyChanged
-tapahtumaa, mutta meiltä puuttuu vielä tapa kutsua sitä.
Tavoitteenamme on tehdä tänne kantaluokalle mahdollisimman yksinkertainen tapa saada ammuttua PropertyChanged
-tapahtuma, kun perivällä luokalla propertyn arvo muuttuu.
Lisätään funktio OnPropertyChanged
, jonka tehtävänä on ampua PropertyChanged
-ilmoitus ilmoille:
protected virtual void OnPropertyChanged(string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
string propertyName = null
funktion parametrina tarkoittaa sitä, että parametri on optionaalinen: Jos parametria ei anneta, niin arvoksi asetetaan null
. Propertyn arvon päivittäminen SetPropertyllä yksinkertaiseksi
Perusidea propertyjen päivittämisessä on, että kun propertylle annetaan uusi arvo - eli kutsutaan sen setteriä - niin tällöin tästä ammuttaisiin PropertyChanged
-tapahtuma automaattisesti.
Jos emme lisäisi enää mitään kantaluokalle, niin perivien luokkien tulisi tehdä jotain tämän tapaista, jos haluttaisiin ilmoittaa muutoksista vain silloin, kun propertyn arvo on oikeasti muuttunut:
private string _nimi;
public string Nimi
{
get
{
return _nimi;
}
set
{
// Varmistetaan, että muutos tehdään vain arvon oikeasti muuttuessa
if(EqualityComparer<string>.Default.Equals(_nimi, value))
{
_nimi = value;
// Laukaistaan muutoksen tapahtuma
OnPropertyChanged(nameof(Nimi));
}
}
}
Tässä olisi aika paljon kirjoitettavaa, jos tämä pitäisi tehdä jokaisen propertyn kohdalla!
Ratkaistaan tämä ongelma lisäämällä kätevä SetProperty
-funktio kantaluokallemme, joka hoitaa kaiken tuon seremonian meille automaattisesti taustalla:
protected virtual bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
{
if (EqualityComparer<T>.Default.Equals(field, value))
{
return false;
}
field = value;
OnPropertyChanged(propertyName);
return true;
}
Tässä listaus funktiossa käytetyistä konsepteista:
-
field
javalue
:field
on backing field, jota ollaan päivittämässävalue
-parametrin arvolla. -
T
-tyyppi: Kyse on ns. “määrittämättömästä”, eli geneerisestä tyypistä. Aiheesta löytyy lisää termillä “generics”.SetProperty
ottaa siis sisäänsä kaksi arvoa välittämättä arvojen suhteen mistään muusta, kuin että arvot ovat samaa tyyppiäT
. -
ref
-avainsana: Arvo syötetään funktiolle referenssinä arvon tyypistä riippumatta (value/reference). Tämä mahdollistaa sen, että kun tässä funktiossa kutsummefield = value
, niin oikeasti propertyn backing field päivitetään kutsuvassa luokassa. Kätevää! -
[CallerMemberName]
-attribuutti: Tällä vältetään määrittelemästä aina erikseen, että minkä niminen property on muuttunut.OnPropertyChanged
ottaa sisäänsä muuttuneen propertyn nimen, jolloin tämän attribuutin avulla meidän ei tarvitse eksplisiittisesti syöttääSetProperty
-funktiolle muuttuvan propertyn nimeä. Nice. -
EqualityComparer<T>
: Genericsit eivät toimi==
-operaattorin kanssa, joten joudumme käyttämään tätä referenssien vertailutapaa.
- Funktion
bool
-paluuarvo: Funktio palauttaaTrue
, jos annettu arvo on eri kuin nykyinen arvo, muutoinFalse
. Tätä voidaan käyttää kätevästi hyödyksi kun haluamme laukaista jonkin toisen propertyn päivityksen näkymälle nykyisen propertyn muuttuessa.
ViewModel-kantaluokka on nyt valmis tarpeisiimme!
Tähän lopuksi vielä käydään periyttämässä MainViewModel ViewModelistamme, jolloin MainViewModel.cs tulee näyttämään tältä:
using System.Windows.Input;
using Kettunen.BMICalculator.WPFClient.MVVM;
namespace Kettunen.BMICalculator.WPFClient
{
public class MainViewModel : ViewModel
{
public ICommand Navigate { get; }
public string NavigateText => "CALCULATE";
}
}
Seuraavassa osassa luodaan toteutus ICommand
-rajapinnalle, jotta pääsisimme navigoimaan piakkoin tulosnäkymään painiketta painamalla.