ToyStore - sklep z zabawkami ( Sitemesh, JSF, Spring, Hibernate, Oracle Express )- CZĘŚĆ I - warstwa danych i biznesowa

Wstęp
Już pierwszy kontakt z frameworkiem JSF, jaki zyskałem po zrobieniu pierwszej aplikacji (spring+jsf) przekonał mnie, że obrany przeze mnie kierunek jest jak najbardziej właściwy. JSF wydaje się być tym czego od dawna już szukałem - sam framework i biblioteka komponentów, wprowadzają rewolucję do projektowania interfejsów webowych. Koniec z JSTL i iterowaniem po kolekcjach :))).

Zachęcony pierwszym sukcesem postanowiłem przystąpić do realizacji trochę bardziej złożonego projektu.

Według moich założeń za warstwę prezentacji bezpośrednio odpowiadałby JSF udekorowany przy pomocy SiteMesha, do tworzenia obiektów warstwy biznesowej i dao chciałbym użyć Springa, jako ORMa użyłbym Hibernate. Jako bazę relacyjną wybrałem Oracle Express. Dodatkowo Spring wystąpiłby w roli frameworka integracyjnego i głównego "wstrzykiwacza" zależności ;)). Wszystko składaneby było do kupy Antem i deployowane na JBoss-a 4.0.5.

Założenia projektu:
Cała aplikacja realizowałaby kila przypadków użycia z zakresu prezentacji i zarządzania produktów przypisanych do określonych kategorii.


No cóż... moja aplikacja swą złożonością na pewno nikogo nie powali na kolana, ale do prezentacji możliwości frameworków wydaje się całkiem ok.

Środowisko projektowe:

Jak większość moich projektów, tak i ten powstanie w Eclipse dozbrojonym przez Hibernate Tools, WTP i JBoss IDE. Projekt będzie się nazywał MWToyStoreJSFApp i będzie miał następującą strukturę:


Kolejny krok to import i kompletowanie wszystkich potrzebnych bibliotek. Ich lista w końcu ustaliła się na:
********************
antlr-2.7.6.jar
asm-attrs.jar,asm.jar
glib-2.1.3.jar, cglib-nodep-2.1_3.jar
commons-beanutils.jar, commons-collections-2.1.1.jar, commons-collections.jar, commons-digester.jar, commons-logging-1.0.4.jar, commons-logging.jar
dom4j-1.6.1.jar
ehcache-1.2.jar
hibernate3.jar
html_basic.tld
jdbc2_0-stdext.jar
jsf-api.jar, jsf-impl.jar
jsf_core.tld, jstl.jar
jta.jar
log4j-1.2.11.jar
ojdbc14.jar
servlet-api.jar
sitemesh-2.3.jar, sitemesh-decorator.tld, sitemesh-page.tld
spring-aspects.jar, spring-mock.jar, spring.jar, standard.jar

********************

Pierwsze planowanie

I w ten oto sposób zrobiłem pierwszy krok - powstał zaczątek mojego projektu. Nadszedł czas tworzenia koncepcji. Na chwilę obecną skupię się na zaplanowanie warstwy danych i biznesowej, JSF ciągle stanowi dla mnie zagadkę, więc tworzeniem modelu i implementacją warstwy web zajmę się w późniejszym etapie realizacji.

Na początek stworzyłem iście imponujący model dziedziny:


Zarządzanie obiektami encji odbywać się będzie za pośrednictwem warstwy danych, na którą w mojej aplikacji złoży się niewielki interfejs IProductManagerDao i implementująca go klasa dostępowa ProductManagerDaoHibernate.


Pomiędzy warstwą danych a web wprowadzę biznesową warstwę pośredniczącą. Tak samo jak w warstwie niższej złoży się na nią jeden interfejs metod biznesowych IProductManager i implementująca go klasa ProductManager.


Tak jak wspomniałem już wcześniej warstwę web muszę potraktować w sposób indywidualny, dlatego na tę chwilę wstrzymam się z projektowaniem i wreszcie zacznę implementację :))).

Przygotowanie bazy danych:
Na samym początku na podstawie modelu dziedziny piszę skrypt inicjalizacji bazy danych:

