ฉันจะควบคุมมากขึ้นใน ASP.NET ได้อย่างไร?


124

ฉันกำลังพยายามสร้าง "ไมโครเว็บแอป" ที่เรียบง่ายมากซึ่งฉันสงสัยว่าจะเป็นที่สนใจของ Stack Overflow'rs สองสามตัวถ้าฉันทำสำเร็จ ฉันโฮสต์ไว้บน C # ของฉันในไซต์ Depth ซึ่งเป็นวานิลลา ASP.NET 3.5 (เช่นไม่ใช่ MVC)

การไหลนั้นง่ายมาก:

  • หากผู้ใช้เข้าสู่แอปด้วย URL ที่ไม่ได้ระบุพารามิเตอร์ทั้งหมด (หรือหากพารามิเตอร์ใด ๆ ไม่ถูกต้อง) ฉันต้องการเพียงแค่แสดงการควบคุมอินพุตของผู้ใช้ (มีแค่สองอัน)
  • หากผู้ใช้เข้าสู่แอปที่มี URL ที่ไม่มีพารามิเตอร์ที่จำเป็นทั้งหมดผมต้องการที่จะแสดงผลและควบคุมการป้อนข้อมูล (เพื่อให้พวกเขาสามารถเปลี่ยนพารามิเตอร์)

ความต้องการที่กำหนดขึ้นเองของฉันมีดังนี้

  • ฉันต้องการให้การส่งใช้ GET แทนการโพสต์ส่วนใหญ่เพื่อให้ผู้ใช้สามารถบุ๊กมาร์กหน้าได้อย่างง่ายดาย
  • ฉันไม่ต้องการให้ URL ดูงี่เง่าหลังจากการส่งโดยมีบิตและชิ้นส่วนที่ไม่เกี่ยวข้องอยู่ โปรดระบุ URL หลักและพารามิเตอร์จริง
  • ตามหลักการแล้วฉันต้องการหลีกเลี่ยงการใช้ JavaScript เลย ไม่มีเหตุผลที่ดีในแอปนี้
  • ฉันต้องการเข้าถึงตัวควบคุมในช่วงเวลาแสดงผลและตั้งค่าเป็นต้นโดยเฉพาะอย่างยิ่งฉันต้องการให้สามารถตั้งค่าเริ่มต้นของการควบคุมเป็นค่าพารามิเตอร์ที่ส่งเข้ามาหาก ASP.NET ไม่สามารถทำได้โดยอัตโนมัติ สำหรับฉัน (ภายในข้อ จำกัด อื่น ๆ )
  • ฉันยินดีที่จะทำการตรวจสอบพารามิเตอร์ทั้งหมดด้วยตัวเองและฉันไม่ต้องการอะไรมากกับเหตุการณ์ฝั่งเซิร์ฟเวอร์ ง่ายมากที่จะตั้งค่าทุกอย่างในการโหลดหน้าแทนที่จะแนบเหตุการณ์กับปุ่มต่างๆเป็นต้น

ส่วนใหญ่ก็โอเค แต่ฉันไม่พบวิธีใดในการลบ viewstate อย่างสมบูรณ์และรักษาฟังก์ชันที่มีประโยชน์ที่เหลือไว้ การใช้โพสต์จากบล็อกโพสต์นี้ฉันได้จัดการเพื่อหลีกเลี่ยงการรับค่าที่แท้จริงสำหรับ viewstate - แต่มันก็ยังคงเป็นพารามิเตอร์ใน URL ซึ่งดูน่าเกลียดจริงๆ

ถ้าฉันทำให้มันเป็นรูปแบบ HTML ธรรมดาแทนที่จะเป็นแบบฟอร์ม ASP.NET (เช่น take out runat="server") ฉันจะไม่ได้รับ magic viewstate ใด ๆ - แต่ฉันไม่สามารถเข้าถึงการควบคุมทางโปรแกรมได้

ผมสามารถทำทั้งหมดนี้โดยไม่สนใจมากที่สุดของ ASP.NET และสร้างเอกสาร XML กับ LINQ กับ XML IHttpHandlerและการใช้ รู้สึกว่าอยู่ในระดับต่ำไปหน่อย

ฉันตระหนักดีว่าปัญหาของฉันสามารถแก้ไขได้โดยการผ่อนคลายข้อ จำกัด ของฉัน (เช่นใช้ POST และไม่สนใจเกี่ยวกับพารามิเตอร์ส่วนเกิน) หรือโดยใช้ ASP.NET MVC แต่ข้อกำหนดของฉันไม่สมเหตุสมผลจริง ๆ หรือไม่?

บางที ASP.NET อาจไม่ลดขนาดเป็นแอปประเภทนี้? มีทางเลือกอื่นที่เป็นไปได้มาก: ฉันแค่โง่และมีวิธีง่ายๆในการทำที่ฉันไม่พบ

ความคิดใคร? (ความคิดเห็นเกี่ยวกับวิธีที่ผู้ยิ่งใหญ่ล้มลง ฯลฯ ไม่เป็นไร - ฉันหวังว่าฉันจะไม่เคยอ้างว่าเป็นผู้เชี่ยวชาญ ASP.NET เนื่องจากความจริงค่อนข้างตรงกันข้าม ... )


16
"ความคิดเห็นที่บ่งบอกว่าผู้ยิ่งใหญ่ล้มลงได้อย่างไร" - เราต่างคนต่างไม่รู้ แต่คนละเรื่อง ฉันเพิ่งเริ่มเข้าร่วมที่นี่เมื่อไม่นานมานี้ แต่ฉันชื่นชมคำถามมากกว่าทุกประเด็น เห็นได้ชัดว่าคุณยังคงคิดและเรียนรู้ ขอชื่นชมคุณ
duffymo

15
ฉันไม่คิดว่าฉันจะสนใจคนที่เลิกเรียน :)
Jon Skeet

1
จริงในกรณีทั่วไป จริงมากในวิทยาการคอมพิวเตอร์
Mehrdad Afshari

3
และหนังสือเล่มต่อไปของคุณจะเป็น "ASP.NET in Depth" หรือไม่? :-P
chakrit

20
ใช่มันมีกำหนดออกในปี 2568;)
Jon Skeet

คำตอบ:


76

โซลูชันนี้จะช่วยให้คุณสามารถเข้าถึงการควบคุมแบบเป็นโปรแกรมได้อย่างครบถ้วนรวมถึงแอตทริบิวต์ทั้งหมดบนตัวควบคุม นอกจากนี้เฉพาะค่ากล่องข้อความเท่านั้นที่จะปรากฏใน URL เมื่อส่งดังนั้น URL คำขอ GET ของคุณจะ "มีความหมาย" มากขึ้น

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="JonSkeetForm.aspx.cs" Inherits="JonSkeetForm" %>
<!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>Jon Skeet's Form Page</title>
</head>
<body>
    <form action="JonSkeetForm.aspx" method="get">
    <div>
        <input type="text" ID="text1" runat="server" />
        <input type="text" ID="text2" runat="server" />
        <button type="submit">Submit</button>
        <asp:Repeater ID="Repeater1" runat="server">
            <ItemTemplate>
                <div>Some text</div>
            </ItemTemplate>
        </asp:Repeater>
    </div>
    </form>
</body>
</html>

จากนั้นในโค้ดของคุณคุณสามารถทำทุกอย่างที่คุณต้องการบน PageLoad

public partial class JonSkeetForm : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        text1.Value = Request.QueryString[text1.ClientID];
        text2.Value = Request.QueryString[text2.ClientID];
    }
}

หากคุณไม่ต้องการฟอร์มที่มีrunat="server"คุณควรใช้ตัวควบคุม HTML ง่ายกว่าในการทำงานตามวัตถุประสงค์ของคุณ เพียงใช้แท็ก HTML ปกติแล้วใส่runat="server"และให้ ID จากนั้นคุณสามารถเข้าถึงได้โดยใช้โปรแกรมและรหัสโดยไม่ต้องใช้ViewStateไฟล์.

