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 ที่สร้างมาใหม่
  • จบเพียงเท่านี้

สร้าง class ที่ implement IRouteHandler

ใน ASP.NET MVC เราใช้ System.Web.Mvc.MvcRouteHandler เป็นตัว Route Handler เพื่อเรียกใช้ action ให้ตรงกับที่เรา Route ไว้ แต่สำหรับ Webform เราจำเป็นจะต้องทำตัว Route Handler ขึ้นมาเอง

ด้านล่างนี้จะเป็นตัวอย่างง่ายๆของการทำ Route Handler

public interface IRoutablePage
{
    RequestContext RequestContext { set; }
}

public class WebformRouteHandler : IRouteHandler
{
    public WebformRouteHandler(string virtualPath)
    {
        this.VirtualPath = virtualPath;
    }
    public string VirtualPath { get; private set; }
    #region IRouteHandler Members

    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        IHttpHandler page = BuildManager.CreateInstanceFromVirtualPath( this.VirtualPath  , typeof(Page)) as IHttpHandler;
        if (page != null)
        {
            IRoutablePage ir = page as IRoutablePage;
            if (ir != null)
                ir.RequestContext = requestContext;
        }
        return page;
    }

    #endregion
}

ตัว interface IRouteHandler นั้นมี method ที่บังคับให้ implement เพียง 1 method คือ GetHttpHandler ซึ่งเราจะใช้ method BuildManager.CreateInstanceFromVirtualPath ช่วยในการสร้าง instance ของ Page หรือหน้าต่างๆขึ้นมา นอกจากนี้เราจะส่ง RequestContext ให้กับหน้านั้นๆหากหน้านั้นทำการ implement IRoutablePage เอาไว้ ซึ่งทำให้เราสามารถเข้าถึง RouteValue ได้ และช่วยให้เราใช้ประโยชน์จาก RouteConstraint ได้ด้วย

สร้าง Route ให้ url ที่ต้องการ วิ่งไปที่ IRouteHandler ที่สร้างมาใหม่

ขั้นตอนนี้จะเป็นการกำหนด Route table ให้กับ web application ของเรา ขั้นแรกให้เพิ่ม Global.asax ขึ้นมาก่อน แล้วใน method Application_Start ให้เพิ่มโค้ดดังนี้

    protected void Application_Start(object sender, EventArgs e)
    {
        RegisterRoutes(RouteTable.Routes);
    }


    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.Add("HelloRoute", new Route
        (
           "{name}", new WebformRouteHandler("~/default.aspx")
        ));
    }

แบบนี้หมายความว่า ไม่ว่าอะไรก็ตาม ให้ route เข้า default.aspx ให้หมด

สำหรับ default.aspx ผมเพิ่ม literal control เอาไว้หนึ่งคอนโทรล จะไม่ขอแสดงโค้ด แต่จะใส่ Code Behind เอาไว้ให้

public partial class _Default : System.Web.UI.Page, IRoutablePage
{

    protected void Page_Load(object sender, EventArgs e)
    {
        if (this.requestContext != null)
            Literal1.Text = string.Format("Hello, {0}", this.requestContext.RouteData.Values["name"]);
    }

    #region IRoutablePage Members
    protected RequestContext requestContext;
    RequestContext IRoutablePage.RequestContext{ set { this.requestContext = value; } }
    #endregion
}

ในตอนนี้ถ้าเราลองใช้งาน Routing ดู จะพบว่ายังเจอหน้า 404 อยู่หากเข้าไปที่ ~/{a} เมื่อ {a} เป็น url ใดๆ เพราะ ASP.NET ยังไม่เข้าใจการ Routing เราต้องเพิ่ม Module สำหรับการ Routing ลงไปใน web.config ด้วย




และ

    <httpModules>
        <add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
        <add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
    </httpModules>

เมื่อทดลองก็จะใช้งานได้

That’s it

เอา ASP.NET Routing มาใช้ไม่ใช่เรื่องยาก แต่ว่ายังมีปัญหาอื่นๆตามมาอีกมิใช่น้อยไม่ว่าจะเป็นเรื่อง Security หรือ ASP.NET Ajax เพราะ Web form ไม่ได้ถูกออกแบบมาให้ใช้กับ Routing ตั้งแต่แรกเหมือนพวก framework อื่นๆ

