niedziela, 12 marca 2017

Widok, Model Widoku - proste połączenie

Hej,
w tym poście chciałbym zaprezentować prosty i skuteczny sposób na kojarzenie dwóch tytułowych elementów w jeden. Słowem wstępu - najpierw kilka informacji o działaniu widoków w Xamarin.Forms:
Widoki możemy tworzyć na dwa sposoby: używając XAMLa oraz całkowicie w kodzie c#. Osoby korzystające z Xamarina lub WPFa pewnie z XAMLem miały już do czynienia, a jak nie to krótko ujmując jest to język oparty na XMLu służący właśnie do tworzenia warstwy interfejsu użytkownika. Ma on tą przewagę nad tworzeniem widoków w kodzie, że po krótkiej praktyce, jestem w stanie z wizualizować sobie interfejs w głowie - przydatna umiejętność.
Pisząc XAMLowy widok w Xamarin.Forms jest on podzielony na dwa elementy - plik *.xaml, w którym jest xml z widokiem i plik tzw "codebehing" - *.xaml.cs który jest podpięty pod plik *.xaml i wygląda on mniej więcej tak:
public partial class WidokTest : ContentPage
{
    public WidokTest ()
    {
        InitializeComponent();
    }
}
Jak widać dla naszego testowego widoku mamy klasę, w której określamy bazowy widok po którym dziedziczymy - ContentPage lub ContentView najczęściej, a w konstruktorze wywołujemy inicjalizację komponentu.
Na pytanie skąd ten partial odpowiedź jest prosta - drugą częścią naszej klasy jest właśnie nasz plik xamlowy, który kompilator przerabia na jedną, wspólną klasę.

Dobrą praktyką jest aby w codebehind robić jak najmniej, a wszystkie operacje na widoku działy się poprzez bindingi. No ale w naszej klasie testowej brakuje co najmniej jednego elementu: ustalenia właściwości BindingContext, który powinien być instancją jednego z naszych modeli widoków.

Stworzyłem więc taką klasę bazową:

public abstract class ViewPage<T> : ContentPage where T : IViewModel
{
    readonly T _viewModel;
    public T ViewModel
    {
        get { return _viewModel; }
    }

    public ViewPage()
    {
        using (var scope = IoC.container.BeginLifetimeScope())
        {
            _viewModel = scope.Resolve<T>(new TypedParameter(this.GetType(), this));
        }
        BindingContext = _viewModel;
    }

    public ViewPage(object viewModelContext)
    {
        using (var scope = IoC.container.BeginLifetimeScope())
        {
            _viewModel = scope.Resolve<T>(new TypedParameter(this.GetType(), this), new NamedParameter("context", viewModelContext));
        }
        BindingContext = _viewModel;
    }
}
Stosując taką generyczną klasę bazową dla widoków mogę łatwo i szybko wyciągnąć z kontenera odpowiedni model i podpiąć go pod widok. Korzystając z niej, nasza testowa klasa widoku wyglądałaby następująco:

public partial class WidokTest : ViewPage<WidokTestModel>
{
    public WidokTest ()
    {
        InitializeComponent();
    }
}

public partial class WidokTest : ViewPage<WidokTestModel>
{
    public WidokTest (object kontekstWidoku) : base(kontekstWidoku)
    {
        InitializeComponent();
    }
}

Jak widać ilość kodu w codebehind wzrosła bardzo nieznacznie, a mamy załatwiony najważniejszy element. Niestety wszystko ma swoją cenę - trzeba przypomnieć sobie o tym nieszczęsnym partialu - nasz klasa dziedziczy teraz po nowym elemencie  i musimy analogicznie zmienić to w XAMLu:

<test:ViewPage xmlns:test="clr-namespace:MyApp.Test" x:TypeArguments="test:WidokTestModel"

Wystarczy dodać namespace w którym jest nasza klasa bazowa, zmienić nazwę głównego elementu oraz dodać generyczny parametr. W ten sposób wszystko nam się ładnie kompiluje, a my w prosty sposób spinany ze sobą widoki i modele widoków.

0 komentarze: