Komplexní vlastnosti a jejich stav v ASP.NET

25. března 2004

V předchozích článcích jsem většinou u serverových ovládacích prvků definoval jen jednoduché vlastnosti typu string, int a podobně. Jednoduché vlastnosti jsou nenáročné na napsání a běhové prostředí i RAD designér si s nimi většinou poradí bez naší asistence. Tento článek vás uvede do problematiky komplexních vlastností.

Rozhraní složitějšího ovládacího prvku je při použití pouze jednoduchých vlastností zapleveleno množstvím tématicky neseskupených vlastností, a proto sofistikované ovládací prvky mívají rozhraní s vlastnostmi, jejichž návratovým typem je jiná třída nebo struktura. Zabývat se budu hlavně serializací komplexních vlastností na aspx stránce a ukládáním stavu vlastností do ViewState.

Komplexní vlastnosti bez komplexů a traumat

U jiných ovládacích prvků v ASP.NET jste si pravděpodobně všimli syntaxe při zápisu komplexních vlastností na stránce. Jedná se o takzvanou deklarativní perzistenci, která má dvě ekvivalentní formy. První slouží k uložení komplexní vlastnosti jako atributu ve značce ovládacího prvku pomocí „pomlčkového“ oddělovače. Například DataGrid má vlastnost PagerStyle typu DataGridPagerStyle, u níž můžeme způsob stránkování zadat takto.

<asp:DataGrid id=“dtg1″ runat=“server“ PagerStyle-Mode =“NextPrev“ AllowPaging=“true“>
</asp:DataGrid>

Vlastnost PagerStyle také může být zanořena do značky asp:DataGrid následujícím způsobem.

<asp:DataGrid id=“dtg1″ runat=“server“ AllowPaging=“true“>
  <PagerStyle Mode =“NextPrev“> </PagerStyle>
</asp:DataGrid>

Když vkládáme značky do aspx stránky ručně, záleží jen na nás, jakou syntaxi budeme u komplexních vlastností preferovat, protože parser aspx stránek si poradí s oběma typy zápisu. Když ale hodnoty komplexních zadáme na stránce vlastností ve VS.Net, musí se serializér rozhodnout, jakou syntaxi použije. Namísto házení mincí serializér očekává, že mu tuto informaci poskytne přímo komplexní vlastnost. To ale není vše. Jak serializér pozná, které komplexní vlastnosti má serializovat a které ne? Jak parser aspx stránek rozliší, zda má vnořené značky interpretovat jako komplexní vlastnosti nebo vnořené ovládací prvky? Jedinou odpovědí na všechny otázky jsou atributy.

Samotný ovládací prvek musí být označen atributy [ParseChildren(true)] a [PersistChildren(false)]. Atribut [ParseChildren(true)] je pokynem pro parser aspx stránek, aby se vnořenými značkami v prvku zabýval. Atribut [PersistChildren(false)] upřesňuje, že vnořené značky má parser interpretovat jako vlastnosti a ne ovládací prvky. Na třídu WebControl jsou již oba atributy se zmíněnými hodnotami aplikovány a všechny z ní odvozené ovládací prvky je automaticky dědí.

Komplexní vlastnosti určené k serializaci jsou dekorovány atributem [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]. Stejným atributem s hodnotou DesignerSerializationVisibility.Hidden ( [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] ) naopak označujeme vlastnosti, k jejichž serializaci v žádném případě nesmí dojít. Vlastnost bude nyní serializována jako atribut značky ovládacího prvku. Jestliže chceme vlastnost serializovat do samostatné značky vnořené ve značce ovládacího prvku, tak ji musíme ještě dekorovat atributem [PersistenceMode(PersistenceMode.InnerProperty)].

Komplexní vlastnost určená k serializaci musí být také označena atributem [NotifyParentProperty(true)], aby byly změny v jejich vlastnostech propagovány až k serializéru ovládacího prvku. Ze stejného důvodu je nutné aplikovat atribut [NotifyParentProperty(true)] na vlastnosti třídy, která představuje typ komplexní vlastnosti.S komplexními vlastnostmi jsou těsně svázány konvertory typů (TypeConverter), které dokáží převést textovou reprezentaci vlastnosti na příslušný objekt a naopak. Typovým konvertorům věnuji příští článek, dnes jen využijeme dodávaný ExpandableObjectConverter, aby uživatel ovládacího prvku mohl rozbalit a editovat komplexní vlastnost na stránce vlastností ve VS.NET, jak je zvyklý u jiných prvků.

Ukládání stavu komplexních vlastností http protokolu navzdory

