venerdì 28 novembre 2008

Il pattern observer e il MVC... come separare un Model dalle Views

Il pattern Model-View-Controller (MVC)  indica che  una applicazione deve essere separata in tre componenti architetturali. Il Model è il core del programma: esegue gli algoritmi, gestisce i dati.  La View è l'interfaccia del programma verso l'utente. Il controller tiene insieme Model e View facendoli comunicare (in particolare, permette alla View di accedere ai metodi del Model).
Ulteriori dettagli su wikipedia.

I concetti fondamentali sono questi:
  • La View (o le views) conosce il Model, e ne può invocare i metodi, ma solo tramite il controller
  • Il Model non conosce le view, non sa se c'è una View ad essa associato, nè tantomeno come è fatta e quali metodi ha.
I vantaggi di questo pattern sono ovvi:
  • Ad un dato Model si possono associare quante View si desiderano, senza dover toccare una virgola del Model stesso
  • Il Model ha una API chiara, ovvero non sporcata da eventuali metodi/parti di codice legati all'interazione con l'utente
  • Fatta una view, è relativamente facile crearne un'altra come copia della prima, salvo poi modificare i dettagli voluti (ad es cambiare la lingua dei messaggi...) 

Usando il pattern observer è facile creare una architettura di questo genere.

Il pattern observer (wikipedia qui) serve a disaccoppiare due classi: un publisher di eventi ed un subscriber di eventi. Proprio come per l'abbonamento ad un giornale, gli eventi (i nuovi numeri del giornale) vengono notificati (spediti) dal publisher (l'editore) ai subscribers (gli abbonati).
E' dunque necessaria una prima fase di registrazione (creazione dell'abbonamento) in cui il subscriber attivamente si registra presso il publisher. Una volta "abbonato" il subscriber diventa passivo e si limita a ricevere le chiamate del publisher che gli notifica nuovi eventi.
Grazie all'observer le due classi sono disaccoppiate (NB: in un verso solo!!  Il Publisher non conosce i subscriber ma i subscriber sanno benissimo che esiste un certo Publisher a cui sono registrati!).
 (Nota: a volte il publisher è chiamato "subject", i subscriber son chiamati "observer"... ma è solo una questione di nomi)

Tipicamente il publisher ha una struttura di questo genere:
  • metodo per registrare nuovi subscriber
  • metodo per de-registrare un vecchio subscriber
  • set di metodi per notificare eventi ai subscibers
I subscriber invece hanno questi metodi:
  • set di metodi per reagire alla notifica degli eventi


Nel nostro caso (l 'MVC), le View sono i subscriber, mentre il Model è il publisher.
Il Model, quando ne ha voglia o necessiatà, invia un messaggio a tutte le View ad esso registrate con la notifica di un evento. Se non c'è nessuna View registrata, semplicemente il messaggio non arriva a nessuno, ma di questo il Model non si cura affatto. Il Model quindi comunica con le View senza conoscerle e addirittura senza sapere se esistono o meno.

Dunque d'ora in poi
subscriber=View
publisher=Model


In questo esempio C# costruisco una applicazione in cui ad un Model sono associate due Views. 
Il Model, banalissimo, non fa altro che cambiare il valore di una sua variabile di stato.
Le view saranno
  •  una classe che scrive lo stato del Model su console 
  • una windows form che scrive lo stato del Model su un'interfaccia grafica
Vedremo che di fronte ad una modifica dello stato del Model, tutte e due le View ne saranno contemporaneamente informate, ed entrambe mostreranno a video l'evento.

Sebbene sia utile avere due view per visualizzare i dati del modello, è sconsigliabile avere due fonti di "input" per il programma. Dunque decidiamo che la view console serve solo a mostrare gli output, mentre gli input al programma vengono dati tramite la view windows form.

Il modello è questo:

  public class MyModel:ConcreteModel
    {
        public int statusVariable;

        public void doWork() {

            statusVariable = 0;
            NotifyToObserver();

            //do something
            
            //send a message to the views
            string msg = "hello!";
            NotifyMessageToObserver(msg);

            //send to the views the information that a status variable has changed
            statusVariable = 1;
            NotifyToObserver();

        }
    }


Ha una variabile di stato che rappresenta un possibile stato interno del modello. Il metodo doWork() non fa altro che modificare questo stato, portandolo prima a 0 e poi  a 1.

I metodi "Notify" servono a inviare alle View registrate le notifiche di modifica dello stato "NotifyToObserver()" o un messaggio particolare "NotifyMessageToObserver(msg)". Questi metodi si trovano nel ConcreteModel, il prototipo generico di un modello:

 public class ConcreteModel:IModel
    {
        List registeredViews = new List();

        #region IModel Members

        public void RegisterObserver(IView paramView)
        {
            registeredViews.Add(paramView);
        }

        public void RemoveObserver(IView paramView)
        {
            registeredViews.Remove(paramView);
        }

        public void NotifyToObserver()
        {
            foreach (IView view in registeredViews) {
                view.Update(this);
            }
        }

        public void NotifyMessageToObserver(string message)
        {
            foreach (IView view in registeredViews)
            {
                view.NewMessage(message);
            }
        }

        #endregion
    }

Il ConcreteModel contiene l'elenco delle viste registrate, ed i metodi per notificare ad esse le modifiche del Model. Inoltre contiene i metodi per registrare/de-registrare le viste.

Il ConcreteModel implementa l'interfaccia IModel.

 public interface IModel
    {
       

        #region Observer pattern
        ///
        /// Attach an observer view to the model. Max 1 view! If a view is currently registered, it will be unregistered and overwritten by the new one
        ///
        ///
        void RegisterObserver(IView paramView);

        ///
        /// Unregister the view currently attached to the model
        ///
        ///
        void RemoveObserver(IView paramView);

        ///
        /// Notify any change in the model to the attached view
        ///
        void NotifyToObserver();

        ///
        /// Notify a string message to the attached view
        ///
        ///
        void NotifyMessageToObserver(string message);

               
        #endregion
    }



Questa è invece l'interfaccia che ogni vista deve implementare. Contiene i metodi per "reagire" alle notifiche inviate dal Model.

public interface IView
    {
      
        #region  observer pattern methods
        
        ///
        /// Update the view given the current instance of the model
        ///
        ///
        void Update(IModel paramModel);


        ///
        /// Update the view given a message string coming from the model
        ///
        ///
        void NewMessage(string message);
      

        #endregion
    }

Il metodo "Update(IModel paramModel)" gestisce le notifiche inviate con "NotifyToObserver()".
Il metodo "NewMessage(string message)" gestisce le notifiche inviate con "NotifyMessageToObserver(string message)".
Ovviamente è possibile definire altre coppie di notifiche/reazioni a seconda delle necessità.



Ecco una possibile implementazione di View. Questa View manda i messaggi alla console:

 class MyViewConsole: IView
    {

        #region IView Members

        void IView.Update(IModel paramModel)
        {
            ConcreteModel model = paramModel as ConcreteModel;
            if (model != null)
            {
                //do something
                MyModel myModel = model as MyModel;
                if (myModel != null) {
                    Console.WriteLine("Internal status:" + myModel.statusVariable);
                }
            }
        }

        void IView.NewMessage(string message)
        {
            //do something
            Console.WriteLine("Message from model:"+message);
        }

        #endregion
    }

Il metodo "Update(IModel paramModel)" permette di stampare a Console lo stato interno del modello.
Il metodo "NewMessage(string message)" permette di stampare a console il messaggio inviato dal modello.



Come detto in precedenza una delle View deve essere quella "di input", cioè quella da cui si inviano comandi al Model tramite il Controller.  Questa View dovrà avere la reference al Controller. Definiamo quindi l'interfaccia di "View di Input":

public interface IInputView
    {
         void setController(Controller c);
        
    }


Dunque creaiamo una View concreta che sia contemporaneamente di output e input. In questo caso estende anche Windows.Form.


public partial class MyViewForm : Form,IView,IInputView
    {
        public MyViewForm()
        {
            InitializeComponent();
      
        }



        #region IView Members

        void IView.Update(IModel paramModel)
        {
            ConcreteModel model = paramModel as ConcreteModel;
            if (model != null)
            {
                //do something
                MyModel myModel = model as MyModel;
                if (myModel != null)
                {
                    textBox1.Text= myModel.statusVariable+"";
                }
            }
        }

        void IView.NewMessage(string message)
        {
           //do something
           MessageBox.Show("Message from model:" + message);
        }

        #endregion

        Controller controller;

        #region IInputView Members

        public void setController(Controller c)
        {
           controller=c;
        }

        #endregion

        private void button1_Click(object sender, EventArgs e)
        {
            controller.doWorkModel();
        }

        private void button2_Click(object sender, EventArgs e)
        {
            this.Close();
        }

      
    }


Il metodo "Update(IModel paramModel)" permette di stampare in una casella di testo lo stato interno del modello.
Il metodo "NewMessage(string message)" permette di mostrare una messagebox con il messaggio inviato dal modello.
Il metodo banale "setController(Controller c)" non fa altro che creare una referenza al controller e dunque, di riflesso, al modello.

Ed infine manca solo il controller anch'esso banale: gira ogni comando ricevuto verso il modello.
Volendo nel controller si può (e si deve) mettere tutta la logica che riguarda la modifica delle View in base allo stato delle View stesse e del controller (ad es abilitazione/disabilitazione tasti...)

 public class Controller
    {

        public Controller( MyModel m) {
            model = m;
        }

        MyModel model;

        public void doWorkModel() {
            model.doWork();
        }
    }


Il programma principale instanzia il Model, le due View e il Controller. Poi registra le View sul Model ed attacca il Controller alla View di Input. Infine lancia la View di Input

 class Program
    {
        static void Main(string[] args)
        {
            //create the model
            MyModel myModel = new MyModel();
            
            //create the 2 views
            MyViewConsole myViewConsole = new MyViewConsole();
            MyViewForm myViewForm = new MyViewForm();
          
            //register the 2 views(subscribers) to the model(publisher)
            myModel.RegisterObserver(myViewConsole);
            myModel.RegisterObserver(myViewForm);

            //create the controller to the model
            Controller c = new Controller(myModel);

            //attach the controller to the input view
            myViewForm.setController(c);

            //run the input view
            Application.Run(myViewForm);

           
        }
    }


Il risultato è questo:

Start del programma:


Click sul bottone "model di work"


Click sull'ok del messagebox

Si può notare come l'update delle due viste, quella console e quella windows form, sia parallelo e contemporaneo.

Aggiungere una terza vista (ad esempio una console in un'altra lingua, o addirittura una pagina web in ASP)  sarebbe banale.


giovedì 27 novembre 2008

Linkare come referenza diversi progetti in solutions .NET

Supponiamo di avere una solution contenente 3 progetti, P1 ,P2,P3. Poniamo che P1 sia un eseguibile, mentre P2 e P3 siano DLL accessorie. Poniamo anche che

P1 dipenda dal progetto P2
P2 dipenda dal progetto P3.

Nelle impostazioni di progetto, bisogna inserire le giuste referenze (references).
Nelle proprietà di P2 bisogna inserire la referenza a P3.
Nelle proprietà di P1 bisogna inserire la referenza a P2. E' perfettamente inutile, se non controproducente, inserire in P1 la referenza a P3.


Quando si compila la soluzione, il compilatore crea automaticamente nella cartella dove viene creato l'eseguibile (di default bin/debug) anche le due dll dipendenti (sia P2 che P3). In questo modo, se si vuole pubblicare l'applicazione in un altra cartella/macchina, è sufficente copiare l'intero contenuto di questa cartella senza preoccuparsi di andare a recuperare le dll chissà dove.



Lettori fissi