MVP-powiązanie Presenter-View

0

Czy byłby ktoś na tyle miły i postarał się wytłumaczyć mi jak poprawnie powiązać Presenter z View? Załóżmy, że po kliknięciu na kontrolkę na formie chcę otworzyć okno dialogowe i pobrać ścieżkę do wybranego pliku. Jak coś takiego zaimplementować? Sporo googlowałem, ale ciężko o jakieś jednoznaczne informacje poparte przykładami. Jak na razie mam coś takiego:
View:

public partial class Form1 : Form, IView
    {
        private readonly IPresenter _presenter;

        public Form1()
        {
            InitializeComponent();
            _presenter=new PresenterMainForm(this);

        }

        private void openFileToolStripMenuItem_Click(object sender, EventArgs e)
        {
            _presenter.OpenFileDialog();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            
        }
    } 

Presenter:

class PresenterMainForm : IPresenter
    {
        private const string OpenFileDialogTitle = "Open File";
        private const string OpenFileDialogInitialDirecotry = @"C:\";
        private const string OpeningFileErrorMessage = "Something went wrong when trying to open the chosen file";

        private string _fileToBeOpen;

        private IView _view;
        public PresenterMainForm(IView view)
        {
            _view = view;
            
        }

        public void OpenFileDialog()
        {
            var openFileDialog = new OpenFileDialog();
            openFileDialog.Title = OpenFileDialogTitle;
            openFileDialog.InitialDirectory = OpenFileDialogInitialDirecotry;

            if (openFileDialog.ShowDialog() == DialogResult.OK)
            {
                try
                {
                    _fileToBeOpen = openFileDialog.FileName;
                    Console.WriteLine(_fileToBeOpen);
                }
                catch(IOException)
                {
                    MessageBox.Show(OpeningFileErrorMessage);
                }
            }
        }
    } 

IPresenter:

interface IPresenter
    {
        void OpenFileDialog();
    } 

No ale to chyba bieda straszna, bo tu Presenter operuje na kontrolkach... Byłbym ogromnie wdzięczny za jakiś prosty przykład lub chociaż wypisanie w punktach jak to poprawnie zaimplementować.

EDIT: Czy może ten kod:

public void OpenFileDialog()
        {
            var openFileDialog = new OpenFileDialog();
            openFileDialog.Title = OpenFileDialogTitle;
            openFileDialog.InitialDirectory = OpenFileDialogInitialDirecotry;

            if (openFileDialog.ShowDialog() == DialogResult.OK)
            {
                try
                {
                    _fileToBeOpen = openFileDialog.FileName;
                    Console.WriteLine(_fileToBeOpen);
                }
                catch(IOException)
                {
                    MessageBox.Show(OpeningFileErrorMessage);
                }
            } 

wykonywać w jakiś sposób w View i tylko w jakiś sposób zwracać wartość do Presentera? Jeśli tak to jak? Jakimś seterem w Presenterze?

1

Tutaj masz opisane dwa różne podejścia. Przykłady są całkiem sensowne.

The Passive View:
The Passive View - opis
The Passive View - przykład

The Supervising Controller Pattern:
The Supervising Controller - opis
The Supervising Controller - przykład

3
Endrew napisał(a):

No ale to chyba bieda straszna, bo tu Presenter operuje na kontrolkach...

To zdecydowanie nie jest MVP.

Byłbym ogromnie wdzięczny za jakiś prosty przykład lub chociaż wypisanie w punktach jak to poprawnie zaimplementować.

  1. Użytkownik klilka w przycisk.
  2. Metoda obsługi zdarzenia kliknięcia woła metodę ZróbCośZPlikiemw Prezenterze.
  3. Metoda ZróbCośZPlikiemz Prezentera woła metodę GetFilePathz Widoku.
  4. W metodzie tej używasz OpenFileDialog albo JakakolwiekKontrolkaPozwalającaNaPobranieNazwyPliku (z punktu widzenia Prezentera to nie ma znaczenia). Metoda ta zwraca string z nazwą wybranego pliku.
  5. Zwróconą wartość pobierasz i używasz do czegoś tam w kodzie Prezentera.

Jakimś seterem w Presenterze?

Widok w ogóle nie jest aktywny i niczego w Prezenterze nie ustawia. To Prezenter prosi Widok o dane, a ten je zwraca przez metody lub właściwości.
Zresztą, setterów lepiej w ogóle nie używać.

0

Panowie @DibbyDum i @somekind wielkie dzięki . Prosiłbym o sprawdzenie poprawności "pełnego" wzorca. Idea jest taka:
1.Wczytanie ścieżki do pliku w View.
2.Przekazanie ścieżki poprzez Presenter do Modelu.
3.Przetworzenie danych w modelu.
4.Update wykresu w View w oparciu o dane z Modelu.

Interfejsy:

public interface IView
    {
        string GetFilePath();
        void UpdateChart(List<StockPrice> stockPrices);

    } 
public interface IPresenter
    {
        void HandleOpenFileClick();
        void SetView(IView view);
        void SetModel(IModel model);
    } 
public interface IModel
    {
        void ReadFile(string filePath);
        List<StockPrice> GetStockPrices();
        void TransformLinesIntoStockPrices();

    } 

View:

public partial class Form1 : Form, IView
    {
        private readonly IPresenter _presenter;

        private const string OpenFileDialogTitle = "Open File";
        private const string OpenFileDialogInitialDirecotry = @"C:\";
        private const string OpeningFileErrorMessage = "Something went wrong when trying to open the chosen file";
        private string _fileToBeOpen;

         



        public Form1(IPresenter presenter)
        {
            InitializeComponent();
            this._presenter = presenter;
        }

        private void openFileToolStripMenuItem_Click(object sender, EventArgs e)
        {
            _presenter.HandleOpenFileClick();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
        }

        public string GetFilePath()
        {
            var openFileDialog = new OpenFileDialog
            {
                Title = OpenFileDialogTitle,
                InitialDirectory = OpenFileDialogInitialDirecotry
            };

            if (openFileDialog.ShowDialog() == DialogResult.OK)
            {
                try
                {
                    _fileToBeOpen = openFileDialog.FileName;
                }
                catch (IOException)
                {
                    MessageBox.Show(OpeningFileErrorMessage);
                }
            }
            return _fileToBeOpen;
        }

        public void UpdateChart(List<StockPrice> stockPrices)
        {
            

            Series price = new Series("price"); // <<== make sure to name the series "price"
            chart1.Series.Add(price);

            // Set series chart type
            chart1.Series["price"].ChartType = SeriesChartType.Candlestick;

            // Set the style of the open-close marks
            chart1.Series["price"]["OpenCloseStyle"] = "Triangle";

            // Show both open and close marks
            chart1.Series["price"]["ShowOpenClose"] = "Both";

            // Set point width
            chart1.Series["price"]["PointWidth"] = "1.0";

            // Set colors bars
            chart1.Series["price"]["PriceUpColor"] = "Green"; // <<== use text indexer for series
            chart1.Series["price"]["PriceDownColor"] = "Red"; // <<== use text indexer for series

            var kursy = stockPrices;
            for (int i = 0; i < kursy.Count; i++)
            {
                Console.WriteLine(kursy.ElementAt(i).Data);
                // adding date and high
                chart1.Series["price"].Points.AddXY(DateTime.ParseExact(kursy[i].Data, "d/MM/yyyy", CultureInfo.InvariantCulture), kursy[i].Max);

                // adding low
                chart1.Series["price"].Points[i].YValues[1] = kursy[i].Min;
                //adding open
                chart1.Series["price"].Points[i].YValues[2] = kursy[i].Open;
                // adding close
                chart1.Series["price"].Points[i].YValues[3] = kursy[i].Close;
            }
            chart1.ChartAreas["ChartArea1"].AxisX.MajorGrid.Enabled = false;
            chart1.ChartAreas["ChartArea1"].AxisY.MajorGrid.Enabled = false;

            chart1.Refresh();
        }
    } 

Presenter:

internal class PresenterMainForm : IPresenter
    {
        private string _fileToBeOpen;

        private IView _view;
        private IModel _model;

        public PresenterMainForm()
        {
        }

        public void HandleOpenFileClick()
        {
            _fileToBeOpen = this._view.GetFilePath();
            Console.WriteLine(_fileToBeOpen);
            _model.ReadFile(_fileToBeOpen);
            _model.TransformLinesIntoStockPrices();
            _view.UpdateChart(_model.GetStockPrices());
        }

        public void SetView(IView view)
        {
            this._view = view;
        }

        public void SetModel(IModel model)
        {
            this._model = model;
        }
    } 

Model:

public class Model: IModel
    {
        public string[] Lines = new string[21];
        public List<StockPrice> StockPrices = new List<StockPrice>();

        public void ReadFile(string filePickedByUser)
        {
            var file = new StreamReader(filePickedByUser);

            for (int i = 0; i <= 20; i++)
            {
                var line = file.ReadLine();
                Lines[i] = line;
            }
        }

        public List<StockPrice> GetStockPrices()
        {
            return this.StockPrices;
        }

        public void TransformLinesIntoStockPrices()
        {
            var x = 1;
            foreach (var line in Lines)
            {
                var parts = line.Split(',');
                var date = (x.ToString(CultureInfo.InvariantCulture) + "/01/2012");

                var open = float.Parse(parts[4], CultureInfo.InvariantCulture.NumberFormat);
                var close = float.Parse(parts[7], CultureInfo.InvariantCulture.NumberFormat);
                var max = float.Parse(parts[6], CultureInfo.InvariantCulture.NumberFormat);
                var min = float.Parse(parts[5], CultureInfo.InvariantCulture.NumberFormat);

                var kurs = new StockPrice(date, open, close, max, min);
                StockPrices.Add(kurs);

                Console.WriteLine(line);
                x++;
            }
        }
    } 
1

Teraz jest to dużo bardziej zgodne z MVP. Za to kilka innych spraw wymaga poprawki.

  1. Do czego służą interfejsy: IPresenter i IModel? Nie widzę, żeby pełniły w tym kodzie jakąś funkcję, wygląda tak, jakby były tylko po to, aby być.
  2. W klasie Form1:
 private string _fileToBeOpen;

Czemu to jest pole? Przecież służy tylko do zwracania wartości z metody, wystarczyłaby zmienna w metodzie.
3) Do czego służy ten kod: var kursy = stockPrices; // a to do czego?
4) Tak przy okazji - pisz kod w jednym języku, albo polski, albo angielski, lepiej oczywiście angielski.
5) To aplikacja okienkowa, a Ty często używasz Console.WriteLine. W jakim celu?
6) Skoro trzymasz się MVP, to JEDYNE miejsce, w którym wyświetlasz coś na ekran (czyli korzystasz z klasy Console) to Widok.
7) W Prezenterze masz:

private IModel _model;

W jakim celu?
8) W jakim celu te pola są publiczne:

public string[] Lines = new string[21];
public List<StockPrice> StockPrices = new List<StockPrice>();
  1. I czemu te pola w ogóle istnieją? W Modelu wystarczyłaby jedna metoda: List<StockPrice> GetStockPrices(string filePath), która wywoła wewnętrznie pozostałe metody. Byłaby wywoływana w Prezenterze i do niego zwracała wynik. Żadne pola nie są do tego potrzebne. Pola to zło, pola przechowujące dane to największe zło.
  2. Metoda ReadFile to to samo co dostępna w .NET metoda File.ReadAllLines, tylko gorzej zaimplementowana, bo nie zwalniasz zasobów.
  3. Jak masz robić coś takiego:
            var x = 1;
            foreach (var line in Lines)
            {
               //
                x++;
            }

to lepiej użyj pętli for.

  1. I na koniec najważniejsze - nazewnictwo.
    a) Nazwy takie jak: IView, IModel, IPresenter nic nie mówią.
    b) Nikt nie domyśli się że PresenterMainForm implementuje IPrezenter.
    c) Celem MVP jest odizolowanie Prezentera od technologii interfejsu użytkownika, a więc Prezenter z definicji to jest coś, co nie służy do obsługi Formów czy Buttonów. Bo ich może nie być, bo Widok równie dobrze może być stroną WWW albo konsolą. Dlatego w nazwach Prezenterów i ich metod nie używa się takich słów jak "Form" czy "HandleClick".
    d) To, że w nazwie wzorca występuje słowo "model" nie znaczy, że klasy Modelu mają mieć to słowo w swoich nazwach. Model to ogólne określenie na wszystko, co nie jest Widokiem ani Prezenterem.
    e) Ergo, te klasy u Ciebie powinny nazywać się np. tak: IStockPricesView, StockPricesPresenter, StockPricesReader.
0

@somekind dzięki, że Ci się chce :) Kolejne parę pytań. Obecnie wygląda to tak:
Presenter:

internal class StockPricesPresenter : IPresenter
    {
        private string _fileToBeOpen;

        private IView _view;
        private IModel _model;

        public StockPricesPresenter()
        {
        }

        public void UpdateChart()
        {
            _fileToBeOpen = this._view.GetFilePath();
            _view.UpdateChart(_model.GetStockPrices(_fileToBeOpen)); // czy może od razu _view.UpdateChart(_model.GetStockPrices(this._view.GetFilePath())) ?
        }

        public void SetView(IView view)
        {
            this._view = view;
        }

        public void SetModel(IModel model)
        {
            this._model = model;
        }
    } 

Model:

public class StockPricesReader: IModel
    {

        public string[] ReadFile(string filePickedByUser)
        {
            var file = new StreamReader(filePickedByUser);
            var lines = new string[21];
            for (int i = 0; i <= 20; i++)
            {
                var line = file.ReadLine();
                lines[i] = line;
            }

            file.Close();

            return lines;
        }
        // 2 takie same returny, na co by to zamienić??
        public List<StockPrice> GetStockPrices(string filePath)
        {
            var stockPrices = new List<StockPrice>();
            var lines=this.ReadFile(filePath);
            
            return this.TransformLinesIntoStockPrices(lines);
        }

        public List<StockPrice> TransformLinesIntoStockPrices(string[] lines)
        {
            var stockPrices = new List<StockPrice>();
            var x = 1;
            for (int i = 0; i < lines.Length; i++)
            {
                var line = lines[i];
                var parts = line.Split(',');
                var date = (x.ToString(CultureInfo.InvariantCulture) + "/01/2012");

                var open = float.Parse(parts[4], CultureInfo.InvariantCulture.NumberFormat);
                var close = float.Parse(parts[7], CultureInfo.InvariantCulture.NumberFormat);
                var max = float.Parse(parts[6], CultureInfo.InvariantCulture.NumberFormat);
                var min = float.Parse(parts[5], CultureInfo.InvariantCulture.NumberFormat);

                var kurs = new StockPrice(date, open, close, max, min);
                stockPrices.Add(kurs);

                Console.WriteLine(line);
                x++;
            }

            return stockPrices;
        }
    } 
  1. Które rozwiązanie w UpdateChart() w Presenterze jest lepsze?
 _view.UpdateChart(_model.GetStockPrices(_fileToBeOpen));

czy może:

_view.UpdateChart(_model.GetStockPrices(this._view.GetFilePath())); 
  1. W modelu 2 metody zwracają to samo co wydaje się być troszkę bez sensu. Jak to lepiej rozwiązać?
  2. Jeśli będę chciał rozbudować ten projekt to do kolejnych funkcjonalności dokładać kolejne presentery i modele, tzn, do każdej funkcjonalności nowy presenter i model czy może jakoś te funkcjonalności grupować? No bo chyba jeden presenter i model to tak nie za bardzo.
0

Jeden widok = jeden prezenter. Przekazywanie danych odbywa się pomiędzy prezenterami.

0
Endrew napisał(a):
  1. Które rozwiązanie w UpdateChart() w Presenterze jest lepsze?
 _view.UpdateChart(_model.GetStockPrices(_fileToBeOpen));

czy może:

_view.UpdateChart(_model.GetStockPrices(this._view.GetFilePath())); 

Najlepiej tak:

public void UpdateChart()
{
    string filePath = this._view.GetFilePath();
    var stockPrices = this._stockPricesReader.GetStrockPrices(filePath);
    this._view.UpdateChart(stockPrices);  
}

Jedna linijka - jedna kropka. Tak się łatwiej czyta i debuguje.

  1. W modelu 2 metody zwracają to samo co wydaje się być troszkę bez sensu. Jak to lepiej rozwiązać?

A co w tym jest złego? Jedna zwraca wynik transformacji linii pliku do obiektu, druga używa jej w celu wykonania większego zadania. W tym nie ma nic złego.

  1. Jeśli będę chciał rozbudować ten projekt to do kolejnych funkcjonalności dokładać kolejne presentery i modele, tzn, do każdej funkcjonalności nowy presenter i model czy może jakoś te funkcjonalności grupować? No bo chyba jeden presenter i model to tak nie za bardzo.

Tak jak napisał kolega wyżej, prezentery tworzy się do widoków, czyli w praktyce okienek albo kontrolek GUI.
Model to mogą być tysiące klas zorganizowane w 50 warstw. Jeden Prezenter może korzystać z wielu klas z Modelu, to nie jest powiązanie 1:1.

A tak poza tym:

  1. Usuń pole _fileToBeOpen z Prezentera, bo jest niepotrzebne. Nie ma sensu tworzyć pól, jeśli używa się ich tylko w jednej metodzie.
  2. IPresenter niech nazywa się IStockPricesPresenter (chociaż moim zdaniem w ogóle nie jest potrzebny taki twór).
  3. IModel niech nazywa się IStockPricesReader.
  4. Pole _model niech nazywa się _stockPricesReader i inicjalizuj jej w konstruktorze, a metodę SetModel wywal.
  5. W metodzie ReadFile użyj using: http://www.dotnetperls.com/using albo zastąp przez File.ReadAllLines: https://msdn.microsoft.com/en-us/library/s2tte0y1%28v=vs.110%29.aspx
  6. Metody ReadFile i TransformLinesIntoStockPrices niech będą prywatne.
  7. Usuń zmienną x z TransformLinesIntoStockPrices, przecież nigdzie jej nie używasz.

1 użytkowników online, w tym zalogowanych: 0, gości: 1