routing

Routing with Webform

คราวก่อนค้างเอาไว้ว่า ถ้าจะทำ Routing ใน ASP.NET Webform จะทำยังไง มาต่อเลยดีกว่า

เนื่องจาก Routing นั้นเป็นส่วนที่จะเพิ่มเข้ามาใหม่ในตัว Framework ของ ASP.NET 3.5 SP1 นั่นก็หมายความว่าเราสามารถใช้ ASP.NET Routing กับ Web Form ได้เหมือนกับที่เราใช้ความสามารถอื่นๆใน Framework นั่นเอง วิธีที่จะเอามาใช้นั้น Chris Cavanagh ได้ระบุวิธีการไว้ดังนี้

After a little experimentation I found some minimal steps that work pretty well:

  • Create a custom IRouteHandler that instantiates your pages
  • Register new Routes associated with your IRouteHandler
  • That’s it!

ถ้าให้แปลเป็นไทยก็คือ

  • สร้าง class ที่ implement IRouteHandler
  • สร้าง Route ให้ url ที่ต้องการ วิ่งไปที่ IRouteHandler ที่สร้างมาใหม่
  • จบเพียงเท่านี้

URL Routing and Rewriting

นี่เป็นคำถามที่อยู่ในหัวผมหลังจากที่อ่านเอกสารใน MSDNแบบผ่านๆในหัวข้อ ASP.NET Routing versus URL Rewriting พอ M3rlinez มาถามก็เลยลองไปหาอ่านละเอียดๆอีกที พบว่าการทำ Routing (ในบริบทของ ASP.NET) มันต่างกับ URL Rewriting อยู่นิดหน่อย

ข้อแรกที่แตกต่างคือ การทำ Routing รองรับการสร้าง URL โดยใส่พารามิเตอร์ที่ต้องการ ซึ่งตรงนี้ใน URL Rewriting ไม่มีกลไกตรงนี้ให้ต้องทำเอง (มันไม่ต่างกันไม่ใช่เรอะ แค่ไมโครซอฟท์ทำไว้ให้แล้ว) ตรงนี้จะว่าเป็นข้อดีก็ได้

ข้อแตกต่างข้อที่สองคือ Routing ทำงานในอีเวนท์ PostResolveCache และ PostMapRequestHandler ซึ่งจุดนี้แหละที่ต่างจากการทำ URL Rewrite จริงๆ

ปกติแล้วการทำ URL Rewrite จะใช้ HttpHandler หรือใช้ HttpModule มาดักตอนอีเวนท์ BeginRequest ซึ่งเป็นอีเวนท์แรกสุด ซึ่งตรงนี้ URL ที่เกิดรีเควส จะถูกแปลงให้อยู่ในรูปของ URL ที่ ASP.NET Webform ธรรมดาๆ เช่น "/products/15" ก็จะถูกเขียนใหม่เป็น "product.aspx?id=15" แล้วค่อยส่งไปให้ไฟล์ดังกล่าว ดังนั้นอีเวนท์ที่เกิดขึ้นด้านหลังอย่าง AuthenticateRequest จะเกิดหลังจากที่ทำการแปลง URL ไปแล้ว ทำให้การตรวจสอบสิทธิต่างๆทำกับไฟล์ปลายทาง (ซึ่งเป็น Physical file) แทนที่จะเป็นไฟล์ที่เห็นจริงๆ แต่สำหรับ URL Routing แล้วเราสามารถใช้งานระบบ Authentication/Authorization ของ ASP.NET กับ logical file ผ่าน web.config ได้ทันที สิ่งนี้ผมยังไม่ได้ทดลอง แต่สรุปเอาเองหลังจากการดูสายการทำงานของอีเวนท์นะครับ

<location path="products">
  <system.web>
    <authorization>
      <deny users="?"/>
    </authorization>
  </system.web>
</location>

ผลพวงจากการที่ URL ไม่ได้ถูกเปลี่ยนในระหว่างการทำงานทำให้พารามิเตอร์ action ของอีลิเมนท์ form จะถูกตั้งให้เป็น URL ของเดิมจริงๆ ทำให้เราไม่ต้องใช้ Actionless Form เข้ามาใช้

URL Rewriting ที่ผมเคยคิดว่ามันจะกลายเป็นส่วนหนึ่งของ IIS7 นั้นถูกเปลี่ยนให้เป็น Routing แล้วผนวกเข้ามาใน ASP.NET นั้น จุดสำคัญน่าจะมาจากการที่ทีม ASP.NET นั้นได้เริ่มการพัฒนา ASP.NET MVC แล้วก็เลยจำเป็นต้องพัฒนาส่วน Routing ด้วย แล้วไหนๆก็ไหนๆแล้วก็เลยจับมันแยกออกมาเป็นคอมโพเนนท์เลยดีกว่า เพราะว่า ASP.NET Developer เดิมก็มีจำนวนไม่น้อยที่ต้องทำ URL Rewriting ด้วยตัวเอง

