Wie kann ich in ASP.NET mehr Kontrolle übernehmen?

124

Ich versuche, eine sehr, sehr einfache "Micro-Webapp" zu erstellen, von der ich vermute, dass sie für einige Stack Overflow'rs von Interesse ist, wenn ich sie jemals fertig bekomme. Ich hoste es auf meiner C # in Depth-Site, die Vanilla ASP.NET 3.5 (dh nicht MVC) ist.

Der Ablauf ist sehr einfach:

  • Wenn ein Benutzer die App mit einer URL betritt, die nicht alle Parameter angibt (oder wenn einer von ihnen ungültig ist), möchte ich nur die Benutzereingabesteuerelemente anzeigen. (Es gibt nur zwei.)
  • Wenn ein Benutzer die App mit einer URL eingibt , das tut alle erforderlichen Parameter haben, möchte ich die Ergebnisse anzuzeigen und die Eingangskontrollen (so dass sie die Parameter ändern können)

Hier sind meine selbst auferlegten Anforderungen (Mischung aus Design und Implementierung):

  • Ich möchte, dass die Einreichung GET anstelle von POST verwendet, hauptsächlich, damit Benutzer die Seite einfach mit einem Lesezeichen versehen können.
  • Ich möchte nicht, dass die URL nach der Übermittlung albern aussieht, mit überflüssigen Kleinigkeiten. Nur die Haupt-URL und die realen Parameter bitte.
  • Idealerweise möchte ich vermeiden, dass JavaScript benötigt wird. In dieser App gibt es keinen guten Grund dafür.
  • Ich möchte während der Renderzeit auf die Steuerelemente zugreifen und Werte usw. festlegen können. Insbesondere möchte ich die Standardwerte der Steuerelemente auf die übergebenen Parameterwerte festlegen können, wenn ASP.NET dies nicht automatisch tun kann für mich (innerhalb der anderen Einschränkungen).
  • Ich bin froh, die gesamte Parameterüberprüfung selbst durchführen zu können, und ich brauche nicht viel für serverseitige Ereignisse. Es ist wirklich einfach, alles auf Seitenladen einzustellen, anstatt Ereignisse an Schaltflächen usw. anzuhängen.

Das meiste davon ist in Ordnung, aber ich habe keine Möglichkeit gefunden , den Ansichtsstatus vollständig zu entfernen und den Rest der nützlichen Funktionalität beizubehalten. Mit dem Beitrag aus diesem Blog-Beitrag habe ich es geschafft, zu vermeiden, dass ein tatsächlicher Wert für den Ansichtsstatus erhalten wird - aber er endet immer noch als Parameter in der URL, was wirklich hässlich aussieht.

Wenn ich es zu einem einfachen HTML-Formular anstelle eines ASP.NET-Formulars runat="server"mache (dh herausnehmen), erhalte ich keinen magischen Ansichtsstatus - aber dann kann ich nicht programmgesteuert auf die Steuerelemente zugreifen.

All dies könnte ich tun, indem ich den größten Teil von ASP.NET ignoriere, ein XML-Dokument mit LINQ to XML aufbaue und implementiere IHttpHandler. Das fühlt sich allerdings etwas niedrig an.

Mir ist klar, dass meine Probleme gelöst werden können, indem ich entweder meine Einschränkungen lockere (z. B. POST verwende und mich nicht um den Überschussparameter kümmere) oder ASP.NET MVC verwende. Sind meine Anforderungen jedoch wirklich unangemessen?

Vielleicht ASP.NET skaliert einfach nicht nach unten auf diese Art von app? Es gibt jedoch eine sehr wahrscheinliche Alternative: Ich bin nur dumm und es gibt eine ganz einfache Möglichkeit, die ich einfach nicht gefunden habe.

Irgendwelche Gedanken, irgendjemand? (Cue-Kommentare darüber, wie die Mächtigen gefallen sind usw. Das ist in Ordnung - ich hoffe, ich habe nie behauptet, ein ASP.NET-Experte zu sein, da die Wahrheit genau das Gegenteil ist ...)