ถ้าให้สรุปก็คือ ถ้าอยากใช้ก็ไม่มีปัญหา ทนใช้ไปก็ได้ ถ้าทนไม่ไหวก็มีสามทางเลือก คือไปใช้ ASP.NET MVC หรือใช้ URL Rewriting เหมือนเดิม และสุดท้ายคือลองใช้ Framework อื่นๆดู

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 นั่นแหละ

Castle Active Record : Relationship

ต่อจากตอนที่แล้ว

สำหรับ Relation หรือความสัมพันธ์ที่ใช้ใน ActiveRecord จะมีหลักๆดังนี้

  • BelongsTo – ใช้ระบุความสัมพันธ์แบบ many-to-one หรือ one-to-one
  • HasMany – ใช้ระบุความสัมพันธ์แบบ one-to-many
  • HasAndBelongsToMany – ใช้ระบุความสัมพันธ์แบบ many-to-many

    สมมติเราเพิ่มข้อมูลลงไปอีกตาราง คือ files เพื่อเอาไว้เก็บว่าแต่ละคนมีไฟล์อะไรบ้าง โดยมี Schema เป็นแบบนี้
schema

เมื่อเอามาแปลงเป็นคลาสก็จะได้หน้าตาเป็น

[ActiveRecord("files")]
public class UserFile : ActiveRecordBase<UserFile>
{
    [PrimaryKey(PrimaryKeyType.Native)]
    public virtual int ID { get; set; }

    [BelongsTo("user_id")]
    public virtual User Owner { get; set; }

    [Property("Filename")]
    public virtual string Filename { get; set; }

    [Property("type")]
    public virtual string ContentType { get; set; }

    [Property("data")]
    public virtual byte[] FileData { get; set; }

    [Property("size")]
    public virtual int FileSize { get; set; }
}

คราวนี้เปลี่ยนมา inherit จาก ActiveRecordBase แทนแล้ว โดยใน attribute ActiveRecord จะบอกว่าให้ map คลาสนี้เข้ากับตารางอะไร และสำหรับตรง property ก็จะบอกว่า map เข้ากับฟิลด์ไหนในตาราง

สำหรับ Property Owner จะเป็นการแมป Property นี้เข้ากับคลาส User โดยใช้ฟิลด์ user_id เพื่อบอกว่าจะแมปเข้ากับ object ไหนให้ดูจาก user_id เป็นหลัก

เซฟคลาสนี้ในไฟล์ชื่อ App_code/UserFile.cs

จากนั้นเราจะต้องไม่ลืมเพิ่มข้อมูลของคลาส UserFile ลงใน GlobalApplication.cs เพื่อให้ ActiveRecord รู้ว่าเราจะโหลดคลาสนี้เข้าไปด้วย

   public static void InitActiveRecord(IConfigurationSource source)
    {
    ActiveRecordStarter.Initialize(source,
        typeof(User),
        typeof(UserFile)

        )
        ;
    }

หลังจากนั้น เราก็จะต้องมาแก้ไขคลาส User เพื่อเพิ่ม Property Files เพื่อบอกว่ามีไฟล์อะไรเก็บไว้บ้าง
[ActiveRecord]
public class User : ActiveRecordValidationBase
{
[PrimaryKey(PrimaryKeyType.Native)]
public virtual int ID { get; set; }

    [Property]
    public virtual string Email { get; set; }

    [Property]
    public virtual string Password { get; set; }

    [HasMany(typeof(UserFile), "user_id", "files")]
    public virtual IList<UserFile> Files { get; set; }
}

ก่อนอื่นก็จะต้องสร้างหน้าฟอร์มสำหรับการอัพโหลดไฟล์ก่อน ตั้งชื่อว่า UploadForm.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="UploadForm.aspx.cs" Inherits="UploadForm" %>

<!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 runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>

    <asp:DropDownList ID="DropDownList1" runat="server" 
        DataSourceID="ObjectDataSource1" DataTextField="Email" DataValueField="ID">
    </asp:DropDownList>
    <asp:ObjectDataSource ID="ObjectDataSource1" runat="server" 
        SelectMethod="FindAll" TypeName="User"></asp:ObjectDataSource>

    </div>
    <asp:FileUpload ID="FileUpload1" runat="server" />
    <br />
    <asp:Button ID="Button1" runat="server" onclick="Button1_Click" Text="Button" />
    <asp:Label ID="Label1" runat="server"></asp:Label>
    </form>
</body>
</html>

โดยมีเนื้อหาของ Code Behind เป็น

using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Xml.Linq;

public partial class UploadForm : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {    }
    protected void Button1_Click(object sender, EventArgs e)
    {
    UserFile uf = new UserFile();
    uf.Filename = FileUpload1.FileName;
    uf.FileSize = FileUpload1.FileBytes.Length;
    uf.ContentType = FileUpload1.PostedFile.ContentType;
    uf.FileData = FileUpload1.FileBytes;

    uf.Owner = global::User.Find(Convert.ToInt32(DropDownList1.SelectedValue));
    uf.Save();

    Label1.Text = "Upload Completed";
    }
}

จากนั้นลองอัพโหลดไฟล์เข้าไปซัก 2 – 3 ไฟล์

upload

ทีนี้เราลองกลับมาแก้ไขหน้า default ให้แสดงผลรายการของไฟล์ด้วย

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>

<!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 runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" DataSourceID="ObjectDataSource1">
    <Columns>
        <asp:BoundField DataField="ID" HeaderText="ID" SortExpression="ID" />
        <asp:BoundField DataField="Email" HeaderText="Email" SortExpression="Email" />
        <asp:BoundField DataField="Password" HeaderText="Password" SortExpression="Password" />
        <asp:TemplateField HeaderText="File">
        <ItemTemplate>
            <asp:Repeater ID="Repeater1" runat="server" DataSource='<%# Eval("Files") %>'>
            <HeaderTemplate>
                <ul>
            </HeaderTemplate>
            <ItemTemplate>
                <li>
                <%#Eval("Filename") %>
                (<%#Eval("FileSize") %>)</li></ItemTemplate>
            <FooterTemplate>
                </ul></FooterTemplate>
            </asp:Repeater>
        </ItemTemplate>
        </asp:TemplateField>
    </Columns>
    </asp:GridView>
    <asp:ObjectDataSource ID="ObjectDataSource1" runat="server" SelectMethod="FindAll"
    TypeName="User"></asp:ObjectDataSource>
    </form>
</body>
</html>

ที่ทำคือเพิ่ม template field เข้าไปหนึ่งคอลัมน์ใน GridView จากนั้นก็ Bind ตัว property Files เข้าไปกับ Repeater แล้วให้ Repeater แสดงชื่อไฟล์พร้อมกับขนาดออกมา

data
m2m

ทีนี้สมมติว่าเราใส่ tag ให้กับไฟล์ด้วยล่ะ จะทำยังไง

สมมติให้มีอีกตารางชื่อว่า tags

tag

และตารางที่ทำหน้าที่เชื่อมระหว่าง tags และ files ให้ชื่อว่า file_tag

file_tag

ทีนี้สำหรับคลาส Tag ก็ทำคล้ายๆคลาส UserFile ก็คือดังข้างล่าง

using System;
using Castle.ActiveRecord;
using System.Collections.Generic;


[ActiveRecord("Tags")]
public class Tag : ActiveRecordBase<Tag>
{
    [PrimaryKey(PrimaryKeyType.Native)]
    public virtual int ID { get; set; }

    [Property("tag")]
    public virtual string TagName { get; set; }

    [HasAndBelongsToMany(typeof(UserFile), Table="file_tag", ColumnKey="tag_id", ColumnRef="file_id", Inverse=true )]
    public virtual IList<UserFile> Files { get; set; }

    public static Tag FindByTag(string tag)
    {
    Tag t = Tag.FindFirst(Expression.Like("TagName", tag));
    return t;

    }
}

ตรงนี้เพิ่มเมธอด FindByTag เข้ามาโดยจะดูว่ามี Tag ดังกล่าวอยู่ในระบบอยู่แล้วหรือไม่

ทีนี้ลองกลับไปแก้ คลาส UserFile โดยใส่ Property Tags เข้าไปดูบ้าง

[ActiveRecord("files")]
public class UserFile : ActiveRecordBase<UserFile>
{
    [PrimaryKey(PrimaryKeyType.Native)]
    public virtual int ID { get; set; }

    [BelongsTo("user_id")]
    public virtual User Owner { get; set; }

    [Property("Filename")]
    public virtual string Filename { get; set; }

    [Property("type")]
    public virtual string ContentType { get; set; }

    [Property("data")]

    public virtual byte[] FileData { get; set; }

    [Property("size")]
    public virtual int FileSize { get; set; }

            [HasAndBelongsToMany(typeof(Tag), RelationType.Bag, Table="file_tag", ColumnKey="file_id", ColumnRef="tag_id")]
    public virtual IList<Tag> Tags { get; set; }

}

จัดการแก้หน้า UploadForm ให้รองรับเรื่อง Tag ด้วยการเพิ่ม textbox เข้าไปหนึ่งอัน

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="UploadForm.aspx.cs" Inherits="UploadForm" %>

<!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 runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
    <asp:DropDownList ID="DropDownList1" runat="server" DataSourceID="ObjectDataSource1"
        DataTextField="Email" DataValueField="ID">
    </asp:DropDownList>
    <asp:ObjectDataSource ID="ObjectDataSource1" runat="server" SelectMethod="FindAll"
        TypeName="User"></asp:ObjectDataSource>
    </div>
    <asp:FileUpload ID="FileUpload1" runat="server" />
    <br />
    <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
    <br />
    <asp:Button ID="Button1" runat="server" OnClick="Button1_Click" Text="Button" />
    <asp:Label ID="Label1" runat="server"></asp:Label>
    </form>
</body>
</html>

จากนั้นก็ไปแกตอนกดปุ่ม

protected void Button1_Click(object sender, EventArgs e)
    {
    UserFile uf = new UserFile();
    uf.Filename = FileUpload1.FileName;
    uf.FileSize = FileUpload1.FileBytes.Length;
    uf.ContentType = FileUpload1.PostedFile.ContentType;
    uf.FileData = FileUpload1.FileBytes;
    uf.Tags = new List<Tag>();
    foreach (string tag in TextBox1.Text.Split(new char[]{','}, StringSplitOptions.RemoveEmptyEntries ))
    {
        string trimmedTag = tag.Trim();
        if (!string.IsNullOrEmpty(trimmedTag))
        {
        Tag t = Tag.FindByTag(trimmedTag);

        if (t == null)
        {
            t = new Tag();
            t.TagName = trimmedTag;
            t.Save();
        }

        uf.Tags.Add(t);
        }
    }
    uf.Owner = global::User.Find(Convert.ToInt32(DropDownList1.SelectedValue));
    uf.Save();

    Label1.Text = "Upload Completed";
    }

จุดนี้ที่เพิ่มเข้ามาก็คือจะดูว่าใน tag ที่เพิ่มเข้ามานั้น มีอยู่แล้วรึเปล่า ถ้าไม่มีก็จะเพิ่มเข้าไปในฐานข้อมูล แต่ถ้ามีก็จะใช้ tag นั้นแปะเข้าไปใน UserFile ของเราเลย ทีนี้พอลองรันดูก็จะเห็นว่าข้อมูลเข้าไปตามที่เรากำหนด

วิธีทดสอบง่ายๆก็เลยลองสร้างหน้าที่ใช้แสดงรายการไฟล์ต่างๆของ tag ขึ้นมาดู

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default2.aspx.cs" Inherits="Default2" %>

<!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 runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
    <asp:DropDownList ID="DropDownList1" runat="server" AutoPostBack="True" DataSourceID="ObjectDataSource1"
        DataTextField="TagName" DataValueField="ID" OnSelectedIndexChanged="DropDownList1_SelectedIndexChanged">
    </asp:DropDownList>
    <asp:GridView ID="GridView1" runat="server">
    </asp:GridView>
    </div>
    <asp:ObjectDataSource ID="ObjectDataSource1" runat="server" SelectMethod="FindAll"
    TypeName="Tag"></asp:ObjectDataSource>
    </form>
</body>
</html>

โดยมี Code Behind เป็น

using System;

public partial class Default2 : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {    }
    protected void DropDownList1_SelectedIndexChanged(object sender, EventArgs e)
    {
    GridView1.DataSource = Tag.Find(Convert.ToInt32(DropDownList1.SelectedValue)).Files;
    GridView1.DataBind();
    }
}

เมื่อทดลองรันดู ก็ได้ผลน่าพอใจสำหรับการใช้งาน Active Record แล้วนะ

out

สำหรับตอนนี้ก็ขอจบลงเพียงเท่านี้ ในตอนหน้ากำลังคิดอยู่ว่าจะเป็นเรื่อง transaction, validation หรือจะเป็นเรื่อง lazy loading ดี

ไว้เจอกันเมื่อหาเวลาได้

Blend to ASPX

ให้ตายสิไมโครซอฟท์ ออก Microsoft Expression Blend มาให้ใช้ แต่กลับไม่มีวิธีง่ายๆในการฝังไฟล์ Silverlight ลงใน ASP.NET !!!

เรื่องเริ่มต้นที่ว่าตอนหัดใช้ Silverlight ก็ต้องมานั่งแกะเองทีละขั้น ตอนนั้นก็คิดว่า ของใหม่เครื่องมือยังห่วยอยู่ ใช้งานยาก ก็น่าให้อภัย แต่ก็คิดว่า ซักวันมันต้องมีคนมาถามแน่ๆเลยว่าจะเอา XAML ไปใช้ยังไง

วันนี้เพื่อนมาถาม

นาย ม. says:
วีน ถามหน่อย สมมุติทำของใน blend เส็ดแล้ว จะใส่เข้าไปใน aspx ยังไง

โอ้…พระเจ้า ไมโครซอฟท์ทำสิ่งที่เราฝันไว้ได้จริงๆด้วย

อันนี้จะทำเฉพาะ 1.0 ไว้ละกันนะ

เริ่มต้นด้วยการเปิด Blend แล้วสร้าง Project เป็น Silverlight 1.0 Site อันนี้ขอใช้ Blend Preview 2.5 ละกัน

Project

จากนั้นสร้าง XAML ขึ้นมาให้เรียบร้อย

Blend2

แล้วลองเปิดเว็บไซต์ด้วย Visual Studio ซะ แล้วลองเปิดไฟล์ Default.html มาดู จะเห็นเป็นแบบนี้

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<!-- saved from url=(0014)about:internet -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>SilverlightSite4</title>

    <script type="text/javascript" src="Silverlight.js"></script>
    <script type="text/javascript" src="Page.xaml.js"></script>
    <style type="text/css">
        #silverlightControlHost {
            height: 480px;
            width: 640px;
        }
        #errorLocation {
            font-size: small;
            color: Gray;
        }
    </style>
    <script type="text/javascript">
        function createSilverlight()
        {
            var scene = new SilverlightSite4.Page();
            Silverlight.createObjectEx({
                source: "Page.xaml",
                parentElement: document.getElementById("silverlightControlHost"),
                id: "SilverlightControl",
                properties: {
                    width: "100%",
                    height: "100%",
                    version: "1.0"
                },
                events: {
                    onLoad: Silverlight.createDelegate(scene, scene.handleLoad),
                    onError: function(sender, args) {
                        var errorDiv = document.getElementById("errorLocation");
                        if (errorDiv != null) {
                            var errorText = args.errorType + "- " + args.errorMessage;

                            if (args.ErrorType == "ParserError") {
                                errorText += "<br>File: " + args.xamlFile;
                                errorText += ", line " + args.lineNumber;
                                errorText += " character " + args.charPosition;
                            }
                            else if (args.ErrorType == "RuntimeError") {
                                errorText += "<br>line " + args.lineNumber;
                                errorText += " character " +  args.charPosition;
                            }
                            errorDiv.innerHTML = errorText;
                        }   
                    }
                }
            });
        }


        if (!window.Silverlight) 
            Silverlight = {};

        Silverlight.createDelegate = function(instance, method) {
            return function() {
                return method.apply(instance, arguments);
            }
        }
    </script>
</head>

<body>
    <div id="silverlightControlHost">
        <script type="text/javascript">
            createSilverlight();
        </script>
    </div>

    <!-- Runtime errors from Silverlight will be displayed here.
    This will contain debugging information and should be removed or hidden when debugging is completed -->
    <div id='errorLocation'></div>
</body>
</html>

เราจะลองมาชำแหละทีละส่วนดูว่า แต่ละอันมันคืออะไร

เริ่มจาก



SilverlightSite4

อันนี้ไม่มีอะไร แค่เป็นหัวของ HTML ไม่ต้องสนใจ

    <script type="text/javascript" src="Silverlight.js"></script>
    <script type="text/javascript" src="Page.xaml.js"></script>

