Project Introduction: MVC (Model View Controller) framework for ASP.NET 2.0. Contains AJAX module as well. It was created before official Microsoft framework was released so i've learned a lot creating it. (PL Only)
2. 1. Cel projektu
Celem projektu było stworzenie frameworka, dzięki któremu byłoby możliwe tworzenie aplikacji internetowych
w ASP.NET używając wzorca MVC (Model View Controller). ,”MyLittleMVC”, bo tak nazywa się powstały framework,
wspomaga tworzenie, rozwój i testowanie powstającej aplikacji internetowej zgodnie z duchem powyższego wzorca,
realizując całą jego funkcjonalność, czyli podział aplikacji na 3 oddzielne jednostki tj.: Model, Widok, Kontroler.
Każda z części realizuje swoje zadania w sposób możliwie niezależny od pozostałych.
• Model – jest odpowiedzialny za przechowywanie stanu aplikacji, który najczęściej jest ulokowany wewnątrz
bazy danych.
• Widok – jest odpowiedzialny za wyświetlenie gotowego interfejsu użytkownika, w tym wypadku gotowej strony
internetowej. Zazwyczaj widok jest tworzony na podstawie danych pochodzących z modelu.
• Kontroler – jest odpowiedzialny za przetwarzanie akcji użytkownika, obsługę modelu i w końcu wybór widoku
to wyświetlenia. Jest zatem główną jednostką łączącą pozostałe 2 warstwy.
Główną korzyścią z zastosowania MVC jest wprowadzenie wyraźnej separacji między poszczególnymi
warstwami aplikacji (tutaj: model, widok, kontroler), dzięki czemu całość jest łatwiejsza w rozwijaniu oraz testowaniu
(przede wszystkim z wykorzystaniem Test Driven Developmnent).
2. Jak działa ASP.NET
Zanim przedstawię działanie frameworka warto przyjrzeć się jak działa samo ASP.NET.
Rys. 1.1 Page Lifecycle (cykl życia strony aspx)
Kiedy przeglądarka internetowa wysyła do serwera żądanie, serwer (IIS web server)
najpierw sprawdza czy żądana strona jest plikiem, który ma być przez niego
przetworzonym(domyślnie są to pliki zawierające rozszerzenia: .aspx .asmx .ashx .ascx). Jeśli jest,
przekazuje go do ASP.NET runtime (środowiska uruchomieniowego ASP.NET) gdzie jest
przetwarzany a wynik tego procesu zwracany do klienta w postaci gotowej strony. Zauważyć
można, że największą rolę odgrywa tutaj ASP.NET runtime, którego uproszczony schemat
działania przedstawia poniższy rysunek (rys. 1.2.).
3. Rys. 1.2 ASP.NET runtime
Filtr ISAPI (Internet Server Application Programming Interface) w postaci pliku
“aspnet_isapi.dll”, który widać na rysunku (rys. 1.2) w skrócie zajmuje się właśnie przechwyceniem
żądań plików o konkretnym rozszerzeniu (.aspx i inne). „HttpApplication” natomiast jest to
abstrakcyjna klasa reprezentująca aplikacje i ma ona na celu zapewnienie właściwego modelu
programowania upraszczając go i upodobniając do modelu aplikacji dla systemu operacyjnego
Windows (Cały mechanizm nosi nazwę WebForms i nazwa ta jest oczywistą analogią do
WinForms w Windows, a wzorzec który jest domyślnie zaimplementowany w ASP.NET to „Page
Controller”). Obiekty klasy HttpApplication są tworzone w miarę potrzeb tak, aby zachować
ciągłość przetwarzania żądań http (liczba stworzonych obiektów jest ograniczona do maksymalnej
liczby wątków i zależy głównie od sprzętowych możliwości serwera).
Następnie żądanie przechodzi przez tak zwany ASP.NET pipeline, który można skrótowo
przedstawić jako pewien potok, w którym żądanie jest przetwarzane przez wszystkie istniejące
moduły http (Http Modules), które posiadają metody do interakcji z żądaniem. Na końcu trafia
natomiast do jednego z uchwytów http (Http Handlers), które zdefiniowane są do przetwarzania
odpowiednich żądań zależnie od ustalonych reguł adresu URL i są odpowiedzialne za
wygenerowanej właściwej odpowiedzi http (Response) wysyłanej z powrotem do klienta. Należy
zwrócić uwagę, że podczas przetwarzania jednego żądania wywoływany jest tylko jeden
HttpHandler w przeciwieństwie do HttpModules, których wywoływanych jest więcej. Cały proces
ASP.NET pipeline najlepiej ilustruje rysunek (rys. 1.3.).
4. Rys. 1.3. ASP.NET pipeline
3. Jak działa myLittleMVC
Framework „myLittleMVC” swe działanie opiera właśnie na wykorzystaniu uchwytu http
(HttpHandler), który w tym wypadku spełnia jedno z zadań głównego kontrolera (front
controllera) tj. przechwytuje wszystkie żądania, które są do niego kierowane. Domyślnie są to
wszystkie żądania, których url kończy się łańcuchem znaków „default.aspx”. Literał ten jest tak
zwanym inicjatorem akcji.
Czym jest zatem akcja? Inaczej niż w standardowym modelu ASP.NET, adres url żądania
nie odzwierciedla fizycznie istniejącego na serwerze pliku (w tym wypadku strony aspx), lecz jest
raczej poleceniem wykonania danej metody (zwanej tutaj akcją) przez dany kontroler (klasę
dziedziczącą po myLittleMVC.Controller). Przykładowo adres „home/index/default.aspx” oznacza
wykonanie akcji(metody) „index” przez klasę kontrolera o nazwie „home”. Jak napisano wyżej,
default.aspx jest jedynie informacją dla odpowiedniego uchwytu http o przechwyceniu tego właśnie
żądania.
Głównym kontrolerem (uchwytem http w tym wypadku) w myLittleMVC jest klasa
MainHandler, która jak każdy uchwyt http dziedziczy po System.Web.IHttpHandler. Jego główną
metodą jest ProcessRequest, która jest wykonywana przez ASP.NET w celu przetworzenia żądania
i to właśnie w tej metodzie myLittleMVC rozpoczyna swoje działanie przekazując żądanie i
informacje z nim związane do Routera. Ten z kolei zajmuje się przetworzeniem url i rozbiciem go
za pomocą obiektu Action na poszczególne jednostki wywołania czyli:
Nazwę kontrolera, którego metoda akcji ma zostać wykonana,
Nazwę metody akcji do wykonania,
Ewentualne parametry metody akcji.
Tak powstały obiekt klasy Action jest przekazywany do metody InvokeAction klasy
ControllerFactory, gdzie zawarte w nim informacje posłużą do próby wykonania akcji. Proces ten
wykorzystuje wbudowany w platformę .NET system refleksji, który pozwala na przeglądanie i
używanie własnych metadanych. Dalszy proces po pomyślnym wykonaniu akcji danego kontrolera,
zależy od samej akcji i zostanie omówiony w następnym rozdziale podczas tworzenia przykładowej
aplikacji.
5. 4. MyLittleMVC w Akcji
W celu zademonstrowania działania frameworka i opisu dalszego procesu przetwarzania
żądania http przez niego najlepiej będzie posłużyć się przykładem (w katalogu „prgexample”
znajduje się projekt utworzony w Visual Web Developer 2008 EE zawierający przykładową
aplikację, której część stanowi poniższy przykład).
Typową strukturę katalogów w aplikacji opartej na myLittleMVC przedstawia zrzut ekranu
(rys. 4.1).
Rys. 4.1 Struktura folderów aplikacji opartej na myLittleMVC
Folder „App_Code” zawiera kod całej aplikacji podzielony na poszczególne części. W
podfolderze „Controller” znajdować będą się klasy kontrolerów, „Helper” służy do
przechowywania klas pomocniczych, natomiast „Model” zawierać powinien jak sama nazwa
wskazuje klasy warstwy modelu aplikacji. Sama struktura katalogów w folderze „App_Code” ma
jedynie charakter umowny i ma na celu zwiększenie przejrzystości aplikacji. Nic nie stoi na
przeszkodzie by dodać własne podfoldery w miarę potrzeb, zalecane jest jednak trzymanie się
pewnej konwencji znacząco ułatwiającej dalszy rozwój aplikacji.
Folder „Bin” domyślnie zawiera jedynie referencję do biblioteki frameworka. Jego
przeznaczenie nie ulega zmianie w stosunku do tego ze standardowego użycia w ASP.NET.
Folder „View” zawiera wszyskie klasy widoków, które dziedziczą po
myLittleMVC.BasicView lub myLittleMVC.View<>(klasą bazową dla nich pozostaje dalej klasa
System.Web.UI.Page czyli klasa reprezentująca stronę aspx, gdyż właśnie w ten sposób
reprezentowane są widoki). Grupy widoków są posegregowane w katalogach ze względu na
przynależność do danego kontrolera (np. folder home jak widać na rys. 4.1 zawiera wszystkie
widoki przynależące do kontrolera home), jednak jest to również kwestia umowna i chcąc
zwiększyć elastyczność aplikacji możliwe jest wybranie przez dany kontroler dowolnego widoku.
Jedynym warunkiem jest by klasa widoku znajdowała się w folderze „View”. Na uwagę zasługuje
folder „common”, który jest przeznaczony do przechowywania wspólnych widoków oraz
szablonów widoków (klas widoków dziedziczących po myLittleMVC.MasterView a „fizycznie”
reprezentowanych przez klasę bazową System.Web.UI.MasterPage).
Plik „Web.config” zawiera konfigurację całej aplikacji ASP.NET oraz konfigurację
frameworka, której najważniejszą część opisuje listing 4.1.
Istnienie pliku „default.aspx” zapewnia jedynie prawidłowe wykonanie domyślnej akcji.
6. Listing 4.1
<?xml version="1.0"?>
<configuration>
<configSections>
<section name="myLittleMVCRouter" type="myLittleMVC.RouterConfig"/> (1)
<section name="myLittleMVCMain" type="myLittleMVC.MainConfig"/> (2)
</configSections>
<myLittleMVCRouter initActionPath="default.aspx"
urlSeparator="/"
defaultAction="home/index"
loginAction="member/login"
langPrefix="false"/>
<myLittleMVCMain debugFramework="true"
projectNamespace=""/>
<system.web>
<httpHandlers>
<add verb="*" path="*default.aspx" type="myLittleMVC.MainHandler"/>
</httpHandlers>
</system.web>
</configuration>
Kolejno w sekcji configSections odbywa się rejestracja sekcji konfiguracyjnych myLittleMVC,
składających się z głównej konfiguracji(2) oraz konfiguracji routera(1). Główna część konfiguracji
obejmują 2 ustawienia:
debugFramework (true|false) ustawiający tryb debug dla myLittleMVC, wartość
domyślna: „true”
projectNamespace (np. „Test”) zawierający przestrzeń nazw tworzonego projektu
(a dokładnie przestrzeń nazw w której znajdują się kontrolery), wartość domyślna to
„”.
Na ustawienia routera składają się:
initActionPath definiuje tak zwany inicjator akcji, czyli literał dołączany na koniec
adresu URL zawierającego akcje w formie łańcucha znaków. Element ten musi być
zawarty w atrybucie path ustawienia uchwytu myLittleMVC.MainHandler, wartość
domyślna: „default.aspx”
urlSeparator to separator akcji url (znak lub grupa znaków oddzielających od siebie
poszczególne części akcji url), wartość domyślna: „/”,
defaultAction zawiera domyślną akcję w postaci „kontroler/metoda_akcji”, należy
zadbać o odpowiedni separator (w przykładzie powyżej „/”), wartość domyślna:
„home/index” (wielkość liter ma znaczenie),
loginAction zawiera akcję wykonywaną w razie wystąpienia wyjątku
SecurityException (wyjątek występuje podczas próby dostępu do zasobu lub partii
kodu, do którego użytkownik nie ma dostępu), jeżeli element nie zostanie ustawiony
zamiast wywołania akcji zostanie wyświetlona odpowiednia strona błędu.
LangPrefix (true|false) element ustawia prefix (przedrostek) językowy w akcji
znajdującej się w adresie url. Wartość prefixu jest zależna od ustawień regionalnych
i automatycznie dostosowywana podczas zmiany języka. Mechanizm prefiksu
7. językowego zapobiega cachowaniu przez przeglądarkę tej samej strony w innych
językach jako tego samego dokumentu. Wartość domyślna: „true”.
W sekcji „httpHandlers” rejestrowany jest główny uchwyt frameworka
(myLittleMVC.MainHandler), który przechwytuje wszystkie żądania (zarówno GET i POST)
zakończone inicjatorem akcji.
4.1. Kontroler - Akcja - Widok
Nie zmieniając pliku konfiguracyjnego stwórzmy pierwszy kontroler jakim będzie klasa
home (pełną definicję klasy home zawiera listing 4.1.1.). Będzie to nasz kontroler domyślny (taki,
który zawiera domyślną akcję).
public class home : Controller
{
public void index()
{}
}
Jak widać, wraz z kontrolerem została utworzona publiczna metoda „index()”, która będzie
domyślną akcją. Aby jednak tak się stało należy dodać do niej atrybut „ActionMethod”, którym
oznaczane będą wszystkie metody akcji (został on wprowadzony w celach bezpieczeństwa by
zapobiec nieautoryzowanemu wykonaniu dowolnej metody kontrolera odpowiednio przygotowaną
akcją zawartą w adresie url). Prawidłowo więc powyższy przykład powinien wyglądać tak:
public class home : Controller
{
[ActionMethod]
public void index()
{}
}
Po uruchomieniu aplikacji w takiej postaci zostanie wykonana, jak zdefiniowano w pliku
konfiguracyjnym, domyślna akcja tj. „home/index” i na ekranie ukaże nam się białe tło. Oznacza to,
że wszystko przebiegło pomyślnie jednak nie został wyrenderowany żaden widok.
Stwórzmy więc jeden, dodając do katalogu „Viewhome” stronę „home.aspx” i zmieńmy jej
klasę bazową z System.Web.UI.Page na generyczną klasę widoku myLittleMVC.View<> gdzie
parametrem będzie typ String. Dzięki temu za pomocą kontrolera możemy wysłać do widoku dane
(w tym wypadku będzie to zmienna typu String), które mogą być odczytane z właściwości
„DataView” klasy naszego widoku. Typ danych wysyłanych z kontrolera do widoku może być
oczywiście dowolny. „MyLittleMVC” posiada również inny mechanizm wysyłania danych do
widoku za pomocą tzw. „ActionFields”, który zostanie przedstawiony w dalszej części tekstu.
Wracając do naszego przykładu, chcąc wyświetlić przesłane dane posłużymy się kodem
(znajdującym się w pliku home.aspx):
<p>
Przykładowe dane wysłane z kontrolera przez domyślną akcję za pomocą
właściwości DataView: <%=DataView %>
</p>
Dalej jednak, akcja “index” kontrolera „home” nie posiada kodu odpowiedzialnego za wyświetlenie
naszego widoku. Do tego celu służy metoda klasy kontrolera „RenderView” występująca w 3
przeładowanych wersjach (jej uzupełnieniem jest metoda RenderViewMaster). Do naszego celu
posłuży wersja przyjmująca 2 argumenty. Pierwszym jest nazwa widoku do wyrenderowania,
drugim natomiast obiekt reprezentujący dane wysyłane do tego widoku. Jak łatwo się domyśleć są
to właśnie te dane, które będą widoczne w klasie widoku poprzez właściwość „DataView”. Dla
przykładu danymi niech będzie data pobrana z serwera (patrz listing 4.1.1).
8. Kolejnym krokiem niech będzie stworzenie szablonu widoku dla naszej aplikacji. Aby to
zrobić należy dodać plik .master (w naszym przypadku będzie to domyślny „MasterPage.master”)
do katalogu „Viewcommon” i zmienić jego klasę bazową z System.Web.UI.MasterPage na
myLittleMVC.MasterView. Na koniec by nasz poprzedni widok korzystał z utworzonego szablonu
należy zmodyfikować go w następujący sposób (przy zachowanym domyślnym nazewnictwie
kontenerów „head” oraz „ContentPlaceHolder1”):
<%@ Page Language="C#"
MasterPageFile="~/View/common/MasterPage.master" (1)
AutoEventWireup="true"
CodeFile="home.aspx.cs"
Inherits="View_home_home" %>
<asp:Content ID="Content1" ContentPlaceHolderID="head" runat="Server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="”ContentPlaceHolder1"
runat="Server">
<p>
Przykładowe dane wysłane z kontrolera przez domyślną akcję za pomocą
właściwości
DataView:
<%=DataView %>
</p>
</asp:Content>
Kod odpowiedzialny za wykorzystanie szablonu widoku w naszym widoku znajduje się w
miejscu oznaczonym punktem (1). Sam sposób tworzenia szablonów jak widać nie odbiega od tego
jaki się stosuje w „czystym” ASP.NET. Na większą uwagę zasługuje klasa
myLittleMVC.MasterView, po której dziedziczy nasz szablon. Korzystając z niej uzyskujemy
bowiem możliwość wysyłania danych z kontrolera do naszego szablonu widoku. Sposób w jaki
uzyskamy tę funkcjonalność musi być oczywiście inny, niż przedstawiony wyżej bazujący na
generycznej klasie View<>. Użyjemy wspomnianych wcześniej pól „ActionFields”.
Listing 4.1.1.
using System;
using myLittleMVC;
/// <summary>
/// Domyslny kontroler
/// </summary>
public class home : Controller
{
/// <summary>
/// tytul strony wsylany do widoku szablonu (masterpage)
/// </summary>
[ActionField]
public string title; (1)
[ActionMethod]
public void index()
{
title = "home";
string czas = DateTime.Now.ToString();
// Wyswietlenie widoku i wysłanie do niego zmiennej czas
RenderView("home/home", czas);
}
}
9. Polami „ActionFields” są wszystkie publiczne pola klasy kontrolera posiadające atrybut
„ActionField” (1). Kiedy w metodzie akcji zostanie przypisana do takiego pola wartość, zostanie
ono wysłane do widoku podczas wykonania metody wyswietlającej widok („RenderView” lub
„RenderViewMaster”). Aby w widoku (tutaj w szablonie widoku) odebrać wysłane pole
„ActionField” wystarczy zdefiniować wewnątrz klasy widoku publiczne pole tego samego typu i
posiadające taką samą nazwę oraz opatrzyć je atrybutem „ViewField”. W naszym przykładzie plik
codebehind (plik zawierający kod klasy strony aspx a w tym wypadku widoku, zdefiniowanej jako
klasa częsciowa (ang. partial class)) prezentować się będzie następująco:
Listing 4.1.2.
using myLittleMVC;
public partial class View_common_MasterPage : MasterView
{
[ViewField]
public string title;
}
Po tym prostym zabiegu, możemy korzystać ze zmiennej „title” w szablonie widoku. W
przykładzie pole to będzie wykorzystywane do wyświetenia tytułu strony, bez potrzeby
umieszczania go w każdej klasie widoku. Jak widać transfer danych z kontrolera do konretnego
widoku jest bardzo prosty i intuicyjny, a dane są ściśle typowane co znacząco ułatwia i przyspiesza
pracę oraz pozwala uniknąć wielu błędów. Wybór sposobu jakim dane zostaną wysłane zależy
głownie od użytkownika frameworka i jego osobistych preferencji (warto zaznaczyć iż 1 sposób
wykorzystujący generyczną klasę myLittleMVC.View<> został wprowadzony w celu zachowania
zgodności z oficjalnym frameworkiem MVC Microsoftu i mimo iż posiada on kilka niedogodności
w porównaniu z polami „ActionField” może być efektywnie stosowany w celu przesłania do
widoku danych jednego typu).
4.2. Parametry akcji
Wiadomo już jak wywoływana jest akcja, w jaki sposób odbywa się wyświetlenie widoku
oraz przekazanie do niego danych. Jak jednak przesłać do metody akcji parametry? Nic prostrzego.
Jeśli parametry akcji pochodzą z adresu url wystarczy zdefiniować klasyczne parametry metody
będącej akcją w liczbie i kolejności jaka występuje w adresie url. Czasem istnieje potrzeba
zastąpienia parametru pochodzącego z url wartością domyślną. Język C# z uzasadnionych przyczyn
nie zapewnia wbudowanego mechanizmu parametrów domyślnych. Z pomocą przychodzi jeden z
atrybutów z grupy „ActionMethodParams”:
[DefParam(object defValue)], defValue jest wartością domyślną przypisywaną do
parametru w przypadku nie znalezienia wartości w adresie url akcji. Jego stosowanie
do parametrów akcji jest zgodne z przyjętą konwencją stosowania parametrów
domyślnych stosowanych w innych językach (m.in. C/C++) co oznacza ze atrybutem
tym mogą być jedynie oznaczane ostatnie parametry metody akcji (nie biorąc pod
uwagę parametrów oznaczonych innym atrybutem grupy „ActionMethodParam”).
Kolejnymi atrybutami stosowanymi do parametrów metody akcji są:
[FormParam], Atrybut parametru akcji przypisujący do parametru wartość
pochodzącą z formularza. Atrybut usuwa działanie innych atrybutów parametrów
akcji z grupy ActionMethodParams, wiec powinien być stosowany samodzielnie,
[SessionParam], Atrybut parametru akcji przypisujący do parametru wartość
pochodzącą z obiektu sesji,
10. [QueryParam], Atrybut parametru akcji przypisujący do parametru wartość
pochodzącą z query stringa,
[CookieParam], Atrybut parametru akcji przypisujący do parametru wartość
pochodzącą z pamięci cache,
[AppParam], Atrybut parametru akcji przypisujący do parametru wartość
pochodzącą z obiektu Application.
Sam proces mapowania parametrów odbywa się w klasie myLittleMVC.ControllerFactory
tuż przed wykonaniem akcji. Sposób wykorzystania parametrów akcji (ActinonMethodParams) jest
zawarty w dołączonym przykładzie.
4.3. ActionObjects
Ostatnim ważnym mechanizmem jaki wykorzystuje „myLittleMVC” jest grupa atrybutów
„ActionObjects”. Dotyczą one pól klasy kontrolera i dostarczają funkcjonalności mapowania ich na
poszczególne obiekty należące do aplikacji (Cookies, Session, Application).
Grupa „ActionObjects” zawiera:
[SessionField], Atrybut dodawany do pól mapowanych na obiekt sesji (Session),
[AppField], Atrybut dodawany do pól mapowanych na obiekt aplikacji
(Application),
[CookieField(int days[optional])], Atrybut dodawany do pól mapowanych na
obiekt ciastka aktualnej odpowiedzi, konstruktor atrybutu zawiera opcjonalny
parametr „days” ustawiający liczbę dni istnienia ciastka na komputerze klienta
aplikacji. Domyślnie wynosi ona 1 dzień.
Proces mapowania pół zawierających atrybuty z grupy „ActionObjects” odbywa się w
metodach kończących akcję (metodach renderujących, przekierowujących akcję, wysyłających plik
oraz gdy żadna z powyższych metod nie zostanie użyta w metodzie Flush() kontrolera).
Framework zapewnia również usuwanie wartości z obiektów zmapowanych polami
„ActionObjects”. Służy do tego pomocnicza metoda kontrolera „RemoveField(string fieldName)”,
gdzie field name to nazwa pola w postaci łańcucha znaków.
5. Na zakończenie
Mam nadzieję, że w tym dość powierzchownym opisie frameworka „myLittleMVC” udało
mi się choć trochę przybliżyć sposób jego działania oraz użycia go do budowy aplikacji. Sam
framework oferuje znacznie więcej niż tu opisano, jednak z zamierzenia miał to być jedynie szkic
zachęcający do dalszego poznawania go. Więcej przykładów użycia w konkretnych sytuacjach
znajduje się w przykładowej aplikacji. Informacje na temat dalszego rozwoju frameworka oraz jego
kolejnych wersji będą sukcesywnie dodawane na stronie http://www.skowronkow.pl. Wersja o
której mowa w tekście objęta jest licencją GNU GPL.
Literatura
[1] Stephen C. Perry, C# i .NET, Core 2006
[2] Microsoft, msdn, http://www.msdn.com
[3] Microsoft, ASP.NET community, http://www.asp.net
[4] Scott Guthrie, Microsoft, ScottGu's Blog, http://weblogs.asp.net/scottgu/