Tag Archives: WebForms

AJAX: Explicitní volání webových služeb z klienta

Microsoft ASP.NET AJAX 1.0 je právě ve stádiu Release Candidate (RC), nastává tedy správný čas pro bližší seznámení… ;-)

I když většinu potřebných AJAX features najdeme hotových ve formě server controlů, kdy nejsme nuceni napsat ani jediný řádek JavaScriptu,  není od věci vědět, že i explicitní volání webových služeb je z ASP.NET AJAXu velmi jednoduché a snadno použitelné (obzvláště pokud poznáme, že je to prakticky stejné, jako přístup k webovým službám na straně serveru, resp. z .NET Frameworku).

Zpřístupnění webové služby

Základem každé AJAX-enabled stránky je control ScriptManager, který zajišťuje správu klientského AJAXového JavaScriptu. Zpřístupnění webové služby pak není nic jiného, než vytvoření ServiceReference na naší webovou službu

<asp:ScriptManager ID="ScriptManager" runat="server">
   <Services>
      <asp:ServiceReference Path="~/MyWebService.asmx" />
   </Services>
</asp:ScriptManager>

Princip je to prakticky obdobný, jako v případě webových služeb na straně serveru (resp. v .NET aplikacích). Lze to přirovnat k přidání Web Reference do projektu ve Visual Studiu, nebo k použití utility wsdl.exe k vygenerování proxy třídy pro přístup k webové službě. Jediným zásadním rozdílem je, že lze takto přistupovat pouze na vlastní webové služby – tedy webové služby, které jsou součástí naší aplikace. Pokud bychom potřebovali přístup k externím webovým službám, není však problém vytvořit bridge prostřednictvím vlastní proxy webové služby.

Vraťme se však k našemu příkladu, dejme tomu, že webová služba ~/MyWebService.asmx by definovala jednu metodu

using System;
using System.Data;
using System.Web;
using System.Collections;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.ComponentModel;
using System.Web.Script.Services;

namespace MyNamespace
{
    [WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [ScriptService]
    public class MyWebService : System.Web.Services.WebService
    {

        [WebMethod]
        public string GetServerTime(string serverName)
        {
            return serverName + ": " + DateTime.Now.ToShortTimeString();
        }
    }
}

…jedná se o úplně klasickou webovou službu, která má navíc jen atribut [ScriptService] (jinak lze však tuto webovou službu využít jako kteroukoliv jinou). Atribut ScriptService zajišťuje, že nový handler .asmx souborů, s kterým ASP.NET AJAX přichází, dokáže při požadavku GET MyWebService.asmx/js vrátit vygenrovaný JavaScript s příslušnými proxy objekty pro práci s webovou službou. Ve skutečnosti totiž control ScriptManager přidá do stránky kód

</script src="MyWebService.asmx/js" type="text/javascript">

Pomocí atributu [ScriptMethod] u jednotlivých metod jsme navíc schopni řídit další detaily, např. formát, kterým spolu klientská a serverová část komunikují. Ve výchozí podobě totiž není komunikace na bázi XML, nýbrž v podobě JSON (JavaScript Object Notation), např.:

{"prefixText":"M","count":3}
["Michal", "Mirek", "Milan"]  

Volání metod webové služby

Samotné volání metod webové služby je už hračkou, ASP.NET AJAX nám totiž vygeneruje objekt odpovídající webové službě, v namespace odpovídající webové službě a s metodami dle webové služby. Nejjednodušší volání tak může vypadat např.

<script type="text/javascript">
    MyNamespace.MyWebService.GetServerTime('MyServer');
</script>

Musíme si však uvědomit, že veškeré AJAXové requesty jsou asynchronní, tedy musíme metodě říct, jakou funkci má zavolat po dokončení operace (přijetí odezvy od serveru) – výše uvedená nejjednodušší forma se tak moc často neuplatní. Nemáme-li totiž nastaven defaultní callback, pak nemáme jak převzít výsledek a jedná se prakticky jen o jednosměrnou komunikaci (navíc bez success-checkingu).

Další možností je tedy přidat jako další parametr metody funkci, která se má zavolat v okamžiku přijetí odezvy serveru, a která převezme a zpracuje výsledek. Parametrem funkce je výsledek volání metody přijatý v odezvě serveru.

<script type="text/javascript">
    MyNamespace.MyWebService.GetServerTime('MyServer', OnComplete);

