Programowanie aplikacji internetowych 2015/2016 Instrukcja laboratoryjna cz.3 Aplikacje na Windows 8.x Store (JS i HTML5) Prowadzący: Tomasz Goluch Wersja: 2.1
I. Wprowadzenie 1 Cel: Przekazanie podstawowych informacje o laboratorium. Laboratorium odbywa się na maszynach fizycznych które posiadają zainstalowany system Windows 8.x i IDE Visual Studio 2012 (tylko aplikacje Windows 8.0) lub wyższe (również aplikacje Windows 8.1). II. Zakładanie konta developerskiego Cel: Założenie konta developerskiego wymaganego do rozwijania i publikowania aplikacji na Windows Store. W celu rozwoju aplikacji Windows Store wystarczy odnowienie licencji dewelopera systemu Windows 8.1 opisane na początku rozdziału IV Tworzenie aplikacji Windows Store. Jeżeli jesteśmy również zainteresowani publikowaniem aplikacji w Windows Store musimy również posiadać konto developerskie. Szczęśliwie dla członków programu DreamSpark którymi są studenci Politechniki Gdańskiej jest ono darmowe. W tym celu należy uzyskać kod Promo code z witryny projektu DreamSpark. Wybieramy zakładkę students sign in i wybieramy opcję rejestracji z podaniem konta studenckiego. Założenie konta odbywa się na stronie Microsoftu: Windows Dev Center Dashboard. Należy uzupełnić wymagane dane oraz wybrać opcję: Individual 59.00 PLN. W następnym kroku należy podać uzyskany wcześniej kod Promo code co pozwoli uniknąć poniesienia opłaty developerskiej. Należy się liczyć z faktem, że uzyskany kod możemy wykorzystać tylko raz. 1 Instrukcja przygotowana na podstawie laboratoriów Hands-on labs for Windows 8 firmy Microsoft. 1
III. Rodzaje aplikacji Windows Store Cel: Zapoznanie z podstawowymi rodzajami aplikacji Windows Store. Podobnie jak w przypadku innych aplikacji możemy w Visual Studio dokonać wyboru spośród kilku gotowych szablonów: Blank App (Universal Apps), Hub/Pivot App (Universal Apps), Navigation App (Universal Apps), GridApp (Windows), Split App (Windows). Są to proste aplikacje z mniej bądź bardziej rozbudowanym UI, logiką nawigacji (wliczając w to przyciski powrotu), przykładowe dane oraz zasoby. W przykładowych szablonach wykorzystano takie kontrolki jak: GridView, FlipView. Debugowanie aplikacji odbywa się w tradycyjny sposób (F5) może się odbywać na lokalnej albo zdalnej maszynie oraz na symulatorze. Na potrzeby laboratorium przyjrzyjmy się bliżej szablonowi GridApp (Windows). Posiada on cztery strony: default.html, groupeditems.html, itemdetail.html, groupdetail.html. Pierwsza reprezentuje aplikację i jej zasoby (np. nazwę aplikacji wyświetlaną w górnym pasku UI), druga stronę startową a kolejne to strony reprezentujące na różne sposoby szczegółowe informacje. Nowością jest plik manifestu aplikacji package.appxmanifest, który pozwala kontrolować wdrożenie aplikacji pod nowy interfejs Windows 8.x. Jest zbiorem metadanych o aplikacji, takich jak: właściwości związane z UI, np. nazwa wyświetlana (m.in. w menu start), punkt wejścia (domyślnie default.html), domyślny język (związany z krajem), opis aplikacji, obsługiwane orientacje ekranu, logo i ekran powitalny, zasoby do których aplikacja powinna mieć dostęp (zakładka Capabilities ) oraz wiele innych, których nie będziemy szczegółowo opisywać 2.Wyłączenie aplikacji polega na ustawieniu kursora przy górnej krawędzi i w momencie kiedy zamieni się on w łapkę, ściągnięcie aplikacji do dolnej krawędzi ekranu lub przełączeniu się do Visual studio (alt+tab) i wyłączeniu debuggera. Domyślnie w aplikacji Grid App (Windows) otrzymujemy model przykładowych danych SampleData zdefiniowany w pliku data.js. Dane te są zahardkodowane wewnątrz funkcji generatesampledata(), co pozwala na ich wyświetlanie podczas tworzenia designu oraz w uruchomionej aplikacji, jednak w normalnych warunkach nie jest to polecany sposób na przechowywanie większej ilości danych. Zastosowanie modelu danych pozwala na łatwą zmianę ich źródła. Posłuży nam ona jako punkt wyjścia do zbudowania przykładowej aplikacji. IV. Tworzenie aplikacji Windows Store 2 Więcej informacji na: http://msdn.microsoft.com/pl-pl/library/windows-8-szybki-start-dla-programisty-- pierwsza-aplikacja-metro-w-javascript. 2
Cel: Utworzenie aplikacji Windows Store, zamiana wyświetlania domyślnych przykładowych z szablonu rzeczywistymi oraz dostosowanie wyglądu interfejsu użytkownika. Podczas uruchamiania programu lub próby użycia debuggera może pojawić się monit o odnowienie licencji developera systemu Windows 8.1. Zgoda wymaga uprawnień administratora (login i hasło podane przez prowadzącego) oraz aktywnego konta email w domenie hotmail.com. Nazwa i hasło do konta również zostaną podane przez prowadzącego. Do wyrejestrowania licencji deweloperskiej służy w PowerShell u polecenie: Unregister-WindowsDeveloperLicense. Proszę uruchomić PowerShell a w trybie administratora. 1. Utwórz nowy: Store Apps, Grid App (Windows) JavaScript projekt. 2. Zmień, na bardziej przyjazną, nazwę wyświetlaną jako tytuł głównego okna (plik groupeditems.html, element span z class = pagetitle). 3. Dodaj pliki z logo ETI (do ściągnięcia ze strony przedmiotu) do folderu images (pliki: Logo.scale-100.png, SmallLogo.scale-100.png, SplashScreen.scale- 100.png, StoreLogo.scale-100.png). 4. W pliku manifestu aplikacji zmień wyświetlaną nazwę aplikacji na bardziej adekwatną oraz inne właściwości np.: domyślny język, obsługiwane obroty, logo, itp. 5. Odinstaluj z systemu starą wersję aplikacji i zainstaluj nową (powinno być widoczne nowe logo w menu start). 6. Zamień przykładowe dane wyświetlane w projekcie na dane z przepisami kuchennymi (do pobrania na stronie przedmiotu). W tym celu: 3
a. dodaj nowy folder danych do projektu, a następnie umieść w nim plik JSON z przepisami Recipes.txt. b. analogicznie, dodaj do folderu images podfoldery wraz z zawartością reprezentujące rodzaje kuchni (chinese, french, german, indian, italian, oraz mexican) oraz folder z kafelkami (tiles). c. w pliku data.js usuń (zakomentuj) wywołanie funkcji generatesampledata(), a następnie dodaj kod javascript ładujący dodane wcześniej zasoby przepisów opisane w pliku Recipes.txt: Po uruchomieniu strona główna projektu powinna wyglądać mniej/więcej tak: 7. W celu dopasowania wyglądu pozycji kulinarnych: a. w pliku groupeditems.html w elemencie div z class= itemtemplate usuń (zakomentuj) element H6 aby nie wyświetlać danych powiązanych z niezdefiniowanymi właściwościami subtitle. 4
b. W elemencie H4 ustaw wyświetlanie skróconego tytułu (powiazanie z shorttitle zamiast Title): c. W celu lepszego dopasowania wyświetlanych elementów należy zmodyfikować plik groupeditem.css Po uruchomieniu strona główna projektu powinna wyglądać mniej/więcej tak: 5
8. Nadal na stronach powiązanych z poszczególnymi kuchniami występują niezgodności pomiędzy wygenerowanym szablonem a naszym modelem (undefined). 9. W pliku groupdetail.html w elemencie div z class= headertemplate usuń (zakomentuj) element H2 a w elemencie div z class= itemtemplate elementy H6 oraz H4. W tym miejscu dodaj informacje o czasie przygotowania potrawy powiązanej z właściwością preptime obiektu recipe (kod w ramce): 10. Jeszcze małe poprawki w celu umiejscowienia elementów przepisów nieco bliżej siebie i zachowania proporcji obrazu (plik groupdetail.css): 6
Po wybraniu grupy przepisów chińskich strona powinna wyglądać mniej/więcej tak: W ostatnim kroku przedstawimy więcej informacji na temat przepisów, takich jak porady i składniki. W tym celu należy w pliku itemdetail.html wewnątrz elementu section, w miejscu na zawartość (<!-- TODO: Content goes here. -->) dodać element article: <div class="content" aria-label="main content" role="main"> <article> <div> <header> <h2 class="item-title"></h2> </header> <img class="item-image" src="#" /> <h2> Preparation time: <span class="item-subtitle"></span> minutes </h2> <div class="ingredients"> 7
<h2>ingredients</h2> <div class="item-ingredients"> <div class="directions"> <h2>directions</h2> <h2 class="item-directions"></h2> </article> Ponieważ w powyższym pliku nie stosujemy wiązania deklaratywnego musimy to zrobić modyfikując działanie funkcji ready w pliku itemdetail.js: // This function is called whenever a user navigates to this page. It // populates the page elements with the app's data. ready: function (element, options) { var item = options && options.item? Data.resolveItemReference(options.item) : Data.items.getAt(0); element.queryselector(".titlearea.pagetitle").textcontent = item.group.title; element.queryselector("article.item-title").textcontent = item.title; element.queryselector("article.item-subtitle").textcontent = item.preptime; element.queryselector("article.item-image").src = item.backgroundimage; element.queryselector("article.item-image").alt = item.shorttitle; // Display ingredients list var ingredients = element.queryselector("article.item-ingredients"); for (var i = 0; i < item.ingredients.length; i++) { var ingredient = document.createelement("h2"); ingredient.textcontent = item.ingredients[i]; ingredient.classname = "ingredient"; ingredients.appendchild(ingredient); element.queryselector("article.item-directions").innerhtml = item.directions; element.queryselector(".content").focus(); oraz plik itemdetail.css:.itemdetailpage.content article { /* Define a multi-column, horizontally scrolling article by default. */ column-fill: auto; column-gap: 80px; column-width: 480px; height: calc(100% - 183px); margin-left: 120px; margin-top: 133px; display: -ms-grid; -ms-grid-columns: 400px 40px 360px 40px 1fr;.itemdetailpage.content article.ingredients { -ms-grid-column: 3; margin-left: 40px;.itemdetailpage.content article.ingredients.item-ingredients { margin-top: 20px;.itemdetailpage.content article.ingredients.item-ingredients.ingredient { padding-bottom: 12px; 8
font-size: 20px;.itemdetailpage.content article.directions { -ms-grid-column: 5; margin-left: 40px;.itemdetailpage.content article.directions.item-directions { margin-top: 20px; font-size: 20px;.itemdetailpage.content article.item-image { margin-bottom: 3px; width: 400px; Po uruchomieniu strona szczegółowa z przepisem powinna wyglądać mniej/więcej tak: V. Orientacja obrazu urządzenia Cel: Adaptacja układu kontrolek do orientacji obrazu urządzenia. Uruchom napisany w poprzednim rozdziale program za pomocą symulatora, powinien on wyglądać następująco: 9
Obróć ekran o 90ᴼ, teraz program powinien on wyglądać mniej/więcej tak (jest to zasługa automatycznego zachowania się kontrolki GridView): W przypadku wyświetlenia grupy przepisów rozmieszczenie widoku mogło by być lepsze: 10
W tym celu: 1. w pliku groupdetail.js dodaj zmienną informującą o stanie widoku: oraz dwie funkcje: aktualizującą layout wyświetlanej strony: // This function updates the page layout in response to viewstate changes. updatelayout: function (element, viewstate, lastviewstate) { /// <param name="element" domelement="true" />, var listview = element.queryselector(".itemslist").wincontrol; if (lastviewstate!== viewstate) { if (lastviewstate === appviewstate.snapped viewstate === appviewstate.snapped lastviewstate === appviewstate.fullscreenportrait viewstate == appviewstate.fullscreenportrait) { var handler = function (e) { listview.removeeventlistener("contentanimating", handler, false); e.preventdefault(); listview.addeventlistener("contentanimating", handler, false); var firstvisible = listview.indexoffirstvisible; this.initializelayout(listview, viewstate); listview.indexoffirstvisible = firstvisible; // This function updates the ListView with new layouts 11
initializelayout: function (listview, viewstate) { /// <param name="listview" value="winjs.ui.listview.prototype" />, if (viewstate === appviewstate.snapped viewstate === appviewstate.fullscreenportrait) { listview.layout = new ui.listlayout(); else { listview.layout = new ui.gridlayout({ groupheaderposition: "left" ); 2. w pliku navigator.js dodaj zmienną widoku Oraz zaktualizuj wywołanie wcześniejszej funkcji o parametry informujące o aktualnym i wcześniejszym stanie widoku: W celu poprawy położenia elementów należy dodać nast. Kod do pliku groupdetail.css: 12
@media screen and (-ms-view-state: snapped) {.groupdetailpage section[role=main] { -ms-grid-row: 2; -ms-grid-row-span: 1;.groupdetailpage.itemslist.win-vertical.win-viewport.win-surface { margin-bottom: 30px; margin-top: 0;.groupdetailpage.itemslist.win-groupheader { visibility: hidden;.groupdetailpage.itemslist.win-container { margin-left: 13px; margin-right: 35px;.groupdetailpage.itemslist.item { -ms-grid-columns: 60px 1fr; height: 60px; width: 272px;.groupdetailpage.itemslist.item.item-info.item-title { max-height: 30pt;.groupdetailpage.itemslist.item.item-info.item-description { visibility: hidden; @media screen and (-ms-view-state: fullscreen-portrait) {.groupdetailpage.itemslist.win-horizontal.win-viewport.win-surface { margin-left: 100px;.groupdetailpage.itemslist.win-vertical.win-viewport.win-surface { margin-bottom: 35px; margin-left: 45px; margin-right: 120px; margin-top: 128px; Teraz strona grupy przepisów chińskich strona powinna wyglądać mniej/więcej tak: 13
Jak widać poprawy wymaga jeszcze strona wyświetlająca szczegóły związane z przepisem: Wystarczy w pliku itemdetail.css dodać następujący kod: 14
@media screen and (-ms-view-state: fullscreen-portrait) {.itemdetailpage.content article { margin-left: 100px;.itemdetailpage.content article.ingredients { -ms-grid-row: 2; -ms-grid-column: 1; margin-left: 0px; margin-top: 40px;.itemdetailpage.content article.directions { -ms-grid-row: 3; -ms-grid-column: 1; margin-left: 0px; margin-top: 40px; W przypadku dzielenia ekranu na dwie aplikacje (snapping mode) aplikacja zachowuje się podobnie jak w trybie pionowym, aby to sprawdzić należy uruchomić dowolną inną aplikację Windows Store (w przykładzie użyto prognozy pogody) i najechać lewy, dolny róg ekranu kursorem, a następnie po pojawieniu się przeciągnąć naszą książkę kucharską w prawą stronę ekranu. 15
Mimo iż domyślne zachowanie jest całkiem przyzwoite możemy je jeszcze poprawić dodając nast. kod (itemdetail.css): @media screen and (-ms-view-state: snapped) {.itemdetailpage.content { overflow-x: hidden; overflow-y: auto;.itemdetailpage.content article { -ms-grid-columns: 300px 1fr; -ms-grid-row: 2; -ms-grid-rows: 300px auto; display: -ms-grid; height: 100%; margin-left: 20px; overflow-x: hidden; overflow-y: auto; width: 300px;.itemdetailpage.content article.item-image { width: 280px;.itemdetailpage.content article.item-content { padding-bottom: 60px;.itemdetailpage.content article.ingredients { -ms-grid-row: 2; -ms-grid-column: 1; margin-left: 0px; margin-top: 12px;.itemdetailpage.content article.ingredients.item-ingredients { margin-top: 4px; 16
.itemdetailpage.content article.ingredients.item-ingredients.ingredient { padding-bottom: 0px; font-size: 9pt; VI. Semantic Zoom Cel: Dodanie funkcjonalności Semantic Zoom pozwalające na łatwiejszą nawigację po grupach przepisów. 1. W celu dodania funkcjonalności Semantic Zoom do strony startowej: a. w pliku groupeditems.html dodaj <div class="zoomedoutitemtemplate" data-win-control="winjs.binding.template"> <div class="zoomedoutitemcontainer"> <img class="zoomeoutitemimage" src="#" data-win-bind="src: groupimage; alt: title" /> <h4 class="zoomedoutgrouptitle" data-win-bind="textcontent: title; alt: title"> </h4> <div class="zoomedoutitemcount" data-win-bind="textcontent: recipescount;"> <div id="zoom" data-win-control="winjs.ui.semanticzoom" data-win-options="{ initiallyzoomedout: false " style="height: 100%"> <div id="zoomedinlistview" class="groupeditemslist" aria-label="list of groups" data-win-control="winjs.ui.listview" data-win-options="{ selectionmode: 'none' "> <div id="zoomedoutlistview" class="groupeditemslist" aria-label="list of groups" data-win-control="winjs.ui.listview" data-win-options="{ selectionmode: 'none' "> a. wewnątrz pliku groupeditems.js definicje dodatkowych zmiennych: b. oraz zamień implementacje funkcji ready i updatelayout następującymi: // This function is called whenever a user navigates to this page. It // populates the page elements with the app's data. ready: function (element, options) { var semanticzoom = element.queryselector("#zoom").wincontrol; 17
var zoomedinlistview = element.queryselector("#zoomedinlistview").wincontrol; var zoomedoutlistview = element.queryselector("#zoomedoutlistview").wincontrol; zoomedoutlistview.itemtemplate = element.queryselector(".zoomedoutitemtemplate"); zoomedoutlistview.itemdatasource = Data.groups.dataSource; zoomedoutlistview.groupdatasource = null; zoomedoutlistview.layout = new ui.gridlayout({ groupheaderposition: "top" ); zoomedinlistview.groupheadertemplate = element.queryselector(".headertemplate"); zoomedinlistview.itemtemplate = element.queryselector(".itemtemplate"); zoomedinlistview.oniteminvoked = this._iteminvoked.bind(this); if (appview.value === appviewstate.snapped) { // If the app is snapped, configure the zoomed-in ListView // to show groups and lock the SemanticZoom control zoomedinlistview.itemdatasource = Data.groups.dataSource; zoomedinlistview.groupdatasource = null; zoomedinlistview.layout = new ui.listlayout(); semanticzoom.locked = true; else { // If the app isn't snapped, configure the zoomed-in ListView // to show items and groups and unlock the SemanticZoom control zoomedinlistview.itemdatasource = Data.items.dataSource; zoomedinlistview.groupdatasource = Data.groups.dataSource; zoomedinlistview.layout = new ui.gridlayout({ groupheaderposition: "top" ); semanticzoom.locked = false; semanticzoom.element.focus();, // This function updates the page layout in response to viewstate changes. updatelayout: function (element, viewstate, lastviewstate) { /// <param name="element" domelement="true" /> /// <param name="viewstate" value="windows.ui.viewmanagement.applicationviewstate" /> /// <param name="lastviewstate" value="windows.ui.viewmanagement.applicationviewstate" /> var semanticzoom = element.queryselector("#zoom").wincontrol; var zoomedinlistview = element.queryselector("#zoomedinlistview").wincontrol; if (appview.value === appviewstate.snapped) { zoomedinlistview.itemdatasource = Data.groups.dataSource; zoomedinlistview.groupdatasource = null; zoomedinlistview.layout = new ui.listlayout(); semanticzoom.zoomedout = false; semanticzoom.locked = true; else { zoomedinlistview.itemdatasource = Data.items.dataSource; zoomedinlistview.groupdatasource = Data.groups.dataSource; zoomedinlistview.layout = new ui.gridlayout({ groupheaderposition: "top" ); semanticzoom.locked = false;, 18
#zoomedoutlistview.win-surface { margin-left: 120px; #zoomedoutlistview.win-item { height: 500px; width: 250px; #zoomedoutlistview.win-container { margin-right:30px;.zoomedoutitemcontainer { min-height: 500px; height: 100%; width: 250px; display: -ms-grid; -ms-grid-rows: 1fr 130px 400px 1fr; -ms-grid-columns: 1fr;.zoomeOutItemImage { -ms-grid-row-span: 4;.zoomedOutGroupImage { -ms-grid-row: 3;.zoomedOutItemCount { -ms-grid-row: 2; -ms-grid-column-align: end; font-size: 80px; color: rgb(250, 243, 214); margin: -7px 12px 0px 20px; font-weight: 100;.zoomedOutGroupTitle { -ms-grid-row: 2; font-size: 32px; color: rgb(250, 243, 214); margin: 7px 0px 0px 16px; font-weight: 200; 19
VII. Współdzielenie Cel: Dodanie funkcjonalności współdzielenia przepisów z innymi aplikacjami. 1. W celu dodania funkcjonalności Sharing w pliku itemdetails.js dodaj dekalarcję nast. zmiennych: 2. Ponieważ zmienna item została zadeklarowana na zewnątrz należy usunąć jej redeklarację wewnątrz metody ready: 3. Na końcu funkcji ready zarejestruj nasz uchwyt do funkcji ondatarequested (zaimplementowany w nast. punkcie) ze zdarzeniem DataRequested obiektu DataTransferManager. 4. Dodaj dwie funkcje, pamiętaj, o przecinkach rozdzielających ich implementacje. Pierwsza udostępnia szczegółowe informacje o przepisie a druga pozwala na wyrejestrowanie naszej funkcji. 20
ondatarequested: function (e) { var request = e.request; request.data.properties.title = item.title; request.data.properties.description = "Recipe ingredients and directions";, // Share recipe text var recipe = "\r\ningredients\r\n" + item.ingredients.join("\r\n"); recipe += ("\r\n\r\ndirections\r\n" + item.directions); request.data.settext(recipe); unload: function () { WinJS.Navigation.removeEventListener("datarequested", this.ondatarequested); 5. Uruchom aplikację (F5) w zaklęciu Share mamy możliwość wykorzystania udostępnionych informacji np. wysyłając je programem pocztowym. 6. Można również współdzielić grafikę (dodaj poniższy kod do funkcji ondatarequested.js) // Share recipe image var uri = item.backgroundimage; if (item.backgroundimage.indexof("http://")!= 0) uri = "ms-appx:///" + uri; uri = new Windows.Foundation.Uri(uri); var reference = storage.streams.randomaccessstreamreference.createfromuri(uri); request.data.properties.thumbnail = reference; request.data.setbitmap(reference); 7. Tym razem zawartość wysyłanej wiadomości powinna zawierać obrazek: 21
VIII. Dodawanie strony About Cel: Dodanie strony o produkcie. 1. Dodaj w pliku default.js poniższy kod: 2. Dodaj folder about wewnątrz folderu pages i dodaj do niego nowy element page control o takiej samej nazwie (about). 3. Dodaj ciało strony do pliku About.html: <body> <div id="about" data-win-control="winjs.ui.settingsflyout" data-win-options="{width: 'narrow'"> <div class="settingspane"> <div class="win-label"> <button onclick="winjs.ui.settingsflyout.show()" 22
class="win-backbutton"></button> <span class="settingstitle">about</span> <article class="settingscontent"> <h2>książka Kucharska</h2> <h4>wersja eksperymentalna</h4> </article> </body> 4. Dodaj kod do pliku About.css:.SettingsPane { margin-top:36px; margin-left:48px;.settingstitle { margin-left: 36px;.SettingsContent { margin-top: 24px; #about { background-color: gray ; 5. Po uruchomieniu aplikacji i kliknięciu przycisku About wewnątrz zaklęcia Settings powinniśmy uzyskać nast. efekt: 23