piątek, 18 lipca 2014

Wieczór z Madonną cz.2 (WinJS 2.1 na Windows Phone 8.1)

Od ostatniego posta minął miesiąc, coś ten tytułowy wieczór nam się rozciągnął, ale w końcu piszę część drugą i ostatnią tej mikroserii, którą sobie założyłem.  Zrelacjonuję tutaj swoje praktyczne doświadczenia, a także subiektywne odczucia co do pisania uniwersalnych aplikacji w WinJS na Windows i Windows Phone 8.1. Skupię się na telefonie i WinJS 2.1.

W celach eksperymentalnych pozwoliłem sobie przepisać uniwersalną aplikację XAML/C# z poprzedniego posta na HTML5 i WinJS.

wp81winjs

Co mnie zaskoczyło, co było interesujące, z czym są lub były problemy, już piszę. A więc:

1. Kontrolka AppBar z WinJS ma buga na Windows Phone, otóż… sama znika po przejściu na inną stronę. Nie tylko ja na natrafiłem, społeczność światowa zdążyła założyć już odpowiednie wątki na forach. Co robić?  Zamiast na poszczególnych stronach możemy trzymać ją globalnie w pliku default.html.

<body class="phone">
    <div id="contenthost" data-win-control="Application.PageControlNavigator" data-win-options="{home: '/pages/home/home.html'}"></div>
    <div id="appbar" data-win-control="WinJS.UI.AppBar">
        <button data-win-control="WinJS.UI.AppBarCommand" data-win-options="{icon:'add', id:'cmdAdd', label:'Add', section:'global', type:'button'}"></button>
        <button data-win-control="WinJS.UI.AppBarCommand" data-win-options="{icon:'find', id:'cmdSearch', label:'Search', section:'global', type:'button'}"></button>
        <button data-win-control="WinJS.UI.AppBarCommand" data-win-options="{icon:'microphone', id:'cmdVoice', label:'Voice', section:'global', type:'button'}"></button>
        <button data-win-control="WinJS.UI.AppBarCommand" data-win-options="{disabled:true, icon:'download', id:'cmdDownload', label:'download', section:'global'}"></button>
    </div>
</body>

Na poszczególnych stronach włączymy odpowiednie dla danego miejsca komendy. Przykładowo na stronie startowej metoda ready woła kod:

           var appbar = document.querySelector("#appbar").winControl;
            if (appbar) {
                appbar.showOnlyCommands(['cmdAdd', 'cmdSearch', 'cmdVoice']);                            
            }

            …

     var addButton = document.querySelector("#cmdAdd").winControl;
     addButton.addEventListener("click", this.doClickAdd);           

            var searchButton = document.querySelector("#cmdSearch").winControl;
            searchButton.addEventListener("click", this.doClickSearch);           

            var voiceButton = document.querySelector("#cmdVoice").winControl;
            voiceButton.addEventListener("click", this.doClickVoice);

W obiekcie definiującym tę stronę mam oczywiście definicji metod doClickAdd, doClickSearch i doClickVoice. Przy wyładowaniu strony w metodzie unload odpinam się od tych zdarzeń.

2. Stylowanie stron CSS robiło na początku mi niespodzianki, trzeba uważać, najlepiej opierać się na predefiniowanych stylach w projektach Visual Studio. Zdarzało mi się, że po zmianie wysokości sekcji nagłówka, rozepchany nagłówek i część stylowania zostawała po powrocie na stronę startową z pivotem. W końcu postanowiłem na bardziej standardowe rozwiązania i problemy znikły (zamiast pola tekstowego nad tytułem strony zrobiłem go bardziej typowo pod, sprawę ułatwiło też skorzystanie z projektu VS z pivotem zamiast mniej skompilowanego sampla z SDK dla pivota)

3. W WinJS na WP nie ma kontrolki hub (choć jest w XAML). Musimy zadowolić się pivotem, który oficjalnie jest polecany w miejsce huba. Z mniej podstawowych rzeczy zrobiłem sobie pokazywanie AppBar w zależności od aktualnie wyświetlanej sekcji pivota:

            var hub = element.querySelector(".hub").winControl;
            hub.addEventListener("selectionchanged", function (args) {
                if (appbar) {
                    if (args.detail.index === 0) {
                        appbar.disabled = false;
                    }
                    else {
                        appbar.disabled = true;
                    }
                }
            });

4. W WinJS na WP nie ma kontrolki AutoSuggestBox (choć jest to nowość w XAML). Zadowoliłem się zwykłym tekstowym inputem w HTML.

5. Ciężej i jednak mniej wygodnie pisze się kod asynchroniczny w JavaScript za pomocą promis-ów niż przy pomocy async-ów w C#. O ile na typowej stronie czy aplikacji webowej na przeglądarkę nie używa się jeszcze często asynchronicznych metod poza strzałami AJAX-owymi (nie wspominam tu o web workerach), o tyle w Windows niemal każda metoda z API jest asynchroniczna. Stosowanie ciągów then, then, then, done jest całkiem przyjazne, pojawiają się jednak ciekawsze przypadki.

a.  Jak w pętli wywołać n-razy tą samą asynchroniczną metodę ?  Znalazłem fajnego bloga pokazującego odpowiedni pattern.

b. Jak zwrócić zwykłą wartość jako promise by zachować ciąg then, then, …, done?  Możemy wtedy zwrócić  WinJS.Promise.as(wartość).

c. Jak opakować w promise odtworzenie pliku audio? Ponieważ przenosiłem kod będący przeplataniem generowania i rozpoznawania mowy, chciałem za każdym razem poczekać aż telefon powie jakąś kwestię, zanim zacznie nasłuchiwać mojej odpowiedzi. Mając za sobą opakowanie odtwarzania w MediaElement w taska, postąpiłem tutaj podobnie:

var playAudio = function (markersStream) {
            return new WinJS.Promise(function (complete, error, progress) {
                var audio = new Audio();
                var blob = MSApp.createBlobFromRandomAccessStream(markersStream.ContentType, markersStream);
                audio.src = URL.createObjectURL(blob, { oneTimeOnly: true });

                audio.addEventListener('ended', function () {
                    complete();
                }, false);

                audio.play();
            });           
        };   

speechSynthesizer.synthesizeTextToStreamAsync("What song do I play?").then(function (markersStream) {
                return playAudio(markersStream);
            }).then(function () {

               // rozpoznawanie mowy

           }

6.  Gdy chcemy we współdzielonym pliku JavaScript warunkowo wywołać kod w zależności od tego, czy jesteśmy na WP czy na Windows, to robimy warunek na wartość WinJS.Utilities.isPhone.

7. Jeśli chcemy we współdzielonym pliku CSS uzależnić styl od wykonywania na WP, to przed warunkiem dostawiamy klasę .phone.   Przykładowo, gdy ukrywamy na telefonie przycisk Back na stronie  piszemy:

.phone .videoDetails .back-button {
    display: none;
}

Podsumowanie.  Odczucia?  Trochę mniej wygodnie i mniej stabilnie niż w XAML/C# czy WinJS na Windows 8.1.  Nie wiem na ile to kwestia systemu Windows Phone na emulatorze w wersji niefinalnej (btw przy deploywaniu Visual Studio za każdym razem wgrywa WinJS 2.1 na telefon), na ile częściowo inne scenariusze w UI i implementacja większej liczby funkcjonalności wynikających z różnic w API w stosunku do pełnego Windows, a na ile fakt, że mam trochę mieszane uczucia co do samych technologii webowych mających być już tą jedyną i słuszną przyszłością. Ale ogólnie eksperyment udany, technologie webowe zdobyły kolejny bastion.