niedziela, 9 września 2012

WinJS na żywo - odc.5 (m.in search, własny binding)

Tym razem z poziomu WinJS dokonałem integracji z systemową wyszukiwarką, sprawiając tym samym osiągnięcie przez aplikację WinJS tej samej funkcjonalności, którą wcześniej zrealizowałem w C#. Oczywiście wiązały się z tym też mniejsze zadania…

Kontrakt Search oraz stronę z wynikami wyszukiwań najszybciej dodamy korzystając z Visual Studio 2012. Z menu kontekstowego w oknie Solution Explorer wybieramy Add New Item, a następnie szablon Search Contract. Dokumentacja opisuje wszystkie potrzebne czynności oraz co zostaje dodawane do projektu aplikacji po wybraniu szablonu. Chwilę może zająć analiza kodu wygenerowanej strony. Parę rzeczy zwróciło w praktyce moją uwagę. Code-behind strony z wynikami wyszukiwań jest zadeklarowany w default.html, zamiast w HTML tej strony. Jest to podyktowane tym, że:

  • obsłużenie zdarzenia aktywacji aplikacji przez wyszukiwarkę
  • rejestracja na wpisanie zapytania w wyszukiwarce

muszą odbywać się globalnie przy uruchomieniu aplikacji i nie zdecydowano się na modyfikację pliku z kodem globalnym. Konkretnie chodzi o kod:

WinJS.Application.addEventListener("activated", function (args) {
        if (args.detail.kind === appModel.Activation.ActivationKind.search) {
            args.setPromise(ui.processAll().then(function () {
                if (!nav.location) {
                    nav.history.current = { location: Application.navigator.home, initialState: {} };
                }                   

                return nav.navigate(searchPageURI, { queryText: args.detail.queryText });
            }));
        }
    });

appModel.Search.SearchPane.getForCurrentView().onquerysubmitted = function (args) { nav.navigate(searchPageURI, args); };

Jednocześnie HTML strony z wynikami wyszukiwania nie odwołuje się do pliku JavaScript o takiej samej nazwie, co powoduje, że strona w designerze Blenda nie prezentuje się w prawidłowy sposób. Jeśli jednak otworzymy default.html i w trybie interaktywnym przenawigujemy do strony z wynikami, to prezentuje się ona wtedy prawidłowo. Stylowanie jest wtedy przyjemnością….

W wygenerowanej stronie są przykładowe dane, które potrzebujemy z reguły zmienić. W moim przypadku potrzebuję filtrować pobrane pliki wideo, które są zapisywane w folderze lokalnym aplikacji. W definicji strony powstał w związku z tym następujący kod:

ready: function (element, options) {
            …

      this._handleQuery(element, options);

      …

},

_handleQuery: function (element, args) {

           …
           this._searchData(element, this._queryText);

           …
},

_searchData: function (element, queryText) {
            if (window.Data) {
                var folder = Windows.Storage.ApplicationData.current.localFolder;
                var queryOptions = new Windows.Storage.Search.QueryOptions(Windows.Storage.Search.CommonFileQuery.defaultQuery, [".mp4"]);
                queryOptions.folderDepth = Windows.Storage.Search.FolderDepth.deep;
                queryOptions.indexerOption = Windows.Storage.Search.IndexerOption.useIndexerWhenAvailable;

                if (queryText !== "")
                    queryOptions.userSearchFilter = queryText;

                var fileQuery = folder.createFileQueryWithOptions(queryOptions);

                var dataSourceOptions = {
                    mode: Windows.Storage.FileProperties.ThumbnailMode.picturesView,
                    requestedThumbnailSize: 95,
                    thumbnailOptions: Windows.Storage.FileProperties.ThumbnailOptions.useCurrentScale
                };

                var dataSource = new WinJS.UI.StorageDataSource(fileQuery, dataSourceOptions);

                var listView = element.querySelector(".resultslist").winControl;
                listView.itemDataSource = dataSource;
            }
        }

By cieszyć się widokiem:

jstube_11

dokonanałem jeszcze trochę zmian w kodzie i stylach strony. Na łamach krótkiego postu nie wydają się one aż tak istotne, by je omawiać, z wyjątkiem kolorowania wyszukiwanej frazy na elementach ListView.

Wygenerowana strona otrzymuje już taką funkcjonalność w postaci zdefiniowanej funkcji konwertera  _markText. W czym więc problem? Pułapka tkwi w klasie StorageDataSource. Jak już ostatnio w swoich postach pisałem, wymaga ona bindingów onetime. Jak zdefiniować w WinJS binding onetime, który korzysta z konwertera? Nie jest to tak oczywiste jak… w XAML-u. Sprawę zakończyło zdefiniowanie nowego bindingu zawierającego w sobie także logikę konwertera:

_bindMarkedText: function (source, sourceProperty, destination, destinationProperty) {
            var element = destination;
            var text = source && source[sourceProperty];
            if (!text) return;
            if (this._lastSearch)
                destination[destinationProperty] = text.replace(this._lastSearch, "<mark>" + this._lastSearch + "</mark>");
            else
                destination[destinationProperty] = text;
        },

Większa powszechność tworzenia własnych bindingów i ich większa otwartość jest typowa także dla innych bibliotek JS (np. Knockout’a). W ich logice oprócz konwersji mogą znajdować się także operacje na elementach wizualnych, w tym animacje. Wracając do naszego przypadku chciałbym zwrócić uwagę na funkcję WinJS.Binding.initializer, której użyłem do zadeklarowania, że funkcja _bindMarkedText może być stosowana w deklaratywnym data bindingu.

_handleQuery: function (element, args) {
            …
            WinJS.Namespace.define("searchVideos", { bindMarkedText: WinJS.Binding.initializer(this._bindMarkedText.bind(this)) });            
            …

        },

Teraz wystarczy dopilnować, by w HTML tekst w szablonie listy był zbindowany tak:

<div class="itemtemplate" data-win-control="WinJS.Binding.Template">
        <div class="item">

            …
            <div class="item-content">
                <h3 class="item-title win-type-x-small" data-win-bind="innerHTML: name searchVideos.bindMarkedText"></h3>               
            </div>
        </div>
    </div>

Na koniec zmieniłem sobie w CSS kolor zaznaczonego tekstu z domyślnego niebieskiego na pomarańczowy:

.searchVideos section[role=main] .resultslist .item .item-content mark {
                    background: transparent;
                    color: #D8512B;

     }

Do usłyszenia.

czwartek, 6 września 2012

WinJS na żywo - odc.4 (m.in Flyout, progress, stylowanie, binding, transfer w tle)

Tym razem padło na wizualizację dla pobieranych plików. Wzorowałem się na wcześniej powstałej wersji w C#, ale niektóre zagadnienia rozwiązałem inaczej i nieco przestylowałem wygląd… Jak zwykle Blend okazał się bardzo pomocny, a tryb interaktywny nieoceniony… Pewnej przebudowy wymagała też implementacja pobierania plików, tak że obecnie dostarcza listę bindowalnych obiektów z informacjami o transferach… Ale po kolei.

Jak zrobić coś ala-dropdownbutton w aplikacji Windows 8? Najbardziej odpowiednia wydaje się do tego kontrolka Flyout. W jej wnętrzu umieściłem pole tekstowe z nagłówkiem oraz ListView z layoutem listy. HTML wygląda następująco:

  <div id="statusFlyout" data-win-control="WinJS.UI.Flyout">
        <div>
            <div id="progressheader">
                <span>Downloading&nbsp;</span><span id="downloadscount">3</span><span>&nbsp;files</span>
            </div>           
            <div id="progresslist" data-win-control="WinJS.UI.ListView"
                data-win-options="{layout:{type:WinJS.UI.ListLayout}, selectionMode:'none', tapBehavior:'none'}">
            </div>
        </div>
    </div>  

Kontrolka Flyout wraz z zawartością nie jest domyślnie widoczna. Chcemy pokazywać ją pod przyciskiem statusButton wyświetlającym informacje o ilości pobieranych plików, który ma także powodować jej wyświetlenie. Do tego przycisku podpinamy więc handler:

function showStatusFlyout() {
        var statusButton = document.getElementById("statusButton");    
        document.getElementById("statusFlyout").winControl.show(statusButton, "bottom", "right");     
    }

W tym przypadku metodzie show przekazałem:

  • obiekt, do którego chcemy się doczepić (statusButton)
  • krawędź doczepienia (bottom)
  • wyrównanie (right - do prawej krawędzi statusButton)

Dotknięcie każdego punktu poza obszarem popup’a spowoduje jego zamknięcie.

ListView umieszczony we Flyout zachowuje się nieco inaczej. Jeśli ustawimy źródło danych na ListView, w momencie, gdy nie jest widoczne, to po otwarciu Flyout… lista będzie pusta. Podyktowane jest to pewną optymalizacją, która zaszła w wersji finalnej - niewidoczne kontrolki nie podlegają przetwarzaniu. Możemy jednak przed pokazaniem ListView wymusić przetworzenie jego layoutu:

var statusFlyout = element.querySelector("#statusFlyout");
            statusFlyout.addEventListener("beforeshow", function (event) {               
                var progresslist = element.querySelector("#progresslist").winControl;               
                progresslist.forceLayout();               
            });

Teraz trochę o stylowaniu.

jstube_9

HTML szablonu elementu listy wizualizującej transfery plików wygląda następująco:

<div class="progressitemtemplate" data-win-control="WinJS.Binding.Template">
        <div class="progressitem">          
            <h4 class="progressitem-title" data-win-bind="textContent: fileName"></h4>               
            <progress class="progressitem-progress" max="100" data-win-bind="value:progress"></progress>
            <div class="progressitem-details">               
                <span data-win-bind="textContent: bytesReceived"></span>
                <span class="text-part">of</span>
                <span class="text-part" data-win-bind="textContent: totalBytesToReceive"></span>
                <span class="text-part">KB</span>
            </div>
            <button class="progressitem-cancelbutton" type="button"></button>          
        </div>
    </div>

Nie sposób w tym dość krótkim poście przedstawić wszystkie szczegóły związane ze stylowaniem, zwrócę jedynie uwagę na pewne aspekty związane z ListView oraz na kontrolkę <progress>.

Aby ustawić przezroczyste tło na ListView, wyłączyć standardową wizualizację na najeżdżanie,  zrobić odpowiedni margines wystarczy:

#progresslist .win-container:not(.footprint):not(.hover)
{
    background-color: transparent;
}

#progresslist .win-container:hover {
    outline: transparent solid 0px;
}

#progresslist > .win-vertical .win-container {
    margin: 5px;
}

Przy okazji mała dygresja odnośnie animacji w WinJS. Jakby komuś przyszło do głowy wyłączyć standardowo wbudowane animacje w ListView, to okaże się, że przełącznika w CSS do tego… nie ma. Jest natomiast możliwość globalnego wyłączenia/włączenia efektów animacyjnych dla całej aplikacji (to trochę bardziej złożone zagadnienie, odsyłam do opisów metod WinJS.UI.enableAnimations() i WinJS.UI.disableAnimations() w MSDN). Tutaj nie wyłączałem animacji…

Kontrolka <progress> jest standardowa dla HTML5, tutaj ma domyślny wygląd w stylu Modern UI/Metro. Styl CSS dla niej przyjął u mnie ostatecznie postać:

#progresslist .progressitem .progressitem-progress {
    width: 100%;
    color: #D8512B;
    background-color: #3D3D3D;
    -ms-grid-row: 2;
    margin-top: 10px;
    margin-bottom: 10px;
    margin-left: 1px;
}

Co potrzebujemy jeszcze zrobić, by cieszyć się widokiem, takim jak poniżej?

jstube_10

Z pewnością potrzebujemy powiązać informacje o transferach z UI. Możemy w WinJS.Binding.List trzymać bindowalne obiekty, które możemy sobie budować na podstawie obiektów DownloadOperation:

function makeListEntryFromDownload(download, existing) {
       
        var entry = {};       
      
        entry.guid = download.guid;
        entry.fileName = download.resultFile.name.replace(".mp4","");
        entry.bytesReceived = download.progress.bytesReceived;
        entry.totalBytesToReceive = download.progress.totalBytesToReceive;
        entry.progress = (entry.bytesReceived / entry.totalBytesToReceive) * 100.0;
       
        if (existing) {
            entry.promise = download.attachAsync();
        }
        else {           
            entry.promise = download.startAsync();
        }

        entry.promise = entry.promise.then(
            function () {
                downloadComplete(entry, download);
            },
            function (e) {
                downloadError(entry, e);
            },
            function () {
                downloadProgress(entry, download);
            }
        );
       
        entry.cancelDownload = function (e) {
            entry.promise.cancel();
        }

        return (entry);
    }

Metoda WinJS.Binding.as(data) robi z podanego obiektu bindowalny obiekt. 

Przedstawiony wyżej kod wykorzystujemy dla obiektów DownloadOperation aktualnie zakolejkowanych transferów w serwisie w następujący sposób:

var entry = makeListEntryFromDownload(download, true);
bindingList.push(WinJS.Binding.as(entry));

BindingList to WinJS.Binding.List, którą możemy powiązać z ListView. W przypadku zlecenia nowego transferu napisaną przez nas metodą wywołujemy z parametrem false zamiast true.

Bindowalne obiekty możemy uaktualniać w następujący sposób:

function downloadProgress(entry, download) {
        var bound = WinJS.Binding.as(entry);
        bound.bytesReceived = download.progress.bytesReceived;
        bound.totalBytesToReceive = download.progress.totalBytesToReceive;
        bound.progress = (bound.bytesReceived / bound.totalBytesToReceive) * 100.0;
    }

Całość kodu zorganizowałem sobie podobnie jak w przykładzie Mike Taulty'ego, który ostatnio przypadł mi do gustu. Polecam zapoznać się z nim.

Przy okazji mała dygresja odnośnie transferu plików w tle. W trakcie rozwijania Windows 8, ostatecznie stanęło na tym, że transfery plików są kontynuowane przy uśpionej aplikacji. Jeśli proces aplikacji po jej uśpieniu zostanie zakończony, to przy ponownym uruchomieniu aplikacji, transfery plików są restartowane (zapamiętywana jest lista plików do transferu).

Stay tuned!

sobota, 1 września 2012

WinJS na żywo - odc. 3 (m.in pobieranie plików w tle, komponent WinRT, wyświetlanie miniatur plików w ListView, odtwarzanie video)

Przepisywanie z XAML i C# na HTML5 i WinJS nabrało tempa. Obecnie mogę pobierać pliki, przeglądać je i odtwarzać. Na tej drodze było parę niespodzianek, parę ciekawostek i na te tematy będzie dzisiejszy post.

Pewne zmiany nastąpiły przy przepisywaniu logiki wyciągania adresu ze strony YouTube z C# na JS (tak wiem, mogłem tego nie przepisywać, zrobić sobie komponent WinRT w C#, ale chciałem zobaczyć jak wyjdzie to całościowo w Java Script). O ile trzeba pamiętać, że niektóre tak samo nazywające się metody przy podaniu takich samych parametrów zadziałają inaczej (np. substring), o tyle kwestia unescapingu podanego napisu z linkiem okazała się trudniejsza. W WinJS mamy do tego metodę Windows.Foundation.Uri.unescapeComponent(toUnescape), w .NET mamy podobną metodę Uri.UnescapeDataString(stringToUnescape). Fragment strony HTML był z powodzeniem przetwarzany przez metodę w .NET, ale w Java Script już nie (zdaje się, że nie są to do końca odpowiedniki - JS ma jedną metodę do escape-owania spodziewającą się Uri, .NET ma dwie metody - jedną dla Uri i jedną dla stringa ze słowem DataString). Może mogłem przebudować algorytm tak by lepiej pasował do dostępnej funkcji w JavaScript, ale na tym etapie nie uznałem tego za celowe, więc … użyłem metody z C#. Jak? Opakowałem ją w komponent WinRT:

public sealed class EncodingUtils
    {
        public string UnescapeDataString(string stringToUnescape)
        {
            return Uri.UnescapeDataString(stringToUnescape);
        }
    }

i wykorzystuję w kodzie Java Script:

(function () {
    "use strict";   

var utils = new MyTubeUtils.EncodingUtils();

function getFormatUrl(buffer, format) {
      ….

     var str3 = utils.unescapeDataString(subStr);

      ….

}

})();

Przejdźmy teraz do pobierania plików w tle. Całkiem dobra implementacja zawarta jest w referencyjnym Background Transfer sample, przy czym tam zlecanie i wyświetlanie pobrań odbywa się w ramach jednej strony. U mnie zlecanie pobierania plików odbywa się z poziomu innej strony (Add New Videos) niż monitorowanie pobrań (Start), więc tę czynność musiałem sobie wydzielić:

function downloadFile(fileName, url) {

        var uri = new Windows.Foundation.Uri(url);

        Windows.Storage.ApplicationData.current.temporaryFolder.createFileAsync(fileName, Windows.Storage.CreationCollisionOption.replaceExisting).done(function (newFile) {
            var downloader = new Windows.Networking.BackgroundTransfer.BackgroundDownloader();
            var download = downloader.createDownload(uri, newFile);
            download.startAsync();
        });       
    }

Po otwarciu strony startowej zawsze sprawdzam, czy nie ma aktywnych pobrań plików i ustawiam w prawym górnym rogu odpowiedni status. Gdy plik zostanie z sukcesem pobrany przenoszę go z temporaryFolder do localFolder. Pojawia się nam teraz kolejne zagadnienie jak prezentować w ListView pobrane pliki video z miniaturami.

jstube_7

W C# używałem do tego klasy FileInformationFactory, która dla zdefiniowanego file query zwracała wirtualizowany wektor plików, który można było zbindować do ListView. Dzięki temu mieliśmy automatyczne odświeżanie listy, gdy nowy plik został pobrany (zapisany w localFolder). Jak to wygląda w WinJS? Okazuje się, że mamy podobne możliwości, ale nieco inne API.

Zacznijmy od zdefiniowania file query, którego definiowanie przypomina definiowanie w C#. W moim przypadku przedstawia się to następująco:

var folder = Windows.Storage.ApplicationData.current.localFolder;

var queryOptions = new Windows.Storage.Search.QueryOptions(Windows.Storage.Search.CommonFileQuery.defaultQuery, [".mp4"]);
queryOptions.folderDepth = Windows.Storage.Search.FolderDepth.deep;
queryOptions.indexerOption = Windows.Storage.Search.IndexerOption.useIndexerWhenAvailable;

var fileQuery = folder.createFileQueryWithOptions(queryOptions);

Musiałem dodać tutaj filtr na pliki o rozszerzeniu .mp4 (w takim formacie zapisuję na razie wszystkie pobierane pliki), ponieważ zauważyłem że aplikacja pisana w HTML5 tworzy sobie plik sesji w lokalnym folderze i … na początku trafił mi do ListView.

I teraz rzecz najciekawsza. W WinJS mamy specjalny storage typu StorageDataSource, który należy zbindować do ListView:

var dataSourceOptions = {              
                mode: Windows.Storage.FileProperties.ThumbnailMode.picturesView,
                requestedThumbnailSize: 283,
                thumbnailOptions: Windows.Storage.FileProperties.ThumbnailOptions.useCurrentScale
            };

var dataSource = new WinJS.UI.StorageDataSource(fileQuery, dataSourceOptions);

listView.itemDataSource = dataSource;

Dzięki temu mamy jak w C# automatyczne odświeżanie ListView, jeśli zapiszemy do localStorage nowy plik .mp4. Ze StorageDataSource wiąże się też parę faktów. Nie wspiera widoków grupowych oraz wymaga określonego rodzaju bindingów. Przy uruchamianiu aplikacji z dotychczas używanym szablonem dla elementu listy, otrzymałem wyjątek Cannot define property '_getObservable': object is not extensible. Trzeba zmienić domyślny rodzaj bindingu na onetime. Przykładowo, aby na szablonie elementu wyświetlić tytuł nagrania, robimy to tak:

<h4 class="item-title" data-win-bind="textContent: name WinJS.Binding.oneTime"></h4>

Jest też kwestia wyświetlenia obrazka na elemencie. W C# miałem do tego konwerter, który tworzył mi BitmapImage ze strumienia. W WinJS wyświetlenia obrazka również nie dostaniemy za darmo, trzeba napisać sobie własną funkcję do bindingu: 

(function () {
    "use strict";

function bindThumbnail(source, sourceProperty, destination, destinationProperty) {

    …
}

    WinJS.Utilities.markSupportedForProcessing(bindThumbnail);

    WinJS.Namespace.define("Start", {       
        bindThumbnail: bindThumbnail
    });
})();

Metoda WinJS.Utilities.markSupportedForProcessing służy do rejestracji naszej metody do deklaratywnego użycia (cześć bardziej standardowych metod np. dla strony, dla konwersji ma to zapewnione automatycznie)

Polecam tutaj skorzystać z implementacji bindThumbnail z referencyjnego StorageDataSource and GetVirtualizedFilesVector sample. Jak zobaczymy, oprócz konwersji obiektu pliku na URL i przypisania do do image.src autorzy wzbogacili ją o animacje w wybranych przypadkach. Przy okazji mała dygresja - pisanie bindingów własnego rodzaju za pomocą funkcji kojarzy mi się z Knockoutem.

Ostatecznie szablon elementu dla ListView wygląda następująco:

   <div class="itemtemplate" data-win-control="WinJS.Binding.Template">
        <div class="item">
            <img class="item-image" src="#" data-win-bind="src: thumbnail Start.bindThumbnail" style="opacity: 0;"/>
            <div class="item-overlay">
                <h4 class="item-title" data-win-bind="textContent: name WinJS.Binding.oneTime"></h4>               
            </div>
        </div>
    </div>

Teraz ostatnie na dziś główne zagadnienie - odtwarzanie plików video.

jstube_8

Aby po dotknięciu elementu ListView przejść na stronę, na której wyświetlimy wideo przydatny będzie zdefiniowanie funkcji _itemInvoked w funkcji definiującej stronę:

ui.Pages.define("/pages/start/start.html", {

….,

_itemInvoked: function (args) {
            args.detail.itemPromise.then(function (invokedItem) {
                nav.navigate("/pages/videoDetails/videoDetails.html", { video: invokedItem.data });
            }, false);                    
        }

Możemy teraz na stronie z wideo zamienić otrzymane dane na URL, który przekażemy elementowi <video> z HTML5.

WinJS.UI.Pages.define("/pages/videoDetails/videoDetails.html", {
       ready: function (element, options) {
            if (options && options.video) {
                var fileItem = options.video;
                var video = document.getElementById("player");
                video.src = URL.createObjectURL(fileItem);
                video.play();
            }           
        }
    });

Należy tu dodać, że dostajemy za darmo ładnie ostylowane UI z paskiem postępu, przyciskami i regulacją głośności (btw w przeglądarkach kontrolka <video> też oferuje podobne możliwości i jest otrzymuje predefiniowane UI, w aplikacji Windows 8 dostajemy styl Modern UI/Metro). Aby otrzymać wygląd kontrolki z przyciskami ze screenshota powyżej wystarczy wewnątrz strony użyć:

<video id="player" controls>                        
</video>

Oczywiście trochę trzeba było przestylować domyślny szablon strony, zmienić trochę CSS-ów, by kontrolka <video> zajmowała cały obszar strony i aby znajdowała się pod przyciskiem Back. Zastosowałem tutaj z-index by sekcja z nagłówkiem pojawiała się pod contentem strony z wideo. Aczkolwiek z nowym Blendem była to czysta przyjemność…

Jeszcze małe porównanie z XAML i C#. W wersji końcowej Windows 8 została wyrzucona kontrolka MediaPlayer, mamy do dyspozycji w jedynie MediaElement, który nie ma predefiniowanego UI do kontroli odtwarzania… W tym przypadku lepiej wygląda to w HTML5, ale piszący w C# mogą skorzystać z Player Framework for Windows 8, który oferuje gotową kontrolkę odtwarzacza oraz z przykładów.

Ciąg dalszy nastąpi.