Como posso ter mais controle no ASP.NET?

124

Estou tentando criar um "micro-webapp" muito, muito simples, que suspeito que seja do interesse de alguns Stack Overflow'rs, se eu conseguir fazê-lo. Estou hospedando-o no meu site C # in Depth, que é o ASP.NET 3.5 baunilha (ou seja, não o MVC).

O fluxo é muito simples:

  • Se um usuário entrar no aplicativo com um URL que não especifique todos os parâmetros (ou se algum deles for inválido), quero exibir apenas os controles de entrada do usuário. (Existem apenas dois.)
  • Se um usuário entra no aplicativo com um URL que faz ter todos os parâmetros necessários, eu quero mostrar os resultados e os controles de entrada (para que eles possam alterar os parâmetros)

Aqui estão meus requisitos auto-impostos (mistura de design e implementação):

  • Quero que o envio use GET em vez de POST, principalmente para que os usuários possam marcar a página com facilidade.
  • Eu não quero que o URL para acabar parecendo bobo após a apresentação, com pedaços estranhos e pedaços sobre ele. Apenas o URL principal e os parâmetros reais, por favor.
  • Idealmente, eu gostaria de evitar a exigência de JavaScript. Não há uma boa razão para isso neste aplicativo.
  • Quero poder acessar os controles durante o tempo de renderização e definir valores etc. Em particular, quero poder definir os valores padrão dos controles para os valores de parâmetro passados, se o ASP.NET não puder fazer isso automaticamente para mim (dentro das outras restrições).
  • Fico feliz em fazer toda a validação de parâmetros e não preciso muito de eventos do lado do servidor. É realmente simples definir tudo no carregamento da página em vez de anexar eventos aos botões etc.

A maior parte disso está bem, mas não encontrei nenhuma maneira de remover completamente o viewstate e manter o restante da funcionalidade útil. Usando a postagem deste post do blog, eu consegui evitar obter qualquer valor real para o viewstate - mas ainda acaba sendo um parâmetro no URL, que parece realmente feio.

Se eu criar um formulário HTML simples em vez de um formulário ASP.NET (ou seja, remover runat="server"), não receberei nenhuma viewstate mágica - mas não consigo acessar os controles programaticamente.

Eu poderia fazer tudo isso ignorando a maior parte do ASP.NET e criando um documento XML com LINQ to XML e implementando IHttpHandler. Isso parece um pouco baixo, no entanto.

Percebo que meus problemas podem ser resolvidos relaxando minhas restrições (por exemplo, usando POST e não me importando com o parâmetro excedente) ou usando o ASP.NET MVC, mas meus requisitos são realmente irracionais?

Talvez o ASP.NET não se reduz a esse tipo de aplicativo? Existe uma alternativa muito provável: estou apenas sendo estúpido, e há uma maneira perfeitamente simples de fazer isso que simplesmente não encontrei.

Alguma idéia, alguém? (Sugira comentários sobre como os poderosos caíram, etc. Tudo bem - espero nunca ter afirmado ser um especialista em ASP.NET, pois a verdade é exatamente o oposto ...)

Jon Skeet
fonte
16
"Sugira comentários sobre como os poderosos caíram" - todos somos ignorantes, apenas de coisas diferentes. Só recentemente comecei a participar aqui, mas admiro a pergunta mais do que todos os pontos. Você obviamente ainda está pensando e aprendendo. Parabéns a você.
duffymo
15
Eu não acho que eu nunca prestar atenção a alguém que tinha desistido de aprender :)
Jon Skeet
1
Verdadeiro em caso geral. Muito verdadeiro em ciência da computação.
Mehrdad Afshari
3
E seu próximo livro será "ASP.NET in Depth"? :-P
chakrit
20
Sim, será lançado em 2025;)
Jon Skeet

Respostas:

76

Esta solução fornecerá acesso programático aos controles na íntegra, incluindo todos os atributos nos controles. Além disso, somente os valores da caixa de texto aparecerão no URL após o envio, para que o URL da solicitação GET seja mais "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>

Depois, no code-behind, você pode fazer tudo o que precisa no 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 você não quiser um formulário que tenha runat="server" , use os controles HTML. É mais fácil trabalhar com seus objetivos. Basta usar tags HTML regulares e colocar runat="server"e fornecer um ID. Em seguida, você pode acessá-los programaticamente e codificar sem a ViewState.

A única desvantagem é que você não terá acesso a muitos dos controles "úteis" do servidor ASP.NET como GridViews. Incluí um Repeaterno meu exemplo porque estou assumindo que você deseja ter os campos na mesma página que os resultados e (que eu saiba) a Repeateré o único controle DataBound que será executado sem um runat="server"atributo na marca Form.

Dan Herbert
fonte
1
Eu tenho tão poucos campos que fazê-lo manualmente é realmente fácil :) A chave era que eu não sabia que poderia usar runat = server com controles HTML normais. Ainda não implementei os resultados, mas essa é a parte mais fácil. Quase lá!
11139 Jon Skeet
De fato, um <form runat = "server"> adicionaria o campo oculto __VIEWSTATE (e alguns outros) mesmo quando você define EnableViewState = "False" no nível da página. Este é o caminho a seguir, se você quiser perder o ViewState na página. Quanto à facilidade de uso do URL, a gravação de URL pode ser uma opção.
Sergiu Damian
1
Não há necessidade de reescrever. Essa resposta funciona bem (embora signifique ter um controle com um ID de "usuário" - por algum motivo, não posso alterar o nome de um controle de caixa de texto separadamente de seu ID).
11139 Jon Skeet
1
Só para confirmar, isso funcionou muito bem mesmo. Muito obrigado!
Jon Skeet
14
Parece que você deveria ter escrito no asp clássico!
9119 ScottE
12

Você está definitivamente (IMHO) no caminho certo ao não usar runat = "server" na sua tag FORM. Isso significa que você precisará extrair valores diretamente do Request.QueryString, como neste exemplo:

Na própria 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>

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

O truque aqui é que estamos usando literais do ASP.NET dentro dos atributos value = "" das entradas de texto, para que as próprias caixas de texto não precisem executar o runat = "server". Os resultados são agrupados dentro de um ASP: Panel e a propriedade Visible configurada na página é carregada, dependendo se você deseja exibir resultados ou não.

Dylan Beattie
fonte
Funciona muito bem, mas os URLs não serão tão amigáveis ​​quanto, por exemplo, o StackOverflow.
Mehrdad Afshari
1
Os URLs serão bastante amigáveis, eu acho ... Parece uma solução muito boa.
11139 Jon Skeet
Argh, eu li seus tweets antes, tinha pesquisado isso, e agora eu perdi a sua pergunta preparando meus filhos ittle para a banheira ... :-)
splattne
2

Ok Jon, a questão do viewstate primeiro:

Não verifiquei se há algum tipo de alteração de código interno desde a versão 2.0, mas aqui está como lidei com a eliminação do viewstate há alguns anos. Na verdade, esse campo oculto é codificado no HtmlForm; portanto, você deve derivar o seu novo e entrar na renderização fazendo as chamadas sozinho. Observe que você também pode deixar __eventtarget e __eventtarget de fora se seguir os controles de entrada antigos simples (o que eu acho que você desejaria, pois também ajuda a não exigir JS no 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);
}

Então você pega esses 3 MethodInfo estáticos e os chama de ignorar essa parte do viewstate;)

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

e aqui está o construtor de tipos do seu formulário:

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 estou acertando sua pergunta, você também não deseja usar o POST como a ação dos seus formulários. Veja como você faria isso:

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

    // the rest of it...
}

Eu acho que é isso mesmo. Deixe-me saber como vai.

EDIT: esqueci os métodos Page viewstate:

Portanto, seu Form: HtmlForm personalizado obtém seu novo resumo (ou não): 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;
}

Nesse caso, selo os métodos porque você não pode selar a página (mesmo que não seja abstrata, Scott Guthrie a envolverá em mais uma: P), mas você pode selar seu formulário.

user134706
fonte
Obrigado por isso - embora pareça bastante trabalho. A solução de Dan funcionou bem para mim, mas é sempre bom ter mais opções.
9139 Jon Skeet
1

Você já pensou em não eliminar o POST, mas redirecionar para um URL GET adequado quando o formulário é POST. Ou seja, aceite GET e POST, mas no POST construa uma solicitação GET e redirecione para ela. Isso pode ser tratado na página ou através de um HttpModule, se você quiser torná-lo independente da página. Eu acho que isso tornaria as coisas muito mais fáceis.

EDIT: Suponho que você tenha EnableViewState = "false" definido na página.

tvanfosson
fonte
Boa ideia. Bem, ideia horrível em termos de ser forçado a fazê-lo, mas agradável em termos de ele provavelmente trabalho :) Vou tentar ...
Jon Skeet
E sim, eu tentei EnableViewState = false em todo o lugar. Não o desativa completamente, apenas o reduz.
11139 Jon Skeet
Jon: Se você não usar os malditos controles do servidor (sem runat = "server") e não tiver um <form runat = "server">, o ViewState não será um problema. Por isso eu disse para não usar controles de servidor. Você sempre pode usar a coleção Request.Form.
Mehrdad Afshari
Porém, sem runat = server nos controles, é difícil propagar o valor para os controles novamente durante a renderização. Felizmente, os controles HTML com runat = server funcionam bem.
Jon Skeet
1

Eu criaria um módulo HTTP que lida com roteamento (semelhante ao MVC, mas não sofisticado, apenas algumas ifinstruções) e o entregaria para aspxou ashxpáginas. aspxé preferido, pois é mais fácil modificar o modelo da página. Eu não usaria WebControlsno aspxentanto. Apenas Response.Write.

A propósito, para simplificar as coisas, você pode fazer a validação de parâmetros no módulo (como provavelmente compartilha o código com o roteamento), salvá-lo HttpContext.Itemse renderizá-lo na página. Isso funcionará da mesma forma que o MVC, sem todos os sinos e assobios. Foi isso que fiz muito antes dos dias do ASP.NET MVC.

Mehrdad Afshari
fonte
1

Fiquei realmente feliz em abandonar totalmente a classe de página e manipular todas as solicitações com um grande caso de mudança baseado em URL. Todas as "páginas" se tornam um modelo html e um objeto ac #. A classe de modelo usa um regex com um delegado de correspondência que se compara a uma coleção de chaves.

benefícios:

  1. É muito rápido, mesmo após uma recompilação, quase não há atraso (a classe da página deve ser grande)
  2. o controle é realmente granular (ótimo para SEO e criar o DOM para funcionar bem com JS)
  3. a apresentação é separada da lógica
  4. jQuery tem controle total do html

chatices:

  1. coisas simples demoram um pouco mais, pois uma única caixa de texto exige código em vários lugares, mas aumenta muito bem
  2. é sempre tentador fazê-lo com a visualização de página até que eu veja uma viewstate (urgh) e depois volto à realidade.

Jon, o que estamos fazendo no SO em uma manhã de sábado :)?

missaghi
fonte
1
É sábado à noite aqui. Isso faz tudo bem? (Eu adoraria ver um gráfico de dispersão dos meus momentos de postagem / dias, btw ...)
Jon Skeet
1

Eu pensei que o asp: controle de repetidor era obsoleto.

O mecanismo de modelo do ASP.NET é bom, mas você pode facilmente repetir com um loop 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>

O ASP.NET Forms está bem, existe um suporte decente do Visual Studio, mas essa coisa runat = "server" está errada. ViewState para.

Eu sugiro que você dê uma olhada no que torna o ASP.NET MVC tão bom, quem se afasta da abordagem do ASP.NET Forms sem jogar tudo fora.

Você pode até escrever seu próprio material de provedor de compilação para compilar visualizações personalizadas como o NHaml. Eu acho que você deve procurar aqui mais controle e simplesmente confiar no tempo de execução do ASP.NET para agrupar o HTTP e como um ambiente de hospedagem CLR. Se você executar o modo integrado, poderá manipular a solicitação / resposta HTTP também.

John Leidegren
fonte