ข้อเสียเพียงอย่างเดียวคือคุณจะไม่สามารถเข้าถึงตัวควบคุมเซิร์ฟเวอร์ ASP.NET ที่ "มีประโยชน์" มากมายเช่นGridViews ฉันรวมตัวอย่างไว้Repeaterในตัวอย่างของฉันเพราะฉันสมมติว่าคุณต้องการมีฟิลด์ในหน้าเดียวกับผลลัพธ์และ (ตามความรู้ของฉัน) a Repeaterเป็นตัวควบคุม DataBound เดียวที่จะทำงานโดยไม่มีrunat="server"แอตทริบิวต์ในแท็กฟอร์ม


1
ฉันมีฟิลด์ไม่กี่ช่องที่การทำด้วยตนเองนั้นง่ายมาก :) ที่สำคัญคือฉันไม่รู้ว่าฉันสามารถใช้ runat = server กับการควบคุม HTML ปกติได้ ฉันยังไม่ได้ใช้ผลลัพธ์ แต่นั่นเป็นบิตที่ง่าย ใกล้ถึงแล้ว!
Jon Skeet

อันที่จริง <form runat = "server"> จะเพิ่มฟิลด์ __VIEWSTATE (และอื่น ๆ ) ที่ซ่อนอยู่แม้ว่าคุณจะตั้งค่า EnableViewState = "False" ที่ระดับหน้า นี่เป็นวิธีที่จะไปหากคุณต้องการคลาย ViewState บนเพจ สำหรับความเป็นมิตรกับ URL นั้นการเขียน urlrewriting อาจเป็นตัวเลือก
Sergiu Damian

1
ไม่จำเป็นต้องเขียนใหม่ คำตอบนี้ใช้ได้ดี (แม้ว่าจะหมายถึงการมีการควบคุมด้วย ID ของ "ผู้ใช้" ก็ตาม - ด้วยเหตุผลบางประการฉันไม่สามารถเปลี่ยนชื่อของตัวควบคุมกล่องข้อความแยกต่างหากจาก ID ได้)
Jon Skeet

1
เพียงเพื่อยืนยันสิ่งนี้ได้ผลดีมากแน่นอน ขอบคุณมาก ๆ!
Jon Skeet

14
ดูเหมือนว่าคุณควรจะเขียนมันใน classic asp!
ScottE

12

คุณ (IMHO) มาถูกทางแล้วโดยไม่ใช้ runat = "server" ในแท็ก FORM นั่นหมายความว่าคุณจะต้องดึงค่าจาก Request.QueryString โดยตรงดังตัวอย่างนี้:

ในหน้า. aspx:

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

<!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>ASP.NET with GET requests and no viewstate</title>
</head>
<body>
    <asp:Panel ID="ResultsPanel" runat="server">
      <h1>Results:</h1>
      <asp:Literal ID="ResultLiteral" runat="server" />
      <hr />
    </asp:Panel>
    <h1>Parameters</h1>
    <form action="FormPage.aspx" method="get">
    <label for="parameter1TextBox">
      Parameter 1:</label>
    <input type="text" name="param1" id="param1TextBox" value='<asp:Literal id="Param1ValueLiteral" runat="server" />'/>
    <label for="parameter1TextBox">
      Parameter 2:</label>
    <input type="text" name="param2" id="param2TextBox"  value='<asp:Literal id="Param2ValueLiteral" runat="server" />'/>
    <input type="submit" name="verb" value="Submit" />
    </form>
</body>
</html>

และในรหัสหลัง:

using System;

public partial class FormPage : System.Web.UI.Page {

        private string param1;
        private string param2;

        protected void Page_Load(object sender, EventArgs e) {

            param1 = Request.QueryString["param1"];
            param2 = Request.QueryString["param2"];

            string result = GetResult(param1, param2);
            ResultsPanel.Visible = (!String.IsNullOrEmpty(result));

            Param1ValueLiteral.Text = Server.HtmlEncode(param1);
            Param2ValueLiteral.Text = Server.HtmlEncode(param2);
            ResultLiteral.Text = Server.HtmlEncode(result);
        }

        // Do something with parameters and return some result.
        private string GetResult(string param1, string param2) {
            if (String.IsNullOrEmpty(param1) && String.IsNullOrEmpty(param2)) return(String.Empty);
            return (String.Format("You supplied {0} and {1}", param1, param2));
        }
    }

เคล็ดลับคือเราใช้ ASP.NET Literals ภายในแอตทริบิวต์ value = "" ของอินพุตข้อความดังนั้นกล่องข้อความเองจึงไม่จำเป็นต้อง runat = "เซิร์ฟเวอร์" ผลลัพธ์จะถูกรวมไว้ใน ASP: Panel และคุณสมบัติ Visible ที่ตั้งค่าไว้ในการโหลดเพจขึ้นอยู่กับว่าคุณต้องการแสดงผลลัพธ์ใด ๆ หรือไม่


มันทำงานได้ดี แต่ URL จะไม่เป็นมิตรกับ StackOverflow
Mehrdad Afshari

1
URL จะค่อนข้างเป็นมิตรฉันคิดว่า ... นี่ดูเหมือนเป็นทางออกที่ดีจริงๆ
Jon Skeet

อ๊ะฉันอ่านทวีตของคุณก่อนหน้านี้ได้ค้นคว้าแล้วและตอนนี้ฉันพลาดคำถามของคุณในการเตรียมลูก ๆ ของฉันสำหรับอ่างอาบน้ำ ... :-)
splattne

2

โอเคจอนปัญหาวิวสเตทก่อน:

ฉันไม่ได้ตรวจสอบว่ามีการเปลี่ยนแปลงรหัสภายในประเภทใดตั้งแต่ 2.0 แต่นี่คือวิธีที่ฉันจัดการกับการลบ viewstate เมื่อสองสามปีก่อน จริง ๆ แล้วฟิลด์ที่ซ่อนอยู่นั้นถูกเข้ารหัสไว้ใน HtmlForm ดังนั้นคุณควรได้รับฟิลด์ใหม่ของคุณและก้าวเข้าสู่การแสดงผลการโทรด้วยตัวเอง โปรดทราบว่าคุณสามารถออกจาก __eventtarget และ __eventtarget ได้หากคุณยึดติดกับการควบคุมการป้อนข้อมูลแบบเดิม ๆ (ซึ่งฉันเดาว่าคุณต้องการเนื่องจากยังช่วยไม่ต้องใช้ JS บนไคลเอนต์):

protected override void RenderChildren(System.Web.UI.HtmlTextWriter writer)
{
    System.Web.UI.Page page = this.Page;
    if (page != null)
    {
        onFormRender.Invoke(page, null);
        writer.Write("<div><input type=\"hidden\" name=\"__eventtarget\" id=\"__eventtarget\" value=\"\" /><input type=\"hidden\" name=\"__eventargument\" id=\"__eventargument\" value=\"\" /></div>");
    }

    ICollection controls = (this.Controls as ICollection);
    renderChildrenInternal.Invoke(this, new object[] {writer, controls});

    if (page != null)
        onFormPostRender.Invoke(page, null);
}

ดังนั้นคุณจะได้รับ 3 MethodInfo แบบคงที่และเรียกพวกเขาว่าข้ามส่วน viewstate นั้นออกไป;)

static MethodInfo onFormRender;
static MethodInfo renderChildrenInternal;
static MethodInfo onFormPostRender;

และนี่คือตัวสร้างประเภทของแบบฟอร์มของคุณ:

static Form()
{
    Type aspNetPageType = typeof(System.Web.UI.Page);

    onFormRender = aspNetPageType.GetMethod("OnFormRender", BindingFlags.Instance | BindingFlags.NonPublic);
    renderChildrenInternal = typeof(System.Web.UI.Control).GetMethod("RenderChildrenInternal", BindingFlags.Instance | BindingFlags.NonPublic);
    onFormPostRender = aspNetPageType.GetMethod("OnFormPostRender", BindingFlags.Instance | BindingFlags.NonPublic);
}

หากคำถามของคุณถูกต้องคุณก็ไม่ควรใช้ POST เป็นการดำเนินการในแบบฟอร์มของคุณดังนั้นคุณจะต้องดำเนินการดังนี้

protected override void RenderAttributes(System.Web.UI.HtmlTextWriter writer)
{
    writer.WriteAttribute("method", "get");
    base.Attributes.Remove("method");

    // the rest of it...
}

ฉันเดาว่ามันน่ารักมาก แจ้งให้เราทราบว่าเป็นอย่างไร

แก้ไข: ฉันลืมวิธีการดูหน้าเว็บ:

ดังนั้นแบบฟอร์มที่กำหนดเองของคุณ: HtmlForm ได้รับบทคัดย่อใหม่ล่าสุด (หรือไม่) หน้า: System.Web.UI.Page: P

protected override sealed object SaveViewState()
{
    return null;
}

protected override sealed void SavePageStateToPersistenceMedium(object state)
{
}

protected override sealed void LoadViewState(object savedState)
{
}

protected override sealed object LoadPageStateFromPersistenceMedium()
{
    return null;
}

ในกรณีนี้ฉันปิดผนึกวิธีการเนื่องจากคุณไม่สามารถปิดผนึกหน้าได้ (แม้ว่าจะไม่ใช่แบบนามธรรมก็ตาม Scott Guthrie จะรวมไว้ในอีกอันหนึ่ง: P) แต่คุณสามารถปิดผนึกแบบฟอร์มของคุณได้


ขอบคุณสำหรับสิ่งนี้ - แม้ว่าจะดูเหมือนงานค่อนข้างเยอะ วิธีแก้ปัญหาของ Dan ทำงานได้ดีสำหรับฉัน แต่ก็ยังดีที่จะมีตัวเลือกเพิ่มเติม
Jon Skeet

1

คุณเคยคิดที่จะไม่กำจัด POST แต่จะเปลี่ยนเส้นทางไปยัง GET url ที่เหมาะสมเมื่อโพสต์แบบฟอร์ม นั่นคือยอมรับทั้ง GET และ POST แต่ใน POST จะสร้างคำขอ GET และเปลี่ยนเส้นทางไป สิ่งนี้สามารถจัดการได้ทั้งบนเพจหรือผ่านทาง HttpModule หากคุณต้องการทำให้เพจเป็นอิสระ ฉันคิดว่าสิ่งนี้จะทำให้สิ่งต่างๆง่ายขึ้นมาก

แก้ไข:ฉันถือว่าคุณได้ตั้งค่า EnableViewState = "false" ไว้บนหน้า


ความคิดดี. ความคิดที่น่ากลัวในแง่ของการถูกบังคับให้ทำ แต่ก็ดีในแง่ของมันอาจใช้งานได้ :) จะพยายาม ...
Jon Skeet

ใช่ฉันได้ลอง EnableViewState = false ทั่วทุกที่แล้ว มันไม่ได้ปิดการใช้งานอย่างสมบูรณ์เพียงแค่ตัดมันลง
Jon Skeet

จอน: หากคุณไม่ใช้การควบคุมเซิร์ฟเวอร์ที่เสียหาย (ไม่มี runat = "เซิร์ฟเวอร์") และคุณไม่มี <form runat = "server"> เลย ViewState ก็จะไม่เป็นปัญหา นั่นเป็นเหตุผลที่ฉันบอกว่าจะไม่ใช้การควบคุมเซิร์ฟเวอร์ คุณสามารถใช้คอลเลกชัน Request.Form ได้ตลอดเวลา
Mehrdad Afshari

แต่ถ้าไม่มีเซิร์ฟเวอร์ runat = บนตัวควบคุมมันเป็นเรื่องยากที่จะเผยแพร่ค่าไปยังตัวควบคุมอีกครั้งเมื่อแสดงผล โชคดีที่การควบคุม HTML ด้วย runat = เซิร์ฟเวอร์ทำงานได้ดี
Jon Skeet

1

ฉันจะสร้างโมดูล HTTP ที่จัดการการกำหนดเส้นทาง (คล้ายกับ MVC แต่ไม่ซับซ้อนเพียงแค่ifคำสั่งสองสามข้อ) และส่งไปที่aspxหรือashxเพจ aspxเป็นที่ต้องการเนื่องจากง่ายต่อการแก้ไขเทมเพลตเพจ ฉันจะไม่ใช้WebControlsในaspxอย่างไรก็ตาม เพียงResponse.Write.