CREATE TABLE CATEGORY(ID NUMBER NOT NULL PRIMARY KEY,
DESCRIPTION VARCHAR(500),
NAME VARCHAR(50),
LOGO VARCHAR(10));

CREATE TABLE PRODUCT(ID INT NOT NULL PRIMARY KEY,
CATID INT NOT NULL,
DESCRIPTION VARCHAR(500),
PRICE NUMBER(10),
NAME VARCHAR2(50),
LOGO VARCHAR2(50),
CONSTRAINT fk_products_in_cat FOREIGN KEY (CATID)REFERENCES CATEGORY (ID)
);

INSERT INTO CATEGORY(ID,NAME,DESCRIPTION) VALUES(1,'Ksiazki','Wszystkie ksiazki');
INSERT INTO CATEGORY(ID,NAME,DESCRIPTION) VALUES(2,'Czasopisma','Dzienniki, miesięczniki');
INSERT INTO CATEGORY(ID,NAME,DESCRIPTION) VALUES(3,'Broszury','Przewodniki turystyczne');

INSERT INTO PRODUCT(ID,NAME,CATID,DESCRIPTION,PRICE) VALUES(1,'Winnetou',1,'Wszystkie trzy tomy',10);
INSERT INTO PRODUCT(ID,NAME,CATID,DESCRIPTION,PRICE) VALUES(2,'Tomek Wilmowski',1,'Wszystkie części powieści przygodowej',50);

INSERT INTO PRODUCT(ID,NAME,CATID,DESCRIPTION,PRICE) VALUES(3,'Motor',2,'Miesięcznik',10);
INSERT INTO PRODUCT(ID,NAME,CATID,DESCRIPTION,PRICE) VALUES(4,'Piłka Nożna',2,'Miesięcznik',10);
INSERT INTO PRODUCT(ID,NAME,CATID,DESCRIPTION,PRICE) VALUES(5,'Mój Pies',2,'Miesięcznik',10);

INSERT INTO PRODUCT(ID,NAME,CATID,DESCRIPTION,PRICE) VALUES(6,'Przewodnik turystyczny',3,'Broszura',10);

Jeszcze tylko kilka ustawień w moim build. xml, uruchomienie, radość z poprawnego wykonania się skryptu i oto moje tabele znalazły się w bazie danych. Uff.. a więc coś się ruszyło. :).

Tworzenie mapowań ORM (Category.hbm.xml i Product.hbm.xml) obiektów domeny
Mapowania te stworzyłem przy pomocy Hibernate Tools. Na początek, posługując się wizzardem(File->New->Project->Hibernate->Other->Hibernate Configuration File) zdefiniowałem dane potrzebne do ustanowienia połączenie do bazy danych. W wyniku jego działania otrzymałem plik konfiguracyjny hibernate.cfg.xml . Ma on następującą postać:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"> <hibernate-configuration>
<session-factory>
<property name="hibernate.connection.driver_class">oracle.jdbc.driver.OracleDriver
</property>

<property name="hibernate.connection.password">abc</property>
<property name="hibernate.connection.url">jdbc:oracle:thin:@localhost:1521:XE</property>

<property name="hibernate.connection.username">wojcikm</property>
<property name="hibernate.default_schema">WOJCIKM</property>

<property name="hibernate.dialect">org.hibernate.dialect.Oracle9Dialect</property>

</session-factory>

</hibernate-configuration>

Po stworzniu pliku konfiguracyjnego przełączam perspektywe na "Hibernate Console"
(Window->Open Perspective->Hibernate Console) , i w jej obrębie otwaram sobie widok Hibernate Configurations (Window->Show View->Hibernate Configurations) .
Teraz w jego obrębie, klikam na prawy przycisk myszy i po otwarciu menu kontekstowego wybieram opcję Add Configuration. Otwiera się wizzard, w którym podaję dane konfiguracji:

Kluczowym krokiem jest podanie "Configuration file" - tu należy wskazać wygenerowany wcześnej plik "hibernate.cfg.xml" .

Po poprawnym zdefiniowaniu połączenia plugin pozwala na przeglądanie obiektów bazy danych.

Po rozwinięciu węzła "Database" powinny pojawić się obiekty DB.

UWAGA!!!
Wiele czasu i nerwów straciłem na dociekanie przyczyn wystąpienia pewnego błędu. Otóż przy próbie rozwinięcia gałązki nie pojawiało się nic.

Okazało się że przyczyna leży w pliku hibernate.cfg.xml, a dokładniej w jego opcji:

<property name="hibernate.default_schema">WOJCIKM</property>


Z niewiadomych mi przyczyn jej wartość musi zostać napisana WIELKIMI LITERAMI.

Po zdefiniowaniu "Console Configuration" czas przystąpić do wygenerowania plików mapowań Hibernate. W tym celu przez wybranie opcji z belki narzędziowej uruchamiam kolejny wizzard:

Po otwarciu się okna neleży wypełnić opcje:
(zakładka Main)

  • Console Confiugration - z listy należy wybrać nazwę ostatnio zdefiniowanej konfiguracji
  • Output Directory - katalog do którego wygenerowane zostaną pliki
  • Package - pakiet do którego należeć będą pliki (mw.toystore.domain)
(zakładka Exporters)
  • Hibernate XML Mappings (.hbm.xml)

Po wciśnięciu przycisku Run do wskazanego katalogu zostaną wygenerowane pliki mapowań.

Plugin tworzy oddzielny plik dla każdej encji db, ja łączę je w jeden duży plik HibernateMapping.hbm.xml i umieszczam w katalogu WEB-INF/context . Plik ten ma następującą postać. Należy pamiętać, że mapowania w tym pliku muszą być całkowicie zgodne z właściwościami obiektów domeny (mw.toystore.domain.*). Obiekty domenowe mogą również zostać wygenerowane przez plugin - jednak ja wolałem zrobić to ręcznie.

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="mw.toystore.domain.Category" table="CATEGORY">
<id name="id" column="ID">
<generator class="sequence">
<param name="sequence">WOJCIKM.CATEGORY_ID_SEQ</param>
</generator>
</id>
<property name="description" type="string">
<column name="DESCRIPTION" length="500" />
</property>
<property name="name" type="string">
<column name="NAME" length="50" />
</property>
<property name="logo" type="string">
<column name="LOGO" length="10" />
</property>
<set name="products" inverse="true">
<key>
<column name="CATID" precision="22" scale="0" not-null="true" />
</key>
<one-to-many class="mw.toystore.domain.Product" />
</set>
</class>


<class name="mw.toystore.domain.Product" table="PRODUCT">
<id name="id" column="ID">
<generator class="sequence">
<param name="sequence">WOJCIKM.PRODUCT_ID_SEQ</param>
</generator>
</id>
<many-to-one name="category" class="mw.toystore.domain.Category" fetch="select">
<column name="CATID" precision="22" scale="0" not-null="true" />
</many-to-one>
<property name="description" type="string">
<column name="DESCRIPTION" length="500" />
</property>
<property name="price" type="java.lang.Long">
<column name="PRICE" precision="10" scale="0" />
</property>
<property name="name" type="string">
<column name="NAME" length="50" />
</property>
<property name="logo" type="string">
<column name="LOGO" length="50" />
</property>
</class>
</hibernate-mapping>


Kod obiektów domeny zgodny z powyższym mapowaniem wygląda następująco:




Realizacja warstwy biznesowej - dostępu do danych (dao)
Kod metod klasy realizującej operacje na obiektach domeny - mw.toystore.db.dao.ProductManagerDaoHibernate (klasa ta implementuje interfejs mw.toystore.db.dao.IProductManagerDao) ma ma następującą postać (kod niekompletny):

package mw.toystore.db.dao;

import mw.toystore.domain.Product;
import mw.toystore.domain.Category;
import java.util.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
import org.hibernate.*;

/**
* @created 18-sie-2006 11:49:42
* @version 1.0
*/
public class ProductManagerDaoHibernate extends HibernateDaoSupport implements
IProductManagerDao {

private Log logger = LogFactory.getLog(getClass());
public List getCategoryList() {
return getHibernateTemplate().find("from Category");
}

public Category getCategoryById(int id) {
return (Category) getHibernateTemplate().get(Category.class,
new Integer(id));
}

public void saveCategory(Category cat) {
getHibernateTemplate().saveOrUpdate(cat);
}

public void removeCategory(Category cat) {
Category c=this.getCategoryById(cat.getId().intValue());
this.getHibernateTemplate().delete(c);
}
....
}



Zadaniem metod których kod zamieściłem powyżej jest manipulacja obiektami domenowymi. Klasa ta jest wydziedziczona z klasy HibernateDaoSupport pochodzącej ze Springa. Cecha ta powoduje że w kodzie metod można wykorzystywać springowe wsparcie dla frameworka Hibernate. Powoduje to daleko idące uproszczenie kodu metod. Nice !!!! :))


Realizacja warstwy biznesowej
Tak jak wspominałem wcześniej logika biznesowa jest realizowana przez klasę mw.toystore.ProductManager która implementuje interfejs mw.toystore.IProductManager.
Fragment jej kodu wygląda następująco:

public class ProductManager implements IProductManager {

private Log logger = LogFactory.getLog(getClass());
private IProductManagerDao productManager;

public Product getProductById(int id){
return productManager.getProductById(id);
}

public void saveProduct(Product p){
this.productManager.saveProduct(p);
}
...

}


W moim przypadku znaczenie tej warstwy jest niewielkie. Kod metod odwołuje się do adekwatnych metod z warstwy Dao za pośrednictwem interfejsu IProductManagerDao.

Nie muszę martwić się o prawidłową jego inicjalizacje. O to zadba framework IoC - czyli Spring(ale o tym za chwilę ) :).

Konfiguracja kontekstu springowego
Kontrolę nad poprawną inicjalizacją obiektów sprawuje kontroler IoC (Inversion of Control), czyli w przypadku mojej aplikacji framework Spring. Aby mógł od dobrze wykonywać swoją pracę musi on zostać gruntownie poinformowany o wszystkich obiektach aplikacji i wzajemnych relacjach, które pomiędzy nimi zachodzą.

Pliki konfiguracyjne kontenera IoC umieściłem w podkatalogu /WEB-INF/contex. Przy inicjalizacji całej aplikacji automatycznie wczytywany jest plik /WEB-INF/applicationContext.xml (nazwa musi być ustandaryzowana !!!!) w nim to zawarte są odwołania do kilku plików z podkatalogu /WEB-INF/context.

Wymagane jest istnienie tylko pliku /WEB-INF/applicationContext.xml, mogłyby znaleźć się tu wszystkie mapowania jednak w celu łatwiejszego zapanowania nad konfiguracją podzieliłem ją na pliki:

  • /WEB-INF/applicationContext.xml
  • /WEB-INF/context/springapp-servlet-datasources.xml
  • /WEB-INF/context/springapp-servlet-db.xml
  • /WEB-INF/context/HibernateMapping.hbm.xml
  • /WEB-INF/context/springapp-servlet-bus.xml

Plik applicationContext.xml

Plik springapp-servlet-datasources.xml

<!--
*********************************************************
************* Warstwa konfiguracji źródeł danych
*********************************************************
-->

<beans>
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName">
<value>oracle.jdbc.driver.OracleDriver</value>
</property>
<property name="url">
<value>jdbc:oracle:thin:@localhost:1521:XE</value>
</property>
<property name="username">
<value>wojcikm</value>
</property>
<property name="password">
<value>abc</value>
</property>
</bean>
</beans>

W pliku tym jest zdefiniowany tylko jeden bean "dataSource" - w jego właściwościach podane są podstawowe informacje, które pozwalają na nawiązanie połączenia z bazą danych.

Plik springapp-servlet-db.xml
Spring oferuje szereg obiektów wspomagających realizację odwzorowań obiektowo relacyjnych. Aby mogły być one poprawnie zainicjalizowane musi zostać zdefinowanych szereg parametrów.

<!--
*********************************************************
************* Warstwa odwzorowań obiektowo-relacyjnych
*********************************************************
-->

<beans>
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="dataSource">
<ref bean="dataSource" />
</property>
<property name="mappingLocations">
<value>/WEB-INF/context/HibernateMapping.hbm.xml</value>
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">
org.hibernate.dialect.Oracle9Dialect
</prop>
</props>
</property>
</bean>