    function OnComplete(result)
    {
        alert(result);
    }
</script>

Další vhodnou možností je přidat jako další parametr volání metody funkci, která se má zavolat v případě neúspěchu, v případě chyby:

<script type="text/javascript">
    MyNamespace.MyWebService.GetServerTime('MyServer', OnComplete, OnError);
    function OnComplete(result, context)
    {
        alert(result);
    }
    function OnError(result)
    {
        alert(result.get_message());
    }
</script>

Jako poslední parametr lze přidat ještě určitý context, prostě obecná data, která se mají předat do metody OnComplete (neposílají se na server):

<script type="text/javascript">
    MyNamespace.MyWebService.GetServerTime('MyServer', OnComplete, OnError, 'context');
    function OnComplete(result, context)
    {
        alert(context);
        alert(result);
    }
    
    function OnError(result)
    {
        alert(result.get_message());
    }
</script>

Webové metody ve stránce

Abychom nemuseli vždy vytvářet úplnou webovou službu, máme též možnost jednoduché lokální metody umístit přímo do třídy samotné webové stránky (do kódu stránky), odekorovat je atributem [WebMethod] a následně k nim můžeme přistupovat prostřednictvím objektu PageMethods, aniž bychom museli jakokouliv webovou službu registrovat. Musíme jen povolit přepínač EnablePageMethods ScriptManageru:

public partial class _Default : System.Web.UI.Page
{
    [System.Web.Services.WebMethod]
    public static string GetHelloWorld()
    {
        return "Hello world!";
    }
}
<script type="text/javascript">
    PageMethods.GetHelloWorld(OnHWComplete);
    function OnHWComplete(result)
    {
        alert(result);
    }
</script>

Webová metoda uvnitř stránky musí být statická!

Shrnutí

Explicitní volání webových služeb z klientské strany je velmi silnou zbraní ASP.NET AJAXu, pomocí které lze realizovat prakticky libovolnou AJAX funkčnost pro kterou nenajdeme vhodný hotový control. Pokud si navíc uvědomíme, že webová služba může pracovat prakticky s libovolnými serializovatelnými datovými typy (včetně DataSetu!), pak tímto dostáváme velmi příjemný nástroj pro klientské skriptování.

Související odkazy

Nested Repeaters – vnořování repeaterů

Vnořit Repeatery se může zdát potíž, dokud poprvé neuvidíte, jak je to jednoduché. Celý fígl totiž spočívá v data-bindingu vnitřních repeaterů v obsluze události ItemDataBound vnějšího Repeateru.

V příkladu vnější Repeater iteruje přes všechny obory činnosti (kategorie, skupiny) a vnitřní Repeater zobrazuje položky (zde „zápisy do katalogu“) příslušející danému oboru činnosti (kategorii, skupině).

MyPage.aspx

<asp:Repeater ID="OboryCinnostiRepeater" runat="server">
  <ItemTemplate>
     
   <%# ((OborCinnosti)Container.DataItem).Nazev %>
   
   <asp:Repeater ID="ZapisyRepeater" runat="server">
    <ItemTemplate>
      <%# ((ZapisDoKatalogu)Container.DataItem).Jmeno %>
     </ItemTemplate>
   </asp:Repeater>
  
  </ItemTemplate>
 </asp:Repeater>

MyPage.aspx.cs

private void OboryCinnostiRepeater_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
   RepeaterItem item = e.Item;
  
   // zajímají nás jen datové řádky, ne hlavička ani patička
   if ((item.ItemType == ListItemType.Item) || (item.ItemType == ListItemType.AlternatingItem))
   {
    // najdeme si vnitřní Repeater
    Repeater zapisyRepeater = (Repeater)item.FindControl("ZapisyRepeater");
  
    // a nabidnujeme mu data příslušející položce (oboru)
    OborCinnosti obor = (OborCinnosti)item.DataItem;
    ZapisDoKataloguCollection = obor.GetZapisy();
    
    zapisyRepeater.DataSource = zapisyOboru;
    zapisyRepeater.DataBind();
   }
}