อย่างไรก็ตามเพื่อให้ง่ายขึ้นคุณสามารถทำการตรวจสอบพารามิเตอร์ในโมดูล (เนื่องจากอาจใช้รหัสร่วมกับการกำหนดเส้นทาง) และบันทึกลงในHttpContext.Itemsแล้วแสดงผลในหน้า สิ่งนี้จะทำงานได้ดีเหมือน MVC โดยไม่มีกระดิ่งและนกหวีด นี่คือสิ่งที่ฉันทำมากก่อนวัน ASP.NET MVC


1

ฉันมีความสุขมากที่ได้ละทิ้งคลาสเพจโดยสิ้นเชิงและเพียงแค่จัดการทุกคำขอด้วยตัวพิมพ์ใหญ่สลับตาม url Evey "page" กลายเป็นเทมเพลต html และ ac # object คลาสเทมเพลตใช้นิพจน์ทั่วไปกับตัวแทนการจับคู่ที่เปรียบเทียบกับคอลเลกชันคีย์

ประโยชน์:

  1. มันเร็วมากแม้ว่าจะทำการคอมไพล์ใหม่แล้วก็แทบจะไม่มีความล่าช้าเลย (คลาสของเพจต้องใหญ่)
  2. การควบคุมนั้นละเอียดมาก (เหมาะสำหรับ SEO และการสร้าง DOM เพื่อให้เล่นกับ JS ได้ดี)
  3. การนำเสนอแยกจากตรรกะ
  4. jQuery มีการควบคุม html ทั้งหมด

Bummers:

  1. สิ่งง่ายๆใช้เวลานานกว่าเล็กน้อยเนื่องจากกล่องข้อความเดียวต้องใช้รหัสในหลาย ๆ ที่ แต่มันปรับขนาดได้ดีจริงๆ
  2. มันเป็นสิ่งที่ดึงดูดเสมอที่จะทำด้วยการดูหน้าจนกว่าฉันจะเห็น viewstate (urgh) แล้วฉันก็กลับมาสู่ความเป็นจริง

จอนเรากำลังทำอะไรกับ SO ในเช้าวันเสาร์ :)?


1
เย็นวันเสาร์ที่นี่ มันทำให้โอเคไหม? (ฉันชอบดูกราฟกระจายของเวลาโพสต์ / วันของฉัน btw ... )
Jon Skeet

1

ฉันคิดว่า asp: การควบคุมตัวทำซ้ำล้าสมัยแล้ว

เอ็นจิ้นเทมเพลต ASP.NET นั้นดี แต่คุณสามารถทำซ้ำได้อย่างง่ายดายด้วย for loop ...

<form action="JonSkeetForm.aspx" method="get">
<div>
    <input type="text" ID="text1" runat="server" />
    <input type="text" ID="text2" runat="server" />
    <button type="submit">Submit</button>
    <% foreach( var item in dataSource ) { %>
        <div>Some text</div>   
    <% } %>
</div>
</form>

แบบฟอร์ม ASP.NET นั้นโอเคมีการสนับสนุนที่ดีจาก Visual Studio แต่สิ่งนี้ runat = "เซิร์ฟเวอร์" นั้นไม่ถูกต้อง ViewState เป็น

ฉันขอแนะนำให้คุณดูว่าอะไรทำให้ ASP.NET MVC นั้นยอดเยี่ยมมากใครจะย้ายออกจากแนวทาง ASP.NET Forms โดยไม่ต้องทิ้งมันไปทั้งหมด

คุณสามารถเขียนสิ่งที่ผู้ให้บริการสร้างของคุณเองเพื่อรวบรวมมุมมองที่กำหนดเองเช่น NHaml ฉันคิดว่าคุณควรดูที่นี่เพื่อการควบคุมที่มากขึ้นและเพียงแค่อาศัยรันไทม์ ASP.NET สำหรับการตัด HTTP และเป็นสภาพแวดล้อมการโฮสต์ CLR หากคุณเรียกใช้โหมดรวมคุณจะสามารถจัดการคำขอ / การตอบกลับ HTTP ได้เช่นกัน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.