Witaj na Zine.net online Zaloguj się | Rejestracja | Pomoc

WPF to Go: sposoby na zachowanie proporcji kontrolek

Męczyłem się wczoraj z różnymi sposobami żeby kontrolka zachowywała ustalone proporcje. Sposobów w sumie będzie kilka, ale chyba tylko jeden wydaje mi utrzymywalny i daje efekt taki jakiego można się spodziewać. A więc dzisiaj będzie o "aspect ratio".

Jeszcze słowem wstępu. Zmieniłem nazwę cyklu na WPF to Go (głównie żeby pasował do nazwy bloga na zine.net) oraz pozbyłem się numeracji odcinków, żeby nie wprowadzać zamieszania. W międzyczasie dwa "odcinki" cyklu pojawiły się na spotkaniach warszawskiej grupy .NET (WG.NET): wykład o RoutedEvents, oraz godzinna klepanina kodu pt: zróbmy własny custom Panel (układanie kontrolek a'la wachlarz kart na ręku). RoutedEvents posiadam w wersji screencastowej ale bez dźwięku, który leży gdzieś tam i czeka na obrobienie przez życzliwą duszę (jeżeli masz doświadczenie w edycji audio i video zgłoś się do WG.NET - potrzebujemy Cię !!). Custom Panel najprawdopodobniej wrzucę tutaj jak tylko znajdę czas i natchnienie ;). Ok to zaczynamy.

Post widoczny również na [2-many.net]


Potrzebowałem aby Grid tudzież inna kontrolka zachowayała proporcje np aby boki miały długości w propocjach 3/2. Po kilku minutach znalazłem kontrolkę Viewbox. Marzenie: w Viewboxa wrzucamy co nam się podoba (sztuk jeden) i ustawiamy właściwość Stretch, która mówi czy zagnieżdżony element będzie zachowywał proporcje (Uniform) czy np. wypełniał całą dostępną przestrzeń (Fill) itp. Wygląda to mniej więcej tak

<Viewbox Stretch="Uniform">

    <!-- Jaka˜ tam kontrolka -->

</Viewbox>


Niestety jest jeden minus: to co jest w Viewboxie musi mieć wprost podane wymiary: Width i Height. Kłóci się to dla mnie z ideą WPF'a gdzie powinno się unikać takich manewrów. Ale spróbować warto. Wrzuciłem w Viewboxa Grida z ustawionymi rozmiarami 300 / 200, włączyłem gridline'y (ShowGridLines="True") i nacisnąłem F5. Zobaczyłem to:

Viewbox powiększa / pomniejsza nie tylko kontrolkę, ale wszystko inne co ma rozmiar np: border'y, gridline'y i zapewne też paddingi i marginy. Słabe...

Podejście drugie: odziedziczę jakiś tam mój panel po Gridzie, w metodzie MeasureOverride  ustawię jeden z rozmiarów "na sztywno" na podstawie drugiego rozmiaru i jakiego DependencyProperty (właściwości) takiego jak "AspectRatio". Do moich celów by wystarczyło (przyjąłem że Grid ma być wyższy niż szerszy, więc Width byłoby wyliczane). Coś mi jednak mówiło, że musi być prostsza metoda... mam za każdym dziedziczyć kontrolkę kiedy chcę zrobić jakąś mini rzecz? To na pewno się da zrobić w XAML'u...

Oczywiście,  że się da ;) Do pomocy przyszedł DataBinding i Convertery. Nie będę się teraz rozpisywał o DataBindingu bo przyjdzie na to czas - jest banalnie prosty. Może wyglądać mniej więcej tak:

<Grid x:Name="PlayersLayout"

           Width="{Binding

                           RelativeSource={RelativeSource self},

                           Path=ActualHeight,

                           Mode=OneWay}">


RelativeSource w Bindingu może wskazywać na dowolny element potomny czy poprzedzający w hierarchi kontrolek (np można użyć FindAncestor zamiast self, który będzie szukać przodka). Użyłem self czyli właśiwość Width będzie powiązana z inną właściwością Grid'a - podaje się ją jako Path (w tym przypadku ActualHeight). Bindowanie jest tylko w jedna stronę (tak na wszelki wypadek i żeby WPF się za bardzo nie męczył. Tym samym udało się spowodować że kontrolka zachowuje proporcje 1:1. W Path niestety nie można wpisać np ActualHeight * 2 ;) Do pomocy przychodzi interfejs IValueConverter.

Czasem zdarzy się że oprócz tego, że chcemy się zbindować do jakiejś wartości, będziemy ją chcieli przekonwertować (najprostszy przykład przekonwertować np typy). W tym celu należy zaimplementować IValueConverter z dwiema metodami Convert i ConvertBack:

    1 [ValueConversion(typeof(Double), typeof(Double))]

    2 public class AspectRatioConverter : IValueConverter

    3 {

    4     #region IValueConverter Members

    5 

    6     public object Convert(object value, Type targetType,

    7         object parameter, CultureInfo culture)

    8     {

    9         Double num = (Double)value;

   10 

   11         if (parameter != null)

   12             num /= System.Convert.ToDouble(parameter, NumberFormatInfo.InvariantInfo);

   13 

   14         return num;

   15     }

   16 

   17     public object ConvertBack(object value, Type targetType,

   18         object parameter, CultureInfo culture)

   19     {

   20         Double num = (Double)value;

   21 

   22         if (parameter != null)

   23             num *= System.Convert.ToDouble(parameter);

   24 

   25         return num;

   26     }

   27 

   28     #endregion

   29 }


Tak naprawdę wszystko co robią te metody to biorą jeden obiekt, mnożą go dzielą przez parametr i zwracają. Atrybut ValueConversion nie jest niezbędny, ale zaleca się stosowanie go, aby inni wiedzieli jak go stosować i/lub podpowiadało nam VS (w końcu metody przyjmują gołe obiekty). Najważniejsze w tym kodzie jest chyba ta linijka i NumberFormatInfo.

num /= System.Convert.ToDouble(parameter, NumberFormatInfo.InvariantInfo);

Oczywiście kiedy parametr w XAMLu przekazałem jako cyfrę 1.5 dostałem wyjątkiem FormatException (w końcu w Polsce używa się przecinka a nie kropki). Zmiana na przecinek oczywiście spowodowała, że rozwiązanie się nie kompilowało. Trzeba przekazać NumberFormatInfo.InvariantInfo (chociaż podejrzewałem że będzie przekazywany InvariantCulture domyślnie... widać nie).

Żeby użyć naszego konwertera w XAMLu trzeba się trochę nagimnastykować. Trzeba stworzyć jego instancję i jakoś wykorzystać w XAML'u - służą do tego Resource'y (znowu  wybiegam trochę z cyklem ale co tam ;)). Najprościej będzie zrobić to np: w oknie, w którym Converter będzie użyty:

<Window

        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        x:Class="Zombiaki.App.MainWin"

        x:Name="Window"

        xmlns:zombiaki="clr-namespace:Zombiaki.UI;assembly=Zombiaki.UI">

        <Window.Resources>

                <zombiaki:AspectRatioConverter x:Key="aspectRatio" />

        </Window.Resources>


Po pierwsze należy zdefinować przestrzeń nazw xml'ową, wskazującą na przestrzeń nazw CLR'ową, a następnie przypisać jakiś klucz naszemu konwerterowi (Jeżeli ktoś się zastanawia co to za namespace: Zombiaki... to jest już inna historia, o której opowiadałem w czwartek na spotkaniu WG.NET ;)). Teraz w naszym Gridzie można zrobić tak (i w każdej innej kontrolce):

<Grid x:Name="PlayersLayout"

            Width="{Binding

            RelativeSource={RelativeSource self},

            Path=ActualHeight,

            Mode=OneWay,

            Converter={StaticResource aspectRatio},

            ConverterParameter=1.2}"

>


Do Bindigu dodajemy elementy Converter (wskazujący na StaticResource, który przed chwilą zdefiniowaliśmy) oraz ConverterParameter, czyli nasz aspect ratio.

Trochę wyprzedziłem cykl opowiadając o DataBindigu i Resource'ach - wydaje mi się jednak że jest to na tyle proste, że można się w tym połapać. Convertery są bardzo proste i bardzo potężne. W połączeniu z DataBindigiem dają nam naprawdę dużo możliwości. Zachęcam do eksperymentowania.
opublikowano przez yoshi | 0 komentarzy
Filed under:

WPF to Go: DependencyProperty

Druga część serii o WPF trochę się opóźniła. Z Agnieszką reanimowaliśmy naszego bloga.

Tym razem druga część cyklu w postaci screencasta - z tego właśnie powodu umieściłem go na naszym prywatnym blogu, aby nie "zabić" zine'a, który jakby nie patrzeć stoi koleżeńsko na darmowym hostingu.

Temat screencasta: DependencyProperty. Czekam na komentarze i wybaczcie mój brak profesjonalizmu w tego typu prezentacjach :)

Screencast do obejrzenia tutaj: [http://2-many.net/post/2008/02/Poczatki-z-WPF-czesc-2-DependencyProperty.aspx]

Załączniki: [FontSizeSample] | [SecurityFields]

opublikowano przez yoshi | 5 komentarzy
Filed under:

WPF to Go część 1

Minął długi czas odkąd cokolwiek pisałem na tym blogu. Nowy rok sprzyja postanowieniom, więc czas wcielić w życie pomysł, który kołatał mi się od dłuższego czasu po głowie.

Szukając odskoczni od codziennej pracy z aplikacjami webowymi, postanowiłem poduczyć się trochę programowania aplikacji desktopowych za pomocą książki Charles’a Petzolda „Application = Code + Markup” (którą polecam). Zapraszam zatem do cyklu (mam nadzieję, że w miarę regularnego) artykułów o moich początkach z WPF.

 WPF – tak w zasadzie to po co?

Windows Presentation Foundation  weszło w .NET 3.0 (trochę więcej o wpf na np.: http://msdn2.microsoft.com/en-us/library/aa663364.aspx). Po co było wprowadzać coś nowego, jeżeli Windows Forms działały i miały się dobrze? XAML pozwala na odseparowanie designu aplikacji, od jego zachowania i programistycznych „bebechów”. Zdecydowany plus…, ale XAML mógł być równie dobrze dodany do Windows Forms. Po co więc nowe kontrolki, namespace’y, itd.? MS doszedł do wniosku (moim zdaniem słusznego), że to co było wcześniej jest niewystarczające dla tworzenia atrakcyjnie wyglądających (czy wręcz „fajerwerkowych”) aplikacji windowsowych. Oczywiście takie programy da się tworzyć bez WPF, ale nakład pracy w celu uzyskania takich samych efektów byłby naprawdę duży. WPF ma służyć głównie tworzeniu świetnie wyglądających aplikacji i ma w tym zadaniu pomagać programistom najlepiej jak można.

Programowanie aplikacji tak, aby wyglądały dobrze u większości użytkowników wcale nie jest takie proste. Jeszcze kilka lat temu rozdzielczości ekranów spotykane u użytkowników były co najwyżej 2, 3 różne. Teraz mamy monitory do 20-kilku cali, panoramiczne, nie panoramiczne, laptopy duże, małe. Ta sama aplikacja może wyglądać całkowicie inaczej na dwóch różnych komputerach. Taka mała powtórka z rozrywki z tworzenia stron internetowych. Developerzy webowi mają do dyspozycji cssy, em-y, divy, itp., itd. i dają sobie radę tymi narzędziami. A co mamy dla windowsów?

Jeżeli nie piksele to co?

Jak zaczynać to oczywiście od aplikacji Hello World :) Na jakiś czas nie będę sięgał do xaml’a i designera z Visual Studio czy Expression Blend’a. Pierwszy powód to taki, że uważam, iż warto wiedzieć, jak poradzić sobie bez xaml’a - czysto programistycznie i bez designerów (albo przynajmniej rozumieć, co nam maszyna wygenerowała). Drugi powód jest prosty… jestem jeszcze za „zielony” :) i po prostu uczę się w takiej kolejności jak w książce (najpierw „Code” potem „Markup”).

Chciałem wyświetlić okno z tytułem Hello World, powiedzmy o wielkości 288x192, na środku ekranu. Taki kod:

this.Title = "Hello world!";

 

this.Width = 320;

this.Height = 240;

 

this.Top = (SystemParameters.WorkArea.Height - this.Height) / 2

        + SystemParameters.WorkArea.Top;

this.Left = (SystemParameters.WorkArea.Width - this.Width) / 2

        + SystemParameters.WorkArea.Left;

 

Jeden rzut oka na intellisense w VS i widać że coś jest nie tak. Width i Height są podawane jako double a nie jako int. Odpowiedź jest prosta. Większość rozmiarów w WPF nie jest podawana w pikselach. Wysokość i szerokość podawana jest w niezależnych jednostkach. Jedna taka jednostka jest równa 1/96 cala (stąd podane przeze mnie rozmiary okna 288x192 to tak naprawdę 3 cale na 2 cale). Jeżeli w Windowsie jest ustawione skalowanie DPI na 96, wówczas podane wymiary będą równały się ilościom pikseli na ekranie. Podejście takie pozwala na uniezależnienie się od tego na jakim ekranie (czy urządzeniu) będzie wyświetlana aplikacja. Wbrew pozorom to już jest „problemem”. Dario  może potwierdzić. Dostał laptopa o rozdzielczości „Full HD” i nic na nim nie widział :) Zwiększenie DPI pomogło, ale niektóre aplikacje po prostu się „rozjechały”.

Jeszcze jedno wyjaśnienie do kodu: aby wycentrować okno aplikacji użyłem użyłem SystemParameters.WorkArea, które zwraca prostokąt odpowiadający dostępnemu obszarowi dla aplikacji (ekran minus wszystkie taskbary, sidebary itp.).

Kolory

Takich nowośći w WPF jest więcej. Kolejna to – kolory. Przyzwyczajeni jesteśmy do podawania kolorów w skali RGB, czyli każdy ze składników w skali od 0 do 255. W WPF jest jeszcze możliwość podawania składowych czerwonego, zielonego i niebieskiego w liczbach rzeczywistych. Ta skala nazywa się scRGB – 0 scRGB odpowiada 0 w RGB, a 1 scRGB – 255 RGB. To co mi się podoba to bardziej naturalne (przynajmniej dla mnie) odwzorowanie skali kolorów tzn: 0.5-0.5-0.5 w skali scRGB jest subiektywnie w połowie między białym a czarnym – natomiast 127-127-127 RGB już nie bardzo :) scRGB ma lepiej odwzorowywać nieliniowość (trudne słowo) percepcji (jeszcze trudniejsze słowo) jasności i kolorów. Lepiej zobrazuje to pewnie poniższa tabela z książki Charles’a Petzolda:

scR/G/B

R/G/B

<= 0

0

0.1

89

0.2

124

0.3

149

0.4

170

0.5

188

0.6

203

0.7

218

0.8

231

0.9

243

>= 1.0

255

Jak łatwo zauważyć w pierwszym i ostatnim wierszu są znaki większości i mniejszości. Jak się okazuje można podawać poszczególne składowe jako liczby ujemne i większe od 1 (w końcu mogą to być liczby rzeczywiste). Takie podejście ma umożliwić zdefiniowanie kolorów na inne urządzenia niż monitory, urządzenia które mogą wyświetlać więcej kolorów niż monitory. Trochę trudno mi to sobie wyobrazić, ale możliwość jest.