Jon Skeet
quelle
16
"Cue Kommentare darüber, wie die Mächtigen gefallen sind" - wir sind alle unwissend, nur von verschiedenen Dingen. Ich habe erst vor kurzem angefangen, hier teilzunehmen, aber ich bewundere die Frage mehr als alle Punkte. Sie denken und lernen offensichtlich immer noch. Hut ab.
Duffymo
15
Ich glaube nicht, dass ich jemals jemandem Aufmerksamkeit schenken würde, der das Lernen aufgegeben hat :)
Jon Skeet
1
Richtig im allgemeinen Fall. Sehr wahr in der Informatik.
Mehrdad Afshari
3
Und wird Ihr nächstes Buch "ASP.NET in Depth" sein? :-P
Chakrit
20
Ja, es ist für 2025 geplant;)
Jon Skeet

Antworten:

76

Mit dieser Lösung erhalten Sie programmgesteuerten Zugriff auf die Steuerelemente in ihrer Gesamtheit, einschließlich aller Attribute auf den Steuerelementen. Außerdem werden bei der Übermittlung nur die Textfeldwerte in der URL angezeigt, sodass Ihre GET-Anforderungs-URL "aussagekräftiger" ist.

<%@ 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>

Dann können Sie in Ihrem Code-Behind alles tun, was Sie für PageLoad benötigen

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];
    }
}

Wenn Sie kein Formular mit haben möchten runat="server", sollten Sie HTML-Steuerelemente verwenden. Es ist einfacher, für Ihre Zwecke zu arbeiten. Verwenden Sie einfach normale HTML-Tags runat="server"und geben Sie ihnen eine ID. Dann können Sie programmgesteuert darauf zugreifen und ohne a codieren ViewState.

Der einzige Nachteil ist, dass Sie keinen Zugriff auf viele der "hilfreichen" ASP.NET-Serversteuerelemente wie GridViews haben. Ich habe a Repeaterin mein Beispiel aufgenommen, weil ich davon Repeaterausgehe, dass Sie die Felder auf derselben Seite wie die Ergebnisse haben möchten und (meines Wissens) a das einzige DataBound-Steuerelement ist, das ohne runat="server"Attribut im Formular-Tag ausgeführt wird.

Dan Herbert
quelle
1
Ich habe so wenige Felder, dass es wirklich einfach ist, es manuell zu machen :) Der Schlüssel war, dass ich nicht wusste, dass ich runat = server mit normalen HTML-Steuerelementen verwenden kann. Ich habe die Ergebnisse noch nicht implementiert, aber das ist das Einfache. Fast da!
Jon Skeet
In der Tat würde ein <form runat = "server"> das versteckte Feld __VIEWSTATE (und ein anderes) hinzufügen, selbst wenn Sie EnableViewState = "False" auf Seitenebene festlegen. Dies ist der richtige Weg, wenn Sie den ViewState auf der Seite verlieren möchten. Was die URL-Freundlichkeit betrifft, könnte URL-Schreiben eine Option sein.
Sergiu Damian
1
Kein Umschreiben erforderlich. Diese Antwort funktioniert einwandfrei (obwohl dies bedeutet, dass ein Steuerelement mit der ID "Benutzer" vorhanden ist - aus irgendeinem Grund kann ich den Namen eines Textfeldsteuerelements nicht getrennt von seiner ID ändern).
Jon Skeet
1
Nur um zu bestätigen, dass dies in der Tat sehr gut funktioniert hat. Vielen Dank!
Jon Skeet
14
Sieht so aus, als hättest du es einfach in klassischem Asp schreiben sollen!
ScottE
12

Sie sind definitiv (IMHO) auf dem richtigen Weg, indem Sie nicht runat = "server" in Ihrem FORM-Tag verwenden. Dies bedeutet jedoch nur, dass Sie Werte direkt aus dem Request.QueryString extrahieren müssen, wie in diesem Beispiel:

Auf der ASPX-Seite selbst:

<%@ 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>

und im Code-Behind:

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));
        }
    }

Der Trick dabei ist, dass wir ASP.NET-Literale in den Attributen value = "" der Texteingaben verwenden, sodass die Textfelder selbst nicht runat = "server" müssen. Die Ergebnisse werden dann in ein ASP: Panel verpackt und die Visible-Eigenschaft beim Laden der Seite festgelegt, je nachdem, ob Sie Ergebnisse anzeigen möchten oder nicht.

Dylan Beattie
quelle
Es funktioniert ziemlich gut, aber die URLs sind nicht so freundlich wie beispielsweise StackOverflow.
Mehrdad Afshari
1
Die URLs werden ziemlich freundlich sein, denke ich ... Das sieht nach einer wirklich guten Lösung aus.
Jon Skeet
Argh, ich habe deine Tweets früher gelesen, hatte sie recherchiert und jetzt habe ich deine Frage verpasst, die meine kleinen Kinder auf die Badewanne vorbereitet ... :-)
splattne
2

Okay, Jon, zuerst das Viewstate-Problem:

Ich habe seit 2.0 nicht mehr überprüft, ob sich der interne Code geändert hat, aber hier ist, wie ich vor einigen Jahren damit umgegangen bin, den Viewstate loszuwerden. Tatsächlich ist dieses versteckte Feld in HtmlForm fest codiert, daher sollten Sie Ihr neues ableiten und in dessen Rendering einsteigen und die Aufrufe selbst ausführen. Beachten Sie, dass Sie __eventtarget und __eventtarget auch weglassen können, wenn Sie sich an einfache alte Eingabesteuerelemente halten (was Sie wahrscheinlich möchten, da dies auch dazu beiträgt, dass JS auf dem Client nicht erforderlich ist):

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);
}

Also holst du dir diese 3 statischen MethodInfos und rufst sie aus, indem du diesen Viewstate-Teil überspringst;)

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

und hier ist der Typkonstruktor Ihres Formulars:

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);
}

Wenn ich Ihre Frage richtig stelle, möchten Sie POST auch nicht als Aktion für Ihre Formulare verwenden. So gehen Sie vor:

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

    // the rest of it...
}

Ich denke das ist so ziemlich alles. Lass mich wissen, wie es geht.

BEARBEITEN: Ich habe die Methoden für den Seitenaufrufstatus vergessen:

So erhält Ihr benutzerdefiniertes Formular: HtmlForm seine brandneue Zusammenfassung (oder nicht) Seite: 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;
}

In diesem Fall versiegele ich die Methoden, weil Sie die Seite nicht versiegeln können (auch wenn sie nicht abstrakt ist. Scott Guthrie wird sie in eine weitere einbinden: P), aber Sie können Ihr Formular versiegeln.

user134706
quelle
Danke dafür - obwohl es ziemlich viel Arbeit klingt. Dans Lösung hat für mich gut funktioniert, aber es ist immer gut, mehr Optionen zu haben.
Jon Skeet
1

Haben Sie darüber nachgedacht, den POST nicht zu entfernen, sondern zu einer geeigneten GET-URL umzuleiten, wenn das Formular POST ist? Akzeptieren Sie also sowohl GET als auch POST, aber erstellen Sie beim POST eine GET-Anforderung und leiten Sie zu dieser um. Dies kann entweder auf der Seite oder über ein HttpModule erfolgen, wenn Sie es seitenunabhängig machen möchten. Ich denke, das würde die Dinge viel einfacher machen.

EDIT: Ich gehe davon aus, dass Sie EnableViewState = "false" auf der Seite gesetzt haben.

Tvanfosson
quelle
Gute Idee. Nun, schreckliche Idee in Bezug darauf, gezwungen zu sein, aber schön in Bezug darauf, dass es wahrscheinlich funktioniert :)
Jon Skeet
Und ja, ich habe überall EnableViewState = false ausprobiert. Es deaktiviert es nicht vollständig, sondern reduziert es nur.
Jon Skeet
Jon: Wenn Sie die verdammten Serversteuerelemente nicht verwenden (kein runat = "server") und überhaupt keinen <form runat = "server"> haben, wird ViewState kein Problem sein. Deshalb habe ich gesagt, keine Serversteuerelemente zu verwenden. Sie können jederzeit die Request.Form-Sammlung verwenden.
Mehrdad Afshari
Ohne runat = server auf den Steuerelementen ist es jedoch schwierig, den Wert beim Rendern erneut an die Steuerelemente weiterzugeben. Glücklicherweise funktionieren HTML-Steuerelemente mit runat = server gut.
Jon Skeet
1

Ich würde ein HTTP-Modul erstellen, das das Routing handhabt (ähnlich wie MVC, aber nicht ausgefeilt, nur ein paar ifAnweisungen) und es an aspxoder ashxSeiten übergeben. aspxwird bevorzugt, da es einfacher ist, die Seitenvorlage zu ändern. Ich würde WebControlsdas aspxaber nicht benutzen . Einfach Response.Write.

Übrigens können Sie zur Vereinfachung die Parameterüberprüfung im Modul durchführen (da es wahrscheinlich Code mit dem Routing teilt) und ihn speichern HttpContext.Itemsund dann auf der Seite rendern. Dies funktioniert ziemlich ähnlich wie beim MVC ohne all die Glocken und Pfeifen. Dies ist, was ich vor ASP.NET MVC-Tagen viel getan habe.

Mehrdad Afshari
quelle
1

Ich war wirklich froh, die Seitenklasse komplett verlassen zu haben und einfach jede Anfrage mit einem großen Schalter zu bearbeiten, der auf der URL basiert. Jede "Seite" wird zu einer HTML-Vorlage und einem ac # -Objekt. Die Vorlagenklasse verwendet einen regulären Ausdruck mit einem Übereinstimmungsdelegierten, der mit einer Schlüsselsammlung verglichen wird.

Leistungen:

  1. Es ist sehr schnell, auch nach einer Neukompilierung gibt es fast keine Verzögerung (die Seitenklasse muss groß sein)
  2. Die Steuerung ist sehr detailliert (ideal für SEO und das Erstellen des DOM, um gut mit JS zu spielen).
  3. Die Präsentation ist von der Logik getrennt
  4. jQuery hat die vollständige Kontrolle über das HTML

bummers:

  1. Einfache Dinge dauern etwas länger, da für ein einzelnes Textfeld an mehreren Stellen Code erforderlich ist, der sich jedoch sehr gut skalieren lässt
  2. Es ist immer verlockend, es einfach mit der Seitenansicht zu tun, bis ich einen Ansichtsstatus (urgh) sehe, dann kehre ich zur Realität zurück.

Jon, was machen wir an einem Samstagmorgen auf SO :)?

Missaghi
quelle
1
Es ist Samstagabend hier. Ist das in Ordnung? (Ich würde gerne ein Streudiagramm meiner Posting-Zeiten / Tage sehen, übrigens ...)
Jon Skeet
1

Ich dachte, die asp: Repeater-Steuerung sei veraltet.

Die ASP.NET-Vorlagen-Engine ist nett, aber Sie können das Wiederholen genauso einfach mit einer for-Schleife durchführen ...

<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 Forms ist in Ordnung, es gibt gute Unterstützung von Visual Studio, aber diese Sache mit runat = "server" ist einfach falsch. ViewState to.

Ich schlage vor, Sie werfen einen Blick darauf, was ASP.NET MVC so großartig macht, wen es vom ASP.NET Forms-Ansatz entfernt, ohne alles wegzuwerfen.

Sie können sogar Ihre eigenen Build-Provider-Inhalte schreiben, um benutzerdefinierte Ansichten wie NHaml zu kompilieren. Ich denke, Sie sollten hier nach mehr Kontrolle suchen und sich einfach auf die ASP.NET-Laufzeit verlassen, um HTTP und als CLR-Hosting-Umgebung zu verpacken. Wenn Sie den integrierten Modus ausführen, können Sie auch die HTTP-Anforderung / Antwort bearbeiten.

John Leidegren
quelle