SessionPageStatePersister: Ukládání ViewState do Session

ViewState se standardně ukládá do formulářového hidden input-fieldu do stránky, posílá se tedy sem a tam na klienta (GET) a zpět na server (POST).

Díky mechanizmu page-adapterů a připraveného SessionPageStatePersister-u lze v ASP.NET velmi snadno přesměrovat ukládání ViewState do Session, tedy na stranu serveru. Primárně je tento mechanizmus určený pro použití se zařízeními (browsery), kde není možné nebo žádoucí view-state pomocí hidden-fieldu přenášet (PDA, mobily, atp.). Nic nám však nebrání rozšířit jeho využití na všechny stránky.

Potřebujeme pouze dvě věci:

1. Připravit page-adapter, který používá SessionPageStatePersister

Vysvětlovat mechanizmus control-adapterů je mimo rozsah tohoto článku – je to prostě něco, co dokáže poměrně dost modifikovat výchozí chování controlů (a stránka je control), například zajistit jiné renderování, nebo právě způsob ukládání ViewState.

Potřebný page-adapter bude vypadat takto:

public class PageAdapter : System.Web.UI.Adapters.PageAdapter
{
   public override PageStatePersister GetStatePersister()
   {
      return new SessionPageStatePersister(this.Page);
   }
}

…nejde o nic jiného, než říci, že se má použít připravený SessionPageStatePersister.

2. Aplikovat page-adapter pomocí .browser souboru

ASP.NET řekneme, že má příslušný page-adapter použít, tak, že modifikujeme/vytvoříme příslušný .browser soubor. Modifikovat lze buď globální nastavení ve složce <windir>\Microsoft.NET\Framework\<ver>\CONFIG\Browsers, nebo pro jednotlivou aplikaci vytvořit .browser soubor do složky ~/App_Browsers.

Soubor ~/App_Browsers/My.browser bude vypadat třeba takto:

<browsers>
   <browser refID="Default">
      <controlAdapters>
         <adapter controlType="System.Web.UI.Page" adapterType="MyNamespace.PageAdapter"/>
      </controlAdapters>
   </browser>
</browsers>

…to je prakticky vše, co musíme udělat pro ukládání ViewState do Session.

Alternativa – overrride Page.PageStatePersister

Pokud máme ve svém projektu zavedenu společnou bázovou třídu všech stránek (což každopádně i jinak doporučuji), pal můžeme místo PageAdapteru můžeme i rovnou overridování property PageStatePersister:

protected override PageStatePersister PageStatePersister
{
    get
    {
        if (_pageStatePersister == null)
        {
            _pageStatePersister = new SessionPageStatePersister(this.Page);
        }
        return _pageStatePersister;
    }
}
private PageStatePersister _pageStatePersister;

…nevýhodou je pevné zakomponování volby Session jako ViewState uložiště do aplikace, na rozdíl od předchozí konfigurační možnosti nad hotovou aplikací.

POZOR!!! Od property PageStatePersister se nedokumentovaně očekává, že ji bude během jednoho requestu možno volat opakovaně a stále bude vracet stejnou instanci! Zatímco metoda PageAdapter.GetStatePersister() je zvnějšku obalena cachováním instance, v property si tento mechanizmus musíme zajistit sami pomocí private fieldu.

Konfigurace SessionPageStatePersisteru

SessionPageStatePersister se dá konfigurovat z web.configu pomocí elementu <sessionPageState />:

<system.web>
  <sessionPageState historySize="9" />
</system.web>

Jediným nastavitelným atributem je historySize, kterým se volí počet ViewState záznamů, které má persister udržovat. Výchozí hodnota je 9.

Úskalí použití SessionPageStatePersisteru

  • ViewState je ukládán jako položky Session + existuje slovník, který udržuje jaké klíče v Session odpovídají jakému požadavku.
  • Výchozí velikost tohoto slovníku je 9 záznamů, lze však změnit pomocí konfigurace.
  • Každý požadavek vytvoří nový záznam, desátý požadavek vytěsní první.
  • Pokud si tedy uživatel otevře více než 9 oken, pak načtení view-state selhává!!! Metoda není tedy ve výchozím nastavení příliš vhodná pro stránky s frames, nebo různá dialogová okna.
  • Ztrátou Session ztratíme i ViewState, pokud tedy máme například InProc session a restartujeme webovou aplikaci, ViewState je pryč.

Východiskem z některých situací je detekce ztráty ViewState.

Co by měl každy vědět o ViewState

1. ViewState není odpovědný za zachování vlastních hodnot controlů mezi postbacky

ViewState není potřeba pro zachování hodnot (Value) TextBoxů, CheckBoxů, DropDownListů a jiných Web controlů mezi postbacky. Tyto hodnoty jsou uloženy standardně ve formulářových postback datech (POST/GET) a ASP.NET je nastaví v metodě LoadPostData() pro všechny prvky, které implementují rozhraní IPostBackDataHandler. Na to, aby nám mezi roundtripy zůstal text v TextBoxu tedy nepotřebujeme ViewState!!!

2. Na co tedy ViewState?

ViewState je vlastnost každého controlu zděděná od System.Web.UI.Control (má ji tedy i Page) a jeho základní funkčnost je založena ná následující implementaci vlastností:

public string NavigateUrl
{
   get
   {
      string text = (string) ViewState["NavigateUrl"];
      if (text != null)
         return text;
      else
         return string.Empty;
   }
   set
   {
      ViewState["NavigateUrl"] = value;
   }
}

Hodnoty vlastností (property) se tedy ukládají do a čtou z ViewState, veškeré jejich změny se promítají do ViewState.

3. ViewState je typu StateBag

ViewState je typu System.Web.UI.StateBag, která implementuje mj. IDictionary (slouží k ukládání párů klíč-hodnota) a interně používá HybridDictionary.
Důležitou metodou StateBagu je SaveViewState(), která odpovídá za uložení ViewState.
Celý fígl je v tom, že metoda SaveViewState() uloží jenom ty vlastnosti, které se změnily po zavolání metody TrackViewState().

4. Co se tedy ukládá při uložení ViewState?

Funkčnost ViewState je úzce spojena s life-cyclem stránky a okamžikem volání metody TrackViewState(). Ta je volána na konci události Init každého controlu, a tedy i stránky (pro zjištění, zda-li již byla volána, lze použít property IsTrackingViewState).
Změny properties provedené před koncem události Init každého controlu se tedy s ViewState neukládají, veškeré další změny až do volání metodySaveViewState() (v události SaveViewState) ano.

5. Jak tedy stránka/control s ViewState funguje?

Vezměme si krátký příklad:

   private void btnSubmit_Click(object sender, EventArgs e)
   {
      lblMessage.Text = "Goodbye, Everyone!";
   }

Co se stane při první návštěvě stránky:

  1. „Instantiation stage“: Nastaví se lblMessage.Text=“Hello, World!“
  2. „Load ViewState stage“: nic se nestane, není postback
  3. „Save ViewState stage“: nic se nestane, nejsou změny ViewState
  4. „Render Stage“: Label je renderován s „Hello, World!“

Co se stane při kliku na Change Message tlačítko:

  1. „Instantiation stage“: Nastaví se lblMessage.Text=“Hello, World!“
  2. „Load ViewState stage“: nic se nestane, ViewState stránky je prázdný
  3. „Raise Postback Event“: btnSubmit_Click nastaví
  4. lbl.Message=“Goodbye, Everyone!“
  5. „Save ViewState stage“: property Text od Labelu je uložena do ViewState, protože se změnila (po volání TrackViewState())
  6. „Render Stage“: Label je renderován s „Goodbye, everyone!“

Co se stane při kliku na Empty postback tlačítko:

  1. „Instantiation stage“: Nastaví se lblMessage.Text=“Hello, World!“
  2. „Load ViewState stage“: nastaví se lblMessage.Text=“Goodbyw, Everyone!“ z ViewState
  3. „Save ViewState stage“: property Text od Labelu je uložena do ViewState, protože se změnila (po volání TrackViewState())
    „Render Stage“: Label je renderován s „Goodbye, everyone!“

Shrnutí & spol.

  1. Do ViewState se ukládají všechny změny v properties controlů provedené po ukončení události Init.
  2. Protože ViewState ukládá pouze vlastnosti controlů a ne controly samotné, musíme dynamicky přidávané controly přidávat při každém postbacku stránky znovu a znovu – nejlépe během události Init (uděláme-li to však i později, metoda .Add() zajistí nahrání ViewState do přidávaných controlů).
  3. U editovatelných DataGridů je vypnutí ViewState docela dřina.
  4. ViewState se ukládá rekurzivně včetně ViewState child-controlů a to v serializované podobě pomocí LOSFormateru.
  5. ViewState lze ukládat i na serveru na disk nebo do databáze pomocí překrytí metod SavePageStateToPersistenceMedium() aLoadViewStateFromPersistenceMedium(), které standardně právě používají hidden-field ___VIEWSTATE. V některém z dalších článků si ukážeme ukládání do Session.
  6. Další možností na zmenšení ViewState je jeho komprese a dekomprese.
  7. ViewState je chráněn před změnami pomocí machine authentication check (MAC), který však pouze kontroluje, je-li ViewState od stejné verze stránky.
  8. ViewState lze i šifrovat, rozhodně se však nedoporučuje pro ukládání citlivých informací.

Vlastní primitivní Repeater – templated data-bound control

Primitivní Repeater

Začněme velmi jednoduchým příkladem, primitivní napodobeninou Repeateru, na níž si ukážeme základy vytváření složených datově-vázaných controlů (composite data-bound controls) a použití šablon v controlech (templates).

Dejme tomu chceme vytvořit control, který bude podle šablon vypisovat seznam zákazníků. Oproti standardnímu Repeateru z ASP.NET však bude pracovat se strong-typed prvky a přidáme si jednoduchou podporu pro načítání šablon ze souborů. Spokojíme se prozatím bez podpory ViewState (data bude tedy nutno bindovat v každém roundtripu) i bez eventů (IremCreated, ItemDataBound, Command, …).

<havit:MyRepeater ID="cosi" runat="server">
      <ItemTemplate>
         Jméno: <%# Container.Zakaznik.Jmeno %><br/>
         Datum narození: <%# Container.Zakaznik.DatumNarozeni.ToShortDateString() %></br>
      </ItemTemplate>
</havit:MyRepeater>

Základy práce se šablonami (templates)

Princip šablon je velmi jednoduchý. Když si uvědomíme, že controly tvoří hiearchii (kořenem je Page a podřízené controly jsou vždy v kolekci Controls), pak použití šablony není nic jiného než připojení nové větve controlů do této hiearchie.

V controlu se šablona samotná vytvoří pomocí property typu ITemplate.

[PersistenceMode(PersistenceMode.InnerPropety)]
[TemplateContainer(typeof(MyRepeaterItem))]
public ITemplate ItemTemplate
{
      get { return _itemTemplate; }
      set { _itemTemplate = value; }
}

Atribut PersistenceMode nastavený na InnerPropety nám říká, že ItemTemplate bude ve zdrojovém kódu controlu zapsán jako vnořený tag. Atribut TemplateContainerzase parseru říká, jakého typu je objekt Container, který budeme používat uvnitř template.

Pro začátek je to vše, co potřebujeme pro definici šablony, parser se sám postará o naplnění property ItemTemplate, pokud ji v .aspx stránce použijeme.

Rozhraní ITemplate definuje jedinou metodu InstantiateIn(Control container), která nedělá nic jednoduššího, než že připojí control-hiearchy tvořené šablonou jako větev do containeru (toho containeru, jehož typ jsme udali v atributu TemplateContainer).

Nutno zdůraznit, že container, do kterého template připojujeme, musí implementovat rozhraní INamingContainer, aby nám korektně fungoval objekt Container v kódu šablony.

Základy vytváření data-bound controls

Pro vytváření data-bound control je v ASP.NET 2.0 připraven pattern v podobě abstraktní třídy CompositeDataBoundControl. Composite proto, že nové controly vytváříme skládáním jiných controlů do hiearchie, nikoliv renderováním HTML kódu controlů úplně nových.

Základní implementace se v podstatě celá odehrává v metodě CreateChildControls(IEnumerable dataSource, bool dataBinding), přičemž základní pattern chování by měl být zhruba následující

override int CreateChildControls(IEnumerable dataSource, bool dataBiding)
{
      if (dataBinding)
      {
            // vytahej z dataSource data a ulož si je ve své podobě, obvykle do this.Items
            // this.Items bychom si měli ukládat do ViewState, drží nám data mezi postbacky
      }



      // vytvoř controly pro každou položku this.Items a přidej ji do this.Controls
      // foreach (MyItem item in Items) { ... Controls.Add(..); }
      // return počet vytvořených items
}

Pokud nemáme ViewState, tak lze samozřejmě obě fáze sloučit a rovnou tvořit control-hiearchy z dat v dataSource.

MyRepeater – kombinace CompositeDataBoundControl a ITemplate

No a není nic jednoduššího, než obě možnosti zkombinovat a vytvořit tak control, který bude data-bound a pro reprezentaci dat bude používat šablony:

public class MyRepeater : CompositeDataBoundControl
{
      [PersistenceMode(PersistenceMode.InnerPropety)]
      [TemplateContainer(typeof(MyRepeaterItem))]
      public ITemplate ItemTemplate
      {
            get { return _itemTemplate; }
            set { _itemTemplate = value; }
      }
      private ITemplate _itemTemplate;

      /// <summary>
      /// Soubor .ascx, který představuje případnou externí ItemTemplate.
      /// </summary>
      public string ItemTemplateFile
      {
            get { return _itemTemplateFile; }
            set { _itemTemplateFile = value; }
      }
      private string _itemTemplateFile;
      public Collection<MyRepeaterItem> Items
      {
            get
            {
                  if (_items == null)
                  {
                        _items = new Collection<MyRepeaterItem>();
                  }
                  return _items;
            }
      }
      private Collection<MyRepeaterItem> _items;
      protected override CreateChildControls(IEnumerable dataSource, bool dataBinding)
      {
            // vytaháme data z dataSource
            if (dataBinding)
            {
                  if (dataSource != null)
                  {
                        foreach (object o in dataSource)
                        {
                              if (!(o is Zakaznik))
                              {
                                    throw new ApplicationException("DataSource musí obsahovat prvky typu Zakaznik.");
                              }
                              MyRepeaterItem item = new MyRepeaterItem(o as Zakaznik);
                              Items.Add(item);
                        }
                  }
            }
            // vytvoříme controly
            foreach (MyRepeaterItem in Items)
            {
                  if (!String.IsNullOrEmpty(ItemTemplateFile))
                  {
                        _itemTemplate = Page.LoadTemplate(ItemTemplateFile);
                  }
                  _itemTemplate.InstantiateIn(item);
                  item.DataBind();
                  Controls.Add(item);
            }
            return Items.Count;
      }
}

Kód ukazuje i druhou možnost získávání šablon – jejich načítání z .ASCX souboru pomocí Page.LoadTemplate(). Šablona totiž může úplně stejně, jako ve vlastní .aspx stránce, být uložena v samostatném .ASCX souboru (jen pro Container je tam potřeba použít explicitní přetypování, protože tam parser neví o atributu TemplateContainer).

Pro úplnost ještě MyRepeaterItem:

public class MyRepeaterItem : Control, INamingContainer
{
      public Zakaznik Zakaznik
      {
            get { return _zakaznik; }
            set { _zakaznik = value; }
      }
      public MyRepeaterItem(Zakaznik zakaznik)
      {
            this._zakaznik = zakaznik;
      }
}
Odkazy na související články