W ramach zabawy WPF’em i przeglądania przykładów z książki zmodyfikowałem jeden z podanych tam programów. Tło aplikacji miało się zmieniać w zależności od położenia kursora względem środka okna. Poszczególne składowe koloru w skali scRGB.

Pominę fragment z obliczaniem odległości kursora od środka ekranu – matematykę miałem dawno ;) Najważniejsze linie kodu to.

1.       Ustawienia jako tła aplikacji SolidColorBrush

brush = new SolidColorBrush(Colors.White);

this.Background = brush;

2.       Zamiana właściwości Color tła oraz podmiana tytułu na pasku aplikacji

float scrgb = (float)(vectMouse.Length / ellipse.Length);

brush.Color = Color.FromScRgb(1, scrgb, scrgb, scrgb);

 

this.Title = String.Format("scRGB: {0:F}", scrgb);

Metoda FromScRGB jako pierwszy parametr przyjmuje wartość kanału Alpha czyli przezroczystości. Pełny kod tej małej aplikacji do ściągnięcia poniżej.

Najpiękniejsze jest to że po zamianie właściwości Color nic już nie muszę robić. Żadnego odświeżania czy odmalowywania okna – wszystko dzieje się „automagicznie” i wraz z poruszaniem myszką zmienia się szarość tła okna.  Dzieje się tak dzięki eventowi Changed, o którym pewnie kiedyś napiszę więcej.  W pasku tytułu okna pojawia się wyliczona wartość scRGB – zobaczcie, że po wejściu w jeden z rogów pojawi się liczba > 1. Niestety tło aplikacji bielsze już nie będzie :) (nawet jak potraktujemy je wiodącym proszkiem do prania ;) ).

To by było na tyle tym razem.  Dla ciekawskich dorzucam jeszcze kod innej aplikacji, gdzie jako tło jest RadialBrush, a środek gradientu porusza się za myszką.

Źródła (razem z exe'kami):

1.       Prosta aplikacja HelloWorld – [źródła]

2.       Tło aplikacji zmienia odcień pomiędzy kolorami białym i czarnym w zależności od położenia myszy – [źródła]

3.       Okrągły gradient w tle  porusza się za kursorem myszy – [źródła]

 

opublikowano przez yoshi | 0 komentarzy
Filed under:

Firefox, DOM i textNode revisited

Jeżeli ktoś czytał mój poprzedni wpis w blogu to wie, że ostatnio mocowałem się z firefoxem i javascript'em. Myślałem, że to koniec moich kłopotów. Ale miałem jeszcze jeden problem z opisywanym wcześniej firstChild. Ale od początku.

Taki xml

<table>
    <tr>
        <td>cos</td>
    </tr>
</table>


będzie w d DOM Insectorze widziany tak:
1. Element table
2. TextNode reprezentujący białe znaki
3. Element tr
4. TextNode reprezentujący białe znaki
5. Element td
... itd.

W momencie kiedy dla elementu table będziemy chcieli wyciągnąć właściwość firstChild dostaniemy: w IE element tr, w Firefoxie TextNode.

Problemu nie będzie jeżeli powyższy xml/html będzie wyglądał np. tak:

<table
    ><tr
        ><td>cos</td
    ></tr
></table>

Lekko 'obrzydliwe'. Można też całkowicie wyrzucić białe znaki i znaki nowej linii.

Więc jak szybko przejść do szukanego elementu w js? Wystarczy zawołać:

tableEl.firstChild.nextSibling

który przejdzie do kolejnego elementu.

Ale nie mamy pewności, że kolejnym elementem na pewno będzie szukany przez nas <tr> (może jakiś komentarz się trafi). Jak rozpoznać że dany element jest textNode'm. Pierwsze moje podejście było jak poniżej:

if (tableEl.firstChild.construcor == Text)

(lub też Element jak kto woli). Trafiłem chwilę później na ciekawą listę. Okazuje się, że każdy element ma właściwość nodeType:

1    Element Node
2    Attribute Node
3    Text Node
4    CDATA Section Node
5    Entity Reference Node
6    Entity Node
7    Processing Instruction Node
8    Comment Node
9    Document Node
10    Document Type Node
11    Document Fragment Node
12    Notation Node

Ponieważ operacje robiłem w pętli, wystarczyło coś takiego:

       
for (var k = thisRow.nextSibling; k != null; k = k.nextSibling)
{
     // jeżeli to nie Element to idziemy dalej
     if (k.nodeType != 1)
          continue;
     /* tutaj robię co trzeba */
}


Mam nadzieję, że komuś się przyda
opublikowano przez yoshi | 3 komentarzy
Filed under: , ,

IE Javascript vs Firefox Javascript

Ostatnimi dniami spędziłem trochę czasu, na udrażnianiu aplikacji 'IE-specific' tak aby działała pod Firefoxem (o Operze na razie postanowiłem zapomnieć ;))

