Wprowadzenie do ASP.NET MVC 3.0

Udostępnij na: Facebook

Autor: Piotr Zieliński

Opublikowano: 2011-06-03

Wprowadzenie

ASP.NET MVC 3.0 jest kolejnym, głównym wydaniem frameworku wspierającego wzorzec Model-View-Controller. W wersji 3.0 wprowadzono wiele rozszerzeń ułatwiających tworzenie rozbudowanych aplikacji webowych. Aby zacząć pracę z najnowszą wersją, należy najpierw ściągnąć niezbędne narzędzia ze stron Microsoftu.

Warto zaznaczyć, że wersja 3.0 jest kompatybilna wstecz. Wszelkie projekty napisane z myślą o wersji 2.0 będą również działać na 3.0. Po zainstalowaniu MVC 3.0, projekty, które aktualnie wykorzystują 2.0 wciąż będą korzystać z tej wersji, chyba że sami zmienimy w ustawieniach wersję frameworku. W tym artykule przyjrzymy się kilku ulepszeniom wprowadzonym w najnowszej wersji.

Globalne filtry

W poprzednich wersjach został wprowadzony atrybut HandleError:

[HandleError]
public class HomeController : Controller

"%~dp0” zwróci ścieżkę do katalogu, w którym znajduje się aktualnie wykonywany skrypt (czyli ścieżka do folderu Tasks),

5) w pliku ServiceDefinition.csdef dla danej roli doczepiamy skrypt uruchomieniowy (wywoła się przy starcie roli):

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    filters.Add(new HandleErrorAttribute());
}
protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);
}

Domyślnie, po utworzeniu szablonu, Visual Studio dodaje filtr HandleErrorAttribute:

filters.Add(new HandleErrorAttribute());

Następnie metoda jest wywoływana w momencie startu aplikacji (Application_Start). Koncepcja przypomina programowanie aspektowe – przypisanie atrybutu również jest tzw. problemem cross-cutting.

Dynamiczny ViewModel

W poprzednich wersjach mogliśmy do widoku przekazywać model oparty na własnej klasie lub wykorzystać właściwość ViewData. Przykład:

public ActionResult About()
{
    ViewData["Text"] = "jakis tekst";
    return View();
}

Składnia z wykorzystaniem ViewData jest mało czytelna – wersja 3.0 wprowadza dynamiczny typ ViewBag:

public ActionResult About()
{
    this.ViewBag.Text = "jakis tekst";
    return View();
}

ViewBag jest typu dynamic (został wprowadzony w .NET 4.0). Rozważmy następujący przykład:

dynamic text = "tekst";
text *= 1;

Powyższy kod skompiluje się, ponieważ typy dynamic są sprawdzane w momencie wywołania kodu (uruchomienia aplikacji), a nie kompilacji. Aplikacja wyrzuci wyjątek w momencie działania, ponieważ nie można pomnożyć tekstu przez cyfrę. Gdybyśmy wykorzystali typ string zamiast dynamic, w czasie kompilacji wystąpiłby oczywiście błąd. Dzięki temu konstrukcja z ViewBag, przedstawiona powyżej, jest możliwa – możemy odwoływać się do jakichkolwiek właściwości. Wyświetlenie ViewBaga w widoku wygląda analogicznie:

<%=ViewBag.Text %>

Nowe typy ActionResult

W wersji 3.0 wprowadzono kilka nowych ActionResult. Pierwszym z nich jest HttpNotFoundResult, który zwraca błąd z kodem HTTP 404 („nie znaleziono”):

public ActionResult About()
{
    return new HttpNotFoundResult();
}

Kolejną nowością jest właściwość Permanent w HttpRedirectResult. Tu do dyspozycji mamy trzy nowe metody przekierowywania:

  • RedirectPermanent(),
  • RedirectToRoutePermanent() oraz
  • RedirectToActionPermanent(). 

Permanent Redirect powoduje przekierowanie strony z kodem HTTP 301. Permanentne przekierowania wykorzystywane są wtedy, gdy aplikacja na stałe zmieni adres. Wyszukiwarki internetowe po wykryciu przekierowania aktualizują swój system rankingowy i akceptują nową lokalizację. Stara strona po jakimś czasie będzie miała niski ranking i nie będzie wyświetlana w wynikach wyszukiwania.

Kolejny, nowy typ (HttpStatusCodeResult) umożliwia zwrócenie własnego kodu i opisu statusu.

Walidacja

Wprowadzono też kilka nowych sposobów walidacji danych – przede wszystkim można korzystać z interfejsu IValidatableObject, który pojawił się  w wersji 4.0. Rozważmy przykładową klasę:

public class Product : IValidatableObject
{
    public float Price { get; set; }
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)            
    {
        if (Price < 0)
            yield return new ValidationResult("tekst",new string[]{"Price"});
    }
}

Użycie IValidatableObject wymaga implementacji metody Validate, która zwraca listę błędów. ASP.NET MVC 3.0 podświetli błędne pola na czerwono, bazując na właściwościach modelu.

Drugą nowością jest wsparcie atrybutów .NET 4.0 DataAnnotations. Chodzi o przestrzeń System.ComponentModel.DataAnnotations i atrybuty takie jak RangeAttribute czy RegularExpressionAttribute.

Ponadto można wykorzystywać rozszerzone możliwości ValidationAttribute. Od wersji .NET 4.0 ValidationAttribute posiada przeładowaną metodę IsValid, dostarczającą kontekst walidacji. Umożliwia to dokładniejszą weryfikację modelu, bazując również na innych właściwościach (a nie tylko na pojedynczej).

Warto wspomnieć również o atrybucie SkipRequestValidation. ASP.NET MVC dostarcza wsparcie dla walidacji danych pod kątem ataków XSS. Jeśli jednak z jakiegoś powodu nie chcemy, aby walidacja odbywała się dla określonych właściwości modelu, wystarczy oznaczyć daną właściwość atrybutem SkipRequestValidation – sprawdzanie zostanie pomięte.

Wstrzyknięcie zależności

Wprowadzenie

Jedną z ważniejszych cech ASP.NET MVC jest wysoka testowalność. Dzięki IoC (Inversion of Control) programista może wstrzykiwać konkretną implementację obiektu. Wstrzykiwanie zależności jest szczególnie istotne w testach jednostkowych oraz integracyjnych, ponieważ możemy wykorzystać tzw. obiekty mock.

W wersji 3.0 został wprowadzony interfejs IDependencyResolver, który upraszcza wykorzystywanie różnych frameworków wspierających IoC.Interfejs zawiera dwie metody:

public interface IDependencyResolver
{
    object GetService(Type serviceType);
    IEnumerable<object> GetServices(Type serviceType);
}

Obydwie metody na podstawie dostarczonego typu tworzą instancję obiektu. ASP.NET MVC chcący zatem stworzyć jakiś kontroler (np. HomeController), wywołuje metodę GetService. Implementując IDependencyResolver, zyskujemy kontrolę nad tworzeniem obiektów. Aby ASP.NET MVC mógł wykorzystać implementację IDependencyResolver zdefiniowaną przez programistę, należy wywołać statyczną metodę SetResolver klasy DependencyResolver. Klasa DependencyResolver zawiera również kilka innych statycznych metod:

public class DependencyResolver
{
    static DependencyResolver();
    public DependencyResolver();
    public static void SetResolver(IDependencyResolver resolver);
    public static void SetResolver(object commonServiceLocator);
    public static void SetResolver(Func<Type, object> getService, Func<Type, IEnumerable<object>> getServices);
    // Properties
    public static IDependencyResolver Current { get; }
    //...(omitted)
}

Warto wspomnieć również o SetResolver(object commonServiceLocator). Jako parametr przyjmuje tzw. Common Service Locator.  Common Service Locator jest biblioteką dostępną na CodePlex, umożliwiającą luźne wiązania między aplikacją a frameworkami IoC. Programiści do dyspozycji mają wiele frameworków IoC (np. Unity lub nInject). Wykorzystując któryś z nich we własnym kodzie są skazani na powiązania z frameworkami IoC co powoduje bardzo trudną, ewentualną migrację do innych frameworków. Common Service Locator stanowi warstwę pośrednią między kodem aplikacji a konkretnym frameworkiem IoC.

Wstrzyknięcie instancji obiektu

W tym artykule rozważymy scenariusz z wykorzystaniem nowego (wprowadzonego w 3.0) interfejsu IDependencyResolver. Stworzymy więc własną implementację opartą na UnityContainer:

class ExampleResolver : IDependencyResolver
{
    public ExampleResolver(IUnityContainer container)
    {
        _Container = container;
    }
    private IUnityContainer _Container = null; 
    public object GetService(Type serviceType)
    {
        try
        {
            return _Container.Resolve(serviceType);
        }
        catch
        {
            return null;
        }
    }
        public IEnumerable<object> GetServices(Type serviceType)
    {
        return _Container.ResolveAll(serviceType);
    }
}

Następnie rejestrujemy ją za pomocą wspomnianej klasy:

IUnityContainer container = new UnityContainer();
DependencyResolver.SetResolver(new ExampleResolver(container));

Wykorzystajmy teraz wstrzyknięcie zależności w praktyce. Wyobraźmy sobie, że mamy jakaś usługę reprezentowaną przez następujący interfejs:

public interface ISampleService
{
    string GetText();
}

Napisaliśmy również przykładową implementację:

public class HelloWorldService : ISampleService
{
    public string GetText()
    {
        return "Hello world!";
    }
}

Następnie kontroler wykorzystuje luźne wiązania do usługi:

public class HomeController : Controller
{
    private ISampleService _Service = null;
    public HomeController(ISampleService service)
    {
        _Service = service;
    }       
    public ActionResult About()
    {
        ViewBag.Text = _Service.GetText();
        return View();
    }
}

Konkretna implementacja SampleService jest wstrzykiwana za pomocą konstruktora. Do testów można wykorzystać zatem inną usługę, niż w środowisku produkcyjnym. W testach jednostkowych z pewnością będzie wstrzykiwany obiekt izolowany od zewnętrznych zasobów, takich jak np. baza danych czy usługa sieciowa. Wstrzykiwanie odbywa się za pomocą wcześniej zdefiniowanego kontenera, tj.:

IUnityContainer container = new UnityContainer();
container.RegisterType<ISampleService, HelloWorldService>();
DependencyResolver.SetResolver(newExampleResolver(container));

IControllerActivator oraz IControllerFactory

W poprzednich wersjach klasy implementujące IControllerFactory były odpowiedzialne za tworzenie instancji kontrolerów na podstawie nazwy. Wykonywały więc tak naprawdę dwie czynności: identyfikację typu kontrolera na podstawie jego nazwy oraz inicjalizację obiektu. W wersji 3.0, aby zapewnić większą kontrolę, zdecydowano się na rozdzielenie tych operacji. Samą inicjalizacją na podstawie typu zajmuje się już interfejs IControllerActivator (a raczej klasy implementujące ten interfejs):

public interface IControllerActivator
{
    IController Create(RequestContext requestContext, Type controllerType);
}

Zadaniem aktywatora jest stworzenie obiektu na podstawie dostarczonego typu.

Z kolei IControllerFactory wygląda następująco:

public interface IControllerFactory
{
    IController CreateController(RequestContext requestContext, string controllerName);
    void ReleaseController(IController controller);
}

W domyślnej implementacji IControllerFactory konstruktor jako jeden z parametrów przyjmuje IControllerActivator, który następnie jest wykorzystywany w metodzie CreateController. CreateController najpierw identyfikuje typ na podstawie nazwy kontrolera (np. wykorzystując słownik), a następnie za pomocą IControllerActivator tworzy nową instancję. Wstrzyknięcie własnych implementacji również odbywa się za pomocą resolvera:

IUnityContainer container = new UnityContainer();                
container.RegisterType<IControllerActivator,new ExampleControllerActivator>());
DependencyResolver.SetResolver(new ExampleResolver(container));

Ajax – binding modelu

Rozważmy teraz następującą akcję w kontrolerze:

public ActionResult AjaxMethod(Product product)
{
        //...
}

W wersji 3.0 powyższa metoda może przyjmować  silnie typowany parametr,  ponieważ został wprowadzony binding JSON – na podstawie danych JSON zostanie odtworzony obiekt. Drobne usprawnienie z pewnością poskutkuje bardziej przejrzystym kodem.

Wsparcie dla wielu silników renderujących

W poprzedniej wersji do dyspozycji był wyłącznie jeden silnik odpowiedzialny za renderowanie widoków – aspx. W wersji 3.0 mamy do dyspozycji również Razor. Tworząc nowy widok, można wybrać odpowiedni ViewEngine (domyślnie aspx lub Razor):

Dokładne wyjaśnienie możliwości Razor wykracza jednak poza zasięg tego artykułu.  Składnia Razora jest prostsza i bardziej przejrzysta. Rozważmy na zakończenie dwie konstrukcje z aspx i Razor.

Aspx:

<%if(product.Count>0){ %>
<p>Brak</p>
<%}else{ %>
<p>Produkty dostepne</p>
<%} %>

Razor:

@if(products.Count==0)
{
    <p>Brak</p>
}
else
{
<p>Produkty dostepne</p>
}

Jak widać, składnia Razora jest bardziej zwięzła i nie wymaga użycia nieczytelnych dyrektyw <% %>.

Częściowe keszowanie

W poprzednich wersjach możliwe było buforowanie wyłącznie całej strony. Nierzadko jednak potrzebujemy zbuforować tylko fragment strony. Częściowe keszowanie jest dość proste w implementacji. Należy przede wszystkim stworzyć akcję zwracającą częściowy widok:

[OutputCache(Duration=3600,VaryByParam="parameter")]
public ActionResult ExampleAction(int parameter)
{
    return PartialView();
}

Akcja zwraca keszowany widok zgodnie z określonym czasem wygaśnięcia (3600). W zależności od wartości parametru zdefiniowanego w VaryByParam, powstaną różne kopie. Następnie w widoku, za pomocą Html.Action, należy zdefiniować keszowaną część:

<%=Html.Action("ExampleController","ExampleAction",new {parameter=Model.ParID})%>

Html.Action zwraca odpowiedź wygenerowaną przez akcję – w tym przypadku będzie to odpowiedź keszowana.

Zakończenie

Wersja 3.0 wprowadza znaczące, widoczne dla programisty zmiany. Część z nich, np. ViewBag, jest czystą odpowiedzią ASP.NET MVC na nowości zawarte w .NET 4.0. Udoskonalony i uproszczony mechanizm DI (dependency injection) powinien zachęcić programistów do wykorzystywania testów jednostkowych zgodnie z zasadą izolacji. Sporą nowością jest również wsparcie dla różnych silników renderujących – programiści nie są już uzależnieni od aspx.

W artykule nie omówimy jednak wszystkich nowości, ale warto wspomnieć również o atrybucie AdditionalMetadataAttribute czy tzw. sessionless controller (kontroler, który nie może korzystać z sesji).