Bridge.SSPAF: Sharpie Single Page Application Framework – Parte 2

Riprendiamo la creazione della nostra applicazione proprio dove l’avevamo lasciata. Vi ricordiamo un po’ di link utili: una demo di un progetto “reale”, con i relativi sorgenti; SPAF è listato come framework “Realworld”: https://github.com/gothinkster/realworld

Impostiamo il progetto:

  1. Aggiungiamo un progetto class library fullframework alla solution.
  2. Aggiungiamo il nuget Bridge.Spaf.Sources
  3. Espandiamo le referenze e rimuoviamo tutte le referenze a System.*

*Rimuovere tutti i riferimenti a System?

Le librerie di bridge contengono gli stessi namespace di System per poter transpilare verso JS.

Quando si aggiunge il nuget source, vengono aggiunti diversi file cs al progetto. I file relativi al core di SSPAF sono all’interno della cartella Spaf, mentre CustomRoutesConfig e SpafApp vengono aggiunti a livello di progetto. SpafApp è il nostro entry point dell’applicazione e vederemo piu avanti come customizzarlo.

A questo punto necessitiamo di un progetto web dove verranno inclusi i JS generati. Dato che il risultato sarà una applicazione in JS, possiamo fare un progetto WEB semplice senza tecnologie di backend. Il progetto awesomeapp.spaf ad ogni build genererà codice javascript, che deve essere incluso nel progetto web.

Per configurare l’output dell’emitter Bridge, è sufficiente aprire il file bridge.json, che contiene le configurazioni di bridge, e modificare il setting output:

"output": "$(OutDir)/bridge/",

diventerà:

"output": "../awesomeapp.web/wwwroot/bridge/",

Compilando il progetto ci troveremo nella seguente situazione:

Tutto il codice transpilato da Bridge si trova nella cartella di destinazione wwwroot/bridge e la index.html generata contiene i riferimenti js necessari da includere nella “nostra” index:

Setup navigazione

Completiamo il setup della nostra index.html per configurare la navigazione di SSPAF:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Spaf is awesome</title>
</head>
<body>

<div>Master header</div>
<div id="pageBody"></div>
<div>Master footer</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.0/knockout-min.js"></script>

<script src="bridge/bridge.js"></script>
<script src="bridge/bridge.console.js"></script>
<script src="bridge/bridge.meta.js"></script>
<script src="bridge/jquery-2.2.4.js"></script>
<script src="bridge/awesomeapp.spaf.js"></script>
<script src="bridge/awesomeapp.spaf.meta.js"></script>
</body>
</html>

Includiamo i file javascript come riportato dall’index generato da bridge e il JS di knockout, mentre per la struttura della pagina creiamo un div header, un div pagebody e un div footer. La pagina index verrà caricata all’avvio della nostra applicazione e SSPAF inietterà le pagine all’interno del div pageBody.

Per poter configurare la navigazione è necessario lavorare sulla classe CustomRoutesConfig, che viene aggiunta al progetto all’installazione del Nuget.

class CustomRoutesConfig : BridgeNavigatorConfigBase
{
    public override IList<IPageDescriptor> CreateRoutes()
    {
        return new List<IPageDescriptor>
        {
            new PageDescriptor
            {
                CanBeDirectLoad = ()=>true,
                HtmlLocation = ()=>"pages/home.html", // yout html location
                Key = SpafApp.HomeId,
                //PageController = () => SpafApp.Container.Resolve<HomeViewModel>()
            },
          
        };
    }

    public override jQuery Body { get; } = jQuery.Select("#pageBody");
    public override string HomeId { get; } = SpafApp.HomeId;
    public override bool DisableAutoSpafAnchorsOnNavigate { get; } = false;
}

Questa classe è il core della navigazione e contiene: la lista delle pagine dell’applicazione, l’id della prima pagina da mostrare e tutte le configurazioni riguardanti la navigazione. In questo esempio faremo una navigazione semplice, composta da una pagina di Home e una seconda pagina di esempio.

Come creare una pagina con relativo viewmodel

Per creare una pagina è necessario creare un file html che abbia come tag principale un div, il cui attributo id sia quello della pagina di riferimento, come nell’esempio:

