Tworzenie aplikacji internetowych bez użycia Web Forms

Chris Tavares

Microsoft Corporation

Artykuł ten dotyczy przedpremierowej wersji biblioteki ASP.NET MVC Framework. Przedstawione tu informacje mogą ulec zmianie.

ZAGADNIENIA OMÓWIONE W ARTYKULE:

  • Wzorzec model-widok-kontroler

  • Tworzenie kontrolerów i widoków

  • Tworzenie formularzy i przekazywanie informacji zwrotnych (postback)

  • Fabryki kontrolerów i inne możliwości rozbudowy

ARTYKUŁ DOTYCZY NASTĘPUJĄCYCH TECHNOLOGII:

  • ASP.NET

KOD DOSTĘPNY DO POBRANIA POD ADRESEM:

MVCFramework2008_03.exe (189 KB)

Przejrzyj kod w trybie on-line

On This Page

Wzorzec model-widok-kontroler Wzorzec model-widok-kontroler
Utworzenie kontrolera Utworzenie kontrolera
Utworzenie widoku Utworzenie widoku
Bardziej złożony przykład Bardziej złożony przykład
Tworzenie formularzy i przesyłanie informacji zwrotnych Tworzenie formularzy i przesyłanie informacji zwrotnych
Tworzenie kontrolerów Tworzenie kontrolerów
Inne możliwości rozszerzania Inne możliwości rozszerzania
Pożegnanie z Web Forms? Pożegnanie z Web Forms?

W zawodzie programisty pracuję już od 15 lat, a programowanie było moim hobby już co najmniej 10 lat wcześniej. Podobnie jak większość mojego pokolenia, moją przygodę z informatyką zacząłem od maszyn 8 bitowych, a później przeniosłem się na komputery PC. Korzystając z kolejnych maszyn o coraz większej złożoności, tworzyłem aplikacje wszelkiego typu — od gier, poprzez aplikacje zarządzania danymi osobistymi, po sterowniki urządzeń.

Całe oprogramowanie, jakie napisałem w ciągu pierwszej połowy mojej kariery, miało jedną cechę wspólną — zawsze były to aplikacje lokalne, działające na komputerze użytkownika. We wczesnych latach 90. usłyszałem o nowości nazywanej World Wide Web. Dostrzegłem w tym możliwość stworzenia aplikacji pozwalającej na rozliczanie czasu pracy bez potrzeby fatygowania się z miejsca pracy do biura firmy.

Praktyka dosłownie wprawiła mnie w konsternację. Mój przyzwyczajony do tworzenia aplikacji desktopowych umysł po prostu nie potrafił uporać się z bezstanowymi stronami internetowymi. Do tego jeszcze utrudnione debugowanie, serwer uniksowy, do którego nie ma praw administracyjnych, dziwne nawiasy trójkątne… W efekcie „moje młodsze ja” wstydliwie postanowiło na kolejne kilka lat wrócić do tworzenia aplikacji desktopowych. Trzymałem się z daleka od tworzenia aplikacji internetowych. Wiedziałem, że to ważna gałąź informatyki, ale po prostu nie rozumiałem modelu programistycznego. Po pewnym czasie pojawiły się technologie Microsoft® .NET Framework i ASP.NET. Nareszcie istniała platforma pozwalająca na tworzenie aplikacji internetowych w prawie taki sam sposób, w jaki tworzy się aplikacje desktopowe. Mogłem tworzyć okna (strony), definiować zdarzenia dla kontrolek, a narzędzia graficzne pozwalały uniknąć grzebania w nawiasach trójkątnych. A co najlepsze, obiekt view state, będący elementem ASP.NET, automatycznie załatwiał kwestię bezstanowej natury stron internetowych. Znów byłem szczęśliwym programistą. Do czasu…

W miarę powiększania się mojego doświadczenia, moje decyzje stawały się coraz dojrzalsze. Tworząc aplikacje desktopowe, poznałem wiele dobrych praktyk. Dwie najważniejsze z nich to:

  • rozdział zagadnień — nie należy mieszać logiki interfejsu użytkownika z logiką aplikacji,

  • automatyczne testy jednostkowe — stosowanie ich pozwala sprawdzić, czy kod rzeczywiście działa tak, jak nam się wydaje, że powinien działać.

Zasady te obowiązują niezależnie od wykorzystywanych technologii. Rozdział zagadnień to fundamentalna zasada umożliwiająca poradzenie sobie ze złożonością oprogramowania. Łączenie różnych zadań w ramach jednego obiektu — na przykład obliczanie czasu potrzebnego do ukończenia pracy, formatowanie danych i generowanie wykresu — to proszenie się o problemy z konserwacją oprogramowania. Natomiast automatyzacja testów jest nieodzowna dla uzyskania wysokiej jakości kodu przy jednoczesnym utrzymaniu jego spójności, szczególnie w przypadku aktualizacji istniejącego projektu.

Technologia Web Forms, będąca składnikiem ASP.NET, bardzo ułatwiła mi rozpoczęcie tworzenia aplikacji internetowych, jednak z drugiej strony trudno mi było stosować przyjęte przeze mnie zasady programowania. Technologia Web Forms jest ukierunkowana przede wszystkim na tworzenie interfejsu użytkownika — podstawowym elementem jest strona. Tworzenie aplikacji rozpoczyna się od zaprojektowania interfejsu i rozmieszczenia kontrolek. W efekcie, programistów aż kusi, by całą logikę aplikacyjną umieścić w procedurach obsługi zdarzeń generowanych przez stronę (podobnie jak w przypadku języka Visual Basic® i aplikacji dla systemu Windows®).

Na dodatek realizowanie testów jednostkowych stron internetowych jest często utrudnione. Analiza pełnego cyklu życia obiektu Page bez uruchamiania całego motoru ASP.NET nie jest możliwa. Chociaż aplikacje internetowe można testować poprzez wysyłanie do serwera odpowiednich żądań HTTP lub automatyzację przeglądarki, testy tego rodzaju są podatne na błędy (wystarczy zmienić identyfikator jednej kontrolki i cały test zawodzi), trudne w konfiguracji (serwery na wszystkich maszynach programistów muszą być skonfigurowane dokładnie w taki sam sposób) i powolne.

Gdy zacząłem tworzyć bardziej rozbudowane aplikacje internetowe, abstrakcje wprowadzane przez Web Forms, takie jak kontrolki, widok stanu czy cykl życia strony, zaczęły mi przeszkadzać, zamiast ułatwiać zadanie. Coraz więcej czasu poświęcałem na konfigurowanie wiązania danych (i pisanie mnóstwa procedur obsługi zdarzeń w celu zapewnienia poprawności wiązania). Musiałem znaleźć sposób zmniejszenia rozmiaru view state, by moje strony ładowały się szybciej. Technologia Web Forms wymaga, by każdemu adresowi URL odpowiadał fizyczny plik, co utrudnia tworzenie witryn dynamicznych (na przykład wiki). Co więcej, prawidłowe utworzenie własnej kontrolki internetowej wymaga przejścia przez długi i skomplikowany proces oraz dobrej znajomości zarówno cyklu życia strony, jak i narzędzi projektowania Visual Studio®.

Gdy zostałem pracownikiem Microsoft, zyskałem możliwość podzielenia się swoimi przemyśleniami na temat słabych punktów .NET i nadzieję, że uda się usunąć niektóre z nich. Ostatnio taka możliwość nadarzyła się przy okazji mojego przystąpienia jako programisty do realizowanego w ramach patterns & practices projektu Web Client Software Factory (codeplex.com/websf). Jednym z założeń patterns & practices jest umożliwianie prowadzenia zautomatyzowanych testów jednostkowych. W projekcie Web Client Software Factory w celu umożliwienia testowania formularzy internetowych zaproponowaliśmy wykorzystanie wzorca model-widok-prezenter (Model View Presenter — MVP).

Mówiąc krótko, zastosowanie MVP polega na wyeliminowaniu logiki aplikacyjnej z procedur obsługi zdarzeń strony. Aplikacja budowana jest w taki sposób, że strona (widok) odwołuje się do osobnego obiektu — prezentera. Prezenter realizuje całą logikę niezbędną do zareagowania na zdarzenia zgłaszane przez widok i zwykle korzysta z innych obiektów (model) w celu uzyskania dostępu do baz danych, realizacji logiki biznesowej itp. Po realizacji tego procesu, obiekt prezenter aktualizuje widok. Podejście to pozwala na łatwe testowanie kodu, ponieważ obiekt prezenter jest niezależny od potoku przetwarzania ASP.NET. Komunikacja z widokiem realizowana jest za pośrednictwem interfejsu, co pozwala na testowanie obiektu niezależnie od strony internetowej.

Podejście MVP sprawdza się, ale jego implementacja jest trochę niewygodna — potrzebny jest odrębny interfejs widoku, a w plikach z kodem schowanym trzeba utworzyć mnóstwo funkcji przekazujących zdarzenia. Niestety, jeśli zależy nam na łatwym testowaniu interfejsu użytkownika w aplikacjach Web Forms, jest to najlepsze możliwe rozwiązanie. Wprowadzenie jakichkolwiek udoskonaleń wymagałoby zmodyfikowania wykorzystywanej platformy.

Wzorzec model-widok-kontroler

Na szczęście zespół rozwijający ASP.NET uwzględnił opinie programistów takich jak ja i rozpoczął tworzenie nowej platformy rozwoju aplikacji internetowych, funkcjonującej równolegle ze znanymi i lubianymi formularzami Web Forms, charakteryzującej się odmiennymi celami projektowymi:

  • uwzględnienie standardów HTTP i HTML — bez ukrywania ich,

  • łatwość testowania jednym z założeń projektowych,

  • możliwość rozbudowy praktycznie na każdym poziomie,

  • pełna kontrola nad danymi wyjściowymi.

Nowa platforma oparta jest na wzorcu model-widok-kontroler (Model View Controller), stąd też jej nazwa — ASP.NET MVC. Wzorzec MVC po raz pierwszy zaproponowano w latach 70. w ramach prac nad językiem Smalltalk. Jak wykażę w dalszej części artykułu, wzorzec ten dość dobrze wpisuje się w naturę stron internetowych. Interfejs użytkownika dzielony jest na trzy odrębne obiekty: kontroler (przyjmujący i przetwarzający dane wejściowe), model (zawierający logikę domeny problemu) oraz widok (generujący dane wyjściowe). W przypadku aplikacji internetowej dane wejściowe to żądanie HTTP. Proces przetwarzania żądania przedstawiono na ilustracji 1.

Proces przetwarzania żądania

Ilustracja 1 Proces przetwarzania żądania według wzorca MVC

Proces ten różni się od procesu stosowanego w Web Forms. W przypadku Web Forms dane wejściowe przekazywane są bezpośrednio do obiektu strony — widoku — i obiekt widoku jest odpowiedzialny zarówno za przetworzenie danych wejściowych, jak i wygenerowanie danych wyjściowych. Natomiast w przypadku MVC działania te realizowane są przez osobne obiekty.

W tej chwili część czytelników prawdopodobnie myśli: „O, jakie fajne! Jak to stosować?” albo „Po co tworzyć trzy obiekty, skoro do tej pory wystarczał tylko jeden?”. To świetne pytania, a najlepszą odpowiedzią na nie będzie przykład. Dlatego utworzę teraz niewielką aplikację internetową, opartą na platformie MVC, i zademonstruję zalety tej platformy.

 

Utworzenie kontrolera

Do uruchomienia przykładów potrzebna będzie instalacja Visual Studio 2008 i kopia platformy MVC. Gdy pisałem ten artykuł, platformę można było zainstalować jako element rozszerzeń ASP.NET w testowej wersji Community Technology Preview (CTP) z grudnia 2007 (asp.net/downloads/3.5 extensions). Należy pobrać zarówno rozszerzenia w wersji CTP, jak i zestaw MVC Toolkit, który zawiera kilka bardzo przydatnych obiektów pomocniczych. Gdy wersja CTP zostanie pobrana i zainstalowana, w oknie dialogowym New Project pojawi się nowy typ projektu o nazwie ASP.NET MVC Web Application.

Wybranie nowego projektu aplikacji internetowej MVC powoduje utworzenie rozwiązania różniącego się nieco od typowej aplikacji internetowej. Szablon rozwiązania tworzy aplikację internetową zawierającą kilka nowych katalogów (przedstawiono to na ilustracji 2). Katalog Controllers zawiera klasy kontrolerów, natomiast w katalogu Views i jego podkatalogach znajdują się klasy widoków.

Struktura projektu MVC

Ilustracja 2. Struktura projektu MVC

Utworzę bardzo prosty kontroler, który będzie zwracał imię przekazane w adresie URL. Kliknięcie prawym przyciskiem myszy folderu Controllers i wybranie polecenia Add Item powoduje wyświetlenie typowego okna dialogowego Add New Item. W oknie tym widoczne są jednak nowe elementy, w tym klasa MVC Controller Class oraz kilka składników MVC View. Utworzę jak zwykle fantazyjnie nazwaną klasę HelloController:

        using System;
        using System.Web;
        using System.Web.Mvc;

        namespace HelloFromMVC.Controllers
        {
        public class HelloController : Controller
        {
        [ControllerAction]
        public void Index()
        {
        ...
        }
        }
        }

      

Klasa kontrolera jest znacznie „lżejsza” niż klasa strony. W rzeczywistości jedyne, o czym trzeba pamiętać, to dziedziczenie po klasie System.Web.Mvc.Controller oraz opatrzenie metod akcji atrybutami [ControllerAction]. Akcja to metoda wywoływana w odpowiedzi na żądanie przesłane na określony adres URL. Akcje są odpowiedzialne za realizację niezbędnych procesów przetwarzania danych oraz za wyświetlenie widoku. Zacznę od napisania prostej akcji, przekazującej imię do obiektu widoku:

      [ControllerAction]
      public void HiThere(string id)
      {
      ViewData["Name"] = id;
      RenderView("HiThere");
      }

Metoda akcji pobiera imię z adresu URL z parametru id (więcej o tym za chwilę), zapisuje je w kolekcji ViewData, a następnie renderuje obiekt widoku o nazwie HiThere.

Zanim opiszę sposób wywoływania tej metody czy tworzenia widoku, chciałbym powiedzieć nieco o testowaniu. Wcześniej pisałem, że testowanie klas stron Web Forms było bardzo trudne. Testowanie kontrolerów jest nieporównywalnie łatwiejsze. Można bezpośrednio utworzyć instancję kontrolera i wywołać metody akcji. Nie jest potrzebna żadna dodatkowa infrastruktura. Nie jest potrzebny kontekst HTTP ani serwer — wystarczy tylko środowisko testowe. Na poniższej ilustracji przedstawiłem kod testu jednostkowego Visual Studio Team System (VSTS) dla utworzonej uprzednio klasy.

Ilustracja 3. Test jednostkowy kontrolera

        namespace HelloFromMVC.Tests
        {
        [TestClass]
        public class HelloControllerFixture
        {
        [TestMethod]
        public void HiThereShouldRenderCorrectView()
        {
        TestableHelloController controller = new
        TestableHelloController();
        controller.HiThere("Chris");

        Assert.AreEqual("Chris", controller.Name);
        Assert.AreEqual("HiThere", controller.ViewName);
        }

        }

        class TestableHelloController : HelloController
        {
        public string Name;
        public string ViewName;

        protected override void RenderView(
        string viewName, string master, object data)
        {
        this.ViewName = viewName;
        this.Name = (string)ViewData["Name"];
        }
        }

        }
      

Trzeba wyjaśnić tu kilka spraw. Sam test jest bardzo prosty — utworzenie instancji kontrolera, wywołanie metody z określonym parametrem i sprawdzenie, że wyrenderowany zostanie odpowiedni obiekt widoku. Sprawdzenie realizuję poprzez utworzenie specjalnej klasy testowej zastępującej metodę RenderView. Pozwala mi to pominąć etap generowania kodu HTML. Sprawdzam tylko, czy do obiektu widoku zostały przesłane odpowiednie dane i czy renderowany jest odpowiedni obiekt widoku. Szczegóły obiektu widoku w tym teście mnie nie interesują.

 

Utworzenie widoku

Oczywiście w końcu będę musiał wygenerować trochę kodu HTML, utwórzmy więc widok HiThere. W tym celu muszę najpierw utworzyć w katalogu Views podkatalog o nazwie Hello. Domyślnie kontroler szuka obiektu widoku w katalogu Views\<PrefiksKontrolera> (prefiks kontrolera to nazwa klasy kontrolera po usunięciu słowa „Controller”). Obiekt HelloController będzie więc szukał widoków w katalogu Views\Hello. Struktura projektu ma teraz postać taką, jak widoczna na ilustracji 4.

Dodanie widoku do projektu

Ilustracja 4. Dodanie widoku do projektu

Kod HTML widoku ma następującą postać:

      <html  >;
        <head runat="server">;
          <title>;Hi There!</title>;
        </head>;
        <body>;
          <div>;
            <h1>;
              Hello, <%= ViewData["Name"]
            %>
          </h1>;
          </div>;
        </body>;
      </html>;

Tu także warto zwrócić uwagę na parę spraw. Nie ma znaczników runat="server". Nie ma znacznika form. Nie ma deklaracji kontrolek. Kod wygląda raczej jak klasyczna strona ASP, a nie ASP.NET. Widoki MVC służą jedynie do generowania danych wyjściowych, więc nie są potrzebne procedury obsługi zdarzeń czy złożone kontrolki, typowe dla stron Web Forms.

Platforma MVC zapożyczyła format .aspx jako wygodny język tworzenia szablonów. Można nawet tworzyć pliki z kodem schowanym, jednak domyślnie plik taki ma następującą postać:

      using System;
      using System.Web;
      using System.Web.Mvc;

      namespace HelloFromMVC.Views.Hello
      {
      public partial class HiThere : ViewPage
      {
      }
      }

Nie ma tu metod inicjacji czy ładowania strony, procedur obsługi zdarzeń… Nic oprócz deklaracji klasy bazowej, którą jest klasa ViewPage, a nie Page. To wszystko, co potrzebne jest do utworzenia widoku MVC. Jeśli uruchomimy aplikację i otworzymy w przeglądarce adres http://localhost:<port> /Hello/HiThere/Chris, zobaczymy następujący widok:

Pomyślne uruchomienie widoku MVC

Ilustracja 5. Pomyślne uruchomienie widoku MVC

Jeśli zamiast efektu widocznego na ilustracji 5 zostanie wyświetlona informacja o wystąpieniu wyjątku, nie wpadajmy w panikę. Jeżeli w momencie naciśnięcia klawisza F5 aktywnym dokumentem w Visual Studio jest plik HiThere.aspx, Visual Studio spróbuje uzyskać bezpośredni dostęp do tego pliku. Ponieważ do wyświetlenia widoku MVC konieczne jest wcześniejsze uruchomienie kontrolera, próba bezpośredniego otwarcia strony nie powiedzie się. Wystarczy zmodyfikować adres URL tak, by odpowiadał adresowi widocznemu na ilustracji 5, i wszystko powinno działać.

Skąd platforma MVC wie, którą metodę akcji należy wywołać? W adresie URL nie było nawet rozszerzenia pliku. Odpowiedzialny jest za to routing URL. Jeśli zajrzymy do pliku global.asax.cs, zobaczymy fragment kodu z ilustracji 6. Zmienna globalna RouteTable zawiera kolekcję obiektów Route — reguł routingu. Każdy obiekt Route zawiera opis formy adresu URL oraz sposobu obsługi adresów pasujących do tej formy. Domyślnie tabela zawiera dwa obiekty Route. Za magię, którą przed chwilą obserwowaliśmy, odpowiedzialny jest pierwszy z nich. Mówi on, że każdy adres URL, który po nazwie serwera zawiera trzy elementy, należy zinterpretować w taki sposób, że pierwszy element jest nazwą kontrolera, drugi — nazwą akcji, a trzeci — parametrem id:

Ilustracja 6. Tablica routingu

        public class Global : System.Web.HttpApplication
        {
        protected void Application_Start(object sender, EventArgs e)
        {
        // Change Url= to Url="[controller].mvc/[action]/[id]"
        // to enable automatic support on IIS6

        RouteTable.Routes.Add(new Route
        {
        Url = "[controller]/[action]/[id]",
        Defaults = new { action = "Index", id = (string)null },
        RouteHandler = typeof(MvcRouteHandler)
        });

        RouteTable.Routes.Add(new Route
        {
        Url = "Default.aspx",
        Defaults = new {
        controller = "Home",
        action = "Index",
        id = (string)null },
        RouteHandler = typeof(MvcRouteHandler)
        });
        }
        }

      

FakePre-5531334a50794d4eb524dcbff28b19b4-234314d512984e6383b33a0b1a82e551

To ta domyślna reguła routingu umożliwiła wywołanie metody HiThere. Pamiętacie adres URL http://localhost/Hello/HiThere/Chris? Dzięki tej regule Hello zostało zinterpretowane jako nazwa kontrolera, HiThere jako akcja, a Chris jako id. Następnie platforma MVC utworzyła instancje obiektu HelloController i wywołała metodę HiThere, przekazując jako wartość parametru id słowo Chris.

Domyślna reguła pozwala na wiele, ale można także tworzyć własne reguły. Na przykład, jeśli chciałbym utworzyć rzeczywiście przyjazną witrynę, w której — by zostać przywitanym w spersonalizowany sposób — wystarczy podać jedynie imię, na początku tablicy routingu umieściłbym następującą regułę:

        RouteTable.Routes.Add(new Route
        {
        Url = "[id]",
        Defaults = new {
        controller = "Hello",
        action = "HiThere" },
        RouteHandler = typeof(MvcRouteHandler)
        });

      

W takiej sytuacji mógłbym wywołać akcję i wyświetlić spersonalizowane pozdrowienie, wpisując adres http://localhost/Chris.

Skąd system wiedziałby, który kontroler i akcję należy wywołać? Odpowiedzi należy szukać w parametrze Defaults. W przykładzie kodu zastosowano nową, wprowadzoną w C# 3.0, składnię typów anonimowych. Obiekt Defaults w obiekcie Route może przechowywać różne dodatkowe informacje, w tym znane już nam ustawienia MVC — nazwy kontrolera i akcji. Jeśli kontroler lub akcja nie zostaną zdefiniowane w adresie URL, platforma MVC użyje ustawień z obiektu Defaults. Dlatego właśnie w tym przypadku — mimo że nie są podane w adresie URL — żądanie spowoduje uruchomienie odpowiedniego kontrolera i akcji.

W tym miejscu należy zwrócić uwagę na jeszcze jedną sprawę — napisałem, że nową regułę routingu należy umieścić na początku tabeli. Gdybym umieścił ją na końcu tabeli, próba otwarcia strony kończyłaby się komunikatem o błędzie. W routingu stosowana jest pierwsza pasująca reguła. Podczas przetwarzania adresu URL mechanizm routingu przegląda tabelę z góry na dół i wybiera z niej pierwszą pasującą regułę. Gdyby pierwszą regułą była reguła "[controller]/[action]/[id]", zostałaby ona wybrana, ponieważ zdefiniowano domyślne wartości dla akcji i parametru id. System szukałby kontrolera ChrisController, a ponieważ taki nie istnieje, wyświetlony zostałby komunikat o błędzie.

 

Bardziej złożony przykład

Teraz, gdy znamy już podstawy platformy MVC, chciałbym pokazać bardziej złożony przykład, którego funkcjonalność jest szersza niż proste wyświetlenie łańcucha znaków. Wiki to witryna internetowa, której treść można modyfikować z poziomu przeglądarki internetowej. Można łatwo tworzyć nowe i modyfikować istniejące strony. W oparciu o platformę MVC stworzyłem prostą, przykładową witrynę wiki. Na ilustracji 7. widoczny jest ekran modyfikacji strony.

Edycja strony głównej

Ilustracja 7. Edycja strony głównej

Sposób implementacji funkcjonalności wiki można poznać, analizując kod dostępny do pobrania na początku tego artykułu. W tej chwili chciałbym skoncentrować się na sposobie, w jaki platforma MVC ułatwia publikowanie stron wiki w Internecie. Zacząłem od zaprojektowania struktury adresów URL. Chciałem, by:

  • adres /[pagename] powodował wyświetlenie strony o nazwie pagename,

  • adres /[pagename]?version=n powodował wyświetlenie określonej wersji strony, przy czym 0 oznacza bieżącą wersję strony, 1 — poprzednią wersję, itd.,

  • adres /Edit/[pagename] powodował wyświetlenie ekranu edycji danej strony,

  • nowe wersje strony były przesyłane na adres /CreateNewVersion/[pagename].

Zacznijmy od podstawowej funkcji wyświetlenia strony wiki. W celu jej zaimplementowania utworzyłem nową klasę o nazwie WikiPageController, w której utworzyłem metodę akcji ShowPage. Klasa WikiPageController przybrała postać taką, jak przedstawiona na ilustracji 8. Metoda ShowPage jest dość prosta. Klasy WikiSpace i WikiPage reprezentują odpowiednio zbiór stron wiki oraz określoną stronę (razem z jej poszczególnymi wersjami). Akcja po prostu ładuje model i wywołuje metodę RenderView. A jakie zadanie pełni linia „new WikiPageViewData”?

Ilustracja 8. Implementacja klasy WikiPageController i metody ShowPage

        public class WikiPageController : Controller
        {
        ISpaceRepository repository;

        public ISpaceRepository Repository
        {
        get {
        if (repository == null)
        {
        repository = new FileBasedSpaceRepository(
        Request.MapPath("~/WikiPages"));
        }
        return repository;
        }
        set { repository = value; }
        }

        [ControllerAction]
        public void ShowPage(string pageName, int? version)
        {
        WikiSpace space = new WikiSpace(Repository);
        WikiPage page = space.GetPage(pageName);

        RenderView("showpage",
        new WikiPageViewData
        {
        Name = pageName,
        Page = page,
        Version = version ?? 0
        });
        }
        }

      

W poprzednim przykładzie do przekazania danych z kontrolera do widoku użyłem słownika ViewData. Stosowanie słowników jest równie wygodne, co niebezpieczne. Słownik może zawierać praktycznie dowolne dane, przy dostępie do jego zawartości nie można korzystać z pomocy IntelliSense®, a ponieważ słownik ViewData jest obiektem typu Dictionary<string, object=""> , przy dostępie do jego zawartości trzeba stosować rzutowanie typów.

Gdy wiadomo, jakiego typu dane będą potrzebne w widoku, można przekazywać silnie typizowany obiekt ViewData. W tym przypadku utworzyłem prosty obiekt WikiPageViewData. Jego kod widoczny jest na ilustracji 9. Obiekt ten służy do przekazywania informacji o stronie wiki do obiektu widoku oraz zawiera kilka metod pomocniczych, pozwalających na przykład na pobranie kodu strony wiki w formacie HTML.

Ilustracja 9. Obiekt WikiPageViewData

        public class WikiPageViewData {

        public string Name { get; set; }
        public WikiPage Page { get; set; }
        public int Version { get; set; }

        public WikiPageViewData() {
        Version = 0;
        }

        public string NewVersionUrl {
        get {
        return string.Format("/CreateNewVersion/{0}", Name);
        }
        }

        public string Body {
        get { return Page.Versions[Version].Body; }
        }

        public string HtmlBody {
        get { return Page.Versions[Version].BodyAsHtml(); }
        }

        public string Creator {
        get { return Page.Versions[Version].Creator; }
        }

        public string Tags {
        get { return string.Join(",", Page.Versions[Version].Tags); }
        }
        }

      

Zdefiniowaliśmy już obiekt do przekazywania danych. Jak teraz z niego skorzystać? W pliku ShowPage.aspx.cs znajduje się taki kod:

      namespace MiniWiki.Views.WikiPage {
      public partial class ShowPage : ViewPage<WikiPageViewData>
        {
        }
        }

Klasą bazową jest <WikiPageViewData> . Oznacza to, że właściwość ViewData strony jest typu WikiPageViewData, a nieDictionary, jak w poprzednim przykładzie.

Kod strony .aspx jest dość prosty:

        <%@ Page="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
  AutoEventWireup="true" CodeBehind="ShowPage.aspx.cs"
  Inherits="MiniWiki.Views.WikiPage.ShowPage" %>
          <asp:Content
            ID="Content1"
            ContentPlaceHolderID="MainContentPlaceHolder"
            runat="server">
            <h1>
              <%=ViewData.Name%></h1>
            <div id="content" class="wikiContent">
              <%=ViewData.HtmlBody%></div>
          </asp:Content>

        

Łatwo zauważyć, że w odwołaniach do ViewData nie stosuję operatora indeksowania []. Ponieważ obiekt ViewData jest teraz obiektem silnie typizowanym, mogę bezpośrednio odwoływać się do jego właściwości. Nie jest potrzebne żadne rzutowanie, a w Visual Studio można korzystać z IntelliSense.

Spostrzegawczy czytelnicy z pewnością zauważyli znacznik <asp:Content> . Tak, w widokach MVC można wykorzystywać strony wzorcowe (Master Pages). Strony wzorcowe także mogą być widokami. Zobaczmy, jak wygląda plik kodu schowanego strony wzorcowej.

        namespace MiniWiki.Views.Layouts
        {
        public partial class Site :
        System.Web.Mvc.ViewMasterPage<WikiPageViewData>
          {
          }
          }

        

Kod samej strony przedstawiono na ilustracji 10. Strona wzorcowa pobiera dokładnie ten sam obiekt ViewData, który pobierany jest przez obiekt widoku. Klasą bazową strony wzorcowej jest ViewMasterPage<WikiPageViewData> , dzięki czemu właściwość ViewData jest prawidłowego typu. W strukturze strony wykorzystałem kilka znaczników div do sformatowania strony, wypełniłem listę wersji i umieściłem typowy znacznik asp:ContentPlaceHolder.

Ilustracja 10. Site.Master

        <%@ Master="" Language="C#"
  AutoEventWireup="true"
  CodeBehind="Site.master.cs"
  Inherits="MiniWiki.Views.Layouts.Site" %>
          <%@ Import="" Namespace="MiniWiki.Controllers" %>
            <%@ Import="" Namespace="MiniWiki.DomainModel" %>
              <%@ Import="" Namespace="System.Web.Mvc" %>
                <html >
                  <head runat="server">
                    <title>
                      <%=ViewData.Name%></title>
                    <link href="http://../../Content/Site.css" rel="stylesheet" type="text/css" />
                  </head>
                  <body>
                    <div id="inner">
                      <div id="top">
                        <div id="header">
                          <h1>
                            <%=ViewData.Name%></h1>
                        </div>
                        <div id="menu">
                          <ul>
                            <li>
                              <a href="http://Home">Home</a>
                            </li>
                            <li>
                              <%=Html.ActionLink("Edit this page",new{controller="WikiPage",action="EditPage",pageName=ViewData.Name})%></ul>
                        </div>
                      </div>
                      <div id="main">
                        <div id="revisions">
                          Revision history:
                          <ul>
                            <% 

                                  i = "0"
                              foreach="" (WikiPageVersion="" version="" in="" ViewData.Page.Versions="")
            { %>
                              <li>
                                <a href="http://"
                                  <%= ViewData.Name %>?version=<%= i %>">
                                  <%= version.CreatedOn %>
                                  by
                                  <%= version.Creator %>
                                </a>
                              </li>
                              <%  ++i;
          } %>
                              
                          </ul>
                        </div>
                        <div id="maincontent">
                          <asp:ContentPlaceHolder
                            ID="MainContentPlaceHolder"
                            runat="server">
                          </asp:ContentPlaceHolder>
                        </div>
                      </div>
                    </div>
                  </body>
                </html>

              

Warto zwrócić uwagę na wywołanie metody Html.ActionLink. Jest to przykład wykorzystania funkcji pomocniczej renderowania. Klasy obiektów widoku zawierają właściwości Html i Url. Właściwości te udostępniają metody ułatwiające konstruowanie fragmentów kodu HTML. Metoda Html.ActionLink pobiera przekazany w parametrze obiekt (w tym wypadku typu anonimowego), analizuje go w oparciu o system routingu i tworzy adres URL powodujący uruchomienie wskazanego kontrolera i akcji. Dzięki temu — niezależnie od tego, w jaki sposób w przyszłości zmodyfikuję reguły routingu — łącze „Edit this page” będzie zawsze działało prawidłowo.

W przypadku łączy do wcześniejszych wersji strony byłem zmuszony do rezygnacji ze stosowania funkcji pomocniczej. W obecnej wersji platformy system routingu niezbyt dobrze radzi sobie z generowaniem adresów URL zawierających łańcuch zapytania. Problem ten zostanie rozwiązany w następnej wersji platformy.

 

Tworzenie formularzy i przesyłanie informacji zwrotnych

Przyjrzyjmy się teraz akcji EditPage kontrolera:

        [ControllerAction]
        public void EditPage(string pageName)
        {
        WikiSpace space = new WikiSpace(Repository);
        WikiPage page = space.GetPage(pageName);

        RenderView("editpage",
        new WikiPageViewData {
        Name = pageName,
        Page = page });
        }

      

Ta akcja także nie jest zbyt skomplikowana — jedyne co robi, to wyrenderowanie widoku określonej strony. Dużo ciekawsze rzeczy mają miejsce w obiekcie widoku, którego kod przedstawiono na ilustracji 11. Plik ten zawiera formularz HTML, ale bez atrybutu Runat="server". Do wygenerowania adresu URL, na który przesyłana jest zawartość formularza, stosowana jest funkcja pomocnicza Url.Action. Zastosowano także inne funkcje pomocnicze, takie jak: TextBox, TextArea i SubmitButton. Służą one do generowania kodu HTML pól formularza.

Ilustracja 11. EditPage.aspx

        <%@ Page="" Language="C#"
  MasterPageFile="~/Views/Shared/Site.Master"
  AutoEventWireup="true"
  CodeBehind="EditPage.aspx.cs"
  Inherits="MiniWiki.Views.WikiPage.EditPage" %>
          <%@ Import="" Namespace="System.Web.Mvc" %>
            <%@ Import="" Namespace="MiniWiki.Controllers" %>
              <asp:Content ID="Content1"
                ContentPlaceHolderID="MainContentPlaceHolder"
                runat="server">
                <form action=""
                  <%= Url.Action(
                  new { controller = "WikiPage",
                  action = "NewVersion",
                  pageName = ViewData.Name })%>" method=post>
                  <%
                  if (ViewContext.TempData.ContainsKey("errors"))
                  {
                  %>
                  <div id="errorlist">
                    <ul>
                      <%foreach(stringerrorin(string[])ViewContext.TempData["errors"]){%><li>
                        <%=error%></li>
                      <%}%></ul>
                  </div>
                  <% } %>
                  Your name: <%= Html.TextBox("Creator",
                  ViewContext.TempData.ContainsKey("creator") ?
                  (string)ViewContext.TempData["creator"] :
                  ViewData.Creator)%>
                  <br />
                  Please enter your updates here:<br />
                  <%= Html.TextArea("Body", ViewContext.TempData.ContainsKey("body") ?
                  (string)ViewContext.TempData["body"] :
                  ViewData.Body, 30, 65)%>
                  <br />
                  Tags: <%= Html.TextBox(
                  "Tags", ViewContext.TempData.ContainsKey("tags") ?
                  (string)ViewContext.TempData["tags"] :
                  ViewData.Tags)%>
                  <br />
                  <%= Html.SubmitButton("SubmitAction", "OK")%>
                  <%= Html.SubmitButton("SubmitAction", "Cancel")%>
                </form>
              </asp:Content>

Jedną z bardziej denerwujących spraw w aplikacjach internetowych jest obsługa błędnych danych wprowadzonych w formularzu. Chodzi o to, by wyświetlać komunikaty o błędach, ale jednocześnie zachować już wprowadzone dane. Każdemu z nas na pewno zdarzyło się popełnić błąd podczas wypełniania formularza o 35 polach, po którym strona wyświetliła tuzin komunikatów o błędach i nowy, pusty formularz. Platforma MVC pozwala na przechowywanie wprowadzonych informacji w obiekcie TempData, co pozwala na ponowne automatyczne wypełnienie formularza. Funkcjonalność ta jest podobna do ViewState znanego z formularzy Web Forms, które pozwalało na automatyczne zapamiętywanie zawartości kontrolek.

Chciałem uzyskać analogiczną funkcjonalność w MVC i dlatego wymyśliłem TempData. Obiekt TempData jest słownikiem, tak samo jak nietypizowany obiekt ViewData. Jednakże zawartość obiektu TempData przechowywana jest jedynie w czasie obsługi jednego żądania, a następnie usuwana. Sposób wykorzystania TempData można poznać, analizując kod akcji NewVersion, przedstawiony na ilustracji 12.

Ilustracja 12. Akcja NewVersion

        [ControllerAction]
        public void NewVersion(string pageName) {
        NewVersionPostData postData = new NewVersionPostData();
        postData.UpdateFrom(Request.Form);

        if (postData.SubmitAction == "OK") {
        if (postData.Errors.Length == 0) {
        WikiSpace space = new WikiSpace(Repository);
        WikiPage page = space.GetPage(pageName);
        WikiPageVersion newVersion = new WikiPageVersion(
        postData.Body, postData.Creator, postData.TagList);
        page.Add(newVersion);
        } else {
        TempData["creator"] = postData.Creator;
        TempData["body"] = postData.Body;
        TempData["tags"] = postData.Tags;
        TempData["errors"] = postData.Errors;

        RedirectToAction(new {
        controller = "WikiPage",
        action = "EditPage",
        pageName = pageName });
        return;
        }
        }

        RedirectToAction(new {
        controller = "WikiPage",
        action = "ShowPage",
        pageName = pageName });
        }

      

Najpierw tworzony jest obiekt NewVersionPostData. Jest to kolejny obiekt pomocniczy, udostępniający właściwości i metody pozwalające na przechowanie i wstępne zwalidowanie zawartości formularza. W celu załadowania danych do obiektu postData korzystam z funkcji pomocniczej zestawu MVC. Metoda UpdateFrom jest w rzeczywistości metodą rozszerzającą, dostarczoną w ramach zestawu, korzystającą z refleksji w celu przyporządkowania nazw pól formularza do nazw właściwości obiektu. Efektem jej działania jest zapisanie zawartości wszystkich pól formularza w obiekcie postData. Stosowanie metody UpdateFrom ma jednak jedną wadę, polegającą na pobieraniu wszystkich danych formularza z obiektu HttpRequest, co utrudnia prowadzenie testów jednostkowych.

Następnie akcja NewVersion sprawdza wartość właściwości SubmitAction. Jeśli użytkownik rzeczywiście chciał przesłać zawartość zmodyfikowanej strony i kliknął przycisk OK, właściwość ta ma wartość OK. Jeśli właściwość ta ma inną wartość, akcja przekierowuje przeglądarkę do akcji ShowPage powodującej wyświetlenie oryginalnej wersji strony i kończy pracę.

Jeśli użytkownik kliknął przycisk OK, w następnej kolejności sprawdzana jest właściwość postData.Errors. Jej wartość jest wyznaczana na podstawie walidacji informacji zwrotnych przesłanych przez przeglądarkę. Jeśli nie wystąpiły żadne błędy, tworzona jest i zapisywana nowa wersja strony wiki. Natomiast jeżeli wystąpiły jakieś błędy, robi się ciekawie.

Jeżeli wystąpiły błędy, kod wypełnia poszczególne pola słownika TempData zawartością obiektu postData, a następnie przekierowuje przeglądarkę z powrotem do strony edycji. Ponieważ obiekt TempData został wypełniony danymi, strona wyświetli formularz z polami zainicjowanymi danymi wprowadzonymi ostatnim razem.

Proces przetwarzania informacji zwrotnych, walidacji i obsługi obiektu TempData jest obecnie nieco irytujący i wymaga od programisty większego zaangażowania niż jest to rzeczywiście potrzebne. Następne wydania platformy prawdopodobnie będą zawierały metody pomocnicze, pozwalające na automatyzację przynajmniej części obsługi obiektu TempData. Jeszcze jedna uwaga dotycząca obiektu TempData — jego zawartość przechowywana jest w sesji użytkownika po stronie serwera. Jeżeli obsługa sesji zostanie wyłączona, obiekt TempData nie będzie działał.

 

Tworzenie kontrolerów

Podstawowa funkcjonalność witryny wiki już działa, w jej implementacji brakuje jednak jeszcze w paru miejscach szlifów. Na przykład do separacji funkcjonalności wiki od funkcji składowania danych wykorzystywana jest właściwość Repository. Pozwala to na stosowanie repozytoriów przechowujących treść stron w systemie plików (tak jak zrobiłem w tym przypadku), w bazie danych lub w dowolny inny sposób. Niestety, widzę tu dwa problemy.

Po pierwsze, klasa kontrolera jest ściśle związana z określoną klasą — FileBasedSpaceRepository. Przydałaby się jakaś wartość domyślna, która w przypadku nieustawienia właściwości zapewniałaby rozsądne wyjście z sytuacji. Co gorsza, ścieżka do plików na dysku także została sztywno określona w kodzie programu, a powinna być przynajmniej ładowana z konfiguracji.

Po drugie, obiekt repozytorium jest absolutnie niezbędny — obiekt kontrolera nie będzie bez niego działał. Dobre praktyki projektowania podpowiadają, że w takiej sytuacji repozytorium powinno być parametrem konstruktora, a nie właściwością. Niestety uwzględnienie tej zasady nie jest możliwe, ponieważ platforma MVC wymaga, by konstruktory kontrolerów były bezparametrowe.

Na szczęście istnieje technika pozwalająca na wyjście z tej sytuacji patowej — można zbudować fabrykę kontrolerów. Fabryka kontrolerów robi dokładnie to, co sugeruje jej nazwa — tworzy instancje obiektów kontrolera. Wystarczy jedynie utworzyć klasę implementującą interfejs IControllerFactory i zarejestrować ją w systemie MVC. Można zarejestrować fabryki kontrolerów dla wszystkich kontrolerów albo jedynie dla określonych typów. Na ilustracji 13. przedstawiono kod fabryki kontrolerów WikiPageController, pozwalającej na przekazanie repozytorium jako parametru konstruktora.

        public class WikiPageControllerFactory : IControllerFactory {

        public IController CreateController(RequestContext context,
        Type controllerType)
        {
        return new WikiPageController(
        GetConfiguredRepository(context.HttpContext.Request));
        }

        private ISpaceRepository GetConfiguredRepository(IHttpRequest request)
        {
        return new FileBasedSpaceRepository(request.MapPath("~/WikiPages"));
        }
        }

      

Ilustracja 13. Fabryka kontrolerów

            public class WikiPageControllerFactory : IControllerFactory {

            public IController CreateController(RequestContext context,
            Type controllerType)
            {
            return new WikiPageController(
            GetConfiguredRepository(context.HttpContext.Request));
            }

            private ISpaceRepository GetConfiguredRepository(IHttpRequest request)
            {
            return new FileBasedSpaceRepository(request.MapPath("~/WikiPages"));
            }
            }

          

W tym wypadku implementacja jest całkiem prosta, jednak technika ta pozwala na tworzenie kontrolerów korzystających z bardzo zaawansowanych technik, w szczególności z kontenerów wstrzeliwywania zależności (dependency injection container). W każdym bądź razie, udało nam się rozdzielić kontroler od jego zależności i umieścić je w obiekcie łatwym w zarządzaniu i konserwacji.

Ostatnim krokiem, niezbędnym do uzyskania działającego projektu, jest zarejestrowanie fabryki w platformie. Wykorzystuję do tego celu klasę ControllerBuilder, dodając w metodzie Application_Start w pliku Global.asax.cs (przed lub po konfiguracji reguł routingu) następujący kod:

ControllerBuilder.Current.SetControllerFactory(
          typeof(WikiPageController), typeof(WiliPageControllerFactory));

Kod ten powoduje zarejestrowanie fabryki obiektów WikiPageController. Gdybym w projekcie korzystał z innych kontrolerów, fabryka ta nie byłaby wykorzystywana do ich tworzenia, ponieważ jest ona zarejestrowana jedynie dla typu WikiPageController. Chcąc zarejestrować fabrykę kontrolerów dowolnego typu, należy użyć metody SetDefaultControllerFactory.

 

Inne możliwości rozszerzania

Fabryka kontrolerów to jedynie wstęp do rozszerzania platformy. Nie będę w tym artykule szczegółowo opisywał wszystkich metod — naświetlę jedynie dostępne możliwości. Po pierwsze, jeśli chcemy generować coś innego niż kod HTML lub korzystać z silnika szablonów innego niż Web Forms, możemy zastąpić właściwość ViewFactory kontrolera innym obiektem. Implementując interfejs IViewFactory można uzyskać pełną kontrolę nad sposobem generowania danych wyjściowych. Pozwala to na generowanie danych RSS, XML, a nawet grafiki.

Jak mogliśmy się przekonać, system routingu jest dość elastyczny. W systemie tym nie ma jednak nic, co powodowałoby, że nie da się go zastosować z technologiami innymi niż MVC. Każda reguła routingu ma właściwość RouteHandler. Do tej pory zawsze przypisywałem do niej obiekt MvcRouteHandler. Można jednak zaimplementować interfejs IRouteHandler i wykorzystać system routingu z innymi technologiami internetowymi. W przyszłej wersji platformy planowane jest zaimplementowanie obiektu WebFormsRouteHandler. Inne technologie także w przyszłości będą korzystały z ogólnego systemu routingu.

Kontrolery nie muszą dziedziczyć po klasie System.Web.Mvc.Controller. Jedyne co jest niezbędne, to zaimplementowanie interfejsu IController, zawierającego tylko jedną metodę o nazwie Execute. Poza tym implementacja tego obiektu jest dość dowolna. Natomiast jeśli chcemy jedynie dostosować zachowanie podstawowej klasy kontrolera, w klasie tej znajdziemy wiele funkcji wirtualnych, które można zastąpić:

  • Funkcje OnPreAction i OnPostAction pozwalają na dołączenie kodu uruchamianego przed lub po wykonaniu każdej akcji. Funkcja OnError pozwala na zbudowanie jednego mechanizmu obsługi błędów dla całego kontrolera.

  • Funkcja HandleUnknownAction jest wywoływana w przypadku, gdy adres URL wskazuje na dany kontroler, ale kontroler ten nie implementuje akcji wskazanej przez regułę routingu. Domyślnie metoda ta wyrzuca wyjątek, ale można ją zastąpić tak, by robiła cokolwiek innego.

  • InvokeAction to metoda określająca, którą metodę akcji należy wywołać, i wywołująca ją. Jeśli chcielibyśmy dostosować ten proces (na przykład by nie trzeba było podawać atrybutów [ControllerAction]), należy zastąpić właśnie tę metodę.

Obiekt Controller zawiera także inne metody wirtualne, jednak ich przeznaczeniem jest raczej ułatwienie testowania, a nie rozszerzanie funkcjonalności platformy. Na przykład metoda RedirectToAction jest metodą wirtualną po to, by można było utworzyć klasę potomną, która w rzeczywistości nie realizuje przekierowania. Dzięki temu akcje realizujące przekierowanie można testować bez potrzeby uruchamiania całego serwera internetowego.

 

Pożegnanie z Web Forms?

Niektórzy czytelnicy mogą zacząć zastanawiać się, co się dzieje z Web Forms. Czy MVC zastąpi tę technologię? Odpowiedź brzmi: nie! Web Forms to dobrze znana i popularna technologia i firma Microsoft będzie kontynuowała prace nad jej rozwojem. Istnieje wiele zastosowań, w których formularze Web Forms bardzo dobrze się sprawdzają. Utworzenie typowej aplikacji raportowania z wykorzystaniem Web Forms zajmuje jedynie ułamek czasu, jaki trzeba by poświęcić na budowę tej aplikacji z wykorzystaniem MVC. Ponadto Web Forms pozwala na korzystanie z ogromnego zasobu gotowych kontrolek, z których wiele jest bardzo rozbudowane i pozwala zaoszczędzić czas programistów.

W takim razie kiedy zamiast Web Forms należy wybierać MVC? To w dużej mierze zależy od wymagań i preferencji programisty. Jeśli niezbędne jest uzyskanie określonej postaci adresów URL lub zachodzi konieczność przeprowadzania testów jednostkowych interfejsu użytkownika, wskazane jest zastosowanie MVC. Z drugiej jednak strony, gdy potrzebne są rozbudowane funkcje wyświetlania danych, edytowalne tabele i złożone kontrolki widoku drzewa, lepiej wybrać technologię Web Forms.

Z czasem platforma MVC prawdopodobnie nadrobi zaległości w zakresie interfejsu użytkownika, ale prawdopodobnie nigdy nie będzie tak łatwa w użyciu, jak platforma Web Forms, pozwalająca na budowę funkcjonalności aplikacji poprzez proste przeciąganie i upuszczanie kontrolek. Nie mniej jednak platforma ASP.NET MVC udostępnia programistom internetowym nowy sposób budowy aplikacji w oparciu o platformę Microsoft .NET Framework. Platforma MVC była tworzona z nastawieniem na ułatwienie testów, wykorzystanie charakterystycznych cech protokołu HTTP zamiast ukrywania ich i praktycznie każdy jej element może być rozbudowywany. Jest rozwiązaniem komplementarnym do Web Forms, przeznaczonym dla tych programistów, którzy chcą mieć pełną kontrolę nad swoimi aplikacjami internetowymi.

Chris Tavares jest programistą w zespole patterns & practices w firmie Microsoft. Jego zadaniem jest ułatwienie społeczności programistów zrozumienia najlepszych praktyk budowania systemów informatycznych opartych na technologiach Microsoft. Jest także wirtualnym członkiem zespołu ASP.NET MVC, pomagającym w projektowaniu nowej platformy. Z Chrisem można skontaktować się pod adresem cct@tavaresstudios.com.