Tag Archives: Master Pages

ASP.NET Device Filters

Nedávno se mi připomněla jedna starší lahůdka (čti obskurnost, za kterou může ten zpropadený svět heterogenních browserů) – Device Filters. Dovolím si tedy připomenout:

Jde o víceméně elegantní deklaratorní zápis, jak nastavovat různé hodnoty jednotlivých properties controlů a direktiv v ASP.NET v závislosti na browseru. Syntaxe je jednoduchá:

<asp:Label IE:Text="Používáte Internet Explorer" Mozilla:Text="Používáte Firefox" PIE:Text="Používáte Pocket IE" Text="Používáte Buchvíco" runat="server" />

Není to jen o properties controlů, dá se to použít i pro direktivy, takže například pro <%@ Page %> lze nastavit MasterPageFile či Theme. Rozpoznávané browsery se řídí browser-definition-files (App_Browsers, browserCaps).

form defaultfocus=“..“ defaultbutton=“…“ runat=“server“ při použití MasterPage

Občas jsme donuceni umístit control <form runat=“server“> už do MasterPage, například proto, že máme v MasterPage navigační TreeView či jiný control vyžadující <form>. Pak nastává otázka, jak ve vlastní content-page nastavit formuláři vlastnosti DefaultButton a DefaultFocus na tam umístěné controly.

<%@ Master ... %>
...
<body>
    <form id="MainForm" runat="server">
        ...
        <asp:ContentPlaceHolder ID="BodyCPH" runat="server" />
        ...
    </form>
</body>
...

Content-page s controly:

<%@ Page MasterPageFile="..." ... %>
<asp:Content ContentPlaceHolderID="BodyCPH" runat="server">
    <asp:TextBox ID="UsernameTB" runat="server" />
    <asp:LinkButton ID="LoginLB" runat="server" />
</asp:Content>

Mnohé správně napadne, že Page má property Form, přes kterou to jistě půjde nastavit. Ano, nicméně properties DefaultFocus a DefaultButton jsou typu string a pokud zkusíme například

Page.Form.DefaultButton = "LoginLB";  // špatně !!!
Page.Form.DefaultButton = LoginLB.ID;  // špatně !!!

…pak budeme odměněni výjimkou InvalidOperationException: The DefaultButton of ‚MainForm‘ must be the ID of a control of type IButtonControl.

Správně je v případě DefaultButton potřeba předat UniqueID a v případě DefaultFocus ClientIDdaného controlu, protože je potřeba ho identifikovat včetně NamingContaineru (form a button jsou každý v jiném naming containeru)

Page.Form.DefaultButton = LoginLB.UniqueID;
Page.Form.DefaultFocus = UsernameTB.ClientID;

Stejným způsobem bychom postupovali nejenom v případě MasterPage, ale například i v případě, kdy bychom chtěli vlastnosti DefaultButton či DefaultFocus ovlivnit z nějakého controlu (což bych spíše nedoporučoval).

BUG: MasterPage zobrazuje default hodnotu ContentPlaceHolderu a ignoruje Content

Narazil jsem na zajímavý bug v ASP.NET 2.0 (2.0.50727)…

Pokud v ContentPlaceHolderu definujeme default obsah a použijeme v něm ebedded code block (starý vnořený ASP-style blok kódu <% %>, ale i <%= %>), pak ASP.NET ignoruje Content definovaný v konkrétních stránkách a stále zobrazuje pouze default obsah z MasterPage.

...
<asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
   <% %>   <-- už tohle vadí
   <%= "Tohle taky vadí" %>
   <%= ResolveUrl("~/takhle-na-to-asi-narazime/") %>
</asp:ContentPlaceHolder>
...

Zajímalo mě, kde je pravděpodobná příčina problému, takže jsem se díval, co z toho ASP.NET stvoří a jak se to liší od funkční podoby. Zásadní rozdíl je už v metodě __BuildControl, kde narozdíl od korektní podoby (ContentPlaceHolder1 s jedním Labelem):

private ContentPlaceHolder __BuildControlContentPlaceHolder1()
{
      ContentPlaceHolder holder1 = new ContentPlaceHolder();
      this.ContentPlaceHolder1 = holder1;
      holder1.ID = "ContentPlaceHolder1";
      if (base.ContentTemplates != null)
      {
            this.__Template_ContentPlaceHolder1 = (ITemplate) base.ContentTemplates["ContentPlaceHolder1"];
      }
      if (this.__Template_ContentPlaceHolder1 != null)
      {
            this.__Template_ContentPlaceHolder1.InstantiateIn(holder1);
            return holder1;
      }
      IParserAccessor accessor1 = holder1;
      accessor1.AddParsedSubObject(new LiteralControl("\r\n\t\t\t"));
      Label label1 = this.__BuildControlTest();
      accessor1.AddParsedSubObject(label1);
      accessor1.AddParsedSubObject(new LiteralControl("\r\n        "));
      return holder1;
}

…chybí řádek return holder1;

private ContentPlaceHolder __BuildControlContentPlaceHolder1()
{
      ContentPlaceHolder holder1 = new ContentPlaceHolder();
      this.ContentPlaceHolder1 = holder1;
      holder1.ID = "ContentPlaceHolder1";
      if (base.ContentTemplates != null)
      {
            this.__Template_ContentPlaceHolder1 = (ITemplate) base.ContentTemplates["ContentPlaceHolder1"];
      }
      if (this.__Template_ContentPlaceHolder1 != null)
      {
            this.__Template_ContentPlaceHolder1.InstantiateIn(holder1);
            // return holder1;  <-- pravděpodobně chybí
      }
      holder1.SetRenderMethodDelegate(new RenderMethod(this.__RenderContentPlaceHolder1));
      return holder1;
}
 
private void __RenderContentPlaceHolder1(HtmlTextWriter __w, Control parameterContainer)
{
      __w.Write("\r\n\t\t\t");
}

Jinak bug je již reportován v Microsoft Connect (Feedback center), můžete se připojit k hlasování…

Dynamické přepínání MasterPage

Page má property Page.MasterPageFile.
Hodnotu lze přiřadit výhradně ve fázi Page_PreInit, jinak se vyvolá výjimka.
Hodnotou je cesta k .master souboru.

void Page_PreInit(object sender, EventArgs e)
{
    MasterPageFile = "~/Layout1.master";
}

Implementace dynamických změn není úplně primitivní, protože ve fází PreInit nemáme ještě PostBackData, ani neproběhly žádné RaisedEventy.
V podstatě musíme použít nějaké „nezávislé“ uložiště pro použitý Master file (např. Profile nebo Session) a řešit kolizi, že MasterPageFile je potřeba nastavit u content stránky, kdežto přepínač layoutů může být už v samotném master file.
Jde to udělat nějak takto:

Content.aspx:

void Page_PreInit(object sender, EventArgs e)
{
    MasterPageFile = Profile.MasterPageFile; // Session["MasterPageFile"];
}

Layout.master:

void MasterDDL_Changed(object sender, EventArgs e)
{
   Profile.MasterPageFile = MasterDDL.SelectedValue;
   Response.Redirect(Request.Path); // !!!! nebo ReturnUrl
}

Response.Redirect je zde potřeba, protože ke změně došlo až po fázi Page_PreInit a my potřebujeme znovunačtením stránky projít přes Page_PreInit. MasterPage si můžeme ukládat i do cookie, každopádně mechanizmus přepínání je podobný jako u lokalizace.

Pokud bychom změnu chtěli udělat jediným roundtripem, museli bychom v PreInit sami parsovat data z Forms nebo QueryStringu. Nejjednodušší je přepínání nasměrovat na samostatný soubor s ReturnUrl, jako se to dělá u lokalizace nebo loginu.