<bean id="productManagerDaoHibernate" class="mw.toystore.db.dao.ProductManagerDaoHibernate">
<property name="sessionFactory">
<ref bean="sessionFactory" />
</property>
</bean>
</beans>

Następujący fragment pliku:

<property name="dataSource">

<ref bean="dataSource" />
</property>


stanowi sztandarowy przykład kofiguracji wstrzyknięcia IoC. Bean "sessionFactory" (którego definicji akurat nie musimy tworzyć bo pochodzi ze standardowych bibliotek springowych) posiada właściwość (property) o nazwie
"dataSource" (w jego kodzie MUSZĄ zostać zdefiniowane metody setDataSource() i getDataSource() - zgodnie ze specyfikacją JavaBean).
Jako wartość
podawana jest referencja do innego beana- a mianowicie omawianego wcześniej, zdefiniowanego w pliku springapp-servlet-datasources.xml beana "dataSource" .
Na podstawie omawianych wpisów konfiguracyjnych kontener IoC, stworzy najpierw beana "dataSource", a następnie beana "sessionFactory". Następnie na obiekcie "sessionFactory" wywołana będzie metoda "setDataSource()" a jako jej parametr przekazany zostanie obiekt "dataSource". Od tego momentu programista może w dowolny sposób używać obiektu "sessionFactory" - wartość jego właściwości dataSource nie jest już równa null - została ona zainicjalizowana.

Należy zwrócić szczególną uwagę na ustawienie właściwej wartości dla property "mappingLocations"

<property name="mappingLocations">
<value>/WEB-INF/context/HibernateMapping.hbm.xml</value>
</property>


Za jej pośrednictwem informujemy Springa gdzie znajdują się wygenerowane przez nas wcześniej mapowania obiektowo relacyjne dla istniejących obiektów dziedzinowych.

Plik springapp-servlet-bus.xml

definicja zależności pomiędzy obiektami warstwy biznesowej.
<!--
*************************************************
************* Warstwa usług biznesowych ********
*************************************************
-->
<beans>
<bean id="productManager" class="mw.toystore.bus.ProductManager">
<property name="productManager">
<ref bean="productManagerDaoHibernate" />
</property>
</bean>
</beans>