สำหรับสองบรรทัดนี้ บรรทัดแรกจะ้เป็นการเพิ่ม reference ของ silverlight.js ที่เป็นไฟล์ที่เก็บโค้ดที่ใช้ตรวจสอบว่ามีการติดตั้ง Silverlight ไว้หรือยัง และประกาศเมธอดช่วยต่างๆเช่น Silverlight.createObjectEx เป็นต้น
สำหรับบรรทัดที่สองเป็นการโหลดไฟล์ Page.xaml.js ซึ่งเป็นไฟล์ที่เก็บสคริปต์ที่ใช้ควบคุม ตัว XAML ของเรา

    <style type="text/css">
        #silverlightControlHost {
            height: 480px;
            width: 640px;
        }
        #errorLocation {
            font-size: small;
            color: Gray;
        }
    </style>

ต่อมา ก็เป็น css ที่บอกว่า ให้ element ที่มี id silverlightControlHost มีขนาดกว้างยาวเป็น 640*480 พิกเซล

function createSilverlight()
{
    var scene = new SilverlightSite4.Page();
    Silverlight.createObjectEx({
        source: "Page.xaml",
        parentElement: document.getElementById("silverlightControlHost"),
        id: "SilverlightControl",
        properties: {
            width: "100%",
            height: "100%",
            version: "1.0"
        },
        events: {
            onLoad: Silverlight.createDelegate(scene, scene.handleLoad),
            onError: function(sender, args) {
                var errorDiv = document.getElementById("errorLocation");
                if (errorDiv != null) {
                    var errorText = args.errorType + "- " + args.errorMessage;

                    if (args.ErrorType == "ParserError") {
                        errorText += "<br>File: " + args.xamlFile;
                        errorText += ", line " + args.lineNumber;
                        errorText += " character " + args.charPosition;
                    }
                    else if (args.ErrorType == "RuntimeError") {
                        errorText += "<br>line " + args.lineNumber;
                        errorText += " character " +  args.charPosition;
                    }
                    errorDiv.innerHTML = errorText;
                }   
            }
        }
    });
}

ฟังก์ชันต่อมา ยาวนิดนึง คือฟังก์ชัน createSilverlight() ฟังก์ชันนี้แหละที่เป็นหัวใจสำคัญในการเรียกใช้ Silverlight โดยตอนแรกก็จะสร้าง object ของ Silverlight4.Page ขึ้นมา โดยคลาสนี้จะอยู่ในไฟล์ Page.Xaml.js

หลังจากนั้นก็จะโหลด xaml ด้วยคำสั่ง Silverlight.createObjectEx โดยพารามิเตอร์ที่ใส่เข้าไปจะเป็น hash ที่มีพารามิเตอร์ดังนี้

  • source อันนี้ระบุว่าจะให้โหลดไฟล์ขึ้นมา
  • parentElement ระบุเพื่อบอกว่าให้ใส่ลงไปใน element ไหน
  • id ตั้งชื่อว่าอะไร
  • properties อันนี้กำหนดความกว้าง สูง กำหนดเป็นเปอร์เซนต์ได้ หรือจะกำหนดเป็นพิกเซลก็ได้ จริงๆแล้วก็ตั้งตรงนี้เป็น 100 เปอร์เซนต์ แล้วก็ไปกำหนดตรง CSS ข้างบนเอาดีกว่า
  • events สำหรับอันนี้จะเป็นผูก event เข้ากับ method ต่างๆ ซึ่งในที่นี้ก็คือจะไปผูกกับ method handleLoad

        if (!window.Silverlight) 
            Silverlight = {};
    
        Silverlight.createDelegate = function(instance, method) {
            return function() {
                return method.apply(instance, arguments);
            }
        }
    

ตรงนี้เป็นการสร้าง class Silverlight ขึ้นมาพร้อม static method ชื่อ createDelegate เพื่อจะได้เอาใช้ต่อ

    <div id="silverlightControlHost">
        <script type="text/javascript">
            createSilverlight();
        </script>
    </div>

ตรงนี้จะเป็น element ที่จะเป็นที่วาง Silverlight ลงไป สังเกตว่าจะไปเรียกฟังก์ชัน createSilverlight ข้างบนด้วย

พอเรารู้แบบนี้ เราก็น่าจะพอเดาได้ว่าจะต้องเอาอะไรไปแปะตรงไหนแล้วใช่ไหม
หลักๆที่ต้องเอาไปก็คือ ส่วน script, css, แล้วก็ จุดวาง

ทีนี้ลองสร้าง aspx ขึ้นมาอันนึง

<%@ Page Language="VB" AutoEventWireup="false" CodeFile="Default.aspx.vb" Inherits="_Default" %>

<!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 runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>

    </div>
    </form>
</body>
</html>

แล้วก็ทำแบบเมื่้อกี้ สำคัญสุดคือ script เอาไปไว้ใน tag head แบบอันเก่าก็ได้ หรือถ้าใครใช้ ASP.NET Ajax ก็เอาไปไว้ใน ScriptManager ก็ได้ไม่ว่ากัน

<form id="form1" runat="server">
<div>
    <asp:ScriptManager ID="ScriptManager1" runat="server">
    <Scripts>
        <asp:ScriptReference Path="~/Silverlight.js" />
        <asp:ScriptReference Path="~/Page.xaml.js" />
    </Scripts>
    </asp:ScriptManager>
</div>
</form>

สำหรับใครที่ทำแบบมีหลายๆไฟล์สคริปต์ต้องใส่ให้ครบทุกไฟล์ จุดนี้จะต่างจาก xaml ที่เราเอาไปไว้ใน zip file แล้วใช้ downloader ดึงมาทีหลังได้

ทีนี้ก็เพิ่มฟังก์ชัน createSilverlight , css แล้วก็เพิ่มจุดวางเข้าไป

<%@ Page Language="VB" AutoEventWireup="false" CodeFile="Default.aspx.vb" Inherits="_Default" %>

<%@ Register Assembly="System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
    Namespace="System.Web.UI" TagPrefix="asp" %>

<!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 runat="server">
    <title>Untitled Page</title>
    <script type="text/javascript">
        function createSilverlight()
        {
            var scene = new SilverlightSite4.Page();
            Silverlight.createObjectEx({
                source: "Page.xaml",
                parentElement: document.getElementById("silverlightControlHost"),
                id: "SilverlightControl",
                properties: {
                    width: "100%",
                    height: "100%",
                    version: "1.0"
                },
                events: {
                    onLoad: Silverlight.createDelegate(scene, scene.handleLoad),
                    onError: function(sender, args) {
                        var errorDiv = document.getElementById("errorLocation");
                        if (errorDiv != null) {
                            var errorText = args.errorType + "- " + args.errorMessage;

                            if (args.ErrorType == "ParserError") {
                                errorText += "<br>File: " + args.xamlFile;
                                errorText += ", line " + args.lineNumber;
                                errorText += " character " + args.charPosition;
                            }
                            else if (args.ErrorType == "RuntimeError") {
                                errorText += "<br>line " + args.lineNumber;
                                errorText += " character " +  args.charPosition;
                            }
                            errorDiv.innerHTML = errorText;
                        }   
                    }
                }
            });
        }


        if (!window.Silverlight) 
            Silverlight = {};

        Silverlight.createDelegate = function(instance, method) {
            return function() {
                return method.apply(instance, arguments);
            }
        }
    </script>
    <style type="text/css">
        #silverlightControlHost {
            height: 480px;
            width: 640px;
        }
        #errorLocation {
            font-size: small;
            color: Gray;
        }
    </style>
</head>
<body>
    <form id="form1" runat="server">
    <asp:ScriptManager ID="ScriptManager1" runat="server">
    <Scripts>
        <asp:ScriptReference Path="~/Silverlight.js" />
        <asp:ScriptReference Path="~/Page.xaml.js" />
    </Scripts>
    </asp:ScriptManager>
    <div id="silverlightControlHost">
        <script type="text/javascript">
            createSilverlight();
        </script>
    </div>    
    </form>
</body>
</html>

ลองเปิดดูก็จะได้ผลลัพธ์เหมือนรันใน HTML แล้วล่ะ

SilverlightResult

แต่แบบนี้มันไม่เท่ ถ้าใครใช้ ASP.NET 3.5 แล้วมันจะมี Silverlight Control เพิ่มเข้ามา แต่จากเท่าที่ลองเล่นดูพบว่า ถ้าใช้กับ Silverlight 1.0 มันใช้ยากยิ่งนัก แถมยังไปใช้เทคนิคของ ASP.NET AJAX ที่ไม่ค่อยน่าอภิรมย์ซักเท่าใดนัก แต่สำหรับคนที่ใช้ Silverlight 2.0 ดูท่าว่าจะทำให้ชีวิตสบายขึ้นอย่างมากมาย

ลองดูแล้วกัน ได้ผลยังไงวานบอก