Comment puis-je prendre plus de contrôle dans ASP.NET?

124

J'essaye de construire une "micro-webapp" très, très simple qui, je soupçonne, sera intéressante pour quelques Stack Overflow'rs si jamais je le fais. Je l'héberge sur mon site C # in Depth, qui est vanilla ASP.NET 3.5 (c'est-à-dire pas MVC).

Le déroulement est très simple:

  • Si un utilisateur entre dans l'application avec une URL qui ne spécifie pas tous les paramètres (ou si l'un d'entre eux n'est pas valide), je souhaite simplement afficher les contrôles d'entrée utilisateur. (Il n'y en a que deux.)
  • Si un utilisateur entre dans l'application avec une URL qui contient tous les paramètres requis, je souhaite afficher les résultats et les contrôles d'entrée (afin qu'ils puissent modifier les paramètres)

Voici mes exigences auto-imposées (mélange de conception et de mise en œuvre):

  • Je souhaite que la soumission utilise GET plutôt que POST, principalement pour que les utilisateurs puissent facilement ajouter la page à leurs favoris.
  • Je ne veux pas que l'URL finisse par avoir l'air idiot après la soumission, avec des morceaux superflus. Juste l'URL principale et les vrais paramètres s'il vous plaît.
  • Idéalement, j'aimerais éviter de nécessiter JavaScript. Il n'y a aucune bonne raison pour cela dans cette application.
  • Je veux pouvoir accéder aux contrôles pendant le temps de rendu et définir les valeurs, etc. En particulier, je veux pouvoir définir les valeurs par défaut des contrôles sur les valeurs de paramètres transmises, si ASP.NET ne peut pas le faire automatiquement pour moi (dans les autres restrictions).
  • Je suis heureux de faire toute la validation des paramètres moi-même et je n'ai pas besoin de beaucoup d'événements côté serveur. Il est très simple de tout définir lors du chargement de la page au lieu d'attacher des événements à des boutons, etc.

La plupart de cela est correct, mais je n'ai trouvé aucun moyen de supprimer complètement l'état d'affichage et de conserver le reste des fonctionnalités utiles. En utilisant l'article de cet article de blog, j'ai réussi à éviter d'obtenir une valeur réelle pour l'état d'affichage - mais cela finit toujours comme un paramètre sur l'URL, ce qui semble vraiment moche.

Si j'en fais un formulaire HTML simple au lieu d'un formulaire ASP.NET (c'est-à-dire retirer runat="server"), alors je n'obtiens aucun état de vue magique - mais alors je ne peux pas accéder aux contrôles par programme.

Je pourrais faire tout cela en ignorant la plupart d'ASP.NET et en créant un document XML avec LINQ to XML et en l'implémentant IHttpHandler. Cela semble un peu bas cependant.

Je me rends compte que mes problèmes pourraient être résolus soit en relâchant mes contraintes (par exemple en utilisant POST et en ne se souciant pas du paramètre excédentaire) ou en utilisant ASP.NET MVC, mais mes exigences sont-elles vraiment déraisonnables?

Peut-être qu'ASP.NET ne se réduit tout simplement pas à ce type d'application? Il y a cependant une alternative très probable: je suis juste stupide, et il y a un moyen parfaitement simple de le faire que je n'ai tout simplement pas trouvé.

Des pensées, quelqu'un? (Cue des commentaires sur la manière dont les puissants sont tombés, etc.

Jon Skeet
la source
16
"Cue commentaires sur la façon dont les puissants sont tombés" - nous sommes tous ignorants, juste de choses différentes. Je n'ai commencé à participer ici que récemment, mais j'admire la question plus que tous les points. De toute évidence, vous pensez et apprenez toujours. Bravo à vous.
duffymo
15
Je ne pense pas que je ferais jamais attention à quelqu'un qui avait abandonné l'apprentissage :)
Jon Skeet
1
Vrai dans le cas général. Très vrai en informatique.
Mehrdad Afshari
3
Et votre prochain livre sera-t-il "ASP.NET en profondeur"? :-P
chakrit
20
Oui, il doit sortir en 2025;)
Jon Skeet

Réponses:

76

Cette solution vous donnera un accès par programme aux contrôles dans leur intégralité, y compris tous les attributs des contrôles. De plus, seules les valeurs de la zone de texte apparaîtront dans l'URL lors de la soumission, de sorte que l'URL de votre demande GET sera plus «significative»

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

Ensuite, dans votre code-behind, vous pouvez faire tout ce dont vous avez besoin sur 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];
    }
}

Si vous ne voulez pas d'un formulaire qui a runat="server", vous devez utiliser des contrôles HTML. Il est plus facile de travailler avec vos besoins. Utilisez simplement des balises HTML ordinaires et mettez runat="server"-leur un identifiant. Ensuite, vous pouvez y accéder par programme et coder sans fichier ViewState.

Le seul inconvénient est que vous n'aurez pas accès à la plupart des contrôles serveur ASP.NET «utiles» comme GridViews. J'ai inclus un Repeaterdans mon exemple car je suppose que vous voulez que les champs soient sur la même page que les résultats et (à ma connaissance) a Repeaterest le seul contrôle DataBound qui s'exécutera sans runat="server"attribut dans la balise Form.

Dan Herbert
la source
1
J'ai si peu de champs que le faire manuellement est vraiment facile :) La clé était que je ne savais pas que je pourrais utiliser runat = server avec des contrôles HTML normaux. Je n'ai pas encore mis en œuvre les résultats, mais c'est la partie la plus facile. Presque là!
Jon Skeet
En effet, un <form runat = "server"> ajouterait le champ caché __VIEWSTATE (et un autre) même si vous définissez EnableViewState = "False" au niveau de la page. C'est la voie à suivre si vous souhaitez perdre le ViewState sur la page. En ce qui concerne la convivialité des URL, l'écriture d'url peut être une option.
Sergiu Damian
1
Pas besoin de réécriture. Cette réponse fonctionne très bien (même si cela signifie avoir un contrôle avec un ID de "utilisateur" - pour une raison quelconque, je ne peux pas changer le nom d'un contrôle de zone de texte séparément de son ID).
Jon Skeet
1
Juste pour confirmer, cela a très bien fonctionné. Merci beaucoup!
Jon Skeet
14
On dirait que vous auriez dû l'écrire en asp classique!
ScottE
12

Vous êtes définitivement (à mon humble avis) sur la bonne voie en n'utilisant pas runat = "server" dans votre balise FORM. Cela signifie simplement que vous devrez extraire directement les valeurs de Request.QueryString, comme dans cet exemple:

Dans la page .aspx elle-même:

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

et dans le 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));
        }
    }

L'astuce ici est que nous utilisons des littéraux ASP.NET dans les attributs value = "" des entrées de texte, de sorte que les zones de texte elles-mêmes n'ont pas à runat = "server". Les résultats sont ensuite encapsulés dans un ASP: Panel et la propriété Visible est définie lors du chargement de la page selon que vous souhaitez afficher ou non des résultats.

Dylan Beattie
la source
Cela fonctionne plutôt bien, mais les URL ne seront pas aussi conviviales que, par exemple, StackOverflow.
Mehrdad Afshari
1
Les URL seront plutôt conviviales, je pense ... Cela semble être une très bonne solution.
Jon Skeet
Argh, j'ai lu vos tweets plus tôt, je l'avais recherché, et maintenant j'ai manqué votre question sur la préparation de mes enfants ittle pour la baignoire ... :-)
splattne
2

D'accord Jon, le problème de viewstate d'abord:

Je n'ai pas vérifié s'il y avait un quelconque changement de code interne depuis la version 2.0, mais voici comment j'ai géré la suppression de l'état de vue il y a quelques années. En fait, ce champ caché est codé en dur dans HtmlForm, vous devez donc en dériver le nouveau et entrer dans son rendu en effectuant les appels vous-même. Notez que vous pouvez également laisser __eventtarget et __eventtarget si vous vous en tenez aux anciens contrôles d'entrée (ce que je suppose que vous voudriez car cela aide également à ne pas avoir besoin de JS sur le 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);
}

Donc, vous obtenez ces 3 MethodInfo statiques et les appelez en sautant cette partie viewstate;)

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

et voici le constructeur de type de votre formulaire:

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

Si je comprends bien votre question, vous ne souhaitez pas non plus utiliser POST comme action de vos formulaires, alors voici comment procéder:

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

    // the rest of it...
}

Je suppose que c'est à peu près tout. Faites-moi savoir comment ça se passe.

EDIT: j'ai oublié les méthodes d'affichage de la page:

Ainsi, votre formulaire personnalisé: HtmlForm obtient son tout nouveau résumé (ou non) Page: 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;
}

Dans ce cas, je scelle les méthodes car vous ne pouvez pas sceller la page (même si elle n'est pas abstraite, Scott Guthrie l'enveloppera dans une autre: P) mais vous pouvez sceller votre formulaire.

user134706
la source
Merci pour cela - même si cela ressemble à beaucoup de travail. La solution de Dan a bien fonctionné pour moi, mais il est toujours bon d'avoir plus d'options.
Jon Skeet
1

Avez-vous pensé à ne pas éliminer le POST mais plutôt à le rediriger vers une URL GET appropriée lorsque le formulaire est POSTÉ. Autrement dit, acceptez à la fois GET et POST, mais sur POST, construisez une requête GET et redirigez-la vers elle. Cela pourrait être géré soit sur la page, soit via un HttpModule si vous vouliez le rendre indépendant de la page. Je pense que cela rendrait les choses beaucoup plus faciles.

EDIT: Je suppose que vous avez EnableViewState = "false" défini sur la page.

Tvanfosson
la source
Bonne idée. Eh bien, une idée horrible en termes d'être obligé de le faire, mais agréable en termes de fonctionnement probablement :) J'essaierai ...
Jon Skeet
Et oui, j'ai essayé EnableViewState = false partout. Il ne le désactive pas complètement, il le coupe simplement.
Jon Skeet
Jon: Si vous n'utilisez pas les foutus contrôles de serveur (pas de runat = "server") et que vous n'avez pas du tout de <form runat = "server">, ViewState ne sera pas un problème. C'est pourquoi j'ai dit de ne pas utiliser les contrôles serveur. Vous pouvez toujours utiliser la collection Request.Form.
Mehrdad Afshari
Mais sans runat = server sur les contrôles, il est difficile de propager à nouveau la valeur aux contrôles lors du rendu. Heureusement, les contrôles HTML avec runat = server fonctionnent bien.
Jon Skeet
1

Je créerais un module HTTP qui gère le routage (similaire à MVC mais pas sophistiqué, juste quelques ifdéclarations) et le remettrais à aspxou ashxpages. aspxest préférable car il est plus facile de modifier le modèle de page. Je n'utiliserais pas WebControlsdans le aspxcependant. Juste Response.Write.

Au fait, pour simplifier les choses, vous pouvez faire la validation des paramètres dans le module (car il partage probablement le code avec le routage probablement) et l'enregistrer HttpContext.Itemspuis les rendre dans la page. Cela fonctionnera à peu près comme le MVC sans toute la cloche et les sifflets. C'est ce que j'ai fait beaucoup avant les jours d'ASP.NET MVC.

Mehrdad Afshari
la source
1

J'ai vraiment été heureux d'abandonner totalement la classe de page et de gérer chaque requête avec un gros cas de commutation basé sur l'URL. Chaque "page" devient un modèle html et un objet ac #. La classe de modèle utilise une expression régulière avec un délégué de correspondance qui se compare à une collection de clés.

avantages:

  1. C'est vraiment rapide, même après une recompilation, il n'y a presque pas de décalage (la classe de page doit être grande)
  2. le contrôle est vraiment granulaire (idéal pour le référencement et la conception du DOM pour bien jouer avec JS)
  3. la présentation est séparée de la logique
  4. jQuery a le contrôle total du html

déceptions:

  1. les trucs simples prennent un peu plus de temps dans la mesure où une seule zone de texte nécessite du code à plusieurs endroits, mais cela évolue très bien
  2. il est toujours tentant de le faire avec la vue page jusqu'à ce que je vois un état de vue (urgh) puis je reviens à la réalité.

Jon, que faisons-nous sur SO un samedi matin :)?

missaghi
la source
1
C'est samedi soir ici. Est-ce que ça va bien? (J'aimerais voir un nuage de points de mes heures / jours de publication, btw ...)
Jon Skeet
1

Je pensais que le contrôle asp: Repeater était obsolète.

Le moteur de modèle ASP.NET est sympa mais vous pouvez tout aussi facilement répéter avec une boucle 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 est en quelque sorte correct, il y a un support décent de Visual Studio mais ce truc runat = "serveur", c'est tout simplement faux. ViewState à.

Je vous suggère de jeter un coup d'œil à ce qui rend ASP.NET MVC si génial, à qui il s'éloigne de l'approche ASP.NET Forms sans tout jeter.

Vous pouvez même écrire vos propres éléments de fournisseur de build pour compiler des vues personnalisées comme NHaml. Je pense que vous devriez chercher ici plus de contrôle et vous fier simplement au runtime ASP.NET pour envelopper HTTP et en tant qu'environnement d'hébergement CLR. Si vous exécutez le mode intégré, vous pourrez également manipuler la requête / réponse HTTP.

John Leidegren
la source