Nie mówię tutaj o samym wyglądzie, css-ach itp. - to jest odrębna historia i już wiele o tym napisano. Ja męczyłem się głównie z javąscript. Ponieważ aplikacja była pisana z myślą tylko i wyłącznie o IE, autor/rzy używali specyficznych dla IE właściwości i metod - przyznaję przydatnych, ale oczywiście całkowicie niezgodnych ze wszystkimi standardami. W końcu zabrałem się za przepisanie tego kodu, tak aby działał pod IE - dużo ciekawych rzeczy się dowiedziałem i znalazłem parę ciekawych artykułów, którymi chciałem się podzielić.

AJAX - takie modne ostatnio hasło to oczywiście nic innego niż JS i Xml po HTTP. Dzisiaj chyba mało kto wyłącza skrypty js w swojej przeglądarce (z drugiej strony zrobię kiedyś test i zobaczę ile serwisów przestanie chodzić ;)) Pierwszą rzeczą na jaką się natknąłem było właśnie wysyłanie Xml po HTTP.

W IE użylibyśmy takiego kodu: new ActiveXObject("Microsoft.XMLHTTP");. Żadna inna przeglądarka sama z siebie nie wspiera ActiveX - w Firefoxie należy użyć: new XMLHttpRequest();. Co ciekawe w IE 7 też jest już to możliwe.

Ale DOM, DOM'owi nierówny i Microsoft dorzucił od siebie kilka metod i właściwości, których nie znajdziemy w Firefoxie np. właściwości xml. Jak się można domyślić, właściwość ta przechowuje string'ową wersję dokumentu Xml.

var dom = new ActiveXObject("Microsoft.XMLHTTP");

alert(dom.xml); // to zadziała tylko pod IE


W Mozilli należy użyć XmlSerializer'a
           
var s = new XMLSerializer();
var str = s.serializeToString(dom);


Najwięcej grzebania miałem chyba przy event'ach, a raczej obiektem Event. Na początku zdarzenia w przeglądarce, to było tylko wywołanie jakiegoś kodu js, przy kliknięciu, najechaniu itp. Netscape dorzucił obiekt event, gdzie mamy dostęp do różnych ciekawych informacji. Warunek jest taki, że obiekt event musi być przekazany do funkcji jako parametr

<td onclick="zrobCos(event)">...</td>

Microsoft 'wrzucił' event do obiektu window, tak więc mamy do niego dostęp zawsze i z każdego miejsca. Więc po co się trudzić i w zdarzeniach przekazywać dodatkowy parametr jeżeli będziemy mogli mieć do niego zawsze dostęp poprzez window.event. Na tym nie koniec rozbieżności, bo w Event'ie MS jest dużo więcej (albo inaczej ponazywane metody itd) np: ustawienie właściwości returnValue na false w IE spowoduje, że domyślny kod związany z tym zdarzeniem nie zostanie wykonany. W Mozilli należy samemu wywołać funkcję preventDefault() (podobnie jest z cancelBuble - w Mozilli nazywa się to stopPropagation() - poniżej w linkach możecie znaleźć całą listę rozbieżności pomiędzy tymi dwoma przeglądarkami).

