Logo wanie Rejestracja
Logowanie Rejestracja
Wyszukiwarka Użyj bing
Wyszukiwarka Użyj bing
zaawansowana
zaawansowana
Akt ualności Forum Kalendarium Grupy Użyt kownicy Geek Club
Akt ualności Baza wiedzy Forum Kalendarium Grupy Użyt kownicy Geek Club
Artykuły
A A A
Aut or PrzemekG7
Opublikowane: 2006.04.22 13:00 | PrzemekG7 | Aktualizacja: 2010.01.21 2:04
Jak zabrać się za pisanie interfejsów użytkownika?
Art ykuł ma na celu pokazanie jednego ze sposobów na budowanie int erfejsów użyt kownika. Dodat kowo
przybliża t akie wzorce projekt owe jak MVP (Model-View-Present er) oraz Dependency Inject ion.
Załóż konto
Załóż konto
Wstęp
CodeGuru to miejsce dla
Od czasu kiedy pisałem swoje pierwsze aplikacje pod .NET, nurtowało mnie pytanie jak dobrze podejść do pisania
każdego programisty. Przez
aplikacji z interfejsem użytkownika. Problem stawał się tym większy im bardziej rozbudowany stawał się ów interfejs.
lata portal rozwijany był siłami
Zapewne jak większość z czytelników, zaczynałem od pisania całej logiki tzw metodą pod przyciskiem , czyli dla
społeczności i to właśnie
przykładu: kod pobierający dane z bazy znajdował się bezpośrednio w metodzie obsługującej zdarzenie Click jakiegoś
społeczność programistów
przycisku. Z pośród wielu wad takiego rozwiązania wymienię kilka:
jest tutaj najważniejsza. CG
od wielu lat gromadzi wokół
siebie coraz większą grupę
pasjonatów. Warto być jej
brak separacji warstw logiki i prezentacji
częścią!
Dowiedz się więcej o
brak możliwości przetestowania takiego kodu za pomocą testów jednostkowych
CodeGuru
GorÄ…ce tagi
mała czytelność kodu
vGuru, Hyper-V, Private Cloud,
Wirtualizacja, Geek Club, Windows 8,
Azure, Windows Phone, ASP.NET,
Windows, SQL
PDFmyURL.com
Windows, SQL
Przed czytaniem dalszej części artykułu, dobrze byłoby mieć podstawową wiedzę na temat wzorca MVP (Mo de l-
Vie w-Pre se nt e r) oraz wzorca De pe nde nc y Inje c t ion, a także znać podstawowe mechanizmy refleksji w .NET.
Tagi
Zatem zaczynamy od utworzenia nowego projektu (Class Library) w Visual Studio 2005. W moim przypadku projekt
Access 2010 AD RMS BitLocker
będzie się nazywał Light UIFrame work.
BlackBery Crystal Reports DirectX
DLR Dynamic Memory Exchange
Separacja warstw
Online Expression Encoder FxCop
Groove Infrastruktura IronRuby
laboratoria LINQ to Objects Lync
Jako podstawowy wzorzec separacji warstw logiki i interfejsu użytkownika wybieramy wspomniany wcześniej wzorzec
2010 Lync Online MCITP MCSE
MVP. Jest on niemal identyczny z popularnym wzorcem MVC (Model-View-Controler) jednak jego zaletą jest większa
MCT mercurial Microsoft Dynamics
testowalność. Nie będę przedstawiał zasady działania tych wzorców a jedynie zamieszczę schematy dla lepszego
AX Microsoft Dynamics CRM Microsoft
zobrazowania.
Surface
Pokaż wszystkie tagi
Do naszego projektu dodajemy katalog MVP. A w nim tworzymy interfejs IView.cs oraz klasÄ™ PresenterBase.cs
[Kod C#]
PDFmyURL.com
using System;
using System.Collections.Generic;
using System.Text;
namespace LightUIFramework.MVP
{
public interface IView
{
event EventHandler Initialize;
}
}
[Kod C#]
using System;
using System.Collections.Generic;
using System.Text;
using LightUIFramework.Commands;
using LightUIFramework.Services;
namespace LightUIFramework.MVP
{
public abstract class PresenterBase
where T : IView
{
protected T view;
public PresenterBase(T view)
{
this.view = view;
view.Initialize += new EventHandler(OnInitialize);
}
protected virtual void OnInitialize(object sender, EventArgs e)
{
}
}
}
PDFmyURL.com
W praktyce wszystkie kontrolki użytkownika oraz formy będą implementować konkretny interfejs dziedziczący po
IView , a ponadto dla każdej takiej kontrolki użytkownika (lub formy) będzie utworzony prezenter dziedziczący po
klas ie PresenterBase , którego zadaniem jest m.in wykonywanie operacji na widoku. Interfejs widoku posiada
zdarzenie Initialize, które może być wywołane z konkretnego widoku i obsłużone w prezenterze bazowym, bądz też w
prezenterze dedykowanym dla konkretnego widoku (poprzez przeciążenie metody OnInit ializ e ).
Komendy i akcje
Pisząc aplikacje z interfejsem użytkownika bardzo często napotykamy na problem wykonywania pewnych akcji z
różnych miejsc np. akcję Ot wó rz plik chcemy wywołać z poziomu menu oraz przyciskiem na formie. Drugim
problemem jest przechwytywanie wspomnianych akcji dla przykładu: chcemy aby wykonanie akcji Ot wórz plik
aktualizowało paski statusu w różnych miejscach. Zatem spróbujmy zaimplementować jedno z możliwych rozwiązań
tych problemów. Zacznijmy od dodania do projektu nowego folderu o nazwie Commands i zdefiniowania klasy
CommandBase.cs
[Kod C#]
using System;
using System.Windows.Forms;
using System.Collections.Generic;
using System.Text;
namespace LightUIFramework.Commands
{
public class CommandBase
{
private string name;
private List invokers = new List();
private List toolInvokers = new List();
public event EventHandler Action;
public CommandBase(string name)
PDFmyURL.com
{
this.name = name;
}
public string Name
{
get { return name; }
}
public bool Enabled
{
set
{
ChangeEnabled(value);
}
}
public bool Visible
{
set
{
ChangeVisible(value);
}
}
public void AddInvoker(Control control)
{
invokers.Add(control);
control.Click += new EventHandler(OnInvokerClick);
}
public void AddInvoker(ToolStripItem item)
{
toolInvokers.Add(item);
item.Click += new EventHandler(OnInvokerClick);
}
#region Private Methods
PDFmyURL.com
private void OnInvokerClick(object sender, EventArgs args)
{
CommandEventArgs e = new CommandEventArgs(this.name, args);
Action(sender, e);
}
private void ChangeEnabled(bool enable)
{
foreach (Control control in invokers)
{
control.Enabled = enable;
}
foreach (ToolStripItem item in toolInvokers)
{
item.Enabled = enable;
}
}
private void ChangeVisible(bool visible)
{
foreach (Control control in invokers)
{
control.Visible = visible;
}
foreach (ToolStripItem item in toolInvokers)
{
item.Visible = visible;
}
}
#endregion
}
PDFmyURL.com
public class CommandEventArgs : EventArgs
{
private EventArgs senderArgs;
private string commandName;
public CommandEventArgs(string commandName, EventArgs senderArgs)
{
this.commandName = commandName;
this.senderArgs = senderArgs;
}
public EventArgs SenderArgs
{
get { return senderArgs; }
}
public string CommandName
{
get { return commandName; }
}
}
}
Przyjrzyjmy się nieco bliżej klasie CommandBas e. Każda komenda musi posiadać swoją unikalną nazwę. Ponadto
posiada takie właściwości jak:
Enabled włącza lub wyłącza wszystkie kontrolki wywołujące daną akcję
Visible pokazuje lub ukrywa wszystkie kontrolki wywołujące daną akcję
Gdy jedna z kontrolek zgłosi zdarzenie Clic k komenda generuje zdarzenie Ac t io n z następującymi parametrami:
PDFmyURL.com
sender (object ) kontrolka która zgłosiła zdarzenie Clic k
args (CommandEventArgs ) argumenty: nazwa komendy, argumenty przekazane przez zdarzenie z kontrolki
Komentarza wymaga także metoda AddInvoke r po wykonaniu której, dodana kontrolka będzie wywoływać akcję (gdy
zostanie zgłoszone zdarzenie Clic k kontrolki). Nie wchodząc w szczegóły implementacyjne możemy przejść do
utworzenia klasy CommandHandlerAttribute.cs . Jak nazwa wskazuje będzie to klasa definiująca atrybut umożliwiający
metodÄ… opatrzonym tym atrybutem subskrypcje akcji.
[Kod C#]
using System;
using System.Collections.Generic;
using System.Text;
namespace LightUIFramework.Commands
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple=true)]
public class CommandHandlerAttribute : Attribute
{
private string commandName;
public CommandHandlerAttribute(string commandName)
{
this.commandName = commandName;
}
public string CommandName
{
get { return commandName; }
PDFmyURL.com
set { commandName = value; }
}
}
}
Zdefiniowany wyżej atrybut może być przypisywany wyłącznie metodą, oraz może występować wiele razy przy jednej
metodzie. W naszym przypadku znaczy to tyle, że metoda może subskrybować więcej niż jedną akcję.
MajÄ…c te dwie klasy potrzebujemy mechanizmu spinajÄ…cego je ze sobÄ…, zatem dodajmy do projektu klasÄ™
CommandsManager.cs
[Kod C#]
using System;
using System.Collections.Generic;
using System.Text;
namespace LightUIFramework.Commands
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple=true)]
public class CommandHandlerAttribute : Attribute
{
private string commandName;
public CommandHandlerAttribute(string commandName)
{
this.commandName = commandName;
}
public string CommandName
{
get { return commandName; }
set { commandName = value; }
}
}
}
PDFmyURL.com
Zdefiniowany wyżej atrybut może być przypisywany wyłącznie metodą, oraz może występować wiele razy przy jednej
metodzie. W naszym przypadku znaczy to tyle, że metoda może subskrybować więcej niż jedną akcję.
MajÄ…c te dwie klasy potrzebujemy mechanizmu spinajÄ…cego je ze sobÄ…, zatem dodajmy do projektu klasÄ™
CommandsManager.cs
[Kod C#]
using System;
using System.Reflection;
using System.Collections.Generic;
using System.Text;
namespace LightUIFramework.Commands
{
public static class CommandsManager
{
private static Dictionary> handlers =
new Dictionary>();
private static Dictionary commands =
new Dictionary();
public static Dictionary Commands
{
get { return CommandsManager.commands; }
}
public static void RegisterCommand(CommandBase command)
{
commands.Add(command.Name, command);
PDFmyURL.com
handlers.Add(command.Name, new List());
command.Action += new EventHandler(command_Action);
}
private static void command_Action(object sender, CommandEventArgs e)
{
foreach (HandlerInfo handlerInfo in handlers[e.CommandName])
{
object[] parameters = new object[2] { sender, e.SenderArgs };
handlerInfo.MethodInfo.Invoke(handlerInfo.Obj, parameters);
}
}
internal static void RegisterHandlers(object obj)
{
Type type = obj.GetType();
MethodInfo[] methodInfos = type.GetMethods();
foreach (MethodInfo methodInfo in methodInfos)
{
CommandHandlerAttribute[] attributes = methodInfo.GetCustomAttributes(
typeof(CommandHandlerAttribute), true) as CommandHandlerAttribute[];
if (attributes != null)
{
foreach (CommandHandlerAttribute attribute in attributes)
{
HandlerInfo handlerInfo = new HandlerInfo(methodInfo, obj);
handlers[attribute.CommandName].Add(handlerInfo);
}
}
}
}
private class HandlerInfo
{
private MethodInfo methodInfo;
private object obj;
public HandlerInfo(MethodInfo methodInfo, object obj)
{
PDFmyURL.com
this.methodInfo = methodInfo;
this.obj = obj;
}
public MethodInfo MethodInfo
{
get { return methodInfo; }
}
public object Obj
{
get { return obj; }
}
}
}
}
Podobnie jak to miało miejsce wcześniej szczegóły implementacyjne pozostawiam do analizy, natomiast skupię się
na najistotniejszych mechanizmach. Każda komenda musi być zarejestrowana, do tego celu służy metoda
Re gist e rCo mmand, która jako parametr przyjmuje obiekt klasy CommandBase , jak już wspomniałem nazwy
komend muszą być unikalne. Jeśli komenda zostanie prawidłowo zarejestrowana to w momencie zgłoszenia
zdarzenia Ac t io n zostanie ono przechwycone w metodzie c o mmand_Ac t ion. Ta z kolei metoda wywoła wszystkie
metody które subskrybują (opatrzone atrybutem Co mmandHandle r) wywołaną komendę.
DrugÄ… istotnÄ… metodÄ… jest metoda Re gist e rHandle rs, jej zadaniem jest zarejestrowanie wszystkich metod
opatrzonych atrybutem CommandHandle r z danego obietku (przekazanego jako parametr). Jej sposób realizacji
jest prostym przykładem użycia refleksji.
W tym momencie mamy już gotowy mechanizm obsługi komend i pozostaje nam tylko użycie metod rejestrujących w
odpowiednich miejscach. Zakładamy że z mechanizmu będą korzystały obiekty dziedziczące po IView oraz
PresenterBase . Komendy powinny być zarejestrowane przed ich pierwszym użyciem, natomiast obiekty, które będą
wywoływały akcję powinnym być dodane w klasie widoku. Dlatego też do interfejsu IView dodajemy metodę
AddCo mmandInvo ke rs.
[Kod C#]
PDFmyURL.com
using System;
using System.Collections.Generic;
using System.Text;
namespace LightUIFramework.MVP
{
public interface IView
{
event EventHandler Initialize;
void AddCommandInvokers();
}
}
Natomiast rejestracja metod subskrybujących będzie miała miejsce podczas tworzenia obiektu prezentera.
Uwaga! Konstruktor klasy bazowej PresenterBase musi być zawsze wywoływany przez konstruktor klasy potomnej.
[Kod C#]
using System;
using System.Collections.Generic;
using System.Text;
using LightUIFramework.Commands;
using LightUIFramework.Services;
namespace LightUIFramework.MVP
{
public abstract class PresenterBase where T : IView
{
protected T view;
public PresenterBase(T view)
{
this.view = view;
view.Initialize += new EventHandler(OnInitialize);
view.AddCommandInvokers();
CommandsManager.RegisterHandlers(this);
PDFmyURL.com
CommandsManager.RegisterHandlers(view);
}
protected virtual void OnInitialize(object sender, EventArgs e)
{
}
}
}
Serwisy
Ostatnią funkcjonalnością o jaką wzbogacimy nasz projekt będzie mechanizm serwisów. W tym kontekście przez
serwis będziemy rozumieć pewną zewnętrzną funkcjonalność (logikę) udostępnianą poprzez interfejs. Dla przykładu:
nasz prezenter będzie potrzebował pewnej logiki odpowiedzialnej za pobranie danych z bazy. Logika ta będzie
umieszczona w zewnętrznym module. Funkcjonalność oczekiwana przez nasz prezenter jest zdefiniowana przez
interfejs, a dostęp do niej jest możliwy przez publiczne pole w interfejsie. Zadaniem naszego mechanizmu serwisów
będzie dostarczenie implementacji tegoż interfejsu poprzez wstrzyknięcie go do prezentera. Brzmi to trochę zawile
lecz nie jest to tak skomplikowane jak się może wydawać.
Pola zawierające serwisy które mają zostać wstrzyknięte muszą być opatrzone odpowiednim atrybutem. Tworzymy
więc nowy katalog o nazwie Se rvic e s i dodajemy do niego klasę ServiceInjectionAttribute.cs
[Kod C#]
using System;
using System.Collections.Generic;
using System.Text;
namespace LightUIFramework.Services
{
[AttributeUsage(AttributeTargets.Property)]
PDFmyURL.com
public class ServiceInjectionAttribute : Attribute
{
private Type serviceType;
public ServiceInjectionAttribute(Type serviceType)
{
this.serviceType = serviceType;
}
public Type ServiceType
{
get { return serviceType; }
}
}
}
Atrybut zawierał będzie informację o typie serwisu który ma zostać wstrzyknięty (utworzony) w danym polu (używam
nazwy pole mając na myśli Property). Tak jak miało to miejsce w przypadku mechanizmu komend tak również tu
musimy mieć obiekt który będzie zarządzał serwisami. Zatem dodajemy do projektu klasę ServicesManager.cs
[Kod C#]
using System;
using System.Reflection;
using System.Collections.Generic;
using System.Text;
namespace LightUIFramework.Services
{
public static class ServicesManager
{
private static Dictionary services = new Dictionary();
public static void RegisterService(Type serviceInterface, Type serviceImplementation)
PDFmyURL.com
{
if (serviceImplementation.GetInterface(serviceInterface.Name, false) != null)
{
object serviceInstance = Activator.CreateInstance(serviceImplementation);
services.Add(serviceInterface, serviceInstance);
}
else
{
throw new ArgumentException("Object does not implement declared interface");
}
}
internal static void InjectService(object obj)
{
Type objType = obj.GetType();
PropertyInfo[] propInformations = objType.GetProperties();
foreach (PropertyInfo propInfo in propInformations)
{
ServiceInjectionAttribute[] attributes = propInfo.GetCustomAttributes(
typeof(ServiceInjectionAttribute), true) as ServiceInjectionAttribute[];
if (attributes != null)
{
if (attributes.Length == 1)
{
object serviceToInject = services[attributes[0].ServiceType];
propInfo.SetValue(obj, serviceToInject, null);
}
}
}
}
}
}
Serwis przed pierwszym użyciem musi zostać zarejestrowany, do tego celu służy metoda Re gist e rSe rvic e , jej
pierwszym parametrem jest typ interfejsu serwisu, natomiast drugim parametrem jest typ klasy która będzie
PDFmyURL.com
reprezentowała implementację tego interfejsu. Jeden interfejs może posiadać tylko jedną implementację w danej
instancji aplikacji. Drugą metodą naszego managera jest metoda Inje c t Se rvic e której zadaniem jest tworzenie
serwisów w konkretnych obiektach. Metoda szuka pól opatrzonych atrybutem Se rvic e Inje c t io n i wstrzykuje
implementację serwisów.
Ostatnią sprawą do załatwienia jest zarejestrowanie dostępnych serwisów. Powinno się to odbywać przy starcie
aplikacji, zatem utworzymy sobie klasę Lunc her.c s zakładając jednocześnie, że aplikacja korzystająca z naszej
biblioteki będzie musiała mieć klasę z której startuję wydziedziczoną po naszej klasie Luncher.
[Kod C#]
using System;
using System.Reflection;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
namespace LightUIFramework.Startup
{
public class Luncher
where T: Form
{
private Form startupForm;
public Luncher()
{
RegisterServices();
RegisterCommands();
}
protected virtual void RegisterServices()
{
}
protected virtual void RegisterCommands()
{
}
PDFmyURL.com
public void Run()
{
T form = Activator.CreateInstance(typeof(T)) as T;
Application.Run(form);
}
}
}
W momencie tworzenia obiektu zarejestrowane zostaną serwisy i komendy znajdujące się w przeciążonych metodach
w klasie dziedziczącej po Lunc he r. Metoda Run uruchamia aplikację używając przekazanej jako formy startowej
przekazanej przez generics.
W tym momencie nasza biblioteka jest gotowa do użycia.
Przykład wykorzystania
Jako przykład wykorzystania dołączam prostą aplikację wykonującą operację inkrementacji i dekrementacji liczby w
zakresie od -5 do 5. Dodatkowo generowana jest historia zmian. Operacje mogą być wykonywane poprzez menu bądz
też poprzez przyciski. W momencie gdy aktualna wartość jest różna od 0 widoczna jest komenda Reset.
PDFmyURL.com
Nie będą opisywał jak krok po kroku stworzyć taką aplikację, a jedynie skoncentruję się na istotnych punktach.
Przyjrzyjmy się zatem bliżej klasie odpowiedzialnej za uruchomienie naszej aplikacji TestAppLuncher.cs
[Kod C#]
using System;
using System.Collections.Generic;
using System.Text;
using LightUIFramework.Startup;
using LightUIFramework.Commands;
PDFmyURL.com
using LightUIFramework.Services;
using TestApplication.Services;
namespace TestApplication
{
public class TestAppLuncher : Luncher
{
protected override void RegisterCommands()
{
CommandsManager.RegisterCommand(new CommandBase(CommandConstans.INCREMENT));
CommandsManager.RegisterCommand(new CommandBase(CommandConstans.DECREMENT));
CommandsManager.RegisterCommand(new CommandBase(CommandConstans.RESET));
base.RegisterCommands();
}
protected override void RegisterServices()
{
ServicesManager.RegisterService(typeof(ITestFormService), typeof(TestFormService));
base.RegisterServices();
}
}
}
Jak już wspomniałem klasa taka musi dziedziczyć po klasie Luncher zdefiniowanej wcześniej. Nasza aplikacja będzie
korzystać z trzech komend: inkrementacji, dekrementacji oraz resetowania. Są one rejestrowane w metodzie
Re gist e rCommands. Użyty też zostanie jeden serwis który będzie odpowiedzialny za wykonanie wyżej
wymienionych operacji. Serwis rejestrowany jest w metodzie Re gist e rSe rvic e s. Jako forma startowa naszej
aplikacji została zdefiniowana forma TestForm .
[Kod C#]
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using LightUIFramework.Commands;
PDFmyURL.com
using TestApplication.Views;
using TestApplication.Presenters;
namespace TestApplication
{
public partial class TestForm : Form, ITestFormView
{
TestFormPresenter presenter;
public TestForm()
{
InitializeComponent();
presenter = new TestFormPresenter(this);
Initialize(this, null);
}
#region IView Members
public event EventHandler Initialize;
public string Status
{
get
{
return lblInfo.Text;
}
set
{
lblInfo.Text = value;
}
}
public void AddCommandInvokers()
{
CommandsManager.Commands[CommandConstans.INCREMENT].AddInvoker(btnIncrement);
CommandsManager.Commands[CommandConstans.INCREMENT].AddInvoker(incrementToolStripMenuItem);
CommandsManager.Commands[CommandConstans.DECREMENT].AddInvoker(btnDecrement);
PDFmyURL.com
CommandsManager.Commands[CommandConstans.DECREMENT].AddInvoker(decrementToolStripMenuItem);
CommandsManager.Commands[CommandConstans.RESET].AddInvoker(btnReset);
}
#endregion
[CommandHandler(CommandConstans.INCREMENT)]
[CommandHandler(CommandConstans.DECREMENT)]
[CommandHandler(CommandConstans.RESET)]
public void RegisterHistory(object sender, EventArgs args)
{
lstHistory.Items.Add(lblInfo.Text);
}
}
}
W metodzie AddCommandInvoke r spinamy nasze wcześniej zarejestrowane komendy z konkretnymi kontrolkami.
Przykład użycia komend prezentuje metoda Re gist e rHist o ry. Jak można zauważyć prezenter tworzony jest
konstruktorze klasy widoku, jednak możliwe jest stworzenie mechanizmu który automatycznie tworzyłby obiekt
prezentera. Aby to osiągnąć należałoby rozbudować nieco naszą bibliotekę odpowiednio wykorzystując wzorzec
De pe nde nc y Inje c t ion.
Na koniec przykładu jeszcze rzut okiem na funkcje Main która wykorzystując TestAppLuncher uruchamia aplikację.
[Kod C#]
...
[STAThre ad]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
new TestAppLuncher().Run();
PDFmyURL.com
}
Podsumowanie
Poruszona w artykule tematyka jest oczywiście znacznie bardziej rozbudowana niż to przedstawiłem, dlatego
zachęcam do dalszych poszukiwań, a szczególnie do zapoznania się z wydanym przez MS Patterns&Practices
frameworkiem CAB. Zbudowana w artykule biblioteka stanowi jedynie pewien wycinek funkcjonalności jaką powinien
oferować framework do budowania aplikacji klienckich. Nie mniej jednak mam nadzieję, że udało mi się przybliżyć tą
wg mnie dosyć ciekawą tematykę.
Przydatne linki
MVP - http://www.martinfowler.com/eaaDev/ModelViewPresenter.html
Dependency Injection - http://www.martinfowler.com/articles/injection.html
CAB - http://practices.gotdotnet.com/projects/cab
Załąc z niki:
project.zip
Podziel siÄ™:
PDFmyURL.com
Podobne art ykuły
Kome nt arz e 0 Masz uwagi do tej strony? Napisz
Dodaj kome nt arz
Idz na górę strony
O serwisie Kontakt Mapa witryny Zasady użytkowania Znaki towarowe Ochrona prywatności
© 2011 Microsoft Sp z.o.o. Wszelkie prawa zastrzeżone
PDFmyURL.com
Wyszukiwarka
Podobne podstrony:
www haker pl haker start pl warsztaty1 temat=3(1)
micros multimetry www przeklej pl
www apextk pl index
www haker pl haker start pl warsztaty1 temat=30(1)
Raport?danie? Krakow 06 [ www potrzebujegotowki pl ]
program szkolenia specjalistycznego www katalogppoz pl
jarmex instrukcja montazu bramy dwuskrzydlowej (www instrukcja pl)
www mediweb pl sex wyswietl vad php id=703
adam bytof moc autohipnozy www przeklej pl
Krystyna Janda www malpa2 pl
www mediweb pl?ta print php id=695
więcej podobnych podstron