U komplexních vlastností jsme odpovědní za uložení jejich stavu do ViewState. ViewState dovoluje uchovávat stav ovládacích prvků mezi odesláním formuláře (postbacky) a obejít tak omezení bezstavového http protokolu. Jak ViewState přesně funguje se dozvíte například z článku kolegy Radima Hampla. Je striktně doporučeno, aby komplexní vlastnosti byly přístupné jen pro čtení, abyste měli pod kontrolou celý životní cyklus a s ním spojenou správu stavu vlastnosti!

Třídy komplexních vlastností většinou nedědí z třídy Control, která má veřejnou vlastnost ViewState, a proto musíme podporu pro ViewState dopsat sami. Participace třídy na uložení svých stavových informací je dosaženo implementací rozhraní IStateManager.

interface IStateManager
{
  bool IsTrackingViewState{get;}
  void LoadViewState(object state);
  object SaveViewState();
  void TrackViewState();
}

Vlastnost IsTrackingViewState vrací true, když objekt hlídá změny ve vlastnostech, aby je mohl při renderování stránky uložit do ViewState. Metoda TrackViewState je většinou volána po skončení inicializační fáze a zajistí, že objekt hlídá změny atributů. Metoda LoadViewState obnoví stav objektu z argumetu state. Stav objektu ukládá metoda SaveViewState.

Příklad implementace komplexní vlastnosti

Ve vzorovém projektu naleznete serverový ovládací prvek PersonControl s komplexní vlastností PersonInfo, která vrací instanci třídy PersonData. Prvek PersonControl můžete brát jako nadějný zárodek :) prvku pro správu údajů o osobě v jakékoli aplikaci. Zdrojové texty si můžete stáhnout a prostudovat.

Nejdříve se podíváme na třídu PersonData.

[TypeConverter(typeof(ExpandableObjectConverter))]
public class PersonData : IStateManager
{
  private bool m_isTrackingViewState;
  private StateBag m_viewState;
  internal PersonData()
  {
    m_isTrackingViewState = false;
    m_viewState = new StateBag(false);
   }
  [Browsable(false)]
  [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
  public StateBag ViewState
  {
    get
    {
      return m_viewState;
    }
  }
  [Category(„PersonData“)]
  [DefaultValue(„“)]
  [NotifyParentProperty(true)]
  public string FirstName
  {
    get
    {
      string firstName = (string)ViewState[„FirstName“];
      return (firstName == null ? String.Empty : firstName);
    }
    set
    {
      ViewState[„FirstName“] = value;
    }
  }
  [Category(„PersonData“)]
  [DefaultValue(„“)]
  [NotifyParentProperty(true)]
  public string LastName
  {
    get
    {
      string lastName = (string) ViewState[„LastName“];
      return (lastName == null ? String.Empty : lastName);
    }
    set
    {
      ViewState[„LastName“] = value;
    }
  }
  bool IStateManager.IsTrackingViewState
  {
    get
    {
      return m_isTrackingViewState;
    }
  }
  void IStateManager.LoadViewState(object state)
  {
    if (state == null)
      return;
    ((IStateManager) ViewState).LoadViewState(state);
  object IStateManager.SaveViewState()
  {
    return (((IStateManager) ViewState).SaveViewState());
  }
  object IStateManager.SaveViewState()
  {
    return (((IStateManager) ViewState).SaveViewState());
  }
  void IStateManager.TrackViewState()
  {
    m_isTrackingViewState = true;
    ((IStateManager) ViewState).TrackViewState();
  }
}

Třída PersonData implementuje rozhraní IStateManager a je označena atributem [TypeConverter(typeof(ExpandableObjectConverter))], který zajistí rozbalení vlastností na stránce vlastností ve VS.Net. V privátní proměnné m_isTrackingViewState je uložena informace, zda objekt sleduje změny ve svých vlastnostech. Další privátní proměnná m_viewState typu StateBag slouží k ukládání informací do ViewState. StateBag pro každou uloženou položku ukládá příznak, jestli byla změněna poté, co bylo voláním metody TrackViewState zahájeno sledování změn. V konstruktoru inicializujeme proměnnou m_isTrackingViewState hodnotou false, protože změny prozatím nesledujeme. Nové instanci třídy StateBag je do konstruktoru předána hodnota false, aby klíče položek reflektovaly velikost písmen.

Veřejná vlastnost ViewState nemá být vidět na stránce vlastností ani nemá být serializována na aspx stránku, proto je označena atributy [Browsable(false)] a DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden). Naopak vlastnosti FirstName a LastName mají být viditelné na stránce vlastností v kategorii PersonData a serializér je uloží na aspx stránce, protože je o změnách informován, což je zajišteno atributem NotifyParentProperty. Obě vlastnosti ukládají své hodnoty do „našeho“ nového ViewState kontejneru a jejich výchozí hodnotou je prázdný řetězec.

Následuje explicitní implementace rozhraní IStateManager. Vlastnost IsTrackingViewState poskytne klientům třídy informaci, zda jsou sledovány změny ve vlastnostech. Metoda LoadViewState nejdříve zkontroluje, zda argument state se stavovými informacemi je null. Pokud je null, tak žádný ViewState uložen nebyl a metoda vrátí řízení volajícímu objektu. Pokud stav není null, je aktuální stav nahrán do objektu StateBag ve vlastnosti ViewState. Metoda SaveViewState vrátí stav získaný voláním metody SaveViewState objektu StateBag, který všechny změny dat v dané instanci sleduje. Metoda TrackViewState nastaví proměnnou m_isTrackingViewState na true, aby bylo zřejmé, že stav vlastností je nyní sledován a zavolá metodu TrackViewState na objektu StateBag, aby také začal sledovat změny.

[DefaultProperty(„PersonInfo“),
ToolboxData(„<{0}:PersonControl runat=“server“></{0}:PersonControl>“)]
public class PersonControl : WebControl
{
  private PersonData m_personInfo;
  public PersonControl() : base()
  {
    m_personInfo = new PersonData();
  }
  [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
  [PersistenceMode(PersistenceMode.InnerProperty)]
  [NotifyParentProperty(true)]
  public PersonData PersonInfo
  {
    get
    {
      return m_personInfo;
    }
  }
  protected override void RenderContents(HtmlTextWriter writer)
  {
    writer.RenderBeginTag(HtmlTextWriterTag.P);
    writer.WriteLine(String.Format(„Jméno: {0} Příjmení: {1}“, m_personInfo.FirstName, m_personInfo.LastName));
    writer.RenderEndTag();
  }
  protected override void LoadViewState(object savedState)
  {
    object baseState = null;
    object personDataState = null;
    if (savedState != null)
    {
      object[] stateArray = (object[]) savedState;
      baseState = stateArray[0];
      personDataState = stateArray[1];
    }
    base.LoadViewState (baseState);
    ((IStateManager)PersonInfo).LoadViewState(personDataState);
  }
  protected override object SaveViewState()
  {
    object baseState = base.SaveViewState();
    object personDataState = ((IStateManager)PersonInfo).SaveViewState();
    if ((baseState == null) && (personDataState == null))
      return null;
    object[] controlFullState = new object[2] {baseState, personDataState};
    return controlFullState;
  }
  protected override void TrackViewState()
  {
    base.TrackViewState ();
    ((IStateManager)PersonInfo).TrackViewState();
  }
}

Jednoduchý serverový ovládací prvek PersonControl dědí z třídy WebControl. Komplexní vlastnost pouze pro čtení s názvem PersonInfo informuje RAD nástroje, že její obsah má být serializován na aspx stránce ([DesignerSerializationVisibility(DesignerSerializationVisibility.Content)], má být vnořena do značky prvku PersonControl ([PersistenceMode(PersistenceMode.InnerProperty)])) a opět je k serializéru propagována notifikace o její změně ([NotifyParentProperty(true)]). Metoda RenderContents vypíše aktuální hodnoty vlastností FirstName a LastName z třídy PersonData, abyste si ve zkušebním projektu mohli ověřit, že je funkční obnovení a uložení stavu komplexní vlastnosti do ViewState.

Musíme přepsat chráněné metody LoadViewstate a SaveViewState, abychom kromě stavu prvku PersonControl uložili a obnovili stav vlastnosti PersonInfo. Všimněte si, že ViewState bázové třídy a vlastnosti PersonInfo ukládáme do pole o dvou prvcích pouze tehdy, když existuje platný ViewState bázové třídy nebo třídy PersonData. Jestliže někoho z vás zaráží, že z metody, která vrací typ object, vracím pole objektů, tak připomínám, že třída object je bázovou třídou pro všechny třídy v .NET Frameworku včetně třídy Array. Při nahrání ViewState opět zkontrolujeme, jestli byl nějaký stav metodou LoadViewState vydán a když ano, tak po přetypování argumentu state na pole objektů vyzvedneme hodnoty obou prvků pole a uložíme je do proměnných baseState a personDataState, které předáme metodě LoadViewState bázové třídy (baseState) a komplexní vlastnosti PersonInfo (personDataState). Pamatujte, že k volání metody LoadViewState bázové třídy i komplexních vlastností musí dojít vždy, a to i v případě, že předáváte „null“ stav, protože metoda LoadViewState může obsahovat povinný inicializační kód nezávislý na předaném stavu.Metoda TrackViewState zavolá nejdříve implementaci v bázové třídě a poté zahájí sledování změn ve vlastnosti PersonInfo.

Starší komentáře ke článku

Pokud máte zájem o starší komentáře k tomuto článku, naleznete je zde.

Štítky: Články

Mohlo by vás také zajímat

Nejnovější

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *