BETA
Aby się zalogować, najpiew wybierz portal.
Aby się zarejestrować, najpiew wybierz portal.
Podaj słowa kluczowe
Słowa kluczowe muszą mieć co najmniej 3 sąsiadujące znaki alfanumeryczne
Pole zawiera niedozwolone znaki

Baza wiedzy











Wzorce projektowe warstwy prezentacyjnej na platformie .NET

31-01-2006 23:08 | User 83812
Artykuł ten ma na celu zachęcenie do zapoznania się z wzorcami projektowymi, celami ich stosowania, a także korzyściami i stratami jakie wiążą się z wybraniem odpowiedniego wzorca. Wykorzystanie wzorów projektowych może pozytywnie wpłynąć na jakość tworzonego oprogramowania, łatwość jego testowania i utrzymywania.

Wstęp

Aplikacje www cały czas się rozwijają. Początkowo były to statyczne strony wykonane w HTML, później wspierano je skryptami CGI, PHP, następnie  zaczęto pisać w ASP, JSP, oraz ASP .NET 1.x, 2.0. Wraz z rozwojem technologii strony www stawały się coraz bardziej rozbudowane. Coraz częściej są to duże aplikacje z skomplikowaną logiką biznesową, w których pojawiają się problemy związane z redundancją kodu, oraz jego utrzymywaniem jego wysokiej jakości. Ze względu na to, że aplikacje są coraz bardziej złożone ważna jest możliwość oddzielenia od siebie poszczególnych części aplikacji: stron zawierający kod HTML, logikę biznesową, a także bazę danych i kod służący do jej obsługi pomiędzy różnych członków zespołu projektowego, tak aby mogli oni pracować niezależnie. Tworzenie dzisiejszych aplikacji jest dodatkowo utrudnione bardzo często zmieniającymi się wymaganiami klientów: zarówno funkcjonalnymi jak i niefunkcjonalnymi. By sprostać wymaganiom dzisiejszych aplikacji powstaje coraz więcej propozycji architektur aplikacji, oraz tzw. dobrych praktyk publikowanych przez społeczności jak np. Patterns And Practice. Najogólniej architekturę dzisiejszych aplikacji można przedstawić na poniższym rysunku.

architektura_num.JPG

                                                                                                                                            Rys. 1.   Architektura dzisiejszych aplikacji (źródło: [Pnp02])

Architektura aplikacji składa się z trzech podstawowych warstw logicznych (ang. layers), które w zależności od potrzeb można dekomponować na warstwy fizyczne (ang. tiers):

  • warstwa prezentacyjna zawierająca: komponenty IU (1), oraz komponenty procesów IU(2),
  • warstwa biznesowa, zawierająca: Interfejsy usług (6), przepływy biznesowe(3), komponenty biznesowe (4),  klasy biznesowe (8),
  • warstwa dostępu do danych: komponenty dostępu do danych (7), komponenty odpowiadające za komunikacje z innymi aplikacjami zorientowanymi usługowo (np. SOA), źródła danych (np. bazy danych), usługi.

Taka rozbudowana architektura wymaga włożenia większego wysiłku włożonego do rozbudowy i utrzymywania aplikacji, gdyż w wielu miejscach pojawią się te same lub podobne fragmenty kodu. Dodatkowo taki system stawał się trudniejszy do zrozumienia dla programistów, architektów, co powodowało wzrost kosztów tzw. TCO (ang. Total Cost of Ownership.). W celu eliminacji redundancji wspólny kod przenosiliśmy do jednej klasy (bądź grupy współdziałania), co mogło powodować zwiększenie złożoności naszego systemu. Aby uniknąć takich błędów ważne jest zrozumienie podstawowych przeszkód towarzyszących tworzeniu aplikacji, oraz zapoznanie się z możliwością ich rozwiązania. Jednym ze sposobów rozwiązania problemów jest wykorzystanie odpowiedniego wzorca projektowego (definicja: „Wzorce projektowe stanowią zbiór reguł określających jak osiągnąć konkretne cele w dziedzinie wytwarzania oprogramowania (Pree, 1994)”. ) Wzorzec projektowy można rozpatrywać w dwóch poziomach abstrakcji: projektowym i implementacyjnym. Oba poziomy różnią się tym, że drugi narzuca nam technologię implementacyjną.

grupa.jpg

                                                                                                                                  Rys. 2.   Grupa wzorców warstwy prezentacyjnej (źródło: [Pnp2003])

Jak wynika z rysunku rys. 2. istnieje wiele wzorców prezentacyjnej (więcej informacji o nich można znaleźć w [Pnp2003]), ale w artykule zostaną opisane tylko dwa z nich: Model – View – Controller, w skrócie MVC, oraz Page Controller , gdyż są najczęściej wykorzystywane.

Kontekst

                Wszystkie przykłady zamieszczone w tym artykule będą wykorzystywały bazę Northwind, z której pobierane będą dane o pracownikach, oraz użytkownik będzie miał możliwość filtrowania pracowników wg regionu.

Model – View – Controller

Model – View – Controller w teorii

 

                Jak wcześniej zostało przedstawione wzorce rozpatrujemy na dwóch poziomach, dlatego przy omawianiu tego wzorca zasada ta zostanie zachowana.

                Wzorzec MVC separuje na trzy grupy: model danych (klasy trwałe), interfejs użytkownika, oraz akcje wykonywane przez użytkowników na danych:

§         Model – zawiera klasy, które zarządzają zachowaniem i danymi związanymi z dziedziną aplikacji, związanych z żądaniem o informację związaną z i ich stanem (najczęściej pochodzącymi z klas View) bądź zmianą stanu (z klas Controller)

§         View – zawiera klasy odpowiadające za wyświetlanie danych użytkownikowi

§         Controller – zawiera klasy służące do interpretacji akcji użytkownika i na ich podstawie informowanie o odpowiednich zmianach widok lub/i model.

Ten podział ilustruje poniższy rysunek:

struktura MVC.JPG

                                                                                                                                                         Rys. 3.   Struktura wzorca MVC (źródło [Pnp2003])

Warto zauważyć, że kontroler i widok zależą od modelu, który nie jest zależny. Taka separacja umożliwia uniezależnienie widoku od pozostałej części aplikacji. W wielu aplikacjach szczególnie opartych na architekturze tzw. „grubego klienta” separacja pomiędzy widokiem i kontrolerem jest pomijana.

Na zakończenie wstępu teoretycznego warto wspomnieć, że MVC można spotkać w dwóch wariantach: aktywnym i pasywnym. Dokładny opis można znaleźć w [Fowler03].

Implementacja MVC w ASP .NET

                Przykład przedstawiający implementację wzorca MVC w ASP .NET ilustruje pobieranie z bazy danych informacji o pracownikach z możliwością wybrania pracowników z danego regionu (regiony będą pobierane z bazy do listy rozwijanej). Z pozoru tak prosta aplikacja może zostać napisana nie wykorzystując żadnego wzorca, jednak nie będzie to najlepsze rozwiązanie. Obsługę bazy, zdarzeń można zawrzeć w plikach code – behind,  które będą realizowały akcje, jakich oczekuje użytkownik. Takie podejście będzie miało dwie zasadnicze wady: po pierwsze kiedy w innym miejscu pojawi się potrzeba komunikacji z bazą taki kod się powieli, po drugie będzie utrudnione testowanie, gdyż tą samą funkcjonalność trzeba będzie przetestować w wielu miejscach. W konsekwencji otrzymamy system, który będzie trudniejszy w utrzymaniu, a także stanie się mniej zrozumiały.

View

                Implementacja aplikacji rozpoczęta zostanie od widoku. Będzie to prosta strona .aspx, która będzie odpowiadała za wyświetlenie pobranych danych dla użytkownika:

Kod odpowiedzialny za widok znajduje się w pliku pracownicy.aspx. Jej wygląd przedstawia poniższy rysunek:

view.JPG

                                                                                                                                                                                      Rys. 4.   Widok użytkownika

                Na tej stronie użytkownik ma możliwość filtrowania pracowników w zależności od regionu.. Po analizie zawartości strony pracownicy.aspx widać, że zawiera ona tylko kod html, służący do wyświetlenia odpowiednich danych w kolumnach tabeli, oraz listy rozwijanej zajmuje się kontroler. Przy takim rozwiązaniu możemy w bardzo łatwy sposób modyfikować sposób wyświetlania danych nie powodując konieczności zmian w kontrolerze bądź modelu.

Controller           

                Ta część aplikacji będzie odpowiedzialna za wykonywanie żądań wydawanych przez użytkownika widokowi. Innymi słowy będzie wykonywał odpowiednie zadania na polecenia widoku. W technologii ASP .NET będzie to tzw. Code – behind  dla klasy widoku, czyli plik pracownicy.aspx.cs . W nim musimy zawrzeć obsługę zdarzeń, które pozwolą na uzyskanie wyniku przedstawionego na powyższym rysunku.

                Na początku należy wypełnić kontrolki: employeeDG (DataGrid), oraz regionDropDownList odpowiednimi danymi. W tym celu obsługujemy zdarzenia PreRender obu kontrolek. Wypełnienie danymi DataGrid’a z pracownikami realizuje poniższy fragmentu kodu.

private void employeesDG_PreRender(object sender, System.EventArgs e)

{

      if (!IsPostBack)

            FillEmployeesDataGrid();

}

private void FillEmployeesDataGrid()

{

      try

      {                      

            employeesDG.DataSource = AppLogic.GetEmployees();

            employeesDG.DataBind();

      }

      catch(Exception caught) { throw; }

}

private void FillEmployeesDataGrid( int regionID )

{

      try

      {                      

employeesDG.DataSource = AppLogic.GetEmployeesFromRegion(regionID);

            employeesDG.DataBind();

      }

      catch(Exception caught)

      {

            throw;

      }

}

                Następnie należy obsłużyć zdarzenie ItemDataBound DataGrid’a by odpowiednio wyświetlić związane w zdarzeniu PreRender z kontrolką dane.

private void employeesDG_ItemDataBound(object sender,DataGridItemEventArgs e)

{                           

if ( itemType == ListItemType.Item ||

     itemType == ListItemType.AlternatingItem )

      {

         idLabel        = e.Item.FindControl("idLabel") as Label;

         firstNameLabel = e.Item.FindControl("firstNameLabel") as Label;

         lastNameLabel  = e.Item.FindControl("lastNameLabel") as Label;

         titleLabel     = e.Item.FindControl("titleLabel") as Label;

         regionLabel    = e.Item.FindControl("regionLabel") as Label;

         cityLabel      = e.Item.FindControl("cityLabel") as Label;

         addreessLabel  = e.Item.FindControl("addressLabel") as Label;

 

                       

         id = Convert.ToInt32(DataBinder.Eval(e.Item.DataItem,"EmployeeID"));                      

      firstName = Convert.ToString(DataBinder.Eval(e.Item.DataItem, "FirstName"));

      lastName = Convert.ToString(DataBinder.Eval(e.Item.DataItem,  "LastName"));

      title = Convert.ToString(DataBinder.Eval(e.Item.DataItem, "Title"));

      region = Convert.ToString(DataBinder.Eval(e.Item.DataItem, "Address"));

      city  = Convert.ToString(DataBinder.Eval(e.Item.DataItem, "Region"));

      address = Convert.ToString(DataBinder.Eval(e.Item.DataItem, "City"));

      idLabel.Text = id.ToString();

 

      if (firstName.CompareTo(String.Empty) != 0)

            firstNameLabel.Text          = firstName;

      if (lastName.CompareTo(String.Empty) != 0)

            lastNameLabel.Text           = lastName;

      if (title.CompareTo(String.Empty) != 0)

            titleLabel.Text              = title;

      if (firstName.CompareTo(String.Empty) != 0)

            addreessLabel.Text           = address;

      if (region.CompareTo(String.Empty) != 0)

            regionLabel.Text        = region;

      if (city.CompareTo(String.Empty) != 0)

            cityLabel.Text               = city;

}    

}

W celu umożliwienia filtrowania pracowników w zależności od regionu należy obsłużyć zdarzenie SelectedIndexChanged kontrolki regionDropDownList. 

private void regionDropDownList_SelectedIndexChanged(object sender, System.EventArgs e)

{

      RegionID = Convert.ToInt32((sender as DropDownList).SelectedValue);

}

                Po zmianie wartości wybranej w liście zapisujemy w zmiennej ViewState wybrany region.

Gdy użytkownik wybierze opcje filtrowania, musimy ponownie odświeżyć dane w kontrolce emloyeeDG.

            Kod obsługujący to zdarzenie jest bardzo prosty, szczegóły znajdują się w pliku pracownicy.aspx.cs.

Model

                To jest najbardziej rozbudowana część aplikacji. W niej zawarte są klasy związane z obsługą bazy danych, oraz klasy tzw. DALC (Data Access Logic Components). W tym przykładzie będą to klasy odpowiadające za pracowników, oraz regiony, a także klasy związane z logiką biznesową. Ta część aplikacji może podlegać dalszej dekompozycji w zależności od stopnia skomplikowania systemu. Jak widać klasy stanowiące kontroler odwołują się do odpowiedniej metody klasy zawartej w modelu. W tym przykładzie model - view – controller znajdują się w jednej aplikacji. Przy bardziej rozbudowanych systemach model można przenieść na inny węzeł fizyczny, a logikę udostępnić przez .NET Web Service, .Net Remoting lub inny sposób nie zmieniając w żaden sposób widoku. Drobne zmiany pojawią się jedynie w kontrolerze, gdyż w inny sposób będziemy musieli odwoływać się do logiki biznesowej.

                Do pobierania danych o pracownikach kontroler wywołuje metodę AppLogic.GetEmployees();. Jej kod jest następujący:

public static DataSet GetEmployees(  )

{                

      DataSet result = new DataSet();                     

      try

      {

            result = EmployeeDAL.GetEmployees();

      }

      catch (DataAccessException ) { result = null; }                            
      return result;

}

Z kolei metoda GetEmployees wywołuje metodę klasy EmployeeDAL o tej samej nazwie, której kod zamieszczony jest poniżej.

public static DataSet GetEmployees(  )

{

SqlConnection con = null;

      DataSet result = new DataSet();                     

      try

      {

            con = DatabaseGateway.CreateConnection();

            using (con)

            {

                  DatabaseGateway.FillDataSet(con, SP_NAME_GET_ALL_EMPLOYEES, result, DS_NAME_GET_ALL_EMPLOYEES,null);//, cmdParameters);

            }

      }

      catch (DataAccessException exp){ result = null; }

      finally

      {

            if ((con != null) && (con.State == ConnectionState.Open))

                  con.Close();

      }

      return result;

}

public static void FillDataSet(SqlConnection connection, CommandType

commandType, string commandText, DataSet typedDS, string tableName,

params SqlParameter[] commandParams)

{

if (connection == null)

            throw new ArgumentNullException("connection");

      bool closeConnection = false;

      try

      {

            SqlCommand command = new SqlCommand();

            closeConnection =

                        PrepareCommand(command, connection, commandType, commandText, commandParams);

            using (SqlDataAdapter adapter = new SqlDataAdapter(command))

            {               

                  typedDS.Clear();

                  adapter.Fill(typedDS, tableName);

                  command.Parameters.Clear();

            }

      }

      catch (SqlException exc)

      {

            throw new DataAccessException(exc.Message, exc);

      }

      catch (Exception exc)

      {

            throw new DataAccessException(DAL_EXCEPTION, exc);

      }

      finally

      {

            if (closeConnection)

                  connection.Close();

      }

}

                Dzięki zastosowaniu takiego podziału aplikacji zyskaliśmy szereg zalet:

  • Zredukowana zależność pomiędzy widokiem a kontrolerem.
  • Zmniejszenie redundancji kodu: obsługa komunikacji z baza znajduje się w jednym miejscu, więc możemy z niej wielokrotnie korzystać.
  • Możliwość optymalizacji kodu – dzięki separacji poszczególnych klas możemy je niezależnie testować i zwiększać ich wydajność nie powodując zmian w innych częściach aplikacji
  • Łatwiejsze testowanie
  • Separacja klas pełniących różne role od siebie, a tym samym zredukowanie zależności pomiędzy nimi

Jednak wadami takiego rozwiązania są:

·         większa złożoność aplikacji i dodatkowy kod

·         przy dodawaniu nowej strony będzie powielał się kod związany z wyświetlaniem nagłówka, stopki, menu, i innych wspólnych dla strony elementów. Zmiana wyglądu jednego z elementów spowoduje zmianę we wszystkich stronach widoku

Page Controller

Page Controller w teorii

                W Page Controller w odróżnieniu od MVC wprowadzamy jeden główny kontroler, który jest odpowiedzialny za realizację wspólnych akcji dla wszystkich stron aplikacji. Dzięki takiemu rozwiązaniu zyskuje się większą spójność oraz łatwiej będzie testować aplikację, oraz zmniejszy się redundancja kodu. Strukturę wzorca Page Controller przedstawia poniższy rysunek:

PC_pattern.JPG

                                                                                                                                            Rys. 5.   Struktura wzorca Page Controller (źródło [Pnp2003])

PageController jest odpowiedzialny za pobranie żądania strony, oraz parametrów wywołania (z sesji, nagłówka http, przesyłanych w adresie url – zw. query string), a także za modyfikację modelu, oraz „wybranie” (przekierowanie do) odpowiedniego widoku. Widok z kolei jest zależny od modelu, który zawiera klasy trwałe, oraz realizuje logikę biznesową.

Jednak tworzenie dla każdej strony oddzielnego kontrolera powoduje duplikację kodu. By tego uniknąć wprowadzamy kontroler główny, w którym zawieramy wspólny kod dla wszystkich kontrolerów.  Przedstawia to poniższy rysunek:
PC_inheriting.JPG

                                                                                                                                                                Rys. 6.   Główny kontroler (źródło [Pnp2003])

Ten diagram klas ilustruje sytuację kiedy we wszystkich kontrolerach mamy wspólną cześć np. dostosowywanie wyglądu menu w zależności od roli użytkownika. Ponadto różnych kontrolerach możemy wykorzystać odmienne sposoby przesyłania parametrów: query string, zmienne sesyjne, dlatego korzystają one z odpowiednich kontrolerów. Zamiast wprowadzania dziedziczenia można napisać zestaw klas pomocniczych tzw. helperów, które będą realizować wspólne zachowanie.

Ten wzorzec nie jest popularny dla technologii takich jak: JSP, PHP, ASP. Powodem tego jest fakt, że w tych technologiach nie ma separacji pomiędzy: widokiem i kontrolerem. Konsekwencją tego jest to, że wiele programistów sądzi, że korzystanie z tego wzorca to zły styl programowania. Jednak ten wzorzec oraz Front Controller są wzorcami nad użyciem których warto się zastanowić budując aplikację.

Wykorzystanie tego wzorca niesie ze sobą wiele zalet, ale niestety również i wad:

Zalety:

  • aplikacja staje się prostsza. Każda strona ma swój specyficzny kontroler, którego działanie nie wpływa na pozostałe
  • wykorzystuje separacje widoku od kontrolera wbudowana w ASP. NET (code – behind)
  • zwiększa ponowne użycie
  • ułatwia rozszerzalność aplikacji
  • umożliwia niezależną pracą nad aplikacją: programistą, twórcą IU, itp.

Wady:

  • każda strona musi posiadać swój kontroler
  • możliwe głębokie drzewo dziedziczenia
  • zależy od środowiska implementacji

Implementacja Page Controller w ASP .NET

                Implementacja tego wzorca rozpoczęta zostanie od głównego kontrolera. Ponieważ klient, dla którego tworzony jest system zażądał, żeby każda strona aplikacji wyglądała tak samo: miała taki sam nagłówek, stopkę, oraz menu bardziej intuicyjne wydaje się wykorzystanie wzorca Page Controller, gdyż unikniemy redundancji związanej z tworzeniem widoku.

W przedstawionym przykładzie dziedziczenie pomiędzy głównym kontrolerem (base controller) zostało zastąpione możliwością tworzenia kontrolek użytkownika (User Control), które wczytuje główny kontroler na podstawie nazwy przesłanej w adresie url. Takie rozwiązanie w ASP .NET nazywa się Master Pages.

                Na początku zostanie stworzona klasa odpowiadająca za wyświetlenie wspólnej części wszystkich stron. Jej wygląd i kod zamieszczony jest poniżej (rys. 7. i 8.):

szablon.JPG

                                                                                                                                                                Rys. 7.   Widok wspólny dla wszystkich klas.

Nasz szablon można wzbogacić o menu, które będzie wspólne dla wszystkich stron. Klasa ta zawiera sam kod html odpowiadający za wygenerowanie strony przedstawionej powyżej, oraz kontrolkę asp:placeHolder do której wczytywane są kontrolki, które wyświetlają specyficzną zawartość. Oto ten fragment strony BaseController.aspx.


zawartosc.JPG

Rys. 8. Kod widoku dla głównego kontrolera

Gdy już stworzony został widok można napisać główny kontroler – odpowiedzialny za przejęcie żądania strony, oraz wyświetleniem żądanej zawartości – załadowanie odpowiedniej kontrolki.

                Żeby dodawanie nowych kontrolek było łatwe najlepiej jest przygotować jeden katalog, w którym będą wszystkie kontrolki np. Views, a w nim dokonać odpowiedniego podziału specyficznego dla aplikacji. W tym przykładzie kontrolki będą w katalogu Views, a kontroler wczytywał będzie odpowiednią kontrolkę, której nazwa będzie przesyłana przez adres w następujący sposób: BaseController.aspx?Page=Views/NazwaKontrolki (bez roszerzenia).

                Kod odpowiedzialny za wczytanie kontrolki należy umieścić w zdarzeniu OnLoad kontrolera, co ilustruje poniższy fragment kodu:

private const string CONTENT_CONTROL_PATH_KEY = "Page";         

private const string UC_EXTENSION = ".ascx";

private const string PAGE_QUERY_STRING_KEY = "Page";

private const string VIEWS_VIRTUAL_PATH = "Views";

 

#region [2] Zdarzenia strony

private void Page_Load(object sender, System.EventArgs e)

{

      // Put user code to initialize the page here

      InitPage();      

}

#region Inicjalizacja kontrolera i metody pomocnicze

private void InitPage()

{

      LoadPageContent();

}

private void LoadPageContent()

{

      string virtualPath = string.Empty;

      virtualPath = GetContentPagePath();                             

      if(virtualPath.CompareTo(string.Empty) != 0)

      {

            try

            {                 pageContentPlaceHolder.Controls.Add((System.Web.UI.UserControl)LoadControl(virtualPath));                          

            }

            catch (InvalidCastException) { throw; }

      }

}

protected string GetContentPagePath()

{                

      string path             = string.Empty;

      string tmpPath          = string.Empty;

      string rootPath         = string.Empty;

      string virtualPath      = string.Empty;

      int    hashId;

      if (Request.QueryString[CONTENT_CONTROL_PATH_KEY] != null)

            path = Request.QueryString[CONTENT_CONTROL_PATH_KEY];

      if (path.CompareTo(string.Empty) != 0)

      {                      

            hashId = path.IndexOf("#");

            if(hashId > 0)

                  path = path.Substring(0,hashId);

            rootPath = Server.MapPath(VIEWS_VIRTUAL_PATH);

            virtualPath = path + UC_EXTENSION;

            try

            {

                  tmpPath = Server.MapPath(virtualPath);

            }

            Catch { return string.Empty; }

            return virtualPath;               

      }

      #endregion

      #endregion

Zadaniem powyższego fragmentu kodu jest wczytanie do kontrolki pageContentPlaceHolder odpowiedniej kontrolki, której nazwę podano w zmiennej query string o nazwie Page.

                Po załadowaniu kontroli EmployessMgmt.ascx zobaczymy stronę wyglądającą identycznie przedstawione na rys. 4.

                Pozostała część aplikacji jest taka sama jak ta we wzorcu MVC.

Podsumowanie

                Artykuł ten miał na celu zachęcenie do zapoznania się z wzorcami projektowymi, celami ich stosowania, a także korzyściami i stratami jakie wiążą się z wybraniem odpowiedniego wzorca. Wykorzystanie wzorów projektowych wpłynie pozytywnie na jakość tworzonego oprogramowania, łatwość jego testowania, a utrzymywania.

Literatura

[Fowler03] Fowler, Martin. „Patterns of Enterprise Application Architecture. Addison Wesley, 2003.

[PnP02] Microsoft patters & practices “Application Architecture for .NET: Designing Applications and Services”, Microsoft Corporation 2002

[PnP03] Microsoft patters & practices “Enterprise Solution Patterns Using Microsoft .NET”, Microsoft Corporation 2003






Załączniki:

tagi: www aplikacje

Komentarze 0

pkt.

Zaloguj się lub Zarejestruj się aby wykonać tę czynność.