<div id="second">
    Questa è la mia seconda pagina
</div>

L’html è stato salvato in wwwroot/pages/second.html

Nella classe SpafApp aggiungiamo l’id appena assegnato alla nuova pagina (in questo caso, second)

#region PAGES IDS
// static pages id

public static string HomeId => "home";
public static string SecondId => "second";

#endregion

Andremo poi a definire un viewmodel per la pagina appena creata:

public class SecondViewModel : LoadableViewModel
{
    public override string ElementId() => SpafApp.SecondId;
    public override void OnLoad(Dictionary<string, object> parameters)
    {
        Console.WriteLine("Second page caricata");
        base.OnLoad(parameters);
    }
    public override void OnLeave()
    {
        Console.WriteLine("Adios seconda pagina");
        base.OnLeave();
    }
}

I viewmodel debbono estendere la classe base loadable viewmodel, che espone l’id della pagina (SpafApp.SecondId), e gli eventi di navigazione di OnLoad e quelli di OnLeave, che, come è facilmente intuibile, vengono sollevati quando atterriamo sulla pagina e quando la abbandoniamo. Come parametro del metodo OnLoad è presente un dizionario stringa/oggetto dove è possibile leggere i parametri passati alla pagina (vedremo successivamente in che modo è possibile passare parametri).

Per convenzione le classi di viewmodel devono finire con “viewmodel”, in questo modo vengono automaticamente registrate nel container di sspaf. La registrazione automatica registra questi oggetti come transienti, ma è possibile variare questo comportamento decorando la classe con l’attributo SingleInstance. Così facendo il nostro viewmodel sarà registrato nel contaienr come singleton.

Successivamente definiamo un PageDescriptor nella classe CustomRoutesConfig, così da configurarne il routing:

class CustomRoutesConfig : BridgeNavigatorConfigBase
{
    public override IList<IPageDescriptor> CreateRoutes()
    {
        return new List<IPageDescriptor>
        {
            new PageDescriptor
            {
                CanBeDirectLoad = ()=>true,
                HtmlLocation = ()=>"pages/home.html", // your html location
                Key = SpafApp.HomeId,
                PageController = () => SpafApp.Container.Resolve<HomeViewModel>()
            },
            new PageDescriptor
            {
                CanBeDirectLoad = ()=>true,
                HtmlLocation = ()=>"pages/second.html", // your html location
                Key = SpafApp.SecondId,
                PageController = () => SpafApp.Container.Resolve<SecondViewModel>()
            },
          
        };
    }

    // elemento della pagina index che coterrà le pagine iniettate
    public override jQuery Body { get; } = jQuery.Select("#pageBody");

    // id del pagedescriptor che deve essere caricato all’avvio dell’applicaizone
    public override string HomeId { get; } = SpafApp.HomeId;

    // in caso di navigazione senza storico permette l’utilizzo di anchor speciali  
    // per la navigazione dell’applicazione spaf
    public override bool DisableAutoSpafAnchorsOnNavigate { get; } = false;
}

I pagedescriptor descrivono il modo in cui la pagina deve essere caricata, quale è il suo id e come risolvere il suo VM. Nel dettaglio un pagedescriptor è definito:

public class PageDescriptor : IPageDescriptor
{
    public PageDescriptor()
    {
        this.AutoEnableSpafAnchors = () => true;
    }

    public string Key { get; set; }
    public Func<string> HtmlLocation { get; set; }
    public Func<IAmLoadable> PageController { get; set; }
    public Func<bool> CanBeDirectLoad { get; set; }
    public Action PreparePage { get; set; }
    public bool SequentialDependenciesScriptLoad { get; set; }
    public Func<string> RedirectRules { get; set; }
    public Func<bool> AutoEnableSpafAnchors { get; set; }
    public Func<IEnumerable<string>> DependenciesScripts { get; set; }
}

