940 likes | 1.14k Views
ASP.NET MVC 1.0+ Bartłomiej Zass. MVC w Internecie. Ruby on Rails (LAMP) Convention over Configuration Don’t Repeat Yourself ( Keep it DRY) Routing: map.connect ‘: controller /: action /:id ’ DJANGO ( Python ) Regex – mapowanie metoda-URL
E N D
MVC w Internecie • Ruby on Rails (LAMP) • ConventionoverConfiguration • Don’tRepeatYourself (Keepit DRY) • Routing: map.connect ‘:controller/:action/:id’ • DJANGO (Python) • Regex – mapowanie metoda-URL • (r’^(?P<object_id>\d+)/products/category/$’, ‘store.products.view’), • Spring, Struts, JSF(JAVA) • ZEND Framework (ZF) – PHP • MonoRail / Castle Project – ASP.NET • ASP.NET MVC!
Założenia ASP.NET MVC • „Conventionoverconfiguration” • „DRY” (don’trepeatyourself) • Maksymalna elastyczność, rozszerzalność • Serwowanie metod – nie plików • Zejdź mi z drogi! • Dlaczego nie WebForms?! • RAD – podobnie jak aplikacje okienkowe • Często chcemy wiedzieć co się dzieje pod spodem i mieć więcej kontroli • Cykl życia strony i kontrolek (własny) • Całkowicie własny model zdarzeń (TextChanged, Click, itp. oraz zdarzenia strony) • Własne zarządzanie stanem – ViewState • Warstwa abstrakcji – czasem niezastąpiona, czasem uciążliwa
Problemy z WebForms • ViewState • Trudna kontrola nad renderowanym kodem • Client Ids • ctl00$ContentPlaceHolder1$UserControl1$TextBox1 • Problemy z JavaScript • Testy jednostkowe • Symulacja cyklu życia strony poza IIS • Konieczne wykorzystanie zaawansowanych narzędzi takich jak TypeMock
Routing • RESTowy format URLi • Usability (łatwiej zapamiętać, zmienić) • SEO* • NIE odzwierciedla fizycznej lokalizacji! • Cele routingu w ASP.NET MVC • Mapowanie do akcji kontrolera • Konstruowanie URLi • Global.asax varroutes = newRouteCollection(); GlobalApplication.RegisterRoutes(routes);
// Zasada działania routes.MapRoute(“simple”, “{first}/{second}/{third}“); // --- /products/display/123 {first} = products {second} = display {third} = 123 /foo/bar/baz {first} = foo {second} = bar {third} = baz /a.b/c-d/e-f {first} = “a.b” {second} = “c-d” {third} = “e-f”
// Coś bardziej pożytecznego routes.MapRoute(“simple”, “{controller}/{action}/{id}“); // /products/display/123 public class ProductsController : Controller { public ActionResultDisplay(intid) { //Do something return View(); } }
// Przykłady formatów site/{controller}/{action}/{id} // /products/display/123 - // /site/products/display/123 - // Można: {language}-{country}/{controller}/{action} {controller}.{action}.{id} service/{action}-{format} (/service/display-xml) {reporttype}/{year}/{month}/{date} (/sales/2008/1/23) // Nie można:{controller}{action}/{id}
// Mniej oczywiste przykłady Book{title}and{foo} {filename}.{ext} Algorytm zachłanny: /asp.net.mvc.xml – filename=asp.net.mvc (nie asp) My{location}-{sublocation} /MyHouse-LivingRoom (location=„House”, sublocation=„LivingRoom”) {foo}xyz{bar} /xyzxyzxyzblah (foo=„xyzxyz” – zachłannie; bar=„blah”)
// Wartości domyślne public classProductsController : Controller { public ActionResult List() { } } // ---- {controller}/{action} /products/list – OK /products/list/1 – nie zadziała, konieczny drugi route {controller}/{action}/{id} Zamiast nowej trasy – wartość domyślna (RouteValueDictionary) routes.MapRoute(“simple”, “{controller}/{action}/{id}“, new{id = “”}); // ---- routes.MapRoute(“simple”, “{controller-action}“, new {action=”index”}); // /products- ?? NIE – w segmencie (pomiędzy „/”) muszą być wszystkie parametry // W tym przypadku action=index istotne tylko przy generowaniu adresów URL
// Constraints – dodatkowe ograniczenia (regex) routes.MapRoute(“blog”, “{year}/{month}/{day}“, new {controller=”blog”, action=”index”}, new {year=@“\d{4}“, month=@“\d{2}“, day=@“\d{2}“}); //------------------------------------ // Constraints nie muszą być stringiem // Własne constraints public interfaceIRouteConstraint { boolMatch(…); } // „Z pudełka” – implementuje HttpMethodConstraint routes.MapRoute(“name”, “{controller}“, null, new {httpMethod = new HttpMethodConstraint(“GET”)} );
// Parametr catch-all routes.MapRoute(“catchallroute”, “query/{query-name}/{*extrastuff}“); /* /query/select/a/b/c – extrastuff = „a/b/c” /query/select/a/b/c/ - extrastuff = „a/b/c” /query/select/ - extrastuff = „” (OK) */
// Ignorowanie trasy routes.Add(newRoute ( “{resource}.axd/{*pathInfo}“, newStopRoutingHandler() )); // /Webresource.axd // Przekazuje request do standardowego handlera ASP.NET // Domyślnie przy route.MapRoute – MVCRouteHandler // możliwe przekazanie własnej implementacji IRouteHandler // Prościej: routes.IgnoreRoute(“{resource}.axd/{*pathInfo}“);
Reverse - routing • RouteCollection(kolekcja RouteBase) • Dla każdej trasy pytanie – czy możesz wygenerować URL przy pomocy tych parametrów? • Route.GetVirtualPath • Jeśli tak – VirtualPathDataz adresem URL • Jeśli nie – null • Uwaga: wartości domyślne, które nie są parametrem muszą się zgadzać • Todo/{action}; defaults: controller=home;action=index • Podajemy: Controller=„blah”, action=„cokolwiek” – NIE • Controller=„home”; action=„any” – TAK • Trasy nazwane – parametr do GetVirtualPath • GetVirtualPath wykorzystuje parametry z Defaults • Np. piszemy uniwersalną kontrolkę do nawigacji (dalej…)
// AmbientValues // Ponieważ jest action na liście defaults – match public staticvoidRegisterRoutes(RouteCollectionroutes) { routes.MapRoute(null, “todo/{action}/{page}“, new{controller=”todo”, action=”list”, page=0 }); } public string NextPageUrl(intcurrentPage, RouteCollectionroutes) { intnextPage = currentPage + 1; VirtualPathDatavp = routes.GetVirtualPath(null, newRouteValueDictionary(new {page = nextPage})); if(vp!= null) { return vp.VirtualPath; } return null; }
// „Overflowparameters” // Dodatkowe parametry do generacji URL (np. query // string, itp.). routes.Add(newRoute("forum/{user}/{action}", newMvcRouteHandler()) { Defaults = newRouteValueDictionary{ {"controller", "forum"}, {"user", "admin"}} }); VirtualPathData vp2 = routes.GetVirtualPath(null, new RouteValueDictionary(new { action = "Index" , controller = "forum", param="Bartek"})); // controller – nie ma w URL, musi się zgadzać z default // (=forum) // param – jako querystring
Routing pipeline • URLRoutingModulepróbuje znaleźć pasującą trasę w statycznej RouteTable.Routes - GetRouteData() na RouteBase – null lub kolekcja • Znaleziono trasę (pierwszy wygrywa) -> pobieramy IRouteHandler - Dla MVC: MvcRouteHandler • IRouteHandler.GetHandler -> IHttpHandler - Dla MVC: MvcHandler • IHttpHandler.ProcessRequest MvcHandler odpowiedzialny za wybór kontrolera
Web Forms + MVC Uwaga na uwierzytelnienie! ŹLE <?xml version=”1.0”?> <configuration> <system.web> <authorization> <denyusers=”*“ /> </authorization> </system.web> </configuration> DOBRZE UrlAuthorizationModule.CheckUrlAccessForPrincipal(this.VirtualPath, requestContext.HttpContext.User, requestContext.HttpContext.Request.HttpMethod)
Routing i co dalej? • MVCHandler.ProcessRequest • Wypełnia RequestContext.RouteData • ControllerFactory -> IController • IController.Execute() public interfaceIController { voidExecute(RequestContextrequestContext); } • Domyślny ControllerFactory – szuka klasy w odpowiednim katalogu, dodaje „Controller” do parametru z RouteData
Najprostszy kontroler public classSimpleController : IController { public voidExecute(RequestContextrequestContext) { varresponse = requestContext.HttpContext.Response; response.Write(“<h1>Hello World!</h1>”); } } // Podobieństwo do IHTTPHandler // Główna różnica – RequestContext zamiast HttpContext // (dodatkowe informacje związane z requestem MVC) // Klasa abstrakcyjna ControllerBase – wyższy poziom // Właściwości TempData, ViewData, itp.
ABC kontrolera • Controller (dziedziczy po ControllerBase) • Po niej z reguły powinna dziedziczyć klasa kontrolera w ASP.NET MVC • Domyślnie wszystkie publiczne metody bezpośrednio dostępne z URL • tzw. „Akcje” kontrolera • Security! • Dziedzicząc po Controller wyrażamy na to zgodę • /simple2/hello -> Simple2Controller.Hello • Parametry – querystring lub URL • Musi zgadzać się z nazwą zmiennej metody
Akcja kontrolera - przykład public voidDistance(int x1, int y1, int x2, int y2) { doublexSquared = Math.Pow(x2 - x1, 2); doubleySquared = Math.Pow(y2 - y1, 2); Response.Write(Math.Sqrt(xSquared + ySquared)); } /simple2/distance?x2=1&y2=2&x1=0&y1=0 routes.MapRoute(“distance”, “simple2/distance/{x1},{y1}/{x2},{y2}“, new { Controller = “Simple2”, action = “Distance” } ); /simple2/distance/0,0/1,2
ActionResult • Response.Write • Kto ma na to czas? • Tracimy funkcjonalność ASP.NET – np. Master Pages • Kontroler zwraca ActionResult • Wzorzec „Command” (późniejsze wywołanie kodu - np. Undo) public abstractclassActionResult { public abstractvoidExecuteResult(ControllerContextcontext); }
ActionResult – c.d. • Wiele typów odpowiedzi (dziedziczą po ActionResult) • Np. ViewResult • ActionInvoker w późniejszej fazie wywołuje Execute na zwróconym obiekcie • Metody pomocnicze (XYZResult wyjątkiem RedirectToAction)
public ActionResultListProducts() { //Pseudo code IList<Product> products = SomeRepository.GetProducts(); ViewData.Model = products; return new ViewResult {ViewData = this.ViewData }; } // Krócej: public ActionResultListProducts() { //Pseudo code IList<Product> products = SomeRepository.GetProducts(); return View(products); }
ActionResults c.d. • ContentResult • Wykorzystywane, kiedy metoda zwraca inny typ niż ActionResult (void i null – EmptyResult) – wygodne TESTY! • FileResult (abstract), return File() • FilePathResult • FileContentResult • FileStreamResult • JsonResult, return Json() • Uwaga na drzewo obiektu
JavascriptResult public ActionResultDoSomething() { script s = “$(‘#some-div’).html(‘Updated!’);”; return JavaScript(s); } <%= Ajax.ActionLink(“click”, “DoSomething”, new AjaxOptions()) %> <div id=”some-div”></div>
ViewResult • Wywołuje IViewEngine.FindView() • Zwraca IView • IView.Render() • Domyślnie - przeszukiwane konkretne katalogi • ASPX, ASCX
Action Invoker • Routing – wypełnił tylko RouteData • Tak naprawdę nie wywołuje metody kontrolera • ControllerActionInvoker - faktycznie wywołuje akcję kontrolera • Dostępny w IController.ActionInvoker • Lokalizuje metodę (Reflection ze stringa) • Mapuje parametry • Wywołuje metodę i jej filtry • Wywołuje ExecuteResult na zwróconym ActionResult
Wywoływanie metod • Dostępna każda metoda, która: • Nie jest oznaczona [NonAction] • Nie jest konstruktorem, właściwością, zdarzeniem • Nie jest orginalnie zdefiniowana w Object (np. ToString()) lub Controller (np. Dispose() i View()) • Dodatkowe atrybuty • [ActionName(„View”)] • ActionSelectorAttribute
ActionSelectorAttribute public abstractclassActionSelectorAttribute : Attribute { public abstractboolIsValidForRequest(ControllerContextcontrollerContext, MethodInfomethodInfo); } // Invoker zawsze zadaje to pytanie // Jeśli false – metoda nie będzie wywołana /* • [AcceptVerbs(HttpVerbs.Post)] • [NonAction] */
Mapowanie parametrów • Źródła parametrów akcji • Request.Form • Route Data • Request.Querystring • UpdateModel() • <%= Html.TextBox(“ProductName”) %> • ViewData[„product”]; • Walidacja • IDataErrorInfo lub atrybuty (MVC 2) • ModelState.AddError
Walidacja z model binderami public class Product : IDataErrorInfo { … } // Podsumowanie wszystkich błędów public string Error { get {… } } // Błąd dla konkretnej właściwości public string this[string columnName]{ Get { … } }
Domyślne widoki <%@ Page Language=”C#“ MasterPageFile=”~/Views/Shared/Site.Master” Inherits=”System.Web.Mvc.ViewPage” %> <asp:Content ID=”indexTitle” ContentPlaceHolderID=”TitleContent” runat=”server”> Home Page </asp:Content> <asp:Content ID=”indexContent” ContentPlaceHolderID=”MainContent” runat=”server”> <h2><%= Html.Encode(ViewData[“Message”]) %></h2> <p> To learnmoreabout ASP.NET MVC visit <a href=”http://asp.net/mvc” title=”ASP.NET MVC Website”>http://asp.net/mvc</a>. </p> </asp:Content>
Widoki • Dziedziczy po ViewPage / ViewPage<T> • …która dziedziczy po System.Web.UI.Page • Nie ma <form runat=„server”> • Teoretycznie może być – możemy umieszczać kontrolki! • Wysoce niezalecane – kontrolki serwerowe zawierają „kawałki” kontrolera • MVC nie obsługuje ViewState, IsPostBack, itp. • To nie jest stare ASP • ASP miało M, V i C w jednym pliku • Zwracanie widoków • return View() – zgodnie z nazwą akcji • return View(„SpecificView”) • return View(„~/Some/Other/View.aspx”);
// Słabo typowane widoki public ActionResult List() { var products = new List<Product>(); for(int i = 0; i < 10; i++) { products.Add(new Product {ProductName = “Product “ + i}); } ViewData[“Products”] = products; return View(); } <ul> <% foreach(Product p in (ViewData[“Products”] as IEnumerable<Product>)) {%> <li><%= Html.Encode(p.ProductName) %></li> <% } %> </ul>
// Silnie typowane widoki public ActionResult List() { var products = new List<Product>(); for(int i = 0; i < 10; i++) { products.Add(new Product {ProductName = “Product “ + i}); } return View(products); } <%@ Page Language=”C#“ MasterPageFile=”~/Views/Shared/Site.Master” Inherits=”System.Web.Mvc.ViewPage<IEnumerable<Product>>” %> … <ul> <% foreach(Product p in Model) {%> <li><%= Html.Encode(p.ProductName) %></li> <% } %> </ul>
HTML Helpers <% %> vs <%= %> i zapomniany średnik <%= Html.ActionLink(“Link Text”, “Withdraw”, “Account”) %> <%= Html.ActionLink(“Link Text”, “Withdraw”, “Account”, new {id=34231}, null) %> <%= Html.RouteLink(“Link Text”, new {action=”ActionX”}) %> -><a href=”/Home/About”>LinkText</a> <% using(Html.BeginForm(„action”, „controller”)) { %> <% } %> Lub <% Html.BeginForm(); %> (…) <% Html.EndForm(); %>
HTML Helpers – c.d. <%= Html.Encode(string) %> <%= Html.Hidden(“wizardStep”, “1”) %> <%= Html.DropDownList(„lista") %> <%= Html.Password(“my-password”) %> <%= Html.RadioButton(“color”, “red”) %> <%= Html.RadioButton(“color”, “blue”, true) %> <%= Html.RadioButton(“color”, “green”) %> <% Html.RenderPartial(“MyUserControl”); %>
HTML Helpers – c.d. <%= Html.TextArea(“text”, “hello <br/> world”) %> // Kontroler: ViewData[“Name”] = product.Name; <%= Html.TextBox(“Name”) %> // Kontroler: ViewData[„Name”] = product; <%= Html.TextBox(“Product.Name”) %> // Dodatkowe atrybuty <%= Html.TextBox(“Name”, null, new {@class=”klasa”}) %> <%= Html.ValidationMessage(“Name”) %> <span class=”field-validation-error”>Ouch</span> <%= Html.ValidationMessage(“Name”, “Wlasny kom.!”) %>
Walidacja public ActionResult Index() { varmodelState = newModelState(); modelState.Errors.Add(“Ouch”); ModelState[“Name”] = modelState; // lub ModelState.AddModelError(„Age", „Ojj"); return View(); } // Opcjonalnie – dodatkowy komunikat podsumowujący <%= Html.ValidationSummary() %> <ul class=”validation-summary-errors”> <!-- <span>Anerror occurred</span> --> <li>Ouch</li> <li>Ouch</li> </ul>
View Engine • ViewResultiteruje po ViewEngines.Engines i pyta kto może wyrenderować widok • Pierwszy wygrywa • Domyślnie - System.Web.Mvc.WebFormViewEngine • IViewEngine.FndView -> Iview • IView.Render protectedvoidApplication_Start() { ViewEngines.Engines.Clear(); ViewEngines.Engines.Add(newMyViewEngine()); RegisterRoutes(RouteTable.Routes); }
Inaczej - NHAML %h2= ViewData.CategoryName%ul - foreach (var product in ViewData.Products) %li = product.ProductName .editlink = Html.ActionLink("Edit", new { Action="Edit", ID=product.ProductID }) = Html.ActionLink("Add New Product", new { Action="New" })
Inaczej - NVelocity #foreach($person in $people) #beforeall <table> <tr><th>Name</th><th>Age</th></tr> #before <tr #odd Style=’color:gray’> #even Style=’color:white’> #each <td>$person.Name</td><td>$person.Age</td> #after </tr> #between <tr><tdcolspan=’2’>$person.bio</td></tr> #afterall </table> #nodata Sorry No Person Found #end