Beany zdefiniowane w tym pliku odpowiadają za realizację logiki biznesowej. Tworzony jest bean "productManager" będący obiektem klasy
"mw.toystore.bus.ProductManager". Bean ten ma zdefiniowaną właściwość "productManager" (i odpowiednie metody dostępowe). Właściwość ta zostanie zainicjalizowana obiektem "productManagerDaoHibernate" , który zdefiniowany został już wcześcniej. Od tej pory programiści którzy będą wykorzystywać obiekt "productManager" będą mogli odnoscić się niżej, do metod interfejsu DAO implementowanego przez beana "productManagerDaoHibernate".
W kodzie klasy deklaracja właściwości i jej metod dostępowych wygląda następująco (należy zwrócić uwagę na fakt, że do poprawnego wstrzyknięcia przez Springa własności "
productManager" konieczne są wszystkie elementy przedstawione poniżej ):

public class ProductManager implements IProductManager {
private IProductManagerDao productManager;

public IProductManagerDao getProductManager(){
return productManager;
}

public void setProductManager(IProductManagerDao newVal){
productManager = newVal;
}
......
}


Plik HibernateMapping.hbm.xml
Nie jest to plik bezpośrednio związany ze Springiem (choć pośrednio na pewno tak) - znajduje się w nim mapowania obiektów bazy danych na obiekty dziedziny. Struktura, rola i zawartość tego pliku opisałem wcześniej, więc nie ma potrzeby zatrzymywania się przy nim w tej chwili.

Test poprawności
A więc podsumowując w chwili obecnej mamy stworzone dwie warstwy aplikacji - biznesową i danych. Stworzony zostały mapowania Hibernate i konfiguracja kontenera IoC. Kompilacja przebiega bez błędów - czas więc przekonać się czy wszystkie mapowania zostały stworzone prawidłowo, czy aplikacja prawidłowo odwołuje się do bazy i pobiera z niej dane.
W tym celu napisałem niewielką aplikację , która za pośrednictwem fabryki Springa odnosi się do interfejsu warstwy biznesowej "IProductManager" , wywołuje na nim metodę, która sięga do warstwy DAO i pobiera obiekty reprezentujące dane przechowywane w DB. A więc do roboty.

Na początku należy przeprowadzić niewielkie czynności przygotowawcze. Najważniejsza z nich wynika z faktu, że nasza aplikacja nie będzie uruchamiana na serwerze aplikacji. Nie ma więc szans wiedzieć czegokolwiek na temat istnienia katalogów "/WEB-INF" i " /WEB-INF/context" ,
a więc miejsc w których przechowywane są pliki konfiguracyjne kluczowych bibliotek: kontenera IoC (Spring) i bibliotek Hibernate. Aby zmienić ten jakże niepożądany stan wszystkie pliki konfiguracyjne muszą znaleźć się w katalogu dopisanego do classpath. W pewnym uproszczeniu (przy założeniu że nasza aplikacja będzie uruchamiana tylko za pośrednictwem IDE) można przyjąć że pliki te powinny znaleźć się w katalogu "src".

Druga rzecz, którą należy wykonać to w skopiowanym (a więc leżącym w katalogu "src"!!!) pliku należy zmienić ścieżkę do pliku z mapowaniami Hibernate z:

<property name="mappingLocations">
<value>/WEB-INF/context/HibernateMapping.hbm.xml</value>
</property>

na

<property name="mappingLocations">
<value>HibernateMapping.hbm.xml</value>
</property>


I to w zasadzie wszystko. Teraz już można zabrać się za pisanie aplikacji.

Niech aplikacja nazywa się "Main" i leży w pakiecie "mw.toystore".

Na początek importy, które zapewnią nam dostęp do obiektów domeny, obiektów fabryki Springa i obiektów warstwy biznesowej:

import java.util.*;
import mw.toystore.domain.*;

import org.springframework.beans.factory.*;
import org.springframework.context.support.*;
import org.springframework.context.*;
import mw.toystore.bus.*;

W pierwszym kroku, w kodzie aplikacji tworzę obiekt kontekstu aplikacji. Przechowuje on informacje o powiązaniach i zależnościach między mapowanymi obiektami. Jako parametr pobiera on nazwy plików, które zawierają mapowania.

ApplicationContext context = new ClassPathXmlApplicationContext(
new String[] {"springapp-servlet-datasources.xml", "springapp-servlet-db.xml", "springapp-servlet-bus.xml"});

Następnie obiekt ten rzutuję na interfejs umożliwiający bezpośredni dostęp do fabryki beanów:

BeanFactory factory = (BeanFactory) context;

Teraz nic nie stoi na przeszkodzie by poprosić o beana z warstwy biznesowej:

IProductManager p=(IProductManager)factory.getBean("productManager");

Następnie wywołuję na nim metodę pobierającą kolekcję kategorii:

List catList=p.getCategoryList();

Następnie iteruję i próbuję wyświetlić nazwę każdej kategorii w kolekcji:

Iterator itcat=catList.iterator();
while(itcat.hasNext()){
Category c=(Category)itcat.next();
System.out.println(c.getName());

}

I to w zasadzie wszystko. Po uruchomieniu aplikacji na ekran wyrzucony został wynik:

"
....
Czasopisma
Broszury
....
"

Brak pojawienia się wyjątku świadczy o tym że mapowania wykonane zostały prawidłowo i o tym, że kontener springa również został skonfigurowany poprawnie.

Zdaję sobie sprawę, że powyższy test był bardzo pobieżny i niezgodny ze sztuką, ale myślę że przy założeniu że piszemy aplikację - tutorial można uznać go za całkowicie wystarczający.

Uff a więc przebiliśmy się przez dużą część materiału. Można śmiało powiedzieć, że nasza aplikacja DZIAŁA ALE NIE WYGLĄDA. Aby wyglądała należy wzbogacić ją o warstwę web i uruchomić na serwerze aplikacji - ale o tym w następnej części.....


2 komentarze:

Anonimowy pisze...

oooo znalazłam tego czego szukałam! super!!!

Anonimowy pisze...

czego szukalem, dzieki