สรุปแล้ว URL Routing ไม่ได้มีอะไรพิเศษพิสดารต่างจาก URL Rewriting ซักเท่าไหร่ แนวคิดเดียวกันเป๊ะ แค่นำส่วนที่เราต้องทำแน่ๆอย่างการใช้ Regular Expression ในการสกัด URL และพารามิเตอร์เพื่อส่งให้ Action/WebForm ที่ถูก และการสร้าง URL รวมเข้ามาอยู่ในตัวเฟรมเวิร์กเลย และจุดประสงค์จริงๆก็คงจะเอามาใช้กับ ASP.NET MVC นั่นแหละ

URL Routing in ASP.NET MVC

ใน ASP.NET MVC เราสามารถออกแบบ URL ขึ้นมาแล้วส่ง request ไปยัง action ที่กำหนดไว้ได้ ทำให้ URL ของเว็บของเราไม่ขึ้นอยู่กับโครงสร้างของไฟล์ที่อยู่บนดิสก์จริงๆ โดย ASP.NET MVC อาศัย URL Routing ที่เพิ่มเข้ามาใน ASP.NET 3.5 SP1

หลังจากเราสร้าง Project Web Application ขึ้นมาแล้ว ลองเปิดดูที่ Global.asax จะเห็นว่าต่างจาก ASP.NET Web Application ธรรมดา เพราะจะมาการทำ Routing มาให้แล้ว

public class MvcApplication : System.Web.HttpApplication
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
        routes.MapRoute(
            "Default",                                              // Route name
            "{controller}/{action}/{id}",                           // URL with parameters
            new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
        );
    }
    protected void Application_Start()
    {
        RegisterRoutes(RouteTable.Routes);
    }
}

กฏง่ายๆของการ Routing คือ

  • หน้าไปหลัง เริ่มทำการทดสอบจากกฏที่เพิ่มเข้าไปก่อนเป็นลำดับแรก เจออันไหนก่อนก็จะ route ไปตามนั้น
  • ใช้ {} เพื่อเก็บค่าตัวแปร เราสามารถตั้งตัวแปรสำหรับ URL ได้ เช่น /blog/{year}/{month}/{day} จะ match กับ /blog/2009/1/2 โดยจะเกิดตัวแปรของ URL ชื่อ year, month และ day ขึ้นโดยมีค่าเป็น 2009, 2 และ 1 ตามลำดับ
  • ใช้ * เพื่อระบุว่าเป็น parameter ที่ไม่ทราบจำนวน

ASP.NET Routing จะต้องมี Routing Handler ซึ่งจะต้องเป็นคลาสที่ implement interface IRouteHandler ซึ่งปกติใน ASP.NET MVC จะแอบใช้ System.Web.Mvc.MvcRouteHandler

จากด้านบน เราสามารถแปลได้ง่ายๆว่า

  • ไม่ต้องสนใจถ้า request url ที่เป็น .axd โดยส่วนที่อยู่หน้า . จะถูกเก็บไว้ในตัวแปรชื่อ request ส่วนที่เหลือทั้งหมดจะถูกเก็บใน pathInfo
  • Request URL จะถูก Route ไปยัง controller และ action โดยระบุพารามิเตอร์ชื่อ id โดยอัตโนมัติหากไม่มีการกำหนดค่าใดๆ

เรามาลองสร้าง Flickr Mashup ขึ้นมาเล่นๆดูดีกว่า

ก่อนอื่นสร้าง PhotoController ขึ้นมาใหม่ โดยมีโค้ดดังนี้

public class PhotoController : Controller
{        
    public ActionResult Index()
    {
        Flickr f = new Flickr();
        Photos p = f.PhotosGetRecent(25, 1);            
        return View(p.PhotoCollection);
    }
}

จากแอคชันข้างต้น เราดึงรายละเอียดของรูปที่ถูกอัพโหลดขึ้นไปไว้ใน Flickr จำนวน 25 รูปแล้วเก็บไว้ใน ViewData โดยมีใช้ โดยใช้ View ง่ายๆ ชื่อ Index คือ

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="MvcApplication1.Views.Photo.Index" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
<title>MVC Flickr Sample</title>
</head>
<body>
    <div>
    <% foreach (var p in ViewData.Model)
       { %>                     
       <a href="<%= p.WebUrl %>">
       <img src="<%= p.SquareThumbnailUrl %>" alt="<%= p.Title %>" />                  
       </a>
    <% }%>
    </div>
</body>
</html>

โดยที่ View นี้เป็น Strong typed view

namespace MvcApplication1.Views.Photo
{
    public partial class Index : ViewPage<FlickrNet.Photo[]>
    {
    }
}

หลังจากเราตั้งค่า Flickr API เรียบร้อยตาม Document ก็ทดลองรันดูด้วย Url /photo/

ScreenShot001

ทีนี้เราลองเพิ่มอะไรขึ้นมาเล็กน้อยด้วยการเพิ่ม Route เข้าไปดังต่อไปนี้

    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapRoute(
            "Search",                                              // Route name
            "search/{key}/{page}",                           // URL with parameters
            new { controller = "Photo", action = "Search", key = "", page = 1 },  // Parameter defaults
        );

        routes.MapRoute(
            "User",                                              // Route name
            "photo/{username}/{page}",                           // URL with parameters
            new { controller = "Photo", action = "PhotoByUser", page = 1 },  // Parameter defaults
        );

        routes.MapRoute(
            "Default",                                              // Route name
            "{controller}/{action}/{id}",                           // URL with parameters
            new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
        );

    }

จะเห็นว่าเราส่ง Request ไปยัง Action ใดๆ ก็ได้โดยไม่ขึ้นกับชื่อ กล่าวคือ เราจะทำการค้นหารูปหาก Request URL อยู่ในรูปแบบ search/คำค้น/หน้า และหากเป็น /photo/ชื่อผู้ใช้/หน้า ก็จะแสดงรูปของผู้ใช้คนนั้นแทนโดยเราไม่จำเป็นจะต้องสร้าง Search controller แต่อย่างใด แต่เราจะส่งไปที่ action Search และ PhotoByUser ตามลำดับ

โค้ดของ action ทั้งสองเป็นดังนี้

    public ActionResult PhotoByUser(string username, int page)
    {
        Flickr f = new Flickr();
        FoundUser u = f.PeopleFindByUsername(username);            
        if (u != null)
        {               
            Photos p = f.PeopleGetPublicPhotos(u.UserId, 25, page);                
            return View("index", p.PhotoCollection);
        }
        return View("index");
    }

    public ActionResult Search(string key, int page)
    {           
        Flickr f = new Flickr();
        PhotoSearchOptions option = new PhotoSearchOptions();
        option.Page = page;
        option.Text = key;         
        Photos p = f.PhotosSearch(option);                       
        return View("index", p.PhotoCollection);                        
    }

เมื่อเราทดสอบด้วยการเปิด URL /search/grassland/ และ /photo/wiennat จะปรากฎรูปขึ้นมาตามต้องการ

ScreenShot002

แล้วถ้าเราลองเปิดด้วย /search/grassland/ข้อความ และ /photo/wiennat/ข้อความ ล่ะ

ScreenShot004

ผลลัพธ์คือเกิด error ขึ้นเนื่องจากพารามิเตอร์ page นั้นมีค่าเป็น NULL แต่ว่าเราต้องการพารามิเตอร์ที่เป็นตัวเลข เรามีทางแก้สองทางคือ 1. กำหนดให้ประเภทพารามิเตอร์ page ให้เป็น int? 2. จำกัดรูปแบบของพารามิเตอร์ โดยกำหนด Constrain ให้รับแต่ตัวเลข เท่านั้น

    routes.MapRoute(
            "Search",                                              // Route name
            "search/{key}/{page}",                           // URL with parameters
            new { controller = "Photo", action = "Search", key = "", page = 1 },  // Parameter defaults
            new { key="[A-Za-z0-9]+", page="[0-9]*" }
        );

เมื่อผู้ใช้เข้ามาด้วย /search/grassland/ข้อความ ก็จะปรากฏข้อความที่บอกว่าไม่พบเอกสารดังกล่าวแทน

ScreenShot004

เป็นอันจบวิธีการใช้งาน URL Routing อย่างง่ายๆแล้ว สำหรับตอนหน้า เราจะมาดูว่าเราจะใช้ URL Routing ใน Web Form กันอย่างไร

สำหรับการใช้งานอื่นกรุณาทิ้งไว้ใน comment

Pylons, รับน้อง, Routing

รำลึกอดีตให้ฟังหน่อยนึงว่าหลังจากเข้าไปร่วมงาน NWA แล้วผมก็โดนเวทล้างสมองของ Sirn ให้หันมามอง Pylons เลยลองเล่นดูนิดหน่อย แต่ก็ไม่วายโดนรับน้องจนได้

ถ้าเราติดตั้ง Pylons ด้วย easy_install โดยไม่ระบุเวอร์ชัน ตัว Easy_install จะลง เป็นเวอร์ชัน 0.9.7rc1 ให้โดยอัตโนมัติ ซึ่งใน 0.9.7 นั่นมันจะมี Breaking Change อยู่จำนวนหนึ่ง แต่ในวิกิของ Pylons ยังไม่มีข้อมูลนี้

บังเอิญว่าผมดันไปเลือกใช้ 0.9.7 โดยไม่รู้อิโหน่อิเหน่ ค้นวิธีแก้ยังไงก็ไม่เจอ สถานการณ์แบบนี้ผมเรียกว่า ซวย ครับ

หนึ่งใน Breaking Change ที่โดนแน่ๆคือเรื่อง Webhelper แต่นั่นเป็นเรื่องที่เราจะไม่พูดกันครั้งนี้ เพราะครั้งนี้เราจะบอกถึง Breaking Change ที่ทำให้ผมเซ้งเซ็งอีกเรื่องแทน นั่นคือ Routing เป็นยังไงมาติดตามดู

เนื่องจากเป็นเด็กใหม่ เราก็ต้องทำตัวเป็นเด็กใหม่ที่ดีด้วยการศึกษาตาม Getting started ที่เค้าทำมาให้ หลังจากกำลังภายในของผมเริ่มหมดไปกับการถาม sirn และ vsatayamas ไปแล้ว ผมก็ฟันฝ่าตัว Getting Started มาจนถึงการทำ controller และ action แล้วล่ะ จากใน Docs หลังจากสร้าง HelloController เสร็จแล้วก็ต้องก็สร้าง method index ขึ้นมา หลังจากทำขั้นตอนดังกล่าวผมก็เปิดด้วยความกระหยิ่มยิ้มย่อง เข้าหน้า http://localhost:5000/hello ตามสเต็ปเป๊ะๆๆๆ สิ่งที่โผล่ขึ้นมาต้อนรับเป็นข้อความอันคุ้นเคย

404 Not Found

ความคิดที่แว่บเข้ามาในหัว "นั่นไง โดนเข้าแล้ว" เป็นที่ Routing ชัวร์ๆ อีแบบนี้ ก็เลยเปิดดูใน config/routes.py ก็มีบรรทัดแบบนี้อยู่แล้ว

map.connect('/{controller}/{action}') map.connect('/{controller}/{action}/{id}')

อ่านคร่าวๆ หมายความว่าจะได้ Route URL ในลักษณะนี้

/{controller}/{action}/{id} แล้วถ้าไม่ระบุ action ก็ต้อง route แบบนี้ /{controller}/{action}

แล้วผิดตรงไหน ลองไปลองมาก็เลยฉุกคิดได้นิดนึง เปลี่ยนไปเข้า http://localhost:5000/hello/index แทน ปรากฏว่าหรามาเลยครับ Hello world!!!

เลยตะหงิดๆ ขึ้นไปดูในคู่มือของ Routes ได้ความว่า ใน Routes รุ่นใหม่ที่มาพร้อมกับ Pylons 0.97 จะมีเรื่อง minimization เข้ามา คือจะไม่มีการ map url แบบครึ่งๆกลางๆแล้ว ดังนั้นจากที่คิดว่า /{controller}/{action} จะได้ map ไปผูกกับ controller ที่ระบุแล้วกำหนด action เป็น index เองถ้าไม่กำหนด ก็หมดสิทธิทำแบบนี้แล้ว

อธิบายเป็นภาษาคนก็คือ ต้องระบุ action ทุกครั้งด้วยนะจ๊ะเด็กโง่

แต่เฮ้ย ชาวบ้านชาวเมืองเค้าไม่เห็นต้องระบุ action ก็ยังเรียก index ให้ Pylons จะยอมน้อยหน้าได้ไง ขนาด ASP.NET MVC ยังทำได้เลย ถ้า Pylons ทำไม่ได้ย้ายกลับนะเฟร้ย

เลยไปค้นๆมา ได้ความว่ามันก็มีทางเปิดใช้ minimization เพื่อให้ Routes ทำงานตามพฤติกรรมเดิม โดยในไฟล์ Routes.py จะมีบรรทัดนึงที่เขียนว่า

map.minimization = False

ก็แก้เป็น True ซะก็สิ้นเรื่อง ง่ายมะๆ

แต่ช้าก่อน ถ้ามันง่ายขนาดนั้น แล้วไม่เปิดใช้ไว้ตั้งแต่แรกฟระ ความจริงปรากฏว่า Routes รุ่นถัดไปจะไม่ยอมให้เปิดการทำงานของ Minimization แล้ว วิธีแก้ที่ยั่งยืนกว่าคือ เพิ่มกฏเอง ฟังดูเหมือนจะยาก แต่แค่เพิ่มเข้าไปบรรทัดนึงเท่านั้นเอง

map.connect('/{controller}', action='index')

แบบนี้ ถ้าเข้า http://localhost:5000/hello ก็จะวิ่งไป index ให้แล้ว

ก็ง่ายๆเช่นนี้แล ด้วยรักและไพลอนส์

ปล. แต่ว่าเข้า http://localhost:5000/hello/ ก็ยัง 404 นะ