Myślę, że każdy, kto korzysta z rozmaitych stron/aplikacji choć raz spotkał się zakładkami (ang. tabs). Z tego względu nie opiszę co to jest, kto nie wie może zajrzeć tutaj, ale pokażę alternatywne podejście jak można coś takiego (TabControl) zrealizować i zastosować na stronie internetowej. Przykład, który zaprezentuję wykorzystuje ASP.NET 4 WebForms w związku z tym, że sam pomysł zrodził się podczas tworzenia projektu opartego właśnie na tej technologii. Teoretycznie sama koncepcja abstrahuje od technologii więc zachęcam do lektury również tych, którzy nie są zwolennikami WebFormsów.
Zanim przejdę do realizacji i przykładu opiszę pokrótce specyfikę zadania. Odłożę na razie na bok detale i załóżmy, że chcemy wdrożyć do projektu coś takiego:
W zasadzie na tą chwilę problemu nie ma. Każdy zaprawiony w boju developer zaciągnie jQuery UI, AjaxControlToolkit, Juice UI lub jeszcze inne cudo i voilà, gotowe! Taka reakcja jest jak najbardziej słuszna i pożądana do momentu, kiedy nie znamy docelowej zawartości zakładek. Jeżeli przeznaczeniem zakładek jest wyświetlanie statycznych danych bez dodatkowych kontrolek, logiki i innych bajerów to mamy szczęście, ponieważ to jest podstawowy, a zarazem najprostszy przypadek. W takiej sytuacji możemy z powodzeniem wykorzystać którąś z wymienionych wcześniej bibliotek i problem jest rozwiązany.
Gdyby w życiu zawsze było tak pięknie i kolorowo to nie miałbym po co pisać dalej i zakończyłbym wpis na poprzednim paragrafie. Na (nie)szczęście zazwyczaj zmagamy się z zadaniami, które nijak mają się do przykładów zamieszczonych w dokumentacji, czy książce. Tak jest też w przypadku, gdy założymy, że zawartość tabów to np. gridy z dynamicznie ładowanymi danymi, wykresy, czy cokolwiek innego wykraczającego poza standardowy przypadek. Oczywiście w takiej sytuacji również możemy wykorzystać którąś z podanych bibliotek, podmienić zawartość i zapomnieć o temacie. Jeżeli tak to w czym problem?
Problem wynika z podejścia jakie stosują wymienione przeze mnie biblioteki. Najpierw wszystkie zakładki są renderowane, a co za tym idzie dane zostają pobrane, wykonana jest jakaś wewnętrzna logika – to wszystko po to, aby na końcu ustawić jedną aktywną zakładkę i ukryć zawartość pozostałych. Wada takiego rozwiązania powinna nasunąć się sama. Co jeżeli nie zawsze użytkownik będzie przechodził przez wszystkie zakładki? Po co mamy ładować zbędne (w danym kontekście) dane? Co jeżeli potrzebujemy specyficznego zachowania, które może (choć nie powinno) wpłynąć na kontrolki w innych zakładkach? Odpowiedzią na powyższe (i nie tylko) pytania jest rozwiązanie, które chcę zaproponować.
Zamiast zaprzęgać do pracy zewnętrzne biblioteki wykorzystam proste połączenie kontrolek dostępnych wraz z .NET. czyli MenuList + Nested Master Page + ContentPlaceholder. Do tego połączenia dorzucę sitemapę i trochę css’a. Ok, do roboty. Wykonanie całości zamknie się w kilku krokach.
Po stworzeniu standardowego projektu ASP.NET WebForms tworzymy plik Nested Master Page wybierając jako plik nadrzędny Site.master. Stworzony plik będzie stanowił ramę dla naszej kontrolki. Następnym krokiem jest wstawienie do naszej “ramy” kontrolek: MenuList opartej na sitemapie oraz ContentPlaceholder. ContentPlaceholder będzie kontenerem, do którego zostanie załadowana zawartość zakładki w zależności od kontekstu (adresu URL). W efekcie po wykonaniu tych kroków otrzymamy coś takiego:
1 2 3 4 5 6 7 |
<?xml version=<span style="color: maroon;">"1.0"</span> encoding=<span style="color: maroon;">"utf-8"</span> ?> <siteMap xmlns=<span style="color: maroon;">"http://schemas.microsoft.com/AspNet/SiteMap-File-1.0"</span> > <siteMapNode url=<span style="color: maroon;">"~/Tabs"</span> title=<span style="color: maroon;">"Tabs"</span>> <siteMapNode url=<span style="color: maroon;">"~/Tabs/Tab1.aspx"</span> title=<span style="color: maroon;">"Tab1"</span> /> <siteMapNode url=<span style="color: maroon;">"~/Tabs/Tab2.aspx"</span> title=<span style="color: maroon;">"Tab2"</span> /> </siteMapNode> </siteMap> |
Zawartość pliku TabHeaders.sitemap
1 2 3 4 5 6 7 8 9 10 11 |
<%@ Master Language=<span style="color: maroon;">"C#"</span> MasterPageFile=<span style="color: maroon;">"~/Site.Master"</span> AutoEventWireup=<span style="color: maroon;">"true"</span> CodeBehind=<span style="color: maroon;">"TabContainer.master.cs"</span> Inherits=<span style="color: maroon;">"CustomTabControl.TabContainer"</span> %> <asp:Content ID=<span style="color: maroon;">"Content1"</span> ContentPlaceHolderID=<span style="color: maroon;">"HeadContent"</span> runat=<span style="color: maroon;">"server"</span>> </asp:Content> <asp:Content ID=<span style="color: maroon;">"Content2"</span> ContentPlaceHolderID=<span style="color: maroon;">"MainContent"</span> runat=<span style="color: maroon;">"server"</span>> <asp:Menu ID=<span style="color: maroon;">"TabHeaders"</span> ClientIDMode=<span style="color: maroon;">"Static"</span> runat=<span style="color: maroon;">"server"</span> Orientation=<span style="color: maroon;">"Horizontal"</span> DataSourceID=<span style="color: maroon;">"TabHeadersSitemap"</span> /> <asp:SiteMapDataSource ID=<span style="color: maroon;">"TabHeadersSitemap"</span> SiteMapProvider=<span style="color: maroon;">"TabHeadersProvider"</span> runat=<span style="color: maroon;">"server"</span> ShowStartingNode=<span style="color: maroon;">"False"</span> /> <div id=<span style="color: maroon;">"TabContent"</span>> <asp:ContentPlaceHolder runat=<span style="color: maroon;">"server"</span> ID=<span style="color: maroon;">"TabContentPlaceholder"</span>></asp:ContentPlaceHolder> </div> </asp:Content> |
Zawartość pliku TabContainer.master
1 2 3 4 5 |
<siteMap defaultProvider=<span style="color: maroon;">"TabHeadersProvider"</span> enabled=<span style="color: maroon;">"true"</span>> <providers> <add name=<span style="color: maroon;">"TabHeadersProvider"</span> description=<span style="color: maroon;">"Tab headers sitemap provider."</span> type=<span style="color: maroon;">"System.Web.XmlSiteMapProvider"</span> siteMapFile=<span style="color: maroon;">"TabHeaders.sitemap"</span> securityTrimmingEnabled=<span style="color: maroon;">"false"</span> /> </providers> </siteMap> |
Fragment pliku Web.config (znajdujący się wewnątrz znaczników <system.web></system.web>)
Kolejny krok to stworzenie konkretnych zakładek, które w tym przypadku są plikami *.aspx dziedziczącymi po pliku TabContainer.master (w okienku Add New Item wybieramy opcję Web Form using Master Page). Poszczególne zakładki będą ładowane dynamicznie, a na tą chwilę całość wygląda tak:
1 2 3 4 |
<%@ Page Title=<span style="color: maroon;">""</span> Language=<span style="color: maroon;">"C#"</span> MasterPageFile=<span style="color: maroon;">"~/TabContainer.master"</span> AutoEventWireup=<span style="color: maroon;">"true"</span> CodeBehind=<span style="color: maroon;">"Tab1.aspx.cs"</span> Inherits=<span style="color: maroon;">"CustomTabControl.Tabs.Tab1"</span> %> <asp:Content ContentPlaceHolderID=<span style="color: maroon;">"TabContentPlaceholder"</span> runat=<span style="color: maroon;">"server"</span>> Hello from Tab1! </asp:Content> |
Zawartość pliku Tabs\Tab1.aspx
1 2 3 4 |
<%@ Page Title=<span style="color: maroon;">""</span> Language=<span style="color: maroon;">"C#"</span> MasterPageFile=<span style="color: maroon;">"~/TabContainer.master"</span> AutoEventWireup=<span style="color: maroon;">"true"</span> CodeBehind=<span style="color: maroon;">"Tab2.aspx.cs"</span> Inherits=<span style="color: maroon;">"CustomTabControl.Tabs.Tab2"</span> %> <asp:Content ContentPlaceHolderID=<span style="color: maroon;">"TabContentPlaceholder"</span> runat=<span style="color: maroon;">"server"</span>> Hello from Tab2! </asp:Content> |
Zawartość pliku Tabs\Tab2.aspx
Jak widać nie jest to jeszcze oczekiwany efekt, ale dodając odrobinę stylu CSS otrzymamy dokładnie to, do czego dążyliśmy:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#TabHeaders { border-bottom: silver 3px solid; width: 100%; } #TabHeaders li{ border: silver 1px solid; border-bottom: none; height: 35px; line-height: 35px; margin-right: 1px; } #TabHeaders li a { display: block; padding-left: 3px; padding-right: 3px; } #TabHeaders li a.selected { background-color: #b6b7bc } #TabHeaders li:hover { background-color: #cfdbe6; } #TabHeaders li:last-child { margin-right: 0; } #TabContent { background-color: #cccccc; min-height: 400px; } |
Fragment pliku Site.css
Największą korzyścią takiego rozwiązania jest to, że minimalnym nakładem pracy, nie napisaliśmy ani jednej linijki w code behind!, zakładki są ładowane na żądanie. Dzięki temu nie jest wykonywana dodatkowa praca po stronie serwera. Wykorzystując mechanizmy dostępne w ASP.NET nie musimy podpinać żadnych zewnętrznych komponentów, co również można uważać za plus.
Jedyną niedoskonałością przedstawionego rozwiązania, jaką udało mi się stwierdzić, jest fakt, że przełączanie pomiędzy zakładkami powoduje przeładowanie całej strony, co nie zawsze może być porządane. W celu wyeliminowania tej niedoskonałości można pomyśleć o zaprzęgnięciu AJAXa, aczkolwiek ten temat zostawiam chętnym jako zadanie domowe :).
Pingback: dotnetomaniak.pl
jQuery UI umożliwia pobieranie treści po przełączeniu się na zakładkę.
Demo: http://jqueryui.com/tabs/#ajax
Kolejna ciekawa alternatywa, dzięki :). U mnie w projekcie było zastrzeżenie żeby JavaScript w miarę możliwości był ostatecznością, dlatego wyprowadziłem takie rozwiązanie.