Dove:

  • Key:
    Id della pagina.
  • HtmlLocation:
    Dove recuperare l’html della pagina.
  • PageController:
    Come istanziare il viewmodel della pagina.
  • CanBeDirectLoad:
    Se true, è possible navigare verso la pagina direttamente da url, nel caso sia false viene fatto un redirect automatico alla home.
  • PreparePage:
    Viene eseguito in fase di creazione della pagina.
  • SequentialDependenciesScriptLoad
    Forza gli script definiti nella proprietà Dependencies script ad essere caricati in ordine.
  • RedirectRules:
    Viene valutata prima di navigare verso la pagina ed è possibile restituire id diversi in caso si voglia navigare verso un’altra pagina.
  • AutoEnableSpafAnchor:
    Abilita l’uso degli spafanchor, che sono tag <a> dove è definito l’attributo href con la seguente dicitura (utile per IBrowserHistoryManager non parlanti, come vedremo successivamente):
    §  <a href="spaf:pageID">Another Page</a>
  • DependenciesScript:
    È la lista di riferimenti JS strettamente legati ad una determinata pagina

Adesso navighiamo programmaticamente dalla pagina Home alla pagina Second passando un parametro.

Per fare questo aggiungiamo un bottone alla home.html e mediante notazione knockoutjs colleghiamo il click del bottone ad un metodo sul viewmodel. Sui viewmodel possiamo usare dependency injection, dato che ci vengono risolti dal container di SPAF.

Successivamente faremo un breve escursus sulla registrazione e configurazione del container.

<div id="home">
    Questa è la mia pagina iniziale
    <button data-bind="click:GoToSecond">vai a pagina 2</button>
</div>
public class HomeViewModel : LoadableViewModel
{
    private readonly INavigator _navigator;
    public override string ElementId() => SpafApp.HomeId;

    public HomeViewModel(INavigator navigator)
    {
        this._navigator = navigator;
    }

    public void GoToSecond()
    {
        this._navigator.Navigate(SpafApp.SecondId, new Dictionary<string, object>
        {
            {"prova", "Ciao!"}
        });
    }
}

In homeviewmodel iniettiamo INavigator, che espone il metodo navigate, che accetta come parametri l’id della pagina verso cui vogliamo navigare e (facoltativo) il dizionario stringa/oggetto. Così facendo navigheremo verso la pagina second, verrà passato il parametro e verrà invocato il metodo OnLoad su secondviewmodel.

public class SecondViewModel : LoadableViewModel
{
    public override string ElementId() => SpafApp.SecondId;

    public override void OnLoad(Dictionary<string, object> parameters)
    {
        var passedParam = parameters.GetParameter<string>("prova");
        Console.WriteLine($"Alla pagina è stato passato {passedParam}");
        base.OnLoad(parameters);
    }

    public override void OnLeave()
    {
        Console.WriteLine("Adios seconda pagina");
        base.OnLeave();
    }
}

Nel caso si utilizzi l’implementazione di default del navigator:

Container.RegisterSingleInstance<INavigator, BridgeNavigatorWithRouting>();

sarà possibile scrivere anchor di navigazione semplicemente indicando l’id di destinazione:

<div id="second">
    Questa è la mia seconda pagina
    <a href="#home">Torna ad home</a>
</div>

Passaggio di dati e browser history

Sspaf ha 2 implementazioni “out of the box” per gestire il passaggio di dati tra pagine tramite url.

Container.RegisterSingleInstance<IBrowserHistoryManager, QueryParameterNavigationHistory>();

Container.RegisterSingleInstance<IBrowserHistoryManager, ComplexObjectNavigationHistory>(); 
  • QueryParameterNavigationHistory
    • In questa modalità i parametri del dizionario stringa/oggetto vengono scritti in queryparameter
    • Ex: http://yourspafapp.spaf#second?prova=ciao
  • ComplexObjectnavigationHistory
    • In questa modalità il dizionario stringa/oggetto viene serializzato ed è facile passare oggetti complessi tra le pagine
    • Ex: http://yourspafapp.spaf#second=eyJjb21wYXJlciI6e30sImVudHJpZXMiOnsicHJvdmEiOnsia2V5IjoicHJvdmEiLCJ2YWx1ZSI6IkNpYW8hIn19LCJrZXlzIjpbInByb3ZhIl0sImNvdW50IjoxLCJpc1NpbXBsZUtleSI6dHJ1ZSwiS2V5cyI6WyJwcm92YSJdLCJWYWx1ZXMiOlsiQ2lhbyEiXSwiSXNSZWFkT25seSI6ZmFsc2V9

Ha inoltre la possibilità di abilitare o disabilitare gli url parlanti di navigazione. Di default è abilitata la modalità history in quanto viene registrato:

Container.RegisterSingleInstance<INavigator, BridgeNavigatorWithRouting>();

basterà variare la registrazione con:

Container.RegisterSingleInstance<INavigator, BridgeNavigator>();

per avere l’url vuoto e tutte le navigazioni avverranno sempre programmaticamente. Quest’ultima casistica può essere utile per lo sviluppo di applicazioni JS all’interno di shell su dispositivi mobili o su desktop.

SpafApp.cs

Come già detto, quando si installa il nuget verrà aggiunto il file SpafApp.cs che contiene la classe di startup dell’applicazione

public class SpafApp
    {
        public static IIoc Container;

        public static void Main()
        {
            Container = new BridgeIoc();
            ContainerConfig(); // config container
            Container.Resolve<INavigator>().InitNavigation(); // init navigation

        }

        private static void ContainerConfig()
        {
            // navigator
            Container.RegisterSingleInstance<INavigator, BridgeNavigatorWithRouting>();
//            Container.RegisterSingleInstance<IBrowserHistoryManager, QueryParameterNavigationHistory>();
            Container.RegisterSingleInstance<IBrowserHistoryManager, ComplexObjectNavigationHistory>(); // if you don't need query parameters
            Container.Register<INavigatorConfigurator, CustomRoutesConfig>(); 

            // messenger
            Container.RegisterSingleInstance<IMessenger, Messenger.Messenger>();

            // viewmodels
            RegisterAllViewModels();

            // register custom resource, services..

        }

        #region PAGES IDS
        // static pages id


        public static string HomeId => "home";
        public static string SecondId => "second";
       
        #endregion

        #region MESSAGES
        // messenger helper for global messages and messages ids

        public static class Messages
        {
            public class GlobalSender { };

            public static GlobalSender Sender = new GlobalSender();

            public static string LoginDone => "LoginDone";

        }


        #endregion

        /// <summary>
        /// Register all types that end with "viewmodel".
        /// You can register a viewmode as Singlr Instance adding "SingleInstanceAttribute" to the class
        /// </summary>
        private static void RegisterAllViewModels()
        {
            var types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(s => s.GetTypes())
                .Where(w => w.Name.ToLower().EndsWith("viewmodel")).ToList();

            types.ForEach(f =>
            {
                var attributes = f.GetCustomAttributes(typeof(SingleInstanceAttribute), true);

                if (attributes.Any())
                    Container.RegisterSingleInstance(f);
                else
                    Container.Register(f);
            });

        }
    }

Il metodo Main è l’entry point dell’applicazione, dove di default viene istanziato il container esposto come proprietà statica e chiamato il metodo ContainerConfig(), nel quale andremo a registrare i nostri oggetti di servizi e infine inizializzare la navigazione.

Nella regione PagesId verranno definiti tutti gli id delle pagine, per avere un riferimento comune a tutta l’applicazione. La regione messages espone una classe di helper per inoltrare messaggi globali (ne parlerò piu avanti). Infine RegisterAllViewModels registra automaticamente tutte le classi il cui nome termina con “viewmodel” all’interno del container.

ViewModels e Partial

Sspaf utilizza Knockout per la sincronizzazione della vista con lo stato applicativo (e viceversa). Per utilizzare knockout, il nuget ha una dipendenza verso Retyped, che espone binding library Bridge per molte librerie JS. La classe base da cui la gerarchia dei viewmodel inizia è

public abstract class ViewModelBase
{
    private dom.HTMLElement _pageNode;

    /// <summary>
    /// Element id of the page 
    /// </summary>
    /// <returns></returns>
    public abstract string ElementId();

    public dom.HTMLElement PageNode => _pageNode ?? (this._pageNode = dom.document.getElementById(ElementId()));

    public void ApplyBindings()
    {
        knockout.ko.applyBindings(this, this.PageNode);
    }

    public void RemoveBindings()
    {
        knockout.ko.removeNode(this.PageNode);
    }
}

che espone l’id della pagina con cui effettuare il binding e il remove binding. Questa classe è estesa da

public abstract class LoadableViewModel : ViewModelBase, IAmLoadable
{
    protected List<IViewModelLifeCycle> Partials { get; } = new List<IViewModelLifeCycle>();

    public virtual void OnLoad(Dictionary<string, object> parameters)
    {
        base.ApplyBindings();
        this.Partials?.ForEach(f=> f.Init(parameters));
    }

    public virtual void OnLeave()
    {
        this.Partials?.ForEach(f=>f.DeInit());
        base.RemoveBindings();
    }
}

che, come visto in precedenza, sarà la classe base dei nostri viewmodel; questa espone una collezione di viewmodel parziali e i metodi OnLoad e OnLeave che gestiscono i binding di knockout e inizializzano/deinizializzano le partials.

Un viewmodel parziale sarà un componente riutilizzabile e deve essere definito estendendo la classe

public abstract class PartialModel :  IViewModelLifeCycle
{
    private dom.HTMLDivElement _partialElement;

    /// <summary>
    /// Element id of the page 
    /// </summary>
    /// <returns></returns>
    public abstract string ElementId();
    
    /// <summary>
    /// HtmlLocation
    /// </summary>
    protected abstract string HtmlUrl { get; }


    /// <summary>
    /// Init partial
    /// </summary>
    /// <param name="parameters">data for init the partials</param>
    public virtual void Init(Dictionary<string,object> parameters)
    {

        jQuery.Get(this.HtmlUrl, null, (o, s, arg3) =>
        {
            this._partialElement = new dom.HTMLDivElement
            {
                innerHTML = o.ToString()
            };
            var node = dom.document.getElementById(ElementId());
            node.appendChild(this._partialElement);
            knockout.ko.applyBindings(this, this._partialElement);
        });
    }

    public virtual void DeInit()
    {
        // check if ko contains this node
        if (this._partialElement == null) return;
        var data = knockout.ko.dataFor(this._partialElement);
        if (data == null) return;
        
        knockout.ko.removeNode(this._partialElement);
    }
}

questa, similmente a un viewmodel “classico”, espone l’id dell’elemento di vista oltre a l’url dell’html. In fase di init viene caricato l’html e applicati i binding, che vengono rimossi in fase di deinit.

Ecco come utilizzare nella pratica un viewmodel parziale o “componente Spaf”:

Come prima cosa creiamo l’html che rappresenta il componente; questo sarà compost da un testo e un bottone che incrementerà un contatore.

<div id="increment">
    <span data-bind="text:Number"></span>
    <button data-bind="click:Add">Add</button>
</div>

Creiamo un viewmodel parziale per il nostro componente:

public class IncrementViewModel : PartialModel
{
    public override string ElementId() => SpafApp.IncrementId;
    protected override string HtmlUrl { get; } = "components/increment.html";

    public KnockoutObservable<int> Number { get; set; }

    public IncrementViewModel()
    {
        this.Number = ko.observable.Self(0);
    }

    public void Add()
    {
        var actualNumber = this.Number.Self();
        this.Number.Self(++actualNumber);
    }
}

che espone un intero osservabile e il metodo invocato dal bottone Add.

Includiamo il componente nella pagina principale in cui vogliamo che sia utilizzato:

<div id="home">
    Questa è la mia pagina iniziale
    <button data-bind="click:GoToSecond">vai a pagina 2</button>
    <div id="increment"></div>
</div>

E nel viewmodel della vista interessata aggiungiamo il viewmodel alla lista delle partial

this.Partials.Add(SpafApp.Container.Resolve<IncrementViewModel>());

Adesso abbiamo un componente riutilizzabile che possiamo utilizzare in varie pagine dell’applicazione.

Nelle prossime release ho intenzione di semplificare la ridondanza di ID necessari ma, ad oggi, lo stato dell’arte è questo. 🙂

Utilizzare knockout in SPAF

Per implementare il pattern MVVM, SPAF utilizza Knockout, che è una libreria decisamente matura e completa per implementare binding a doppia via su applicazioni JS. Sul sito di riferimento è possibile trovare documentazione e tutorial davvero molto dettagliati, che coprono tutte le casistiche necessarie.

Per poter utilizzare una libreria JS all’interno di un progetto transpilato con Bridge, è necessario includere una “Definition Library” che esponga le api in un contesto .NET, definendo le regole di emitting. Creare questo tipo di librerie è più banale di quanto non sembri, ma per utilizzare Knockout è già disponibile una libreria generata da Retyped (fortunatamente, in quanto le API da wrappare sarebbero state molte).

Retyped è un prodotto degli stessi sviluppatori di Bridge; si tratta di in un tool automatizzato per la generazione di Definition library. Sul sito sono disponibili piu di 3500 librerie utilizzabili.

Di seguito come utilizzare ko su un viewmodel sspaf

Aggiungere il namespace

using static Retyped.knockout;

definire le proprietà osservabili

public KnockoutObservable<int> Number { get; set; }
public KnockoutObservableArray<int> ManyNumber { get; set; }

inizializzarle nel costruttore

this.Number = ko.observable.Self(0);
this.ManyNumber = ko.observableArray.Self<int>();

utilizzare la notazione KO sulla nostra vista:

<span data-bind="text:Number"></span>
<button data-bind="click:Add">Add</button>

dove add sarà un normalissimo metodo definito sul VM:

public void Add()
{
    var actualNumber = this.Number.Self();
    this.Number.Self(++actualNumber);
}

Messenger

Spaf internamente ha una libreria Publisher/Subscriber per agevolare la comunicazione tra i componenti applicativi.

Nel container viene registrato il servizio IMessenger che può essere risolto e iniettato.

Per inviare un messaggio sarà sufficiente

this._messenger.Send(this,SpafApp.Messages.Alert,"Spaf Messenger!");

e nella classe subscriber (nell’esempio ho usato SpafApp) sottoscriversi:

Container.Resolve<IMessenger>().Subscribe<HomeViewModel,string>(new object(), SpafApp.Messages.Alert,
    (model, s) =>
    {
        Global.Alert(s);
    });

Nella classe spafapp è definita una sezione messages, dove definire i messaggi come proprietà statiche, ed un oggetto globalsender per permettere sottoscrizioni globali (senza aver definito un singolo sender). Nelle classi dove ci si sottoscrive è importante dissottoscriversi quando non è piu necessario ricevere il messaggio:

Messanger.Unsubscribe("message", typeof(MyViewModel), typeof(bool), this);

Bridge IOC

Il container definite in SPAF è l’orchestratore degli elementi applicativi ed espone diverse modalità di gestione del ciclo di vita degli oggetti.

interface IIoc
    {
        void RegisterFunc<TType>(Func<TType> func);
        void Register(Type type, IResolver resolver);
        void Register<TType, TImplementation>() where TImplementation : class, TType;
        void Register<TType>() where TType : class;
        void Register(Type type);
        void Register(Type type, Type impl);
        void RegisterSingleInstance<TType, TImplementation>() where TImplementation : class, TType;
        void RegisterSingleInstance<TType>() where TType : class;
        void RegisterSingleInstance(Type type);
        void RegisterSingleInstance(Type type, Type impl);
        void RegisterInstance<TType>(TType instance);
        void RegisterInstance(Type type, object instance);
        void RegisterInstance(object instance);
        TType Resolve<TType>() where TType : class;
        object Resolve(Type type);
    }

Espone sia firme con i generic che con i tipi:

(transienti)

void Register<TType, TImplementation>() where TImplementation : class, TType;
void Register(Type type, Type impl);

(single instance)

void RegisterSingleInstance<TType, TImplementation>() where TImplementation : class, TType;
void RegisterSingleInstance(Type type, Type impl);

Registrazione di istanze:

        void RegisterInstance(Type type, object instance);
        void RegisterInstance(object instance);

e registrazioni con Func:

void RegisterFunc<TType>(Func<TType> func);

La libreria è coperta da Test lato js:

http://tests.markjackmilian.net/bridgeioc/

Come posso testare il mio codice su browser?

Per poter creare una batteria di unit test che venga eseguita direttamente sul browser, ho rilasciato una libreria chiamata Bridge.EasyTest. In questi giorni ho rilasciato la nuova versione del Nuget che include i sorgenti. Per poter creare una batteria di test per prima cosa è necessario aggiungre alla solution un novo progetto class library fullframework e aggiungere il nuget Bridge.EasyTest. Il nuget aggiunge una cartella EasyTest che contiene i sorgenti della libreria di test e una cartella wwwroot che contiene i css e il file index.html su cui tornerò piu avanti.

Apriamo il file bridge.json e modifichiamo l’output dei JS generati:

"output": "wwwroot/bridge/",

Aggiungere il progetto spaf come referenza al progetto di test e dovreste avere una situazione come la seguente:

Per assicurare il corretto funzionamento dell’app di test è necessario “inibire” l’avvio della applicazione SSPAF, in quanto il metodo statico void Main() viene eseguito automaticamente al caricamento della pagina.

Per fare questo creo una configurazione di Test:

Inibisco l’avvio dell’app spaf:

public static void Main()
{
    #if Test
    return;
    #endif
    
    Container = new BridgeIoc();
    ContainerConfig(); // config container
    Container.Resolve<INavigator>().InitNavigation(); // init navigation
    
    Container.Resolve<IMessenger>().Subscribe<HomeViewModel,string>(new object(), SpafApp.Messages.Alert,
        (model, s) =>
        {
            Global.Alert(s);
        });

}

A questo punto siamo in grado di scrivere Il nostro primo test. L’esempio che propongo è un test di HomeViewModel e controllerò che quando viene invocato il metodo GoToSecond venga chiamato il metodo Navigate su INavigator.

Per prima cosa creiamo un fakenavigator che incrementa un contatore quando viene invocato il metodo Navigate:

class FakeNavigator : INavigator
{
    public event EventHandler<IAmLoadable> OnNavigated;
    public IAmLoadable LastNavigateController { get; }
    public void InitNavigation()
    {
        throw new NotImplementedException();
    }

    public void EnableSpafAnchors()
    {
        throw new NotImplementedException();
    }

    public void Navigate(string pageId, Dictionary<string, object> parameters = null)
    {
        this.Called++;
    }

    public int Called { get; private set; }
}

Aggiungo l’attributo Test alla classe HomeViewModelTest e l’attributo TestMethod ai vari metodi della classe. L’attributo accetta anche un parametro facoltativo di descrizione che ci darà maggiori infomazioni nella pagina di output del test.

    [Test("Test homeviewmodel")]
    public class HomeViewModelTest
    {
        [TestMethod("When GoToSecond fired navigator is called")]
        public void WhenGoToSecondIsFiredNavigationInCalled()
        {
            var nav = new FakeNavigator();
            var homeModel = new HomeViewModel(nav,null, null);
            homeModel.GoToSecond();
            
            nav.Called.ShouldBeEquals(1);
        }
    }

EasyTest espone alcune API di assert che potete trovare nella cartella EasyTests/Asserts.

Buildando il progetto verranno generati i file js necessari nella cartella wwwroot/bridge:

A questo punto (solo la prima volta) sarà necessario aprire il file wwwroot/bridge/index.html e copiare le referenze js generate da Bridge:

<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>awesomeapp.test</title>
    
    <script src="bridge.js"></script>
    <script src="bridge.console.js"></script>
    <script src="bridge.meta.js"></script>
    <script src="jquery-2.2.4.js"></script>
    <script src="awesomeapp.spaf.js"></script>
    <script src="awesomeapp.spaf.meta.js"></script>
    <script src="awesomeapp.test.js"></script>
    <script src="awesomeapp.test.meta.js"></script>
</head>
<body>
</body>
</html>

E includerle nel file di wwwroot/index.html in fondo sostituendo:

<!--    ENTER GENERATED JS HERE-->

Aprendo il file wwwroot/index.html in un browser avremo:

Come vedete, ho inserito un valore sbagliato nell’assert in modo da veder fallire il test e in più mostrare le informazioni generate in pagina.

Mettendo il valore corretto:

Conclusioni

La possibilità di scrivere SPA in .NET mi ha permesso di utilizzare le mie conoscenze a pieno e di organizzare una architettura sufficientemente elegante da garantire manutenibilità e efficienza in fase di sviluppo. Con SPAF ho potuto sviluppare applicazioni complesse senza “scrivere” una riga di JS, utilizzando pattern, notazioni e strumenti su cui lavoro quotidianamente.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *