ASP.NET: Repeater cz. 2

 

Po powiązaniu naszej kolekcji z repeaterem za pomocą polecenia DataBind() nasze dane wyświetlane są w wierszach. Wygląda to tak:

Repeater - databind

Zaimplementujmy na początek możliwość dodawania nowego wiersza. Wiersz ten nie może wyświetlać żadnego tekstu, ale jednocześnie repeater wyświetlać musi elementy znajdujące się w kolekcji. Jak uważni czytelnicy zapewne zauważą jest tu pewna sprzeczność. Klasa SimpleItem przechowuje bowiem identyfikator jak typ int, który nie jest nullowalny. Z tego powodu nie można wymagać prostego dodania pustego pola tekstowego i jednoczesnego zapisu do kolekcji (chyba, że typ zostałby zmieniony na nullowalny int, czyli int?). Na szczęście da się wybrnąć z tej sytuacji dzięki istniejącej logice biznesowej, a mianowicie, przyjęciu, że identyfikator nie może mieć wartości 0 (nie jest to naiwne założenie). 

Należy przede wszystkim umieścić na stronie przycisk umożliwiający dodawanie nowego wiersza. Postępowanie przy tym jest następujące:

  1. Odczyt wszystkich wierszy repeatera i zapis ich do nowej kolekcji
  2. Dodanie nowego “pustego” elementu
  3. Powiązanie nowej kolekcji z repeaterem

Punkt pierwszy wynika z tego, że po powiązaniu kolekcji z repeaterem, jeśli nastąpi jakiś postback, to nie da się już uzyskać dostępu do kolekcji (chyba, że zostanie wykorzystany jakiś mechanizm zarządzania stanem aplikacji), natomiast dane cały czas “wędrują” razem z repeaterem, dzięki czemu można je wydobyć. Spójrzmy zatem na kod:

/// <summary>
/// When add row button clicked
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected void btnAddRepeaterRow_Click(object sender, EventArgs e)
{
	//Create new collection
	Collection<SimpleItem> myCollection = new Collection<SimpleItem>();

	//Get collection from repeater
	foreach (RepeaterItem item in repDynamicRows.Items)
	{
		string id = ((TextBox)item.FindControl("txtId")).Text;
		string name = ((TextBox)item.FindControl("txtName")).Text;

		if (string.IsNullOrEmpty(id))
		{
			//Write 0 in collection because it will result in empty textbox when bound
			myCollection.Add(new SimpleItem(0, ""));
		}
		else
		{
			myCollection.Add(new SimpleItem(Convert.ToInt32(id, CultureInfo.InvariantCulture), name));
		}
	}

	//Add additional row
	myCollection.Add(new SimpleItem(0, ""));

	//Bind new data source to repeater
	repDynamicRows.DataSource = myCollection;
	repDynamicRows.DataBind();
}

Do wierszy repeatera możemy się dostać poprzez jego właściwość Items. Z każdego wiersza natomiast można wydobyć zawartość każdej kontrolki, wywołując na obiekcie typu RepeaterItem metodę FindControl() podając odpowiedni identyfikator danej kontrolki. Konieczne jest również rzutowanie na określony typ kontrolki.

W powyższym przykładzie, możliwe jest dodawanie kolejnego wiersza, nawet jeśli poprzedni nie został uzupełniony. Dodanie wiersza do kolekcji wiąże się z zamianą tekstu znajdującego się w polu tekstowym w kolumnie Id na liczbę, czyli wymaga konwersji, która może być zakończona sukcesem jedynie jeśli próbujemy konwertować liczbę. Z tego powodu w pętli znalazł się warunek if…else…, który powoduje to, że jeśli użytkownik nie wpisał nic w polach tekstowych, to zostanie do kolekcji dodany obiekt o id = 0 i pustej nazwie. Na zakończenie, z racji tego, że w końcu naszym życzeniem było dodanie nowego wiersza, to w taki sam sposób dodawany jest nowy element do kolekcji, po czym jest ona bindowana do repeatera. Takie działanie spowoduje, że w momencie wyświetlenia strony użytkownikowi w repeaterze zostanie wyświetlony dodatkowy wiersz:

repeater - row added 

Jak widać na powyższym obrazku, pomimo tego, że dodaliśmy do kolekcji element o identyfikatorze 0, to nie został on wyświetlony. Jak to się stało? Otóż zastosowana została tutaj pewna mała sztuczka. Mianowicie wykorzystane zostało zdarzenie repeatera ItemDataBound, zachodzące podczas wiązania każdego wiersza do repeatera:

/// <summary>
/// When row is bound to repeater insert blank when id is equal to 0
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected void repDynamicRows_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
	TextBox txtId = e.Item.FindControl("txtId") as TextBox;
	if (txtId != null)
	{
		if (txtId.Text == "0")
		{
			txtId.Text = "";
		}
	}
}

Powyższy kod sprawia to, że w momencie, kiedy z kolekcji zostanie odczytany element, który spowoduje wyświetlenie w polu tekstowym w kolumnie Id tekstu “0” to zostanie on zamieniony na pusty tekst, dzięki czemu użytkownik nie zobaczy żadnych danych w tym polu, czyli zamierzony efekt został osiągnięty.

Usuwanie wiersza z repeatera

Jeśli chodzi o usuwanie wybranego wiersza z repeatera, to jak łatwo można wywnioskować służy do tego znak X znajdujący się w trzeciej kolumnie. Jego zadaniem jest, po kliknięciu, usunięcie wiersza, w którym się on znajduje. Postępowanie tutaj jest podobne jak w przypadku dodawania nowego wiersza, z tą różnicą, że przy tworzeniu nowej kolekcji musimy jeden wiersz pominąć (ten, w którym znajduje się znak X) oraz oczywiście nie jest dodawany nowy wiersz. W kodzie wygląda to następująco:

/// <summary>
/// Event after delete link clicked in repeater
/// </summary>
/// <param name="source"></param>
/// <param name="e"></param>
protected void repDynamicRows_ItemCommand(object source, RepeaterCommandEventArgs e)
{
	if (e.CommandName == "Delete")
	{
		Collection<SimpleItem> myCollection = new Collection<SimpleItem>();

		//Add all elements from repeater to collection except the deleted one
		foreach (RepeaterItem item in repDynamicRows.Items)
		{
			LinkButton lkbRemove = (LinkButton)item.FindControl("lkbDelete");

			if (lkbRemove != e.CommandSource)
			{
				string id = ((TextBox)item.FindControl("txtId")).Text;
				string name = ((TextBox)item.FindControl("txtName")).Text;
				if (string.IsNullOrEmpty(id))
				{
					//Write 0 in collection because it will result in empty textbox when bound
					myCollection.Add(new SimpleItem(0, ""));
				}
				else
				{
					myCollection.Add(new SimpleItem(Convert.ToInt32(id, CultureInfo.InvariantCulture), name));
				}
			}
		}

		//Bind collection to repeater
		repDynamicRows.DataSource = myCollection;
		repDynamicRows.DataBind();
	}
}

Tym razem wykorzystane zostało zdarzenie ItemCommand, które występuje w momencie kliknięcia w jakiś element posiadający charakter przycisku, ale znajdujący się w repeaterze. W przedstawionym przykładzie jest to LinkButton wyświetlający X. Jak widać, wszelkie czynności są wykonywane dopiero, gdy sprawdzone zostanie, że komenda to “Delete”:

if(e.CommandName == "Delete")
{
	...
}

Kolejna sztuczka opiera się na odnalezieniu źródła, które spowodowało wykonanie zdarzenia:

...
	LinkButton lkbRemove = (LinkButton)item.FindControl("lkbDelete");

	if (lkbRemove != e.CommandSource)
	{
		...
	}

Dla każdego wiersza odnajdywany jest w nim LinkButton i jeśli nie jest on tym, który spowodował wykonanie zdarzenia to wiersz ten jest dodawany do kolekcji. Łatwo więc zauważyć, że wiersz z którego zdarzenie zostało wykonane nie zostanie dodany do kolekcji, czyli efekt zamierzony został osiągnięty.

Na zakończenie słowo o zapisie kolekcji z repeatera. W tym celu obsłużone musi być zdarzenie kliknięcia w jakiś przycisk znajdujący się poza repeaterem, którego zadaniem jest zapis danych. Należy odczytać wszystkie dane z repeatera i przypisać je do utworzonej kolekcji, co zostało już pokazane.

PS. Kod implementujący opisany mechanizm nie znalazł się w projekcie konkursowym, zatem nie znalazł się on w źródłach umieszczonych na codeplexie, stąd proszę o nie szukanie go tam 🙂

ASP.NET: Repeater cz. 1

 

W kolejnych dwóch postach postaram opisać się moje zmagania z kontrolką ASP.NET o nazwie repeater. Z racji dużej liczby kodu, jaki będzie zamieszczony postanowiłem w niniejszym poście zrobić wprowadzenie, w drugim zaś objaśnić większość implementacji dla postawionego zadania. Zadaniem tym jest stworzenie funkcjonalności, dzięki której pola tekstowe na stronie mogłyby być dodawane w razie potrzeby w nieskończoność. Chcemy zatem dysponować z góry nieznaną liczbą pól, jednak o znanym charakterze. Prościej mówiąc potrzebne są pola tekstowe znajdujące się w wierszach, przy czym liczba wierszy w momencie uruchomienia aplikacji nie jest znana. Wiąże się z tym konieczność zaimplementowania funkcji:

  • dodawania nowego wiersza bo kliknięciu w przycisk
  • usuwania wybranego wiersza z danymi
  • odczytu istniejących danych (w przykładzie, dla uproszczenia, z własnej kolekcji)
  • zapisu danych znajdujących się w polach do kolekcji (de facto jest to punkt pierwszy z wymienionych stąd nie będzie szczegółowo wyjaśniony)

Postanowiłem do tego zadania, aby się czegoś nauczyć, wykorzystać kontrolkę zwaną repeaterem.

Pierwszym krokiem będzie zatem stworzenie własnej kolekcji, przechowującej informacje, powiedzmy pobrane uprzednio z bazy danych. Kolekcja ta ma prostą strukturę, mianowicie ma przechowywać dwa pola – id oraz nazwę, czyli jest w pewnym tego słowa znaczeniu słownikiem. Elementem kolekcji będą obiekty klasy SimpleItem:

[Serializable]
public class SimpleItem
{
	/// <summary>
	/// Default constructor
	/// </summary>
	public SimpleItem()
	{
		this.id = 0;
		this.name = null;
	}

	/// <summary>
	/// Constructor using id and name
	/// </summary>
	/// <param name="id">Id</param>
	/// <param name="name">Name</param>
	public SimpleItem(int id, string name)
	{
		this.id = id;
		this.name = name;
	}

	private int id;
	/// <summary>
	/// Id
	/// </summary>
	public int Id
	{
		get { return this.id; }
		set { this.id = value; }
	}

	private string name;
	/// <summary>
	/// Name
	/// </summary>
	public string Name
	{
		get { return this.name; }
		set { this.name = value; }
	}
}

Zwracam w powyższym kodzie uwagę na znajdujący się na początku atrybut [Serializable], aczkolwiek w tym poście pozwolę sobie go nie omawiać 🙂 Przypominam równiez o konieczności stworzenia własnej implementacji konstruktora domyślnego, w przypadku gdy stworzony został niedomyślny.

Kolekcja takich elementów tworzona jest w sposób następujący:

//using System.Collections.ObjectModel
Collection<SimpleItem> myCollection = new Collection<SimpleItem>();

Przy czym należy pamiętać od dodaniu na początku odpowiedniej przestrzenii nazw. Na potrzeby przedstawionych rozważań, z metod obiektu typu Collection, interesować nas będzie jedynie metod pozwalająca na dodawanie elementów do kolekcji, czyli Add().

Teraz odrobina informacji o tworzeniu repeatera. Po wstawieniu kontrolki:

<asp:Repeater ID="repDynamicRows" runat="server"></asp:Repeater>

należy zdefiniować, jak będzie wyglądał nagłówek oraz jaką postać będzie miał każdy wiersz. Część nagłówkową definiujemy wykorzystując tabele html pomiędzy znacznikami <HeaderTemplate></HeaderTemplate>

<HeaderTemplate>
	<table>
		<tr>
			<td style="width:100px">Id</td>
			<td style="width:100px">Nazwa</td>
		</tr>
	</table>
</HeaderTemplate>

Natomiast to, jak będzie wyglądał każdy wiersz repeatera podajemy w części pomiędzy znacznikami <ItemTemplate></ItemTemplate>

<ItemTemplate>
	<table>
		<tr>
			<td style="width:100px"><asp:TextBox ID="txtId" runat="server" Width="100" Text='<%# Eval("Id") %>'></asp:TextBox></td>
			<td style="width:100px"><asp:TextBox ID="txtName" runat="server" Width="100" Text='<%# Eval("Name") %>'></asp:TextBox></td>
			<td style="width:100px"><asp:LinkButton ID="lkbDelete" runat="server" CommandName="Delete">X</asp:LinkButton></td>
		</tr>
	</table>
</ItemTemplate>

W tym przypadku repeater składał będzie się z dwóch pól tekstowych, umożliwiających edycję kolumny Id oraz Nazwa. W trzeciej kolumnie znajdować będzie się przycisk, którego zadaniem ma być skasowanie wiersza, w którym się on znajduje. Za pomocą obiektu typu LinkButton można zdefiniować nazwę wykonywanej akcji po jego naciśnięciu, stąd właściwość CommandName=”Delete” (tak naprawdę nazwa może być dowolna, chodzi tylko o pewien sposób rozróżniania wykonywanej komendy). W pozostałych dwóch przypadkach, czyli dla pól tekstowych zdefiniowana została właściwość Text, która za pomocą metody Eval(), powoduje wpisanie do pola tekstowego odpowiedniego pola z elementu stworzonej kolekcji, właściwego dla danego wiersza kontrolki.

Dane, które mają wypełniać kontrolkę repeater, jeśli istnieją, podpinane są pod nią w odpowiednim momencie, czyli:

protected void Page_Load(object sender, EventArgs e)
{
	//Set collection only at the first load
	if (!IsPostBack)
	{
		Collection<SimpleItem> myCollection = new Collection<SimpleItem>();
		myCollection.Add(new SimpleItem(1, "Pierwszy"));
		myCollection.Add(new SimpleItem(2, "Drugi"));

		//Bind collection to repeater
		repDynamicRows.DataSource = myCollection;
		repDynamicRows.DataBind();
	}
}

cdn.

Microsoft Project 2010

 

Tym razem post z cyklu “Warsztat pracy”. O potrzebie przechowywania projektów za pomocą systemu kontroli wersji pisałem już jakiś czas temu. Do wygodnej pracy potrzebne moim zdaniem są jeszcze programy do robienia backupów na nośnik zewnętrzny oraz do zarządzania projektem. Ponieważ na szukaniu narzędzii do backupu jeszcze nie skupiłem swojej uwagi, tematem posta będzie zarządzanie projektem.

W momencie pisania niewielkiej aplikacji (rzeczą względną jest pojęcie “niewielka” – podejrzewam, że od kilkunastu do kilkudziesięciu tysięcy linii), przy ciągłej pracy z nią zapewne większość rzeczy do zrobienia przeciętny człowiek jest w stanie zapamiętać. Gdy aplikacja jest większa, ma się słabą pamięć lub nie przesiaduje się codziennie nad danym projektem, przydałoby się jakieś narzędzie do zarządzania projektem, chociażby na potrzeby domowego zacisza. Do tej pory miałem nawyk zapisywania ważniejszych zadań do realizacji po prostu w notatniku, co sprawiało niestety, że postać ta nie była dość czytelna (zamiennie w rachubę wchodziły również kartki, rozwiązanie równie niedoskonałe ze względu na łatwość z jaką ulegają one zagubieniu). Doskonale do tego celu zapewne nadaje się Team Foundation Server, jednak w moje ręce wpadł program Microsoft Project, który jak spodziewam się, przeznaczony jest nie tylko dla programistów. Z pewnością narzędzie to ma o wiele większe możliwości niż oczekuję do swojego zastosowania, ale czemu by się z nim nie zapoznać jeśli już jest taka okazja.

Wersję trial z 2010 w wersji Professional można pobrać ze strony http://www.microsoft.com/project/en/us/default.aspx. Jest to wersja 60-dniowa, przy czym fajnie jest posiadać konto na Windows Live 🙂 Jeśli ktoś może korzystać z dobrodziejstw programu MSDNAA, również i tam, przynajmniej w moim przypadku, znajdują się wersje do pobrania. Warto zaoszczędzić sobie trochę czasu i wybrać wersję zgodną pod względem liczby bitów z zainstalowanym pakietem Microsoft Office. Przynajmniej dla mnie na pierwszy rzut oka nie wynika, że jest to składnik Microsoft Office :). Wersja 2010 jest oczywiście, jak wszystkie produkty z tej serii, bardzo przyjazna dla oka. Nawigacja odbywa się naturalnie za pomocą wstążki.

W programie podstawową funkcją jest możliwość tworzenia tasków. Poza nazwą taska możemy określić czas jego trwania, dzień rozpoczęcia oraz zakończenia, dodać szczegółowe informacje, określić taski będące poprzednikami aktualnie edytowanego i wiele, wiele innych atrybutów. Task oczywiście dobrze jest, jeśli jest przypisany do jakiejś osoby, nieładnie tutaj nazwanej zasobem. Przed przydzieleniem taska warto zatem dysponować jakimiś zasobami do przydzielenia, jednak nie jest to warunek konieczny. Oczywiście do każdego taska może być przydzielony więcej niż jeden zasób, przy czym dla każdego z nich można określić czas pracy nad danym zagadnieniem z podziałem na godziny. Dla każdej z osób można również wyznaczyć stawkę godzinową, kontrolując tym samym koszt wykonania projektu. Jak łatwo można podejrzewać, poza tym co wymieniłem jednym tchem, istnieje przynajmniej 100 razy więcej opcji konfiguracji danego projektu, co czyni to narzędzie dość rozbudowanym. Ważną możliwością, aczkolwiek zapewne będącą standardem przy tego typu programach, jest wizualizacja tasków za pomocą wykresu Gantt’a.

W tym momencie dla moich potrzeb narzędzie jest zbyt rozbudowane, co nie znaczy, że trudne (być może później zmienie zdanię 🙂 ). Dla osób na co dzień zajmujących się zarządzaniem projektami zapewne będzie ono dziecinnie proste w obsłudze. Zachęcam do wypróbowania programu, w szczególności tych, którzy nie mieli wcześniej do czynienia z projektami od strony zarządzania nimi, aby przekonać się na własnej skórze na czym to polega. Zachęcam również tych, którzy mają jakieś rozeznanie w tego typu programach do polecenia innych pozycji (w szczególności darmowych ;)) z tego zakresu.

WPF: Rysowanie 2D

 

Jak to mówią – nie samym konkursem człowiek żyje 🙂 Elementem niezbędnym do powstania mojej pracy magisterskiej jest wykorzystanie grafiki 3D, stąd zapadła decyzja, że powstanie ona w technologii WPF. Nie o grafice trójwymiarowej jednak w tym poście. Okazało się bowiem, że istnieje również potrzeba wykorzystania grafiki 2D do wyświetlania przebiegów funkcji.

Celem, który mi przyświeca od początku pisania aplikacji jest stworzenie architektury, która przynajmniej w jakimś  niewielkim stopniu, na miarę mojej wiedzy, mogłaby być później rozbudowywana. Przede wszystkim to dobry impuls do zastosowania wzorca projektowego obserwatora (Observer Pattern), w odmianie .Net–owej, czyli Event Pattern. Odmiana ta zakłada wykorzystanie zdarzeń oraz delegatów. Samego wzorca nie będę tutaj opisywał – zachęcam do zapoznania się z nim pod adresem: http://msdn.microsoft.com/en-us/library/ee817669.aspx.

Elementem składowym całej układanki jest utworzenie osobnej kontrolki umożliwiającej wyświetlanie rysunków na płaszczyźnie. Jako, że mam pewne doświadczenie w tworzeniu grafiki w Windows Forms, to z lekkim zaskoczeniem przekonałem się, że w WPF sposób tworzenia grafiki jest dość odmienny. Postanowiłem więc moje spostrzeżenia z tego etapu prac umieścić na blogu, aby później odnalezione przeze mnie informacje nie uległy zapomnieniu.

Zacznijmy od tego, że z racji tego, że moje główne okno aplikacji ma być zajęte wyświetlaniem grafiki 3D i innych rzeczy to chciałbym, aby dla grafiki 2D otwierane było nowe okno. W tym celu należy stworzyć nowy element typu Window, w którym umieszczona zostanie przygotowana wcześniej kontrolka. Nowe okno wywołać można  w następujący sposób:

var myWindow = new WindowPlainGraph();
myWindow.Show();

W powyższym kodzie WindowPlainGraph to nazwa pliku wybrana dla elementu typu Window.

Kontrolka została stworzona w tej samej przestrzenii nazw, stąd nie ma konieczności dodawania żadnych referencji do projektu. Przydałoby się jednak powiadomić w jakiś sposób edytor, że kontrolka taka będzie wykorzystywana w projekcie. W kodzie XAML na nowej stronie dla elementu Window, trzeba dodać następujący wpis:

xmlns:myPlainGraphControl="clr-namespace:MyNamespace"
    

W przypadku gdyby konieczne było dodanie kontrolki z zewnątrz projektu trzeba dodać jeszcze parametr assembly. Od tego momentu w kodzie XAML kontrolkę można wstawiać używając składni:

<myPlainGraphControl:PlainGraph/>

gdzie PlainGraph to klasa z kontrolką. Co warte podkreślenia, aby element z tego samego assembly mógł być dostępny w projekcie z kodu, konieczne jest nadanie mu identyfikatora x:Name, zamiast zwyczajnego Name.

Co do samego rysowania przebiegów funkcji, to przynajmniej w moim podejściu do narysowania jest zbiór odcinków wyznaczonych przez pary punktów. Koniec jednego odcinka oczywiście musi być zarazem początkiem drugiego odcinka. W przypadku WPF doskonale do tego celu nadaje się obiekt klasy Polyline. Punkty składowe tego obiektu ustawiane są za pomocą metody:

 polyline.Points.Add(...);

która jako argument przyjmuje obiekt typu Point, którego składowe z kolei są typu double. Sam obiekt Polyline wyświetlany jest na panelu poprzez dodanie go komendą:

pnlDrawing.Children.Add(polyline);

gdzie polyline to nazwa utworzonego obiektu typu Polyline.

Pamiętać należy bezwzględnie o tym, że żaden rysunek nie powstanie, jeśli nie zostanie do niego uprzednio przypisane “narzędzie rysujące”, np:

//Before adding to panel
polyline.Stroke = System.Windows.Media.Brushes.Black;
polyline.StrokeThickness = 2;

ASP.NET: Menu na podstawie XML w master page

 

Menu do nawigacji po stronach w ASP.NET może zostać stworzone w przyjazny sposób przy wykorzystaniu kontrolki Menu. W przypadku Visual Web Developer 2010 Express w dostępnym szablonie w pliku Site.Master kontrolka taka została automatycznie stworzonan i wypełniona paroma pozycjami. Wygląda to następująco:

<asp:Menu ID="NavigationMenu" runat="server" CssClass="menu"
EnableViewState="false" IncludeStyleBlock="false" Orientation="Horizontal">
	<Items>
		<asp:MenuItem NavigateUrl="~/Default.aspx" Text="Home"/>
		<asp:MenuItem NavigateUrl="~/About.aspx" Text="About"/>
	</Items>
</asp:Menu>

Rozwiązanie to pozwala na zachowanie na każdej ze stron osadzonych w wybranej master page identycznego menu. Co jednak w przypadku, w którym znalazłem się i ja, gdy dodany został nowy szablon master page. Powtórzenie identycznego kodu dla każdej ze stron (pomimo iż jest to tylko master page) byłoby marnotrawstwem czasu, wysiłku oraz mogłoby przyczynić się do trudności w utrzymaniu spójności menu na każdej ze stron. Z pomocą przychodzi tu możliwość przechowywania ścieżek nawigacyjnych w osobnym pliku. Plikiem takim, wspieranym poprzez wbudowany mechanizm, jest plik Web.sitemap. Jeśli nie znajduje się on już w solucji to można go dodać tak jak każdy inny element, czyli poprzez wybór z menu kontekstowego pozycji Add->New Item. Co ważne, plik ten musi znajdować się w katalogu głównym projektu.

Struktura pliku Web.sitemap wygląda następująco:

<?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
	<siteMapNode url="~/Default.aspx" title="Home" description="Home Page"/>
</siteMap>

czyli mamy element siteMap, w którym mogą być zagnieżdżane kolejne poziomy menu, które muszą znaleźć się pomiędzy znacznikami <siteMapNode></siteMapNode>. Co warte podkreślenia może istnieć tylko jeden element główny, czyli <siteMapNode>, a pozostałe muszą być w nim zagnieżdżone. Niesie to ze sobą pewne konsekwencje. Mianowicie kod:

<?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
	<siteMapNode url="~/Default.aspx" title="Home" description="Home Page">
		<siteMapNode url="~/About.aspx" title="About" description="About Application">
		</siteMapNode>
	</siteMapNode>
</siteMap>

spowoduje, że zamiast zapewne oczekiwanego uzyskania menu o dwóch elementach: Home oraz About, otrzymujemy jeden element o nazwie Home, po najechaniu na który rozwija się menu zawierające element About. Do zmiany takiego stanu rzeczy konieczna jest zmiana właściwości menu wstawionego w master page – “StaticDisplayLevels” na wartość 2.

<?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
	<siteMapNode url="~/Default.aspx" title="Home" description="Home Page">
		<siteMapNode url="~/About.aspx" title="About" description="About Me And Application">
			<siteMapNode url="~/AboutMe.aspx" title="About Me" description="About Me"/>
			<siteMapNode url="~/AboutApplication.aspx" title="About Application" description="About Application"/>
		</siteMapNode>
	</siteMapNode>
</siteMap>

Takie ustawienie spowoduje, że widoczne będą dwa elementy, czyli Home Page oraz About. Po najechaniu myszką na drugi z nich rozwinięte zostanie menu z dwoma kolejnymi pozycjami.

Na koniec pozostaje tylko “podpięcie” kontrolki do źródła danych, czyli naszego pliku Web.sitemap. Najwygodniej można to uczynić wstawiając na stronę master page kontrolkę SiteMapDataSource i oczywiście ustawienie właściwości DataSourceID dla menu na id źródła danych.

Małe podsumowanie

 

Na wstępie kilka słów o sprawach ogólnych. Z racji natłoku obowiązków w ostatnim czasie, jak łatwo zauważyć, posty na moim blogu pojawiały się w dość nierównych odstępach czasowych, nie spełniając przy tym za każdym razem wymagań konkursowych. Zbliżający się wolny czas sprawia jednak, że jest okazja przysiąść nad projektem i zgłębiać kolejne tajniki ASP.NET. Jednocześnie powoli ku końcowi zbliża się czas trwania konkursu “Daj się poznać!”. Aplikacja, chociaż być może nie wygląda zbyt okazale, a także jest ciężka w ocenie ze względu na to, że nie posiadam żadnego hostingu, a baza danych postawiona jest lokalnie na moim komputerze, to jednak sądze, że poruszyłem w niej kilka interesujących zagadnień, które można prześledzić ściągając kod źródłowy z Codeplexa.

Z mojego punktu widzenia sam udział w konkursie jest już sukcesem. Dzięki temu powstał blog, do którego założenia zbierałem się już od dłuższego czasu, a który to dla mnie akurat ma być źródłem zebranej przeze mnie ulotnej wiedzy. Ze względu na różne motywacje czytelników wyznacznikiem jakości publikowanego materiału na pewno nie jest liczba odwiedzin, ale na obecną chwilę 36 opublikowanych postów i ponad 3300 odwiedzin, uważam również za swój mały sukces. Dla mnie główną nagrodą w tym konkursie jest fakt, iż zmotywowałem się do zapoznania z technologią ASP.NET i muszę przyznać, że bez udziału w nim, w normalnym trybie pracy, nie zmusiłbym się do poznania takiej ilości rzeczy w tak krótkim czasie. Najbardziej jednak ucieszy mnie fakt, że Ktoś śledząc moje poczynania poszerzy dzięki temu swoją wiedzę.

Z dużym prawdopodobieństwem mogę stwierdzić, że projekt będzie nadal aktywnie rozwijany. Wybrany przeze mnie temat jest dość uniwersalny i pozwala na poznanie wielu zagadnień związanych z programowaniem webowym. Jednocześnie zachęcam do dalszego śledzenia bloga, zwłaszcza w perspektywie dłuższego okresu czasu, gdyż w miarę jego upływu i rozwoju mojej wiedzy z pewnością pojawią się na nim zagadnienia bardziej skomplikowane. Na dzień dzisiejszy przewiduje, że będą to rzeczy związane z NHibernate, ASP.NET MVC oraz różnymi sprawami dotyczącymi konfiguracji.

Jednocześnie apelowałbym o pozostawienie, bądź też udostępnienie po zakończeniu konkursu spisu adresów blogów w jakiejś postaci. Chociaż nie było zbyt wiele czasu na śledzenie wszystkich z nich, to jednak wiem, że pojawiło się na nich wiele przydatnych i ciekawych kwestii, do których w wolnej chwili chciałbym powrócić.

User and Role Authorization

 

ASP.NET pozwala na skorzystanie z pewnych mechanizmów zarządzania użytkownikami oraz ich rolami. O możliwości ich konfiguracji z poziomu strony www pisałem w jednym z moich pierwszych postów na blogu. Tym razem chciałbym przedstawić w jaki sposób uzyskać pożądany efekt dokonując modyfikacji w kodzie.

Celem autoryzacji użytkowników jest przekazanie odpowiedniej treści, jedynie wybranym użytkownikom, czyli np. zablokowanie dostępu do pewnych elementów na stronie, czy też całych stron. Dodatkowo, użytkownikom mogą zostać przypisane role, dzięki czemu uzyskamy możliwość zarządzania pewną wydzieloną grupą użytkowników, przy czym każdy z nich może posiadać wiele ról.

Mechanizm, jaki chciałbym zaimplementować, jest dość standardowy. Użytkownik próbuje uzyskać dostęp do aplikacji, jednak bez względu na to, jaką stronę wpisał w adresie, jeśli nie jest już zalogowany, to jest przenoszony na stronę z kontrolką logowania. Po poprawnym podaniu danych logowania jest on przekierowywany na stronę, której ostatnio wyświetlenia żądał. Sprawdzenie, czy użytkownik może zostać zalogowany może następować np. poprzez sprawdzenie jego danych w bazie. W przykładzie jednak, dla mniejszej komplikacji, założone zostanie, że użytkownik podał poprawne dane. Ponadto informacja o tym, że ktoś został zalogowany powinna być dostępna na stałę – nie można na każdej ze stron wymagać logowania się przez osoby odwiedzające stronę. Wiemy już, że protokół http jest bezstanowy, zatem potrzebny jest jakiś mechanizm przechowywania informacji. W tym przypadku często wykorzystywane w tym celu są cookies, szczególnie w zaszyfrowanej wersji.

Przykład

Do “logowania” wykorzystam stworzoną stronę o nazwie MyLogin. Na stronie umieszczony został przycisk, który po kliknięciu będzie symulował sprawdzenie poprawności danych użytkownika i jego autoryzację. Przede wszystkim, należy dodać na początku kodu wpis:

using System.Web.Security;

Zdarzenie kliknięcia w przycisk obsługiwane jest w sposób następujący:

//Login button
protected void Button1_Click(object sender, EventArgs e)
{
	//Fill user data
	string userName = "Michal";
	string userRole = "Administrator";

	//Create ticket
	FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, userName, DateTime.Now,
	DateTime.Now.AddMinutes(1), false, userRole, FormsAuthentication.FormsCookiePath);

	//Encrypt ticket
	string encryptedTicket = FormsAuthentication.Encrypt(ticket);

	//Create cookie
	HttpCookie aCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);

	//Send cookie to the browser
	Response.Cookies.Add(aCookie);

	//Redirect user to originated page
	string url = Request.QueryString["ReturnUrl"];
	Response.Redirect(url);
}

Powyższy kod wymaga nieco wyjaśnienia. Na początku, na stałę, zakodowane są poprawne dane użytkownika – jego nazwa oraz rola. Następnie tworzony jest obiekt zwany ticketem, który określa obiekt identyfikator użytkownika oraz dane dla niego (w tym przypadku będzie to nazwa roli). Tak utworzony ticket jest szyfrowany, a potem na jego podstawie tworzone jest cookie, które wysyłane jest do przeglądarki. Użytkownik żądając strony, do której nie posiada dostępu został przekierowany na stronę z logowaniem. Aby można było powrócić do strony, do której chciał się on dostać, jej adres przekazany został za pomocą query string. Pobierając ten adres możliwe jest przekierowanie na oryginalnie żądaną stronę. Chciałbym zwrócić w tym momencie uwagę na to, że pomimo iż dane o roli zostały zapisane, aby móc na ich podstawie dokonać autoryzacji niezbędne są dodatkowe czynności (autoryzacja użytkownika zadziała po niewielkich zmianach w pliku Web.config).

W pliku Web.config musi znaleźć się wpis:

<authentication mode="Forms">
	<forms loginUrl="MyLogin.aspx" defaultUrl="Default.aspx"></forms>
</authentication>

dzięki czemu niezalogowany użytkownik będzie przekierowywany na stronę MyLogin.aspx, a jeśli nie żądał on wcześniej innej strony to zostanie przekierowany na Default.aspx.

Dostęp do stron może być dla każdej z nich ustawiony w inny sposób. Powiedzmy jednak, że chcemy aby dostęp do jakiejkolwiek ze stron miał jedynie zalogowany użytkownik. W tym celu pomiędzy znacznikami <system.web></system.web> musi znaleźć się kod:

<authorization>
	<deny users="?"/>
</authorization>

Umieszczenie znaku zapytania w powyższej komendzie oznacza, że odrzuceni zostaną wszyscy użytkownicy, z wyjątkiem tych zalogowanych.

Chcielibyśmy jednak dodatkowo rozróżniać rolę użytkownika, czyli np. zezwólmy na dostęp do strony ManageUsers.aspx jedynie użytkownikowi z grupy “Administrator”. W pliku Web.config należy zatem dodać kolejny wpis:

<location path="ManageUsers.aspx">
	<system.web>
		<authorization>
			<deny users="?"/>
			<allow roles="Administrator"/>
			<deny users="*"/>
		</authorization>
	</system.web>
</location>

Aby jednak rola użytkownika była rozpoznawana przez aplikację, musimy udać się do pliku Global.asax i zaimplementować zdarzenie Application_AuthenticateRequest():

void Application_AuthenticateRequest(object sender, EventArgs e)
{
	if (HttpContext.Current.User != null)
	{
		if (HttpContext.Current.User.Identity.IsAuthenticated)
		{
			FormsIdentity id = HttpContext.Current.User.Identity as FormsIdentity;
			if (id != null)
			{
				//Get user ticket
				FormsAuthenticationTicket ticket = id.Ticket;

				//Get user's role
				string userRole = ticket.UserData;

				//using System.Security.Principal is needed
				HttpContext.Current.User = new GenericPrincipal(id, new string[] { userRole });
			}
		}
	}
}

W ten sposób, dostęp do strony ManageUsers.aspx mają jedynie zalogowani użytkownicy i to w dodatku przynależący do roli Administrator.