niedziela, 20 września 2015

Trafiłeś do pierwszego artykułu serii, kolejne artykuły najlepiej będzie jak odpalisz w takiej kolejności:
  • Passive MVC vs MVP Passive View (wkrótce...) 
  • Active MVC vs MVP Passive View (wkrótce...) 
  • Passive MVC vs MVP Supervising Controller (wkrótce...) 
  • MVP Passive View i MVP Supervising Controllet (wkrótce...)

Model

  • Model zawiera kolekcję obserwatorów.
  • Gdy model zmienia swój stan, powiadamia o tym swoich obserwatorów.
  • Obserwatorzy posiadają publiczną metodę Update, którą model wywołuje, gdy się zmieni, w celu przekazania obserwatorom swojego nowego stanu.
  • Innymi słowy, model jest obiektem obserwowanym.
  • Model posiada publiczną metodę Subscribe w celu dodawania do swojej kolekcji obserwatorów, którzy są zainteresowani otrzymywaniem od modelu powiadomień o jego zmianie.
  • Model posiada, również metodę Notify, która iteruje kolekcję obserwatorów aby na każdym z tych obserwatorów wywołać metodę Update, dzięki której obserwatorzy są informowani o zmianie modelu.
  • Metoda Notify jest wywoływana z wnętrza modelu, gdy stan modelu się zmienił. Decyzja - kiedy ta metoda zostanie wywołana - spoczywa na szczególnej implementacja logiki modelu, więc metoda Notify jest nie publiczna.
  • Tymi obserwatorami modelu są obiekty Widoku.
  • Między modelem a widokami jest wzorzec Obserwator, gdzie widok jest obserwatorem a model jest obiektem obserwowanym.
  • Każdy model jest obserwowany czyli musi posiadać swoich obserwatorów, więc obsługę subskrybowania obserwatorów można napisać raz w klasie abstrakcyjnej Observable.
KOD APLIKACJI JEST NA GITHUB... TeoVincent/MVC-by-example

public abstract class Observable
{
    protected readonly List<IObserver> observers;

    protected Observable()
    {
        observers = new List<IObserver>();
    }

    public void Subscribe(IObserver observer)
    {
        observers.Add(observer);
    }

    protected abstract void Notify();
}
  • Metoda Notify implementowana jest bezpośrednio w modelu, w którym ma ona dostęp do aktualnego stanu tego modelu.
public class Model : Observable
{
    private int x;
    private int y;

    protected override void Notify()
    {
        foreach (var observer in observers)
            observer.Update(x, y);
    }
}
  • Powyższa implementacja klasy modelu jest niepełna, brakuje w niej przynajmniej jednej publicznej metody, która zmodyfikuje stan modelu.
  • Na potrzeby przykładu zdefiniuję cztery metody zwiększające i zmniejszające prywatne własności x i y, a każda zmiana tych własności zostanie rozgłoszona wśród obserwatorów za pomocą metody Notify.
  • Model powinien implementować interfejs Modelu, w którym zdefiniowane są publiczne metody wywołujące akcje na modelu, prowadzące do jego zmiany stanu.
public interface IModel
{
    void IncreaseX();
    void DecreaseX();
    void IncreaseY();
    void DecreaseY();
}

public class Model : Observable, IModel
{
    private int x;
    private int y;

    protected override void Notify()
    {
        foreach (var observer in observers)
            observer.Update(x, y);
    }

    public void IncreaseX()
    {
        x++;
        Notify();
    }

    public void DecreaseX()
    {
        x--;
        Notify();
    }

    public void IncreaseY()
    {
        y++;
        Notify();
    }

    public void DecreaseY()
    {
        y--;
        Notify();
    }
}

Widok

  • Skoro widok jest obserwatorem modelu, to implementuje interfejs IObserver, w którym zdefiniowana jest metoda Update.
  • Widok otrzymuje powiadomienia o zmianie modelu za pomocą metody Update, którą wywołuje model przekazując do argumentów tej metody informacje o jego nowym stanie.
public interface IObserver
{
    void Update(int x, int y);
}
  • Metoda Update widoku bazując na argumentach przekazanych od modelu, odświeża swój widok.
  • Poniżej przedstawiam przykładową implementację widoku, który jest konsolą. W wyniku wywołania metody Update, widok maluje się na nowo umieszczając znak '#' na miejscu przesuniętym w prawo o wartość 'x' oraz w dół o wartość 'y'. Za każdym razem gdy model zmieni swój stan, czyli gdy zmienne x i y zmienią się w modelu, wywoła on metodę Update przekazując te wartości do widoku, który namaluje się ponownie umieszczając znak '#' w nowym miejscu.
public class ConsoleView : IObserver
{
    private readonly int width;
    private readonly int height;

    public ConsoleView(int width, int height)
    {
        this.width = width;
        this.height = height;
    }

    public void Update(int x, int y)
    {
        Render(x, y);
    }

    private void Render(int x, int y)
    {
        Console.Clear();

        for (int i = 0; i < height; ++i)
        {
            for (int j = 0; j < width; ++j)
            {
                if (y == i && x == j)
                    Console.Write("#");
                else
                    Console.Write(" ");
            }
            Console.WriteLine();
        }
    }
}
  • Widać tutaj, że możemy implementować wiele różnych rodzajów widoków bez konieczności zmiany implementacji modelu. Wystarczy tylko zaimplementować interface IObserver.
  • Inne przykłady widoku, jakie można by zaimplementować to np. WinForms, lub bardziej odjechane, nic nie mające wspólnego z monitorem a na przykład z głośnikami, jak adapter TTS "Text To Speech", który po każdym uaktualnieniu powie nowe współrzędne x i y.
  • Aby użytkownik mógł za pomocą widoku sterować aplikacją, widok zawiera w sobie instancję Kontrolera.
  • Widok zawiera publiczną metodę SetController, za pomocą której, przypisuje sobie ten kontroler.
  • Kontroler będzie służył widokowi do delegowania akcji, jakie wykonał na widoku użytkownik.
  • Metoda SetController definiowana jest w interfejsie, który każdy widok implementuje.
public interface IView
{
    void SetController(IController controller);
}

public class ConsoleView : IObserver, IView
{
    private IController controller;

    // ... pozostała cześć klasy pozostaje niezmieniona
        
    public void SetController(IController controller)
    {
            this.controller = controller;
    }
}
  • Między widokiem a kontrolerem jest wzorzec Strategia. Kontroler jest strategią widoku.
  • W obiekcie widoku znajdują się metody umożliwiające interakcję użytkownika z widokiem.
  • Widok nie wie jak ma zostać obsłużona dana akcja podjęta przez użytkownika.
  • Widok ma dwa zadania:
    • pierwsze - bycie interfejsem wyświetlającym informacje, o których został poinformowany przez model za pośrednictwem metody Update
    • drugie - umożliwienie interakcji z użytkownikiem, przekazując wywołania do kontrolera, który podejmie decyzję co zrobić z danym żądaniem
  • Zadaniem widoku nie jest obsługa interakcji użytkownika.
  • Widok oddelegowuje żądania użytkownika do kontrolera.
  • Na potrzeby przykładu zaimplementuję możliwość zmieniania wartości x i y za pomocą strzałek na klawiaturze. Zdarzenia wciskania strzałek widok oddeleguje do kontrolera wywołując na tym kontrolerze odpowiednie metody.
  • Poniżej jeszcze raz, tym razem już kompletna implementacja klasy ConsoleView. W porównaniu do poprzednich implementacji pojawiła się nowa metoda Navigate, która czeka na przyśnięcia strzałek klawiatury.
public class ConsoleView : IObserver, IView
{
    private IController controller;

    private readonly int width;
    private readonly int height;

    public ConsoleView(int width, int height)
    {
        this.width = width;
        this.height = height;
    }

    public void Update(int x, int y)
    {
        Render(x, y);
    }

    private void Render(int x, int y)
    {
        Console.Clear();

        for (int i = 0; i < height; ++i)
        {
            for (int j = 0; j < width; ++j)
            {
                if (y == i && x == j)
                    Console.Write("#");
                else
                    Console.Write(" ");
            }
            Console.WriteLine();
        }
    }

    public void SetController(IController controller)
    {
        this.controller = controller;
    }

    public void Navigate()
    {
        while (true)
        {
            var key = Console.ReadKey();

            switch (key.Key)
            {
                case ConsoleKey.UpArrow:
                    controller.MoveUp();
                    break;
                case ConsoleKey.DownArrow:
                    controller.MoveDown();
                    break;
                case ConsoleKey.RightArrow:
                    controller.MoveRight();
                    break;
                case ConsoleKey.LeftArrow:
                    controller.MoveLeft();
                    break;
                case ConsoleKey.Escape:
                    return;
            }
        }
    }
}

Kontroler

  • Kontroler zapewnia widokowi inteligencję.
  • Zawiera instancję modelu, do którego deleguje wywołania otrzymane od widoku.
  • Obsługuje wywołania otrzymane od widoku.
  • Pobiera dane wejściowe użytkownika i określa ich znaczenie dla modelu. Odpowiada za interpretację danych wejściowych i wykonanie odpowiednich operacji na modelu.
  • Żąda od modelu zmiany stanu.
  • Określa, jakie operacje powinny być wykonane na modelu.
  • Oddziela logikę sterowania od widoku separując od siebie widok i model.
  • Między widokiem a kontrolerem jest wzorzec Strategia. Kontroler jest zachowaniem widoku.
  • Zmieniając kontroler zmienimy zachowanie widoku, bez konieczności zmiany widoku.
  • Widok jest obiektem konfigurowalnym za pomocą odpowiedniej strategii, którą zapewnia kontroler.
  • Najpierw widok przekazuje kontrolerowi informacje o czynnościach użytkownika, następnie kontroler inicjuje operacje modelu.
  • Kontroler może wykonywać pewną pracę związaną z określaniem, jakie metody modelu mają zostać wywołane, ale nigdy nie należy to do funkcji określanych jako, "logika aplikacji". Logika aplikacji to kod, który zarządza i operuje danymi. Jest on w całości zawarty w modelu.
public class Controller : IController
{
    private readonly IModel model;

    public Controller(IModel model)
    {
        this.model = model;
    }

    public void MoveRight()
    {
        model.IncreaseX();
    }

    public void MoveLeft()
    {
        model.DecreaseX();
    }

    public void MoveUp()
    {
        model.DecreaseY();
    }

    public void MoveDown()
    {
        model.IncreaseY();
    }
}

Nowy wydajniejszy widok

Dotychczas zaimplementowany widok jest mało wydajny. Każde ponowne narysowanie znaku '#' pociąga za sobą drukowanie wszystkich wierszy konsoli na nowo.
Napisałem nową wydajniejszą wersję widoku. Dzięki wzorcu MVC logika aplikacji od wyświetlania interfejsu jest sprytnie odzielona, tak, że napisanie wydajnieszej wersji widoku nie pociąga za sobą żadnych zmian w logice aplikacji. Nie ma innych zmian w całej aplikacji, jest tylko nowa wersja widoku.
W nowym widoku nie rysuję wszystkich linii na nowo. Efekt poruszania się znaku '#' rozwiązałem za pomocą metody SetCursorPosition klasy Console. Dodatkowo pokusiłem się o namalowanie tła. Implementacja wygląda tak:
public class ConsoleView : IObserver, IView
{
    private IController controller;

    private readonly int width;
    private readonly int height;

    private int currentX;
    private int currentY;

    public ConsoleView(int width, int height)
    {
        this.width = width;
        this.height = height;
        RenderBackground();
    }

    public void SetController(IController controller)
    {
        this.controller = controller;
    }

    public void Update(int x, int y)
    {
        Render(x, y);
    }

    public void Navigate()
    {
        while (true)
        {
            var key = Console.ReadKey();

            switch (key.Key)
            {
                case ConsoleKey.UpArrow:
                    controller.MoveUp();
                    break;
                case ConsoleKey.DownArrow:
                    controller.MoveDown();
                    break;
                case ConsoleKey.RightArrow:
                    controller.MoveRight();
                    break;
                case ConsoleKey.LeftArrow:
                    controller.MoveLeft();
                    break;
                case ConsoleKey.Escape:
                    return;
            }
        }
    }

    private void Render(int x, int y)
    {
        ClearLast(currentX, currentY);
        PrintNew(x, y);
    }

    private void ClearLast(int x, int y)
    {
        Console.SetCursorPosition(x, y);
        Console.Write(" ");
    }

    private void PrintNew(int x, int y)
    {
        if (x >= 0 && y >= 0)
        {
            Console.SetCursorPosition(x, y);
            Console.Write("#");

            currentX = x;
            currentY = y;
        }
    }

    private void RenderBackground()
    {
        Console.BackgroundColor = ConsoleColor.Blue;

        for (int i = 0; i < height; ++i)
        {
            for (int j = 0; j < width; ++j)
                Console.Write(" ");

            Console.WriteLine();
        }

        Console.SetCursorPosition(0, 0);
    }
}

WinForms widok

W celach ćwiczebno-zaczepnych zaimplementowałem, tak dla sportu, nową całkiem inną wersję widoku, opartą o inną technologię budowania interfejsu, nie zmieniając przy tym ani jednej linijki kodu w logice aplikacji.
public partial class WinFormView : Form, IObserver, IView
{
    private IController controller;

    private readonly int width;
    private readonly int height;
    private readonly int scale;

    private readonly Graphics graphics;

    public WinFormView(int width, int height, int scale)
    {
        this.width = width*scale;
        this.height = height*scale;
        this.scale = scale;

        InitializeComponent();
        ClientSize = new Size(this.width, this.height);
        graphics = CreateGraphics();
    }

    public void Update(int x, int y)
    {
        Render(x, y);
    }

    public void SetController(IController controller)
    {
        this.controller = controller;
    }

    private void Render(int x, int y)
    {
        ClearLast();
        PrintNew(x, y);
    }

    private void ClearLast()
    {
        graphics.Clear(Color.Azure);
    }

    private void PrintNew(int x, int y)
    {
        var point = new Point(x*scale, y*scale);
        var size = new Size(scale, scale);
        var rectangle = new Rectangle(point, size);
        graphics.FillEllipse(Brushes.Black, rectangle);
    }

    protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
    {
        if (keyData == Keys.Up)
        {
            controller.MoveUp();
            return true;
        }

        if (keyData == Keys.Down)
        {
            controller.MoveDown();
            return true;
        }

        if (keyData == Keys.Left)
        {
            controller.MoveLeft();
            return true;
        }

        if (keyData == Keys.Right)
        {
            controller.MoveRight();
            return true;
        }

        return base.ProcessCmdKey(ref msg, keyData);
    }
}
Na starcie aplikacji, należy stworzyć nowy obiekt widoku, zarejestrować go jako słuchacza w modelu, oraz wpisać do tego widoku kontroler.
internal class Program
{
    private static void Main(string[] args)
    {
        int width = 50;
        int height = 50;
        int scale = 10;

        var model = new Model();
        var controller = new Controller(model);
        var consoleView = new ConsoleView(width, height);
        var winFormsView = new WinFormView(width, height, scale);

        model.Subscribe(consoleView);
        model.Subscribe(winFormsView);

        consoleView.SetController(controller);
        winFormsView.SetController(controller);

        Task.Factory.StartNew(() => consoleView.Navigate());
        Application.Run(winFormsView);
    }
}

Wzorcowi MVC mówię SPRAWDZAM

zmieniając logikę w modelu i nie zmieniając ani jednej linijki w obu widokach i ich kontrolerze.
Nowa logika, wnosi do modelu cztery ograniczenia:
  • Wartości x i y nie mogą być mniejsze niż zero.
  • Wartości x i y nie mogą być większe niż zdefiniowane ograniczenie.
  • Gdy x lub y posiada wartość zero i zostanie wywołane żądanie zmniejszenia to nowa wartość ma wynosić największą możliwą.
  • Gdy x lub y osiągnie największą możliwą wartość i zostanie wywołane żądanie zwiększenia wówczas wartość ta ma się wyzerować.
public class Model : Observable, IModel
{
    private readonly int minX;
    private readonly int maxX;

    private readonly int minY;
    private readonly int maxY;

    private int x;
    private int y;

    public Model(int minX, int maxX, int minY, int maxY)
    {
        this.minX = minX;
        this.maxX = maxX;
        this.minY = minY;
        this.maxY = maxY;
    }

    protected override void Notify()
    {
        foreach (var observer in observers)
            observer.Update(x, y);
    }

    public void IncreaseX()
    {
        if (x == maxX - 1)
            x = 0;
        else
            x++;

        Notify();
    }

    public void DecreaseX()
    {
        if (x == minX)
            x = maxX - 1;
        else
            x--;

        Notify();
    }

