Ga naar content
Zoek op onderwerpen, blogs, diensten etc.

Business Apps keuken - een voorafje architectuur

Blogs
6-4-2016

Een goede basis is het halve werk

Zoals beloofd in de vorige blog post, gaan we laten zien hoe Wortell business apps in elkaar steekt met behulp van Xamarin Forms. Om één en ander een beetje begrijpelijk te maken, is een basis framework nodig. In deze blog post laten we een voorbeeld framework zien. Dit is niet het echte framework dat we gebruiken, omdat er dan direct een 'door de bomen geen bos meer zien' situatie ontstaat, maar een simpler analoog ervan.

MVVM

Het framework in de voorbeeld app  maakt gebruik van MVVM - Model-View-ViewModel . Dit is een pattern om scheiding van verantwoordelijkheden ('separation of concerns') te realiseren. Het is belangrijk om de business logica van de applicatie en weergave te scheiden. Dit bevordert herbruikbaarheid, leesbaarheid, testbaarheid en onderhoudbaarheid van de code, en voorkomt dat er spaghetti code ontstaat met business logica in de view laag - zoals vroeger in Visual Basic vaak het geval was. Een "model" bevat business logica, de "view" is het scherm met opmaak, en een "viewmodel" is de koppeling tussen view en model. De koppeling tussen view en viewmodel vindt plaats middels data binding, waarover in een latere blog post meer.

Het framework, "DemoFramework" geheten, is gebaseerd op het populaire MVVM framework MVVMLight maar maakt daar slechts in zeer beperkte mate gebruik van, en in de praktijk gebruiken we een zelfgebouwd framework intern maar - het gaat om het idee. "DemoFramework" regelt de absoluut minimale zaken die een applicatie framework moet regelen:

  • Faciliteren van MVVM
  • Registratie van welke view bij welk viewmodel hoort
  • Navigatie van viewmodel naar viewmodel
  • Doorgeven van enkele cruciale gebeurtenissen in het leven van de app naar het viewmodel

Basis klasse voor viewmodels

Een applicaties bestaat uit pagina's (pages), bij elke page definiëren we een viewmodel. Bovenop de bestaande ViewModelBase van MVVMLight bouwen we PageViewModelBase - de basis voor een viewmodel gekoppeld aan een page. Het viewmodel wil de volgende zaken weten:

  • Wanneer de pagina waarbij het hoort verschijnt
  • Wanneer die is geïnitialiseerd
  • Waneer een pagina weer verdwijnt
  • Hoe het naar een ander viewmodel moet navigeren.

Dit implementeren we als volgt:

using GalaSoft.MvvmLight;

namespace DemoViewFramework
{
public class PageViewModelBase : ViewModelBase, IPageViewModelBase
{
public virtual void OnViewInitialized(bool value)
{
ViewIsInitialized = value;
}

private bool _viewIsInitialized;

public bool ViewIsInitialized
{
get { return _viewIsInitialized; }
set { Set(() => ViewIsInitialized, ref _viewIsInitialized, value); }
}

public virtual void OnViewAppearing(object state = null)
{
ViewHasAppeared = true;
}

public virtual void OnViewDisappearing()
{
ViewHasAppeared = false;
}

private bool _viewHasAppeared;
public bool ViewHasAppeared
{
get { return _viewHasAppeared; }
set { Set(() => ViewHasAppeared, ref _viewHasAppeared, value); }
}

public INavigationService NavigationService { get; set; }
}
}

Dit is dus de klasse waar al onze view models, die de pagina's aansturen, vanaf worden geleid. Belangrijk is dat er bepaalde properties worden gezet als bepaalde gebeurtenissen in de levenscyclus van de pagina optreden. Hiervan kan later met data binding gebruik van worden gemaakt. Dit wordt in een volgend artikel uitgelegd.

Oplettende lezers hebben de interface IPageViewModelBase gezien. Die ziet er als volgt uit:

namespace DemoViewFramework
{
public interface IPageViewModelBase
{
void OnViewInitialized(bool value);
void OnViewAppearing(object state = null );
void OnViewDisappearing();

INavigationService NavigationService { get; set; }
}
}

Dit is alles wat de page te zien krijgt van het viewmodel. Dit zijn de enige toegangspunten die er zijn, om de cruciale events in de levenscyclus van de pagina door te geven. De NavigationService is verder nog iets wat aan het viewmodel wordt meegegeven door de SimplePageFactory (waarover later meer), en zorgt ervoor dat er van viewmodel naar viewmodel kan worden genavigeerd.

Basisklasse voor pagina's

Een aantal zaken die in de pagina levenscyclus gebeuren, moeten we weten in het viewmodel, om op die manier acties op de juiste plek of tijd te kunnen aftrappen. Zoals uit de interface IPageViewModelBase kan worden afgeleid, zijn dat er drie. Er is een basis klasse voor pagina's die precies die gegevens doorgeeft:

using Xamarin.Forms;

namespace DemoViewFramework
{
public class BaseContentPage : ContentPage
{
protected override void OnAppearing()
{
base.OnAppearing();
Context?.OnViewAppearing();
}

protected override void OnSizeAllocated(double width, double height)
{
base.OnSizeAllocated(width, height);
Context?.OnViewInitialized(true);
}

protected override void OnDisappearing()
{
base.OnDisappearing();
Context?.OnViewInitialized(false);
Context?.OnViewDisappearing();
}

private IPageViewModelBase Context => (IPageViewModelBase)BindingContext;
}
}

Het is eigenlijk uiterst simpel - als er een bepaalde gebeurtenis plaatsvindt in de page, worden corresponderende methodes van het viewmodel - dat standaard in de "BindingContext" property van een pagina zit - aangeroepen.

Navigatie

Goed, dan hebben we nu basis klasse voor pagina's, maar we moeten ook nog van pagina naar pagina, en aangezien we alles netjes hebben gescheiden, hebben viewmodellen geen flauw idee welke pagina ze moeten hebben. Dus definiëren we een NavigationService klasse die bij elk viewmodel de bijbehorende pagina zoekt:

using Xamarin.Forms;

namespace DemoViewFramework
{
public class NavigationService : INavigationService
{
private readonly INavigation _navigation;
private readonly ISimplePageFactory _pageFactory;

public NavigationService(INavigation navigation,
ISimplePageFactory pageFactory)
{
_navigation = navigation;
_pageFactory = pageFactory;
}

public void NavigateTo<TViewModel>()
{
var page = _pageFactory.CreatePageFor<TViewModel>(this);
_navigation.PushAsync(page);
}
}
}

Deze pagina wordt vervolgens aangemaakt met behulp van een zogenaamde factory - ook een bekend design pattern. De aangemaakte pagina's worden op de standaard Xamarin navigatie stack gepusht. Op deze wijze kan een viewmodel navigeren naar een ander viewmodel, zonder ook maar een flauw idee te (hoeven) hebben van welke pagina's daar nu bij horen. Wederom, separation of concerns. Enfin, daarvoor moeten we dan dus wel die factory hebben, maar ook die is niet zo spannend:

using System;
using System.Collections.Generic;

namespace DemoViewFramework
{
public class SimplePageFactory : ISimplePageFactory
{
private static readonly Dictionary<Type, Type> ViewMap =
new Dictionary<Type, Type>();

public void RegisterPageViewModel<TViewModel, TView>()
where TView : BaseContentPage
where TViewModel : IPageViewModelBase
{
ViewMap[typeof(TViewModel)] = typeof(TView);
}

public BaseContentPage CreatePageFor<TViewModel>(INavigationService navigationService)
{
var page = (BaseContentPage)Activator.CreateInstance(ViewMap[typeof(TViewModel)]);
var viewModel = (IPageViewModelBase)Activator.CreateInstance(typeof(TViewModel));
viewModel.NavigationService = navigationService;
page.BindingContext = viewModel;

return page;
}
}
}

Eén methode om aan te geven welk pagina bij welk viewmodel hoort, en één methode om bij een gegeven viewmodel de pagina ook daadwerkelijk te maken, en navigatie service mee te geven en het viewmodel aan de pagina vast te plakken. Hoe dat dan allemaal aan elkaar wordt geplakt, kun je zien in het startpunt van de app - app.cs, waar het volgende stukje code staat:

var pageFactory = new SimplePageFactory();
pageFactory.RegisterPageViewModel<MainViewModel, StartPage>();
pageFactory.RegisterPageViewModel<AnimationBehaviorsViewModel, AnimationBehaviorsPage>();
pageFactory.RegisterPageViewModel<MenuFromTopViewModel, MenuFromTopPage>();
pageFactory.RegisterPageViewModel<MenuSlideInViewModel, MenuSlideInPage>();

var mainNavigationPage = new NavigationPage();
MainPage = mainNavigationPage;

var navService = new NavigationService(mainNavigationPage.Navigation, pageFactory);
mainNavigationPage.Navigation.PushAsync(
pageFactory.CreatePageFor<MainViewModel>(navService));

Eerst wordt de factory aangemaakt, daarna worden viewmodellen en hun bijbehorende pagina's geregistreerd. Vervolgens wordt wat werk gedaan om navigatie überhaupt mogelijk te maken. De NavigationService wordt aangemaakt, krijg de Xamarin navigation stack mee en de page factory, en de eerste pagina wordt aangemaakt en getoond.

Merk op dat alle objecten alleen elkaars interface kennen, en niet elkaars concrete implementatie, en dat aan elkaar worden doorgegeven via de constructor. Dit is een (hele) eenvoudige vorm van Dependency Injection, wederom een veel gebruikt pattern.

Tot besluit

Zoals gezegd, dit is niet exact wat we gebruiken. De basis klasses voor pagina's en page view modellen zitten nagenoeg 1:1 in onze apps, maar de navigatie service en de page factory hebben een veel eenvoudigere en naïevere implementatie. Dit is gedaan om te voorkomen dat er alleen al drie blogposts moeten worden gewijd aan alle details daarvan. De bedoeling van deze blog post is te laten zien hoe de basis structuur van een goede app in elkaar zit. En zoals beloofd - de code is te downloaden. Hierin kun je het aanmaken van viewmodellen, pagina's, het doorgeven van gebeurtenissen en navigatie live observeren. Daar zit overigens ook al de code van de volgende blog post. De uitleg daarvan komt binnenkort.