W momencie kiedy uporałem się z eventem, większość funkcjonalności zaczęła działać w obu przeglądarkach. To są naprawdę drobnostki, ale znalezienie ich zajęło mi kilka godzin (największy js miał kilka tysięcy linii kodu, a wiecie jak jest z debuggowaniem js'ów ;) )

Kolejne zadanie - naprawienie AJAX'owego podmieniania kontrolek html'owych. Dla utrudnienia, zostało użyte własne rozwiązanie bazujące na podmianie właściwości outerHTML kontenera (np. jakiegoś div'a) z kontrolkami. No i oczywiście outerHTML jest IE specific (swoją drogą zaimplementowanie obsługi innerHTML w FF zostało wywalczone przez developerów). Tutaj z pomocą przyszła mi bardzo ciekawa strona http://webfx.eae.net/dhtml/ieemu/htmlmodel.html - jak się okazuje w FF każdy obiekt jest obiektem JS, czyli możemy dostać się do jego konstruktora, prototypu, w tym i do obiektu document (wolałem takie rozwiązanie niż zmieniać funkcję tak, żeby działała na innerHTML co na pewno odbiłoby się też na zmianach w layoucie). Żeby outerHTML zadziałał wystarczy zdefiniować odpowiedni setter i getter dla prototypu HTMLElement:

HTMLElement.prototype.__defineSetter__("outerHTML", function (sHTML) { /* tu ciało funkcji */ });

Warto zajrzeć na tę stronę - dostępna jest biblioteka IEEmu, która sprawia, że praktycznie wszystko działa w FF jak w IE - Event'y też.
Kolejna rzecz natomiast bardzo mnie zaskoczyła. Wydawało mi się, że wszystko już będzie działać w momencie kiedy mogłem dostać się do outerHTML. Po kolejnej sesji debuggowania, okazało się że: resp.documentElement.firstChild (gdzie resp to oczywiście jakiś obiekt DOM) w FF jest elementem typu Text (spodziewałem się jakiegoś elementu, zresztą serializacja do string'a nie pokazywała nic dziwnego). Cytując za http://developer.mozilla.org/pl/docs/DOM:element.firstChild :

"Przeglądarki oparte o Gecko wstawiają do dokumentu węzły tekstowe reprezentujące białe znaki w kodzie. Dlatego pierwsze dziecko elementu może odnosić się do wstawionego węzła tekstowego, a nie elementu określonego przez następny znacznik w źródle strony.".
       
Tak więc dla Firefoxa musiałem wywołać resp.documentElement.firstChild.nextSibling. Zadziałało :)

Prawdopodobnie część z tych rzeczy jest oczywista dla kogoś kto pracuje dużo pod FF i IE - ja natomiast od dłuższego czasu spędzałem czas przy IE. Mam nadzieję, że ten artykuł pomoże komuś w walce z pisaniem aplikacji webowych na różne przeglądarki. Teraz obiecane linki:

  1. http://developer.mozilla.org/pl/docs/DOM:element.firstChild - dokumentacja dotycząca firstChild elemntów DOM
  2. http://www.reloco.com.ar/mozilla/compat.html - Making your web page compatible with Firefox - element po elemencie wymienione różnie między IE i Firefoxem
  3. http://www.javascriptkit.com/domref/domevent.shtml - trochę jeszcze więcej o event'ach w przeglądarkach
  4. http://webfx.eae.net/dhtml/ieemu/ - emulacja IE w Firefoxie - gorąco polecam
  5. http://getfirebug.com/ - świetna wtyczka do Firefoxa - podgląda cssów, debuggowanie js, wszystko czego dusza zapragnie dla web developera pod FF.
  6. http://hopka.pl/z773 - debuggowanie JS by Ziemowitem Skowroński.

opublikowano przez yoshi | 1 komentarzy

Sooda – proste i potężne narzędzie O/RM

Trochę teorii

Dla tych, którzy nie spotkali się jeszcze z pojęciem O/RM krótkie wyjaśnienie. Skrót oznacza Object-relational mapping. Narzędzia tej klasy służą do odwzorowania relacyjnej bazy danych, na bliższe każdemu programiście obiekty. Mapowanie odbywa się najczęsciej jako odworowanie definicji klas na strukturę tabel, a każdy rekord takich tabel reprezentowany jest przez konkretny obiekt. Jest kilka powodów, dla których warto zastanowić się nad takim rozwiązaniem. W rozwiązaniach nie korzystających z O/RM, musimy używać dwóch zupełnie różnych języków programowania, a kod, gdzie język obiektowy przeplata się z SQL’em, jest ciężki do utrzymania i rozwijania,  nie wspominając o debuggowaniu.  Narzędzia klasy O/RM pozwalają uprościć i zautomatyzować proces rozwijania i utrzymywania aplikacji bazodanych.  Jednym z takich rozwiązań jest Sooda – Simple Object Oriented Data Access, autorstwa Jarka Kowalskiego.

Sooda oparta jest na mechanizmie generacji kodu (o tym trochę więcej za chwilę). W naturalny sposób odwzorowuje obiekty na tabele, wspiera również kolekcje mapując je na relacje 1-N i M-N, dodatkowo umożliwiając dziedziczenie i polimorfizm. Pozwala także osiągnąć wysoką wydajność, m.in poprzez zaawansowane cache’owanie obiektów,  "leniwe ładowanie" (ang. lazy loading), itp. Cała konfiguracja natomiast przechowywana jest w jednym miejscu, co jest dużą zaletą przy np. zapoznawaniu się z aplikacją opartą na Soodzie.

Do tej pory największą bolączką Soody, był brak kompletnej dokumentacji. Od paru dni na stronie domowej Soody, jest kompletna i bardzo szczegółowa dokumentacja – link możecie znaleźć na końcu artykułu. Mam nadzieję, że artykuł ten szczegółowo wyjaśni jak skonfigurować aplikację, żeby zaczęła korzystać z Soody i krok po kroku wprowadzi Was w świat O/RM. Ponieważ najlepiej uczyć się na przykładzie, stworzymy prostą ‘Wypożyczalnię Video’. Osoby obecne na pierwszym spotkaniu Warszawskiej Grupie .NET  z pewnością pamiętają ten przykład. Zacznijmy więc od początku.

Konfiguracja aplikacji

Na początek przyda nam się plik instalacyjny Soody, lub też źródła (odpowiednie adresy na końcu). Mając przykładową bazę danych:

 

chcielibyśmy otrzymać strukturę klas odpowiadającą naszej bazie danych taką, jak przedstawia nam diagram klas:

 

Podczas konfiguracji projektu będziemy korzystać z aplikacji SoodaStubGen, dostępnej w pakiecie instalacyjnym Soody. Ten mały program ma za zadanie z dwóch plików konfiguracyjnych *.soodaproject i schematu bazy danych w pliku XML, wygenerować klasy odpowiadające naszej bazie. Użyteczną funkcją tego programu, jest to, że potrafi zaktualizować projekt Visual Studio, dodając do odpowiedniego pliku *.csproj wygenerowane pliki. Kod wygenerowany przez Soodę (może to być C#, VB.NET, Boo i teoretycznie każdy język wspierający generację kodu ze struktur CodeDom) znajduje się głównie w tzw. Stubs’ach – zachęcam do zerknięcia, co w tym pliku się znajduje. Dodatkowo SoodaStubGen generuje puste klasy, w których będziemy umieszczać naszą logikę biznesową. Powyżej opisany schemat tworzenia kodu wygląda mniej więcej tak:

 

Najwięcej pracy będziemy mieli przy tworzeniu pliku SoodaSchema.xml, odpowiedzialnego za odwzorowanie klas na tabele w bazie danych.  Pierwszą rzeczą jaką musimy umieścić w tym pliku, jest definicja źródła danych – w tej chwili jedynym wspieranym źródłem jest relacyjna baza danych. Aby zdefiniować źródło danych należy na początku pliku SoodaSchema.xml dodać element:

 <datasource name="default" type="Sooda.Sql.SqlDataSource" />

Podstawową tabelą, którą będziemy używać w naszej wypożyczalni jest tabela Video. Zmapujmy więc ją na interesującą nas klasę tak jak poniżej (pełne mapowanie na końcu artykułu):

    1 <class name="Video">

    2     <table name="Video">

    3         <field name="Id" type="Integer" primaryKey="true" nullable="false" />

    4         <field name="Title" type="String" nullable="false" />

    5         <field name="Category" type="Integer" references="VideoCategory" prefetch="1" />

    6         <field name="Status" type="Integer" references="VideoStatus" prefetch="1" />

    7         <field name="RentedOutTo" dbcolumn="rented_out_to" type="Integer" references="Customer" />

    8         <field name="RentedDate" dbcolumn="rented_date" type="DateTime" nullable="true" />

    9         <field name="ReturnedDate" dbcolumn="returned_date" type="DateTime" nullable="true" />

   10         <field name="DirectedBy" dbcolumn="director" type="Integer" references="Artist" prefetch="1" />

   11         <field name="YearOfProduction" dbcolumn="year_of_production" type="Integer" />

   12     </table>

   13 </class>

 

Podstawowym elementem jest class oparty na jednej lub wielu tabelach (możliwe jest również oparcie wielu klas na jednej tabeli) w bazie danych o nazwie „Video” (<table name="Video">). Każdej kolumnie w tabeli odpowiada jedna właściwość:

<field name="RentedDate" dbcolumn="rented_date" type="DateTime" nullable="true" />

Każda taka właściwość  ma oznaczony typ, zazwyczaj taki jak typ w bazie danych, nazwę – niekoniecznie taką samą i atrybut mówiący czy kolumna może być pusta. Poniżej wymieniono najczęściej spotykane typy pól (są też inne np. typ Image – zachęcam do lektury dokumentacji)

·       String - dodatkowo należy wyspecyfikować parametr size dla właściwości

·       Integer

·       DateTime

·       BooleanAsInteger – bardzo ciekawy typ, w bazie danych reprezentowany jako int (o wartości 1 lub 0), natomiast właściwość będzie przedstawiona jako typ boolean.

 

Przynajmniej jedna z właściwości musi być kluczem głównym (primaryKey="true"). Sooda wspiera również złożone klucze główne.  Jak łatwo na pełnym mapowaniu część właściwości ma zdefiniowany parametr references. Użycie go, spowoduje wygenerowanie właściwości  będącej referencją do innej klasy – czyli tak naprawdę klucza obcego np.  w klasie Video, właściwość Status jest tak naprawdę referencją do klasy VideoStatus.

Sooda wspiera również kolekcje jeden do wielu i wiele do wielu. Te pierwsze definiuje się poprzez element:

·        <collectionOneToMany> - reprezentowana jako referencja i kolekcja (np. dla klasy Video zdefiniowana jest kolekcja przetrzymująca hisorię wypożyczeń (VideoHistory)

·        <collectionManyToMany> - reprezentowana jako para kolekcji - dla Video jest to kolekcja aktorów, przypisanych do filmu (Actors2Video). W odróżeniu od klas, relacja jest przedstawiona w pliku mapującym, jako <relation>, gdzie klucze główne są referencjami

 

Przypuśćmy, że chcielibyśmy, aby  w naszej aplikacji oprócz danych klientów, przetrzymywać również dane pracowników, którzy mogliby wypożyczać filmy po specjalnych cenach. Aby nie dublować funkcjonalności w dwóch różnych obiektach (klient i pracownik). Najrozsądniejszym rozwiązaniem jest dziedziczenie klasy pracownik z klasy klient, lub stworzenie klasy abstrakcyjnej, z której dziedziczyłyby te dwie. Sooda wspomaga nas przy takich zadaniach, umożliwiając zdefinowanie pola (selektora), które będzie używane do odróżniania konkretnych podklas danej klasy. Najlepiej to zrozumieć na przykładzie naszej aplikacji. Klasa Person ma zdefniowany selektor: subclassSelectorField="Type". Na podstawie wartości pola Type, rekord z bazy danych będzie zwrócony jako obiekt tej, albo innej klasy. Klasy Customer i Employee dziedziczą z  klasy Person (inheritFrom="Person") na podstawie różnych wartości pola Type (wartość zdefiniowana przez atrybut subclassSelectorValue klasy). Klasa , która nie ma zdefinowanego subclassSelectorValue, będzie klasą abstrakcyjną.

Ostatnią rzeczą, o której chciałbym wspomnieć , są obiekty wyróźnione. Prawdziwą udręką podczas rozwijania aplikacji jest moment, gdy podczas przeglądania zapytania do bazy danych, nie pamiętamy co oznacza wartość 1 czy 2 w polu Status – trzeba przegrzebywać się przez ustalenia, dokumentację, czasem przeglądać zawartość bazy danych. Obiekty wyróżnione mają nam w tym pomóc. Wystarczy zdefiniować dla klasy odpowiednie elementy <const> mówiące, że rekord z tabeli, np. VideoStatus  o kluczu głównym 1 oznacza film wypożyczony (RentedOut).  Od tej pory będzie można poprzez proste wywołanie w kodzie, wyciągnąć ten rekord, w celu użycia go w zapytaniu, porównaniu, itp – ale o tym za chwilę.

Generowanie kodu

Mamy już więc  plik mapujący. W tym momencie będziemy potrzebować SoodaStubGen.exe. Zadaniem tego programu jest wygenerowanie na podstawie przygotowanego przez nas pliku, odpowiednich klas, z właściwościami, kolekcjami, obiektami wyróżnionymi itp. oraz wygenerowanie pustych klas, w których umieszczać będziemy całą funkcjonalność. SoodaStubGen ma wiele parametrów wywołania. Zamiast podawania długiej komendy łatwiej  będzie przygotować plik *.soodaproject – mały pliku xml, w którym zostaną umieszczone te parametry. Dzięki niemu będzie nam łatwiej przekonfigurowywać aplikację. Zawartość przykładowego pliku wygląda np tak:

    1 <?xml version="1.0" encoding="utf-8"?>

    2 <sooda-project xmlns="http://www.sooda.org/schemas/SoodaProject.xsd">

    3     <schema-file>SoodaSchema.xml</schema-file>

    4     <language>c#</language>

    5     <output-namespace>VideoRental</output-namespace>

    6     <output-path>.</output-path>

    7     <nullable-representation>SqlType</nullable-representation>

    8     <not-null-representation>Raw</not-null-representation>

    9     <with-indexers>false</with-indexers>

   10     <with-typed-queries>true</with-typed-queries>

   11     <embedded-schema-type>Binary</embedded-schema-type>

   12     <external-projects>

   13         <project type="vs2005" file="VideoRental.csproj"/>

   14     </external-projects>

   15 </sooda-project>

 

Najważniejsze z  parametrów:

  • schema-file – nazwa pliku mapującego;
  • language – język w którym kod zostanie wygenerowany (C# jest moim zdaniem najlepszym rozwiązaniem, gdyż tylko ten język pozwala na zastosowanie zapytań typowanych, o których za chwilkę opowiem);
  • output-namespace – przestrzeń nazw dla wygenerowanych klas;
  • output-path – miejsce, w którym pliki zostaną zapisane;
  • nullable-representation – reprezentacja właściwości mogących przyjmować wartość NULL. Możliwe jest użycie:
        o       Boxed – w tym przypadku tracimy informacje o typie, a właściwości będą zwracane jako typ object,

o       SqlType – użycie specjalnych typów z przestrzeni nazw System.Data.SqlTypes,

o       Raw – dane są przetrzymywane jako ‘standardowe’ typy,

o       Nullable – użyte będą typy Nullable z .NET 2.0,

o       RawWithIsNull – rozszerzenie opcji Raw. Dla każdej właściwości zostaną wygenerowane metody IsNull zwracające wartość prawda / fałsz.

  • not-null-representation – podobnie jak wyżej, tym razem dla właściwości nienullowalnych;
  • with-indexers – włącza lub wyłącza, indeksery dla wygenerowanych list;
  • with-typed-queries – włącza lub wyłącza używanie zapytań typowanych;
  • external-projects – pozwala zaktualizować pliki projektów VS2005 i VS2003 o wygenerowane pliki

 

Teraz musimy uruchomić SoodaStubGen podając jako parametr nazwę naszego pliku *.soodaproject. Zalecane jest ustawienie w projekcie Visual Studio w pre-build-event command line wywołania SoodaStubGen np. w ten sposób:

$(SolutionDir)\Sooda\SoodaStubGen.exe $(ProjectDir)VideoRental.soodaproject

Spowodouje to wywołanie SoodaStubGen z podktalogu Sooda, a jako parametr należy podajć plik VideoRental.soodaproject z katalogu projektu Wygenerowanie w ten sposób kodu, spowoduje wyświetlenie ostrzeżenia w Visual Studio, że plik projektu został zmieniony i należy go ponownie załadować. Dwie rzeczy, na które warto zwrócić uwagę:

1.      SoodaStubGen rozpoznaje, czy plik mapujący się zmienił – wówczas generuje jeszcze raz plik / pliki szkieletowe. Nie rozpoznaje jednak czy zmienił się plik *.soodaproj. W przypadku, gdy zmienimy coś w tym pliku, najlepiej jest zapisać plik mapującytak, aby zmieniła się data ostatniej modyfikacji pliku.

2.      SoodaStubGen potrafi dodawać nowo wygenerowane pliki do pliku projektu Visual Studio – jednak, co jest oczywist