    public void IncreaseY()
    {
        if (y == maxY - 1)
            y = 0;
        else
            y++;

        Notify();
    }

    public void DecreaseY()
    {
        if (y == minY)
            y = maxY - 1;
        else
            y--;

        Notify();
    }
}
Wszystko działa jak się można było spodziewać. Wciskając klawisze nawigacyjne klawiatury, przesuwamy kursorem w wybranym kierunku, a jeśli osiągniemy granicę przestrzeni, wówczas przeskakujemy na przeciwległą stronę powierzchni, i ani jedna linijka widoku i kontrolera nie została przy tym zmieniona.

Na koniec

Powstała aplikacja, która umożliwia przesuwanie znaczka po ekranie, co nie jest jakimś wyczynem. Celem tej aplikacji jest przećwiczenie teori o MVC, na kodzie nie w ASP.NET MVC.
C.D.N... Będzie jeszcze Passive MVC ...
KOD APLIKACJI JEST NA GITHUB... TeoVincent/MVC-by-example

Update #1

Aby podkreślić cechę, która charakteryzuję odmianę Active MVC zmodyfikowałem implementację Modelu.
W poprzedniej wersji (powyżej) Model zmieniał swoje wartości gdy Kontroler wywołał na Modelu odpowiednie metody. Kontroler wywoływał te metody w wyniku przyciśnięcia na widoku strzałek klawiatury. Efekt był taki, że w wyniku przyciskania strzałek kropka na Widoku przesuwała się o jedno miejsce w danym kierunku.
W nowej wersji zaimplementowałem metodę OverAndOverAgain, która w dodatkowym tasku, non-stop zwiększa lub zmniejsza zmienną x lub y w stałych odstępach czasu. Przyciśnięcie strzałek powoduje wybór czy będzie to zwiększanie czy zmniejszanie, oraz czy zmieniać się będzie wartość x czy y. Efekt jest taki, że kropka niezależnie, cały czas przesuwa się po ekranie, a przyciśnięcie strzałek na klawiaturze zmienia kierunek tego poruszania.
Na tym przykładzie widać, wyraźniej, jak na dłoni, że Model zmienia swój stan niezależnie od Kontrolera oraz Model niezależnie od Kontrolera odpowiedzialny jest za powiadamianie Widoków o zmianie jego stanu.
public class Model : IModel, IObservable
{
    private readonly int minX;
    private readonly int maxX;

    private readonly int minY;
    private readonly int maxY;
        
    private int x;
    private int y;

    private readonly List<IObserver> observers;
    private readonly Task overAndOverAgain;
    private Action action;
    private const int INTERVAL = 50;
    private readonly object lockObj;

    public Model(int minX, int maxX, int minY, int maxY)
    {
        this.minX = minX;
        this.maxX = maxX;
        this.minY = minY;
        this.maxY = maxY;

        observers = new List<IObserver>();

        lockObj = new object();
        action = () => { };
        overAndOverAgain = Task.Factory.StartNew(OverAndOverAgain);
    }

    public void Subscribe(IObserver observer)
    {
        observers.Add(observer);
    }

    public void IncreaseX()
    {
        lock (lockObj)
            action = IncX;
    }

    public void DecreaseX()
    {
        lock(lockObj)
            action = DecX;
    }

    public void IncreaseY()
    {
        lock (lockObj)
            action = IncY;
    }

    public void DecreaseY()
    {
        lock (lockObj)
            action = DecY;
    }

    private void IncX()
    {
        if (x == maxX-1)
            x = 0;
        else
            x++;

        Notify();
    }

    private void DecX()
    {
        if (x == minX)
            x = maxX-1;
        else
            x--;

        Notify();
    }

    private void IncY()
    {
        if (y == maxY-1)
            y = 0;
        else
            y++;

        Notify();
    }

    private void DecY()
    {
        if (y == minY)
            y = maxY-1;
        else
            y--;

        Notify();
    }

    private void Notify()
    {
        foreach (var observer in observers)
            observer.Update(x, y);
    }

    private void OverAndOverAgain()
    {
        while (true)
        {
            lock (lockObj)
            {
                action();
                overAndOverAgain.Wait(INTERVAL);
            }
        }
    }
}

0 komentarze :

Prześlij komentarz