Come posso avere un maggiore controllo in ASP.NET?


124

Sto cercando di creare una "micro-webapp" molto, molto semplice, che sospetto possa interessare ad alcuni Stack Overflow se mai lo avessi fatto. Lo sto ospitando sul mio C # nel sito Depth, che è ASP.NET 3.5 vaniglia (ovvero non MVC).

Il flusso è molto semplice:

  • Se un utente accede all'app con un URL che non specifica tutti i parametri (o se uno di essi non è valido), voglio solo visualizzare i controlli di input dell'utente. (Ce ne sono solo due.)
  • Se un utente immette l'applicazione con un URL che fa avere tutti i parametri richiesti, voglio visualizzare i risultati e i controlli di input (in modo da poter modificare i parametri)

Ecco i miei requisiti autoimposti (miscela di progettazione e realizzazione):

  • Voglio che l'invio utilizzi GET anziché POST, principalmente per consentire agli utenti di aggiungere facilmente la pagina ai segnalibri.
  • Io non voglio che l'URL per finiscono per guardare sciocco dopo la presentazione, con pezzi estranei e pezzi su di esso. Solo l'URL principale e i parametri reali per favore.
  • Idealmente, vorrei evitare di richiedere JavaScript. Non c'è motivo per farlo in questa app.
  • Voglio essere in grado di accedere ai controlli durante il tempo di rendering e impostare valori ecc. In particolare, voglio essere in grado di impostare i valori predefiniti dei controlli sui valori dei parametri passati, se ASP.NET non può farlo automaticamente per me (nell'ambito delle altre restrizioni).
  • Sono felice di fare da solo tutta la convalida dei parametri e non ho bisogno di molto in termini di eventi lato server. È davvero semplice impostare tutto sul caricamento della pagina invece di associare eventi a pulsanti ecc.

La maggior parte di questi va bene, ma non ho trovato alcun modo per rimuovere completamente il viewstate e mantenere il resto della funzionalità utile. Usando il post di questo post sul blog sono riuscito a evitare di ottenere qualsiasi valore effettivo per il viewstate, ma finisce comunque come parametro sull'URL, che sembra davvero brutto.

Se lo trasformo in un semplice modulo HTML anziché in un modulo ASP.NET (ovvero estraggo runat="server"), allora non ottengo alcun viewstate magico, ma non posso accedere ai controlli a livello di codice.

Ho potuto fare tutto questo ignorando la maggior parte di ASP.NET e costruire un documento XML con LINQ to XML, e l'attuazione IHttpHandler. Sembra un po 'basso livello però.

Mi rendo conto che i miei problemi potrebbero essere risolti rilassando i miei vincoli (ad esempio utilizzando POST e non preoccupandosi del parametro surplus) o utilizzando ASP.NET MVC, ma i miei requisiti sono davvero irragionevoli?

Forse ASP.NET non si riduce a questo tipo di app? C'è comunque un'alternativa molto probabile: sono solo uno stupido e c'è un modo perfettamente semplice di farlo che non ho ancora trovato.

Qualche pensiero, qualcuno? (Indica i commenti su come sono caduti i potenti, ecc. Va bene - spero di non aver mai affermato di essere un esperto ASP.NET, poiché la verità è piuttosto l'opposto ...)


16
"Cue commenti su come sono caduti i potenti" - siamo tutti ignoranti, solo di cose diverse. Di recente ho iniziato a partecipare qui, ma ammiro la domanda più di tutti i punti. Ovviamente stai ancora pensando e imparando. Complimenti a te.
Duffymo,

15
Non credo che avrei mai prestato attenzione a qualcuno che aveva smesso di imparare :)
Jon Skeet,

1
Vero in generale. Molto vero in informatica.
Mehrdad Afshari,

3
E il tuo prossimo libro sarà "ASP.NET in profondità"? :-P
chakrit

20
Sì, è previsto per il 2025;)
Jon Skeet,

Risposte:


76

Questa soluzione ti darà accesso programmatico ai controlli nella loro interezza, inclusi tutti gli attributi sui controlli. Inoltre, solo i valori della casella di testo verranno visualizzati nell'URL al momento dell'invio, quindi l'URL della richiesta GET sarà più "significativo"

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

Quindi nel code-behind puoi fare tutto il necessario su 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];
    }
}

