Tehdään WPF-sovellus - 25 - Navigoinnin toteutus

- 5 mins

Olemme aiemmin lisänneet päänäkymälle ResultView-näkymän omana elementtinään. Haluamme kuitenkin vaihtaa tätä näkymää aina, kun alareunan painiketta painetaan. Eli tarvitsemme tähän jonkin hieman dynaamisemman vaihtoehdon.

Laitetaan ResultViewin tilalle ContentControl, joka nimensä mukaisesti huokuu hyvää paikkaa esittää sisältöä. Lisätään myös MainViewModelille uusi property CurrentViewModel, jonka tarkoitus on ylläpitää tietoa missä näkymässä käyttäjä milloinkin on.

Bindataan ContentControlin Content MainViewModelin CurrentViewModel-propertyyn.

-<local:ResultView />
+<ContentControl Content="{Binding CurrentViewModel}" />
MainWindow.xaml
public class MainViewModel : ViewModel
{
    private ViewModel _currentViewModel;
    public ViewModel CurrentViewModel
    {
        get => _currentViewModel;
        private set => SetProperty(ref _currentViewModel, value)
    }
    // ...
MainViewModel.cs

Voimme asettaa CurrentViewModelin setterin private, koska emme halua kuin Navigate-komennon muokkaavan tätä arvoa.

Seuraavaksi tarvitsemme ViewModelit, joiden välillä haluamme Navigate-komennon vaihtavan.

Lisätään MainViewModelille talteen InputViewModel ja ResultViewModel omiin kenttiinsä, jotta voimme antaa komennon asettaa näistä toisen aina vuorollaan CurrentViewModelin arvoksi. Alustetaan myös nämä uudet muuttujat rakentajassa.

public class MainViewModel : ViewModel
{
    private readonly InputViewModel _inputViewModel;
    private readonly ResultViewModel _resultViewModel;
    
    // ...
    
    public MainViewModel()
    {
        _inputViewModel = new InputViewModel();
        _resultViewModel = new ResultViewModel();
    }
MainViewModel.cs

Haluamme, että ensimmäinen näkymä, jonka käyttäjä näkee on painon ja pituuden syötekentät. Asetetaan siis CurrentViewModelin arvoksi suoraan rakentajassa juuri luomamme _inputViewModel:

public MainViewModel()
{
    // ...

    CurrentViewModel = _inputViewModel;
}
MainViewModel.cs

Käynnistetään sovelluksemme tässä vaiheessa ja ihmetellään näppäimistöllä pyyhältäneiden sormiemme tuotoksia:

Kummallista tekstiä.

.. no eihän se nyt aivan oikealta näytä, vai mitä?

Bindasimme aiemmin ContentControl.Content-arvon suoraan ContentViewModel-propertyyn, mutta emme ole kertoneet sovellukselle, että miten haluamme tuota tietoa näyttää.

(vapaaehtoinen vaihe) ToString() ylikirjoittaminen

Voit kokeilla miten ToString():n ylikirjoittaminen oikein käytännössä vaikuttaa näkymään näin:

1) Lisää InputViewModelille ylikirjoitus haluamallasi tekstillä

public override string ToString()
{
    return "Kissimirre";
}
InputViewModel.cs

2) Käännä ja suorita ohjelma

Näkymällä pitäisi näkyä ToString()-funktion palauttama arvo. Hienoa! 😺

Kissimirre.

(vapaaehtoinen vaihe loppui)

DataTemplate määrittää miltä ViewModel näyttää

DataTemplaten avulla voimme kertoa WPF:lle, että miltä haluamme tarjotun tiedon näyttävän ruudulla.

Yksinkertaisuudessaan voimme lisätä kaksi uutta DataTemplate-elementtiä App.xaml-tiedostoon muiden tyylien joukkoon:

<DataTemplate DataType="{x:Type local:InputViewModel}">
    <local:InputView />
</DataTemplate>

<DataTemplate DataType="{x:Type local:ResultViewModel}">
    <local:ResultView />
</DataTemplate>
App.xaml

DataType kertoo DataTemplatelle, että minkä tyyppiselle tiedolle sen tulisi luoda näkymä. Meidän ei välttämättä olisi tarvinnut luoda Input- ja ResultViewejä ollenkaan, mutta tekemällä näin saimme jaettua näkymiä helposti hallittaviin pienempiin palasiin. Olisimme voineet määritellä DataTemplaten myös näin avaamalla ResultViewin auki DataTemplaten sisälle:

<DataTemplate DataType="{x:Type local:ResultViewModel}">
    <Border Margin="24"
            Style="{StaticResource ItemBorder}">
        <Grid VerticalAlignment="Center">
            <Grid.RowDefinitions>
                <RowDefinition />
                <RowDefinition />
            </Grid.RowDefinitions>
            <TextBlock Text="BMI" />
            <TextBlock Grid.Row="1"
                       Text="{Binding Result}" />
        </Grid>
    </Border>
</DataTemplate>
Vaihtoehtoinen tapa toteuttaa `DataTemplate`

Nyt kun suoritamme ohjelman, niin hommat alkavat näyttämään vähän toiveekkaammalta:

Nyt näyttää paremmalta.

Joko sitä komentoa pääsee toteuttamaan?

Jos nyt sitten tämän kerran.

Loimme aiemmin osassa 23 DelegateCommand-toteutuksen ICommand-rajapinnalle. Nyt pääsemme vihdoinkin sitä käyttämään!

Alustetaan Navigate-propertymme DelegateCommandiksi ja nikkaroidaan sen sisälle painikkeen painamisesta tapahtuva logiikka.

Voimme määrittää DelegateCommandin parametrit (execute ja canExecute) kätevästi käyttämällä lambdoja. Asetetaan execute vaihtamaan CurrentViewModel tulosten näyttämisen ViewModeliin. Sallitaan myös vielä tässä vaiheessa painikkeen painaminen aina tilanteesta riippumatta palauttamalla suoraan true canExecute-kyselylle:

Navigate = new DelegateCommand(
    _ =>
    {
        CurrentViewModel = _resultViewModel;
    },
    _ => true);
MainViewModel.cs

Navigointi toimii nyt nätisti tulosnäkymään, kun painiketta painetaan. Käy vaikka kokeilemassa. Hienoa!

Vielä vähän ratkottavaa

Meillä on käsissämme vielä muutama ongelma ratkottavana:

1) Painikkeen teksti ei muutu sanaan “BACK” siirryttäessä tulosnäkymälle

2) Tulosnäkymältä ei pääse palaamaan takaisin

3) Painoindeksiä ei lasketa ollenkaan (hups.)

4) Navigointi tulosnäkymään onnistuu, vaikka emme olisikaan syöttäneet mitään arvoja

Tässä voisi olla hyvää tekemistä seuraavalle osalle!

Anssi Kettunen

Anssi Kettunen

Ohjelmistokehittäjä suorittamassa tehtävää 🦊

rss facebook twitter github gitlab youtube mail spotify lastfm instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora quora