Ao longo dos próximos tempos quero ver se apresento um conjunto de posts sobre o funcionamento interno de alguns dos componentes da plataforma ASP.NET (claro, se o tempo o permitir
). Hoje consegui arranajr algum tempo de forma a seguir todos os passos associados ao cross-posting. O resultado é este post algo longo…
A nova versão da plataforma permite-nos efectuar a navegação de uma página para outra através da utilização da propriedade PostBackUrl exposta pela classe Button. O que não é do conhecimento geral é que, se quisermos, podemos configurar qualquer controlo para efectuar este tipo de operação. Vamos começar por examinar o que acontece quando adicionamos um botão a uma página e atribuimos uma valor válido à propriedade PostbackUrl:
<html>
<body>
<form id="form1" runat="server">
<div>
<asp:Button runat="server" ID="p" Text="postback" PostBackUrl="~/page2.aspx" />
</div>
</form>
</body>
</html>
Ao visualizarmos esta página no browser, obtemos o seguinte HTML (apresento apenas a parte interessante):
…
<input type="submit" name="p" value="postback" onclick="javascript:WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions("p", "", false, "", "page2.aspx", false, false))" id="p" />
…
<div> <input type="hidden" name="__PREVIOUSPAGE" id="__PREVIOUSPAGE" value="28syFYJQ6nikNW14B7byKC1UfoJMDOegDH-zYvMP8s01" />
<input type="hidden" name="__EVENTVALIDATION" id="__EVENTVALIDATION" value="/wEWAgKttZiiBwLQ76ruDOuou7R1ovb+R3uMdD4Ug/lKi/eD" />
</div></form>
Como veremos, o campo __PREVIOUSPAGE é o responsável pelo correcto funcionamento da página! O método WebForm_DoPostBackWithOptions é injectado pela página e é responsável por 1.) efectuar a validação da página (quando tal é necessário), 2.) Modificar a propriedade action do formulário (que indica o destino da submissão do formulário), 3.) obter o elemento que possui o foco (quando for necessário) e 4.) efectuar a submissão da página. Este último passo é feito à custa do método __doPostBack e limita-se a preencher correctamente os campos __EVENTTARGET e __EVENTARGUMENT que indicam o controlo responsável pela operação de submissão e respectivo valor predefinido. Portanto, o processo de submissão para outra página a partir do lado cliente resume-se a configurar a propriedade action do formulário e a invocar o método submit desse formulário. Se tentarmos adicionar código a uma página que efectua as operações descritas iremos deparar-nos com uma excepção. Vejamos o seguinte código:
<html>
<body>
<script type="text/javascript">
function ChangePage()
{
document.forms[0].action = "page2.aspx";
document.forms[0].submit();
}
</script>
<form id="form1" runat="server">
<div>
<asp:Button runat="server" ID="p" Text="postback" OnClientClick="ChangePage()" />
</div>
</form>
</body>
</html>
O resultado é o seguinte:

O problema é o mesmo da primeira versão: a página destino tenta recuperar o viewstate mantido no controlo HTML __VIEWSTATE como se fosse seu (neste caso, o VIEWSTATE aí contido é da página 1!). Durante a primeira versão, a solução passava pela modificação do nome desse campo antes de efectuarmos o postback de forma a não obtermos este erro. Contudo, se analisarmos o código cliente usado durante a submissão da primeira página, reparamos que o campo __VIEWSTATE não é alterado (basta consultar o método WebForm_DoPostBackWithOptions situado no ficheiro WebForms.js que se encontra embebido na assembly System.Web.dll). Logo, podemos concluir que falta-nos algo para obtermos o mesmo comportamento da página inicial…
Se usarmos o Reflector para analisar o código associado ao ciclo de vida de uma página, reparamos no seguinte (método ProcessRequest da classe Page):
VirtualPath path1 = null;
if (this._requestValueCollection["__PREVIOUSPAGE"] != null)
{
try{
path1 = VirtualPath.CreateNonRelativeAllowNull(Page.DecryptString(this._requestValueCollection["__PREVIOUSPAGE"]));
}
catch (CryptographicException) {
this._pageFlags[8] = true;
}
if ((path1 != null) && (path1 != this.Request.CurrentExecutionFilePathObject)) {
this._pageFlags[8] = true;
this._previousPagePath = path1;
}
}
Esta verificação é feita pela página no inicio do ciclo de vida. Sempre que é detectado o campo __PREVIOUSPAGE, a página instancia o caminho até essa página (note-se que este campo contém o caminho virtual até uma página – normalmente, é indicada a página que iniciou o pedido de cross-posting). Existe um pormenor extremamente importante para o desfecho da operação: sempre que existe um campo __PREVIOUSPAGE, a flag _pageFlags[8] é colocado a true. O carregamento do view state é feito sempre da mesma forma, ou seja, sempre que existe um postback, a página utiliza o mesmo algoritmo para recuperar o seu estado interno. O código do método LoadPageStateFromPersistenceMedium explica porque é que neste caso não obtemos a excepção anterior:
[EditorBrowsable(EditorBrowsableState.Advanced)]
protected internal virtual object LoadPageStateFromPersistenceMedium()
{
PageStatePersister persister1 = this.PageStatePersister;
try
{
persister1.Load();
}
catch (HttpException exception1)
{
if (this._pageFlags[8])
{
return null;
}
exception1.WebEventCode = 0xbba;
throw;
}
return new Pair(persister1.ControlState, persister1.ViewState);
}
Ou seja, apesar de obtermos à mesma a excepção, neste caso o método limita-se apenas a enviar null já que foi encontrado o campo __PREVIOUSPAGE no inicio do ciclo de vida. Ora bem, com estes dados podemos configurar qualquer controlo para efectuar uma operação de cross-posting! Em vez de adicionarmos o campo escondido __PREVIOUSPAGE com o caminho virtual encriptado até à pagina inicial, vamos recorrer ao método GetPostBackEventReference da classe ClientScriptManager já que esta classe encarrega-se de configurar a página para efectuar todas essas operações. Para ilustrarmos estes principios, vamos construir uma página que efectua a navegação para uma página final sempre que o utilizador selecciona um radio button:
<%@ Page Language="C#" %>
<script runat="server">
public string Txt
{
get
{
return txt.Text;
}
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
option.Attributes["onclick"] = this.ClientScript.GetPostBackEventReference(new PostBackOptions(option, "", "page2.aspx", true, true, false, true, false, ""));
}
</script>
<html>
<head runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:TextBox runat="Server" ID="txt" />
<asp:RadioButton runat="server" ID="option" Text="navigate" />
</div>
</form>
</body>
</html>
A classe PostBackOptions encapsula um conjunto de dados que geram o código cliente responsável pelo postback. Neste caso, o terceiro parâmetro é importante já que permite definir o destino do postback. Sempre que atribuimos um valor a esse parâmetro, o método GetPostBackEventReference atribui o valor true à propriedade ContainsCrossPagePost da página. Durante o rendering de HTML, a página consulta esta propriedade de forma a saber se deve ou não registar o campo escondido __PREVIOUSPAGE e respectivo valor (esta operação é feita pelo método interno EndFormRender da classe Page). Para verificarmos se a página anterior funciona, só temos mesmo de construir a página final (page2.aspx):
<%@ Page Language="C#" %>
<%@ PreviousPageType VirtualPath="~/page1.aspx" %>
<script runat="server">
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
info.Text = this.PreviousPage.Txt;
}
</script>
<html>
<body>
<form id="form1" runat="server">
<div>
<asp:Label ID="info" runat="server" Text="Label" />
</div>
</form>
</body>
</html>