¿Cómo puedo tener más control en ASP.NET?

124

Estoy tratando de construir una "micro-aplicación web" muy, muy simple, que sospecho que será de interés para algunos desbordadores de pila si alguna vez lo consigo. Lo alojo en mi sitio C # en profundidad, que es ASP.NET 3.5 de vainilla (es decir, no MVC).

El flujo es muy simple:

  • Si un usuario ingresa a la aplicación con una URL que no especifica todos los parámetros (o si alguno de ellos no es válido), solo quiero mostrar los controles de entrada del usuario. (Solo hay dos)
  • Si un usuario introduce la aplicación con una URL que hace que todos los parámetros necesarios, quiero mostrar los resultados y los controles de entrada (para que puedan cambiar los parámetros)

Aquí están mis requisitos autoimpuestos (mezcla de diseño e implementación):

  • Quiero que el envío use GET en lugar de POST, principalmente para que los usuarios puedan marcar la página fácilmente.
  • Yo no quiero la URL para terminar parecer tonto después de la presentación, con trozos y piezas extrañas en él. Solo la URL principal y los parámetros reales, por favor.
  • Idealmente, me gustaría evitar requerir JavaScript en absoluto. No hay una buena razón para ello en esta aplicación.
  • Quiero poder acceder a los controles durante el tiempo de renderizado y establecer valores, etc. En particular, quiero poder establecer los valores predeterminados de los controles a los valores de parámetros pasados, si ASP.NET no puede hacer esto automáticamente para mí (dentro de las otras restricciones).
  • Estoy feliz de hacer toda la validación de parámetros yo mismo, y no necesito mucho en cuanto a los eventos del lado del servidor. Es realmente simple configurar todo en la carga de la página en lugar de adjuntar eventos a botones, etc.

La mayor parte de esto está bien, pero no he encontrado ninguna forma de eliminar completamente el estado de vista y mantener el resto de la funcionalidad útil. Utilizando la publicación de esta publicación de blog , he logrado evitar obtener un valor real para el estado de vista, pero aún así termina como un parámetro en la URL, que se ve realmente feo.

Si lo convierto en un formulario HTML simple en lugar de un formulario ASP.NET (es decir, sacarlo runat="server"), entonces no obtengo ningún estado de vista mágico, pero no puedo acceder a los controles mediante programación.

Yo podría hacer todo esto haciendo caso omiso de la mayoría de ASP.NET y la creación de un documento XML con LINQ to XML, y la implementación IHttpHandler. Sin embargo, eso se siente un poco bajo.

Me doy cuenta de que mis problemas podrían resolverse relajando mis restricciones (por ejemplo, utilizando POST y sin importar el parámetro excedente) o utilizando ASP.NET MVC, pero ¿son realmente irrazonables mis requisitos?

¿Quizás ASP.NET simplemente no se reduce a este tipo de aplicación? Sin embargo, hay una alternativa muy probable: solo estoy siendo estúpido, y hay una manera perfectamente simple de hacerlo que simplemente no he encontrado.

¿Alguna idea, alguien? (Señales de cómo cayeron los poderosos, etc. Está bien, espero nunca haber afirmado ser un experto en ASP.NET, ya que la verdad es todo lo contrario ...)

Jon Skeet
fuente
16
"Comentarios sobre cómo cayeron los poderosos": todos somos ignorantes, solo de cosas diferentes. Hace poco comencé a participar aquí, pero admiro la pregunta más que todos los puntos. Obviamente todavía estás pensando y aprendiendo. Felicitaciones a usted.
duffymo
15
No creo que le preste atención a alguien que ha dejado de aprender :)
Jon Skeet
1
Es cierto en el caso general. Muy cierto en informática.
Mehrdad Afshari
3
¿Y su próximo libro será "ASP.NET en profundidad"? :-P
chakrit
20
Sí, saldrá en 2025;)
Jon Skeet

Respuestas:

76

Esta solución le dará acceso programático a los controles en su totalidad, incluidos todos los atributos de los controles. Además, solo los valores del cuadro de texto aparecerán en la URL al enviarla, por lo que su URL de solicitud GET será más "significativa"

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

Luego, en su código subyacente, puede hacer todo lo que necesita en 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 no desea un formulario que tenga runat="server", entonces debe usar controles HTML. Es más fácil trabajar con usted para sus propósitos. Simplemente use etiquetas HTML normales runat="server"y póngales una identificación. Luego puede acceder a ellos mediante programación y codificar sin a ViewState.

El único inconveniente es que no tendrá acceso a muchos de los controles de servidor ASP.NET "útiles" como GridViews. Incluí un Repeateren mi ejemplo porque supongo que desea tener los campos en la misma página que los resultados y (que yo sepa) a Repeateres el único control DataBound que se ejecutará sin un runat="server"atributo en la etiqueta Form.

Dan Herbert
fuente
1
Tengo tan pocos campos que hacerlo manualmente es realmente fácil :) La clave era que no sabía que podía usar runat = server con controles HTML normales. Todavía no he implementado los resultados, pero esa es la parte fácil. ¡Cerca de allí!
Jon Skeet el
De hecho, un <form runat = "server"> agregaría el campo oculto __VIEWSTATE (y algún otro) incluso cuando configura EnableViewState = "False" a nivel de página. Este es el camino a seguir si desea perder el ViewState en la página. En cuanto a la simpatía de Url, la escritura de url podría ser una opción.
Sergiu Damian
1
No hay necesidad de reescribir. Esta respuesta funciona bien (aunque significa tener un control con una ID de "usuario"; por alguna razón no puedo cambiar el nombre de un control de cuadro de texto por separado de su ID).
Jon Skeet el
1
Solo para confirmar, esto funcionó muy bien. ¡Muchas gracias!
Jon Skeet
14
¡Parece que deberías haberlo escrito en asp clásico!
ScottE
12

Definitivamente (en mi humilde opinión) está en el camino correcto al no usar runat = "server" en su etiqueta FORM. Sin embargo, esto solo significa que deberá extraer valores de Request.QueryString directamente, como en este ejemplo:

En la propia página .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>

y en el código subyacente:

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

El truco aquí es que estamos usando ASP.NET Literals dentro de los atributos value = "" de las entradas de texto, por lo que los cuadros de texto en sí no tienen que ejecutar runat = "server". Los resultados se envuelven dentro de un ASP: Panel, y la propiedad Visible establecida en la carga de la página depende de si desea mostrar los resultados o no.

Dylan Beattie
fuente
Funciona bastante bien, pero las URL no serán tan amigables como, por ejemplo, StackOverflow.
Mehrdad Afshari
1
Las URL serán bastante amigables, creo ... Esto parece una muy buena solución.
Jon Skeet el
Argh, leo sus tweets antes, había investigado, y ahora se perdió su pregunta preparar a mis hijos poco caras para la bañera ... :-)
splattne
2

De acuerdo, Jon, el problema del estado de vista primero:

No he comprobado si hay algún tipo de cambio de código interno desde 2.0, pero así es como manejé deshacerme del estado de vista hace unos años. En realidad, ese campo oculto está codificado dentro de HtmlForm, por lo que debe derivar el nuevo y pasar a su representación haciendo las llamadas usted mismo. Tenga en cuenta que también puede dejar __eventtarget y __eventtarget fuera si se apega a los controles de entrada antiguos simples (lo que supongo que le gustaría, ya que también ayuda a no requerir JS en el cliente):

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

Entonces obtienes esos 3 MethodInfo estáticos y los llamas omitiendo esa parte de viewstate;)

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

y aquí está el constructor de tipos de tu formulario:

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 respondo bien a su pregunta, tampoco desea utilizar POST como acción de sus formularios, así que así es como lo haría:

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

    // the rest of it...
}

Supongo que esto es todo. Déjame saber como va.

EDITAR: Olvidé los métodos de visualización del estado de la página:

Entonces su Formulario personalizado: HtmlForm obtiene su nuevo resumen (o no) Página: 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;
}

En este caso, sello los métodos porque no puedes sellar la Página (incluso si no es abstracta, Scott Guthrie lo envolverá en otro: P) pero puedes sellar tu Formulario.

usuario134706
fuente
Gracias por esto, aunque parece mucho trabajo. La solución de Dan funcionó bien para mí, pero siempre es bueno tener más opciones.
Jon Skeet
1

¿Ha pensado no eliminar la POST sino más bien redirigir a una URL GET adecuada cuando se envía el formulario? Es decir, acepte GET y POST, pero en POST construya una solicitud GET y redirija a ella. Esto podría manejarse en la página o mediante un HttpModule si desea que sea independiente de la página. Creo que esto facilitaría mucho las cosas.

EDITAR: supongo que tiene EnableViewState = "false" establecido en la página.

tvanfosson
fuente
Buena idea. Bueno, una idea horrible en términos de ser forzado a hacerlo, pero agradable en términos de que probablemente funcione :) Trataré ...
Jon Skeet
Y sí, he intentado EnableViewState = false por todas partes. No lo deshabilita por completo, solo lo corta.
Jon Skeet
Jon: Si no usa los controles del servidor (no runat = "server") y no tiene un <form runat = "server">, ViewState no será un problema. Por eso dije que no usara los controles del servidor. Siempre puedes usar la colección Request.Form.
Mehrdad Afshari
Pero sin runat = server en los controles, es difícil propagar el valor a los controles nuevamente al renderizar. Afortunadamente, los controles HTML con runat = server funcionan bien.
Jon Skeet el
1

Crearía un módulo HTTP que maneje el enrutamiento (similar a MVC pero no sofisticado, solo un par de ifdeclaraciones) y lo entregaría a aspxo ashxpáginas. aspxes preferible ya que es más fácil modificar la plantilla de página. Sin embargo, no lo usaría WebControlsen el aspx. Justo Response.Write.

Por cierto, para simplificar las cosas, puede hacer la validación de parámetros en el módulo (ya que probablemente comparte código con enrutamiento) y guardarlo HttpContext.Itemsy luego renderizarlos en la página. Esto funcionará más o menos como el MVC sin todas las campanas y silbatos. Esto es lo que hice mucho antes de los días ASP.NET MVC.

Mehrdad Afshari
fuente
1

Realmente me complace abandonar por completo la clase de página y solo manejar cada solicitud con un gran caso de cambio basado en la URL. Cada "página" se convierte en una plantilla html y un objeto ac #. La clase de plantilla utiliza una expresión regular con un delegado de coincidencia que se compara con una colección de claves.

beneficios:

  1. Es realmente rápido, incluso después de una recompilación, casi no hay retraso (la clase de página debe ser grande)
  2. el control es realmente granular (ideal para SEO y para crear DOM para jugar bien con JS)
  3. la presentación es independiente de la lógica
  4. jQuery tiene control total del html

bummers:

  1. las cosas simples tardan un poco más en que un solo cuadro de texto requiere código en varios lugares, pero se amplía muy bien
  2. Siempre es tentador hacerlo con la vista de página hasta que vea un estado de vista (urgh) y luego vuelva a la realidad.

Jon, ¿qué estamos haciendo en SO un sábado por la mañana :)?

missaghi
fuente
1
Es sábado por la tarde aquí. ¿Eso lo hace bien? (Me encantaría ver un gráfico de dispersión de mis tiempos / días de publicación, por cierto ...)
Jon Skeet
1

Pensé que el asp: el control del repetidor era obsoleto.

El motor de plantillas ASP.NET es agradable, pero puede realizar fácilmente la repetición con un bucle 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á bastante bien, hay un soporte decente de Visual Studio pero esto de runat = "server", eso está mal. ViewState to.

Le sugiero que eche un vistazo a lo que hace que ASP.NET MVC sea tan bueno, a quién se aleja del enfoque de formularios ASP.NET sin tirarlo por completo.

Incluso puede escribir su propio material de proveedor de compilación para compilar vistas personalizadas como NHaml. Creo que debería buscar aquí más control y simplemente confiar en el tiempo de ejecución ASP.NET para envolver HTTP y como un entorno de alojamiento CLR. Si ejecuta el modo integrado, también podrá manipular la solicitud / respuesta HTTP.

John Leidegren
fuente