Se non vuoi un modulo che abbia runat="server", allora dovresti usare i controlli HTML. È più facile lavorare con i tuoi scopi. Basta usare i normali tag HTML e inserire runat="server"e assegnare loro un ID. Quindi è possibile accedervi a livello di codice e codificare senza unViewState .

L'unico aspetto negativo è che non avrai accesso a molti dei "utili" controlli server ASP.NET come GridViews. Ho incluso a Repeaternel mio esempio perché sto assumendo che tu voglia avere i campi nella stessa pagina dei risultati e (per Repeaterquanto ne sappia ) a è l'unico controllo DataBound che verrà eseguito senza un runat="server"attributo nel tag Form.


1
Ho così pochi campi che farlo manualmente è davvero facile :) La chiave era che non sapevo che avrei potuto usare runat = server con normali controlli HTML. Non ho ancora implementato i risultati, ma è semplice. Quasi arrivati!
Jon Skeet,

In effetti, un <form runat = "server"> aggiungerebbe il campo nascosto __VIEWSTATE (e qualche altro) anche quando si imposta EnableViewState = "False" a livello di pagina. Questa è la strada da percorrere se si desidera perdere il ViewState nella pagina. Per quanto riguarda la cordialità dell'Url, l'urlrewriting potrebbe essere un'opzione.
Sergiu Damian,

1
Non è necessario riscrivere. Questa risposta funziona bene (sebbene significhi avere un controllo con un ID di "utente" - per qualche ragione non posso cambiare il nome di un controllo casella di testo separatamente dal suo ID).
Jon Skeet,

1
Giusto per confermare, ha funzionato davvero molto bene. Grazie mille!
Jon Skeet,

14
Sembra che avresti dovuto scriverlo semplicemente in asp classico!
ScottE,

12

Sei sicuramente (IMHO) sulla strada giusta non usando runat = "server" nel tuo tag FORM. Questo significa solo che dovrai estrarre direttamente i valori da Request.QueryString, come in questo esempio:

Nella stessa pagina 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>

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

Il trucco qui è che stiamo usando ASP.NET Literals all'interno degli attributi value = "" degli input di testo, quindi le caselle di testo stesse non devono runat = "server". I risultati vengono quindi racchiusi in un ASP: Pannello e la proprietà Visible impostata al caricamento della pagina dipende dal fatto che si desideri visualizzare o meno i risultati.


Funziona abbastanza bene, ma gli URL non saranno amichevoli come, diciamo, StackOverflow.
Mehrdad Afshari,

1
Gli URL saranno piuttosto amichevoli, penso ... Sembra una soluzione davvero buona.
Jon Skeet,

Argh, ho letto i tuoi tweet in precedenza, l'avevo studiato e ora mi mancava la tua domanda mentre preparavo i miei bambini più piccoli per la vasca da bagno ... :-)
splattne,

2

Ok, Jon, prima il problema con il viewstate:

Non ho verificato se c'è qualche tipo di modifica del codice interno dal 2.0, ma ecco come ho gestito la rimozione del viewstate alcuni anni fa. In realtà quel campo nascosto è hardcoded all'interno di HtmlForm quindi dovresti derivarne uno nuovo ed entrare nel suo rendering effettuando le chiamate da solo. Nota che puoi anche lasciare fuori __eventtarget e __eventtarget se ti attieni ai semplici vecchi controlli di input (che immagino che vorresti fare poiché aiuta anche a non richiedere JS sul client):

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

Quindi ottieni quei 3 MethodInfo statici e li chiami saltando quella parte del viewstate;)

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

ed ecco il costruttore del tipo del tuo modulo:

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

Se sto ricevendo la tua domanda giusta, vuoi anche non utilizzare POST come azione dei tuoi moduli, quindi ecco come lo faresti:

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

    // the rest of it...
}

Immagino che sia praticamente così. Fammi sapere come va.

EDIT: ho dimenticato i metodi viewstate Page:

Quindi il tuo modulo personalizzato: HtmlForm ottiene il suo nuovissimo abstract (o no) Pagina: 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 questo caso sigillo i metodi perché non puoi sigillare la Pagina (anche se non è astratta, Scott Guthrie la avvolgerà in un'altra ancora: P) ma puoi sigillare la tua Forma.


Grazie per questo, anche se sembra piuttosto un sacco di lavoro. La soluzione di Dan ha funzionato bene per me, ma è sempre bello avere più opzioni.
Jon Skeet,

1

Hai pensato di non eliminare il POST ma piuttosto di reindirizzare a un URL GET adatto quando il modulo è POST. Cioè, accetta sia GET che POST, ma su POST costruisci una richiesta GET e reindirizza ad essa. Questo può essere gestito sulla pagina o tramite un HttpModule se si desidera renderlo indipendente dalla pagina. Penso che questo renderebbe le cose molto più facili.

EDIT: suppongo che hai EnableViewState = "false" impostato sulla pagina.


Bella idea Bene, idea orribile in termini di essere costretto a farlo, ma bello in termini di probabilmente funziona :) Ci proverò ...
Jon Skeet,

E sì, ho provato EnableViewState = false dappertutto. Non lo disabilita completamente, lo riduce.
Jon Skeet,

Jon: Se non usi i dannati controlli del server (no runat = "server") e non hai affatto un <form runat = "server">, ViewState non sarà un problema. Ecco perché ho detto di non usare i controlli del server. Puoi sempre utilizzare la raccolta Request.Form.
Mehrdad Afshari,

Ma senza runat = server sui controlli, è difficile propagare nuovamente il valore ai controlli durante il rendering. Fortunatamente, i controlli HTML con runat = server funzionano bene.
Jon Skeet,

1

Vorrei creare un modulo HTTP che gestisca il routing (simile a MVC ma non sofisticato, solo un paio di ifistruzioni) e lo consegni aspxao alle ashxpagine. aspxè preferito poiché è più semplice modificare il modello di pagina. Non vorrei usare WebControlsnel aspxperò. Basta Response.Write.

A proposito, per semplificare le cose, è possibile eseguire la convalida dei parametri nel modulo (poiché probabilmente condivide il codice con il routing) e salvarlo su HttpContext.Itemse quindi renderizzarli nella pagina. Funzionerà praticamente come l'MVC senza campane e fischietti. Questo è quello che ho fatto molto prima dei giorni ASP.NET MVC.


1

Sono stato davvero felice di abbandonare del tutto la classe di pagine e di gestire tutte le richieste con un grande switch switch basato sull'URL. Ogni "pagina" diventa un modello html e un oggetto ac #. La classe template utilizza una regex con un delegato di confronto che confronta con una raccolta di chiavi.

benefici:

  1. È davvero veloce, anche dopo una ricompilazione, non c'è quasi nessun ritardo (la classe di pagine deve essere grande)
  2. il controllo è davvero granulare (ottimo per SEO e creare il DOM per giocare bene con JS)
  3. la presentazione è separata dalla logica
  4. jQuery ha il controllo totale dell'html

bummers:

  1. le cose semplici richiedono un po 'più di tempo in quanto una singola casella di testo richiede codice in più punti, ma si ingrandisce davvero bene
  2. è sempre tentato di farlo solo con la visualizzazione di pagina fino a quando non vedo un viewstate (urgh), quindi torno alla realtà.

Jon, che facciamo sabato sera mattina :)?


1
È sabato sera qui. Va bene? (Mi piacerebbe vedere un grafico a dispersione dei miei tempi / giorni di pubblicazione, a proposito ...)
Jon Skeet,

1

Ho pensato all'asp: il controllo del ripetitore era obsoleto.

Il motore di template ASP.NET è carino ma puoi altrettanto facilmente ripetere con un ciclo for ...

<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 è un po 'okay, c'è un supporto decente da Visual Studio ma questa cosa runat = "server", è semplicemente sbagliata. ViewState a.

Ti suggerisco di dare un'occhiata a ciò che rende ASP.NET MVC così eccezionale, chi si allontana dall'approccio ASP.NET Forms senza buttarlo via.

Puoi persino scrivere le tue cose del provider di build per compilare visualizzazioni personalizzate come NHaml. Penso che dovresti cercare qui un maggiore controllo e fare semplicemente affidamento sul runtime ASP.NET per il wrapping di HTTP e come ambiente di hosting CLR. Se esegui la modalità integrata, sarai in grado di manipolare anche la richiesta / risposta HTTP.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.