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:
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:

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: