Spis treści:

Kategoria:C#Wzorce projektoweASP.NET MVC


Niestandardowy sposób tworzenia kontrolerów w ASP.NET MVC

MVC sposobem na czysty kod

MVC (Model-View-Controller, Model-Widok-Kontroler), podobnie jak kilka innych architektonicznych wzorców projektowych, ma za zadanie odpowiednio zorganizować strukturę aplikacji. Ten akurat wzorzec ma rozdzielić warstwę danych od warstwy interfejsu użytkownika za pomocą kontrolera. To kontroler tutaj rządzi. To on przyjmuje dane i zarządza aktualizacją widoku, to on utrzymuje w ryzach teoretycznie niezależne od siebie modele i widoki. To dlatego kontrolerem, panem i władcą MVC, zajmę się w tym wpisie. Wiemy dobrze, że w domyślnej konfiguracji MVC kontrolery tworzą się same na podstawie wartości otrzymanej po sparsowaniu adresu strony i dopasowaniu do wpisu z tabeli rutingu. Przyjrzyjmy się przykładowemu wpisowi takiej tabeli:

routes.MapRoute(
    "Default"// Nazwa definicji rutingu
    "{controller}/{action}/{id}"// Adres URL z parametrami
    new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Wartości domyślne
);

Pokazane domyślne ustawienie zakłada, że adresy zbudowane są następująco: www.mojastrona.pl/kontroler/akcja/id. Jeżeli pierwsza wartość po ukośniku będzie równa Home lub nie będzie niczego po ukośniu (Home wobec ustawień rutingu zostanie przyjęte domyślnie), wtedy magiczne mechanizmy ASP.NET MVC utworzą nam obiekt kontrolera typu HomeController znajdującego się w przestrzeni nazw NazwaDll.Controllers. Domyślnie też zostanie wywołany konstruktor bezparametrowy. To wszystko stanie się domyślnie...

Podmiana domyślnej fabryki kontrolerów

Aby podmienić domyślną fabrykę kontrolerów wystarczy wykonać dwie czynności:

  1. Zaimplementować własną fabrykę dziedziczącą pośrednio lub bezpośrednio z interfejsu IControllerFactory.
  2. Przekazać utworzoną fabrykę budowiczemu kontrolerów we władanie.

Gdy ASP.NET MVC otrzymuje żądanie, zadanie utworzenia kontrolera zlecane jest właśnie budowniczemu kontrolerów (kolejny wzorzec). Wspomniany budowniczy domyślnie korzysta z implementacji fabryki zdefiniowanej w klasie DefaultControllerFactory. Aby przekazać budowniczemu inną fabrykę należy wywołać metodę SetControllerFactory obiektu budowniczego. Najwygodniej zrobić to podczas startu aplikacji, w metodzie Application_Start, w pliku Global.asax.cs:

protected void Application_Start()
{
    ControllerBuilder.Current.SetControllerFactory(new ExtendedControllerFactory());
    //Pozostałe wywołania
    //...

Pozostałą część zadania wykona już konkretna implementacja - w tym przypadku klasy ExtendedControllerFactory.

Kontroler ASP.NET MVC z parametrami

Nie po to wymyślono konstruktory z parametrami, żeby nas teraz ograniczać! Popularnym i często stosowanym rozwiązaniem jest przekazywanie różnych obiektów do klas w celu zlecania im specjalnych zadań. Takie rozwiązanie pozwala ten obiekt łatwiej testować. Przypuśćmy, że dysponujemy usługą WCF implementującą pewien interfejs. Podczas normalnej pracy aplikacji chcemy, aby była to rzeczywista usługa, jednak podczas testów chcemy pobierać dane testowe ze spreparowanych komponentów symulujących usługę WCF. Co trzeba zrobić? Wystarczy tylko wpiąć implementację, to znaczy przekazać do konstruktora inny obiekt. Takie właśnie zadanie sobie postawiłem - przekazać do konstruktora kontrolera obiekt, który nazwałem repozytorium.

Wspomniałem wcześniej, że należy zaimplementować, pośrednio lub bezpośrednio, interfejs IControllerFactory. Najprostszym sposobem implementacji tego interfejsu jest dziedziczenie z klasy DefaultControllerFactory, która całą implementację załatwia, i nadpisanie dosłownie jednej metody. Resztę można pozostawić bez zmian. Popatrzmy na przykład:

public class ExtendedControllerFactoryDefaultControllerFactory
{
    protected override IController GetControllerInstance(
        System.Web.Routing.RequestContext requestContext,
        Type controllerType)
    {
        var constructor = controllerType.GetConstructor(new Type[] { typeof(IRepository) });
        if (constructor != null)
        {
            return Activator.CreateInstance(controllerType, new Repository()) as Controller;
        }
        else
        {
            return base.GetControllerInstance(requestContext, controllerType);
        }
    }
}

Implementacja bardzo prosta, bo przykładowa. Jeżeli klasa posiada konstruktor z typem IRepository, wywołane zostanie właśnie to przeciążenie, a w postaci parametru przekazane zostanie własne repozytorium. Jeżeli odpowiedniego konstruktora nie ma, wywołana zostanie metodę bazowa. Przypuśćmy teraz, że kontroler HomeControler jest zdefiniowany następująco:

public class HomeControllerController
{
    public HomeController()
    {

    }

    public HomeController(IRepository repository)
    {
        // Wywołany zostanie konstruktor z parametrem
        // Tu zrób coś z repozytorium...

W takim przypadku wywołany zostanie drugi konstruktor, z parametrem. Wartość parametru może zostać zapamiętana i wykorzystana w dowolnej metodzie kontrolera.

Automatyczne uzupełnianie właściwości z repozytorium

Nie jest to jedyny sposób wstrzyknięcia implementacji repozytorium. Pokazany wcześniej przykład znakomicie sprawdza się w przypadku prostych konstruktorów, ze zdefiniowanym zestawem repozytoriów. Gdy jednak mamy do czynienia z dziedziczeniem i klasą bazowa, która również przyjmuje repozytorium w konstruktorze - sprawa nieco się komplikuje. Nie każdemu chce się pisać konstruktor tylko po to, aby wymusić wywołanie konstruktora klasy bazowej z odpowiednim zestawem parametrów. Pokażę nieco inny sposób wpinania obiektów, polegający na dopasowaniu interfejsów. Przypuśćmy, że kontroler HomeController ma pewną właściwość:

public class HomeControllerController
{
    IRepository Repository { getset; }

    // Pozostałe metody i właściwości

Fabryka kontrolerów będzie teraz rozpoznawała odpowiednie typy interfejsów i wstawiała do nich gotowe implementacje:

public class ExtendedControllerFactoryDefaultControllerFactory
{
    protected override IController GetControllerInstance(
        System.Web.Routing.RequestContext requestContext,
        Type controllerType)
    {
        var allProperties = controllerType.GetProperties();
        var matchingProperties = allProperties.Where(a => a.PropertyType == typeof(IRepository));
        
        var controller = base.GetControllerInstance(requestContext, controllerType);
        foreach (var property in matchingProperties)
        {
            property.SetValue(controller, new Repository(), null);
        }

        return controller;
    }
}

Pokazaną metodę łatwo przerobić na taką, która będzie uzupełniała nieskończoną liczbę właściwości reprezentujących repozytoria. Możną ją również nieco przerobić i uzupełniać tylko te właściwości, które oznaczone są określonymi atrybutami. Można ją udoskonalić zapamiętując typ kontrolera i powiązane z tym typem właściwości w celu szybkiego uzupełnienia - nie musimy badać właściwości za każdym razem, gdy tworzymy kontroler danego typu. Można w końcu zastosować pola z opóźnionym ładowaniem repozytorium, jeżeli tworzenie tego repozytorium trwa długo i nie jest ono wykorzystywane przez każdą metodę.

Nie jest ważne, jakie metody rozszerzenia wybierzemy. Ważne jest, że w momencie wywołania akcji rozważanego kontrolera, wszystkie potrzebne repozytoria będą już uzupełnione i gotowe do użycia.

Jeszcze większe możliwości - IControllerFactory

W zaprezentowanych przykładach pokazałem uproszczony sposób tworzenia własnej fabryki kontrolerów. Interfejs fabryki IControllerFactory ma więcej metod. Pozwalają one między innymi na podmianę lokalizacji, z której pobierane będą implementacje. Wiemy, że domyślnie jest to katalog Controllers (a co za tym idzie również przestrzeń nazw). Pamiętajmy równocześnie, że położenie klas z kontrolerami to pewna konwencja. Jeżeli konwencja jest dobrze znana, zyskujemy bardzo dużo. Jeżeli konwencja nie jest znana, może sprawiać wielkie trudności. Zmieniając konwencję należy poinformować wszystkich użytkowników projektu - w przeciwnym razie zrobimy sobie krzywdę. A stawka jest duża: uproszczenie kodu i przeniesienie powtarzalnej logiki w jedno miejsce. Nie trzeba tłumaczyć ile to znaczy w dużych projektach.

Kategoria:C#Wzorce projektoweASP.NET MVC

, 2013-12-20

Brak komentarzy - bądź pierwszy

Dodaj komentarz
Wyślij
Ostatnie komentarze
Dzieki za rozjasnienie zagadnienia upsert. wlasnie sie ucze programowania :).
Co się stanie gdy spróbuję wyszukać:
SELECT * FROM NV_Airport WHERE Code='SVO'
SELECT * FROM V_Airport WHERE Code=N'SVO'
(odwrotnie są te N-ki)
Będzie konwersja czy nie znajdzie żadnego rekordu?