sobota, 24 września 2016

Xamarin.Forms kontra Xamarin odc.2 (XAML, lista, search, własna kontrolka)

Dobry wieczór, Xamarin.Forms kontra Xamarin, witam serdecznie Winking smile Zrobiła się nieco dłuższa przerwa od ostatniego razu, ale powinno zaowocować to mocniejszym materiałem. XAML to język znany od lat dziesięciu. Czy jednak XAML w Xamarin.Forms w zestawieniu z XAML UWP czy WPF nie jest ich dość odległym kuzynem niczym w przyrodzie wilk i wilk workowaty?  Zaczynamy!

Polem do dyskusji tradycyjnie będzie już porcja nowego kodu z repozytorium LightOrgan/Xamarin.Forms/. Od ostatniego odcinka na dwóch stronkach XAML przybyło całkiem sporo elementów. Prezentują się one nieco inaczej na platformie Android:

Screenshot_20160923-020818  Screenshot_20160923-020951

i platformie iOS:

IMG_0059  IMG_0060

 

#1  Pasek odtwarzacza

Mówimy o tym pasku na dole strony MainPage. Widzimy że dość różni się pomiędzy Android i iOS. Tymczasem zrobiłem go za pomocą wspólnego kawałka XAML:

<RelativeLayout Grid.Row="1" BackgroundColor="#e57373">     
      <RelativeLayout.Resources>
        <ResourceDictionary>
            <OnPlatform x:TypeArguments="ConstraintType" Android="RelativeToParent" iOS="Constant" x:Key="XType" />
            <OnPlatform x:TypeArguments="x:Double" Android="-52" iOS="8" x:Key="XConstant" />
            <OnPlatform x:TypeArguments="x:Double" Android="22" iOS="8" x:Key="YConstant" />
            <OnPlatform x:TypeArguments="x:String" Android="AlbumArt" iOS="PlayPauseIcon" x:Key="TitleRelativeElement" />
        </ResourceDictionary>
    </RelativeLayout.Resources>   
      <RelativeLayout.HeightRequest>
        <OnPlatform Android="80" iOS="48" x:TypeArguments="x:Double"/>
      </RelativeLayout.HeightRequest>
      <ContentView x:Name="AlbumArt"
                   Content="{StaticResource AlbumArt}"
                   RelativeLayout.XConstraint="{ConstraintExpression Type=Constant, Constant=8}"
                   RelativeLayout.YConstraint="{ConstraintExpression Type=Constant, Constant=8}"
                   RelativeLayout.HeightConstraint="{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=1, Constant=-16}">       
      </ContentView>     
      <Label x:Name="Title" TextColor="White"
             RelativeLayout.XConstraint="{ConstraintExpression Type=RelativeToView, ElementName={StaticResource TitleRelativeElement}, Property=Width, Factor=1, Constant=16}"
             RelativeLayout.YConstraint="{ConstraintExpression Type=Constant, Constant=16}">
        <Label.FontSize>
          <OnPlatform x:TypeArguments="x:Double" Android="18" iOS="14"/>
        </Label.FontSize>     
      </Label>     
      <Label x:Name="Artist" FontSize="14" TextColor="#ffebee"
             RelativeLayout.XConstraint="{ConstraintExpression Type=RelativeToView, ElementName=AlbumArt, Property=Width, Factor=1, Constant=16}"
             RelativeLayout.YConstraint="{ConstraintExpression Type=Constant, Constant=40}">
        <Label.IsVisible>
          <OnPlatform x:TypeArguments="x:Boolean" Android="True" iOS="False"/>
        </Label.IsVisible>     
      </Label>   
      <Image x:Name="PlayPauseIcon"
             RelativeLayout.XConstraint="{ConstraintExpression Type={StaticResource XType}, Property=Width, Factor=1, Constant={StaticResource XConstant}}"
             RelativeLayout.YConstraint="{ConstraintExpression Type=Constant, Constant={StaticResource YConstant}}">
        <Image.Source>
          <OnPlatform x:TypeArguments="ImageSource" Android="ic_play_arrow_white_36dp.png" iOS="Play.png"/>
        </Image.Source>     
      </Image>     
    </RelativeLayout>   

Aha, zasób AlbumArt jest zdefiniowany w całym MainPage jako:

      <OnPlatform x:Key="AlbumArt" x:TypeArguments="View">       
        <OnPlatform.Android>
          <Image BackgroundColor="#9ccc65" Source="ic_audiotrack_white_36dp.png" Aspect="AspectFit" WidthRequest="64" HeightRequest="64"/>
        </OnPlatform.Android>       
      </OnPlatform> 

Stosując ContentView z zasobem zdefiniowanym wariantowo poprzez OnPlatform udało mi się pokazywać obrazek tylko w Android. Teraz trochę uwag. Czemu ContentView a nie ContentControl? Czemu BackgroundColor a nie Background? Aspect? WidthRequest?  Takich rzeczy nie znajdziemy w żadnym innym gatunku XAML. A RelativeLayout?  Niemal zupełnie inna koncepcja niż tego w XAML UWP z Windows 10!  ConstraintExpression występuje tylko w Xamarin.Forms. Brakuje mi formy znanej nie tylko z Windows 10, ale i natywnego .axml z Android. W dodatku czuję tu pewne ograniczenia. Centrowanie i układanie w stosunku do sąsiadów nie wydaje się dostatecznie wygodne.

 

#2 lista

Znów widzimy różnice. W XAML to jeden kawałek wspólny dla wszystkich platform:

<ListView x:Name="listView" BackgroundColor="{StaticResource PageBackgroundColor}" SeparatorColor="#e57373" ItemTapped="listView_ItemTapped" ItemSelected="listView_ItemSelected">
      <ListView.SeparatorVisibility>
        <OnPlatform x:TypeArguments="SeparatorVisibility" Android="None" iOS="Default"/>
      </ListView.SeparatorVisibility>
      <ListView.RowHeight>
        <OnPlatform x:TypeArguments="x:Int32" Android="68"/>
      </ListView.RowHeight>
      <ListView.ItemTemplate>
        <DataTemplate>
          <ViewCell>
            <StackLayout HorizontalOptions="StartAndExpand" Orientation="Horizontal">
              <StackLayout.Padding>
                <OnPlatform x:TypeArguments="Thickness" Android="12,0,0,0" iOS="6,0,0,0"/>
              </StackLayout.Padding>
              <Image BackgroundColor="#9ccc65" Aspect="AspectFit" WidthRequest="36" HeightRequest="36" VerticalOptions="Center">
                <Image.Source>
                  <OnPlatform x:TypeArguments="ImageSource" Android="ic_audiotrack_white_36dp.png" iOS="audioIcon.png"/>
                </Image.Source>
              </Image>           
              <StackLayout VerticalOptions="Center" Orientation="Vertical">
                <StackLayout.Padding>
                  <OnPlatform x:TypeArguments="Thickness" Android="12,0,0,0" iOS="0"/>
                </StackLayout.Padding>
                <Label Text="{Binding Title}" VerticalTextAlignment="Center" TextColor="White">
                  <Label.FontSize>
                    <OnPlatform x:TypeArguments="x:Double" Android="18" iOS="16"/>
                  </Label.FontSize>
                </Label>
                <StackLayout Padding="0,-5,0,0" HorizontalOptions="StartAndExpand" Orientation="Horizontal">
                  <Label Text="{Binding Artist}" VerticalTextAlignment="Center" TextColor="#ffebee">
                    <Label.FontSize>
                      <OnPlatform x:TypeArguments="x:Double" Android="14" iOS="12"/>
                    </Label.FontSize>
                  </Label>
                  <Label Text="{Binding Duration}" VerticalTextAlignment="Center" TextColor="#ffebee">
                    <Label.FontSize>
                      <OnPlatform x:TypeArguments="x:Double" Android="14" iOS="12"/>
                    </Label.FontSize>
                  </Label>
                </StackLayout>             
              </StackLayout>
            </StackLayout>
          </ViewCell>
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView> 

Niestety można natrafić na kłopoty z ostylowaniem bardziej wysublimowanych elementów w porównaniu do natywnych interfesjów z Android i iOS. W dodatku znów używany tutaj XAML różni od innych gatunków. Ale po kolei.

W Android lista przyjmuje tło strony, ale już w iOS domyślnie jest biała jak oryginał i trzeba jawnie jej ustawić kolor tła. Dużą niegodnością są kolory przyciśniętego i zaznaczonego elementu listy. O ile w Android udało mi się w zrobić tak, by element zaznaczony nie miał innego koloru, a przytrzymany nie miał go prawie w ogóle, o tyle w iOS w pewnym momencie machnąłem już ręką. Tak wiem, że jak mi się niepodoba, to mogę sobie napisać własny natywny renderer dla każdej z platform każdego niemal elementu z Xamarin.Forms, ale czy framework nie mógłby po prostu w ujednoliconym XAML udostępnić nieco więcej właściwości niż teraz? Np. kolor zaznaczonego elementu? Na razie uciekam się do sztuczki, by lista nie miała nigdy zaznaczonego elementu:

private void listView_ItemSelected(object sender, SelectedItemChangedEventArgs e)
{
        listView.SelectedItem = null;
}

A czemu w DataTemplate rootem czynimy ViewCell?  Tak wiem, kojarzy się to choćby z iOS, ale dla ludzi wywodzących się z innych, bardziej popularnych odmian XAML, jest to twór obcy. A czemu StackLayout a nie StackPanel? Może dlatego że można go rozciągać za pomocą HorizontalOptions (czemu nie HorizontalAlignment?) z wartością StartAndExpand? TextColor miast Foreground? Eh…

 

#3 search

Spora porażka, że SearchBar ze stabilnej wersji nie renderuje się w Android 7!  Znajdziemy taki wątek na forum Xamarina (o widzę pojawiła się odpowiedź: jest obejście w postaci jawnego HeighRequest, może wypróbuję). Nie mając jeszcze odpowiedzi zrobiłem póki co podobnie jak w jednym przykładzie od Xamarina, użyłem zwykłego pola tekstowego. Jest nim kontrolka Entry (zamiast TextBox):

    <Frame Padding="8, 0" OutlineColor="Transparent"  BackgroundColor="Transparent" HasShadow="False" HorizontalOptions="FillAndExpand">
      <Frame.Padding>
        <OnPlatform x:TypeArguments="Thickness" Android="8,0" iOS="6,10,6,4"/>
      </Frame.Padding>
      <Entry Placeholder="{i18n:Translate SearchSongs}" TextChanged="SearchBar_OnTextChanged">
        <Entry.FontSize>
          <OnPlatform x:TypeArguments="x:Double" Android="20" iOS="16"/>
        </Entry.FontSize>
        <Entry.TextColor>
          <OnPlatform x:TypeArguments="Color" Android="White" iOS="#e57373"/>
        </Entry.TextColor>
        <Entry.PlaceholderColor>
          <OnPlatform x:TypeArguments="Color" Android="#ffcdd2" iOS="#f8bbd0"/>
        </Entry.PlaceholderColor>
      </Entry>
    </Frame>

Nie wygląda oczywiście to tak super, jak oryginalny search, ale stosując różne ustawienia trochę popracowałem, by wyglądało to w miarę przyzwoicie.

 

#4 własna kontrolka

Narysować kółko, elipsę cóż trudnego powie prawie każdy użytkownik XAML… No właśnie, prawie robi różnicę. Na pewno nie powie tego ktoś, kto pisze w Xamarin.Forms. Trzeba tutaj opakować natywne implementacje pisane na każdą z platform. W moim przypadku opakowałem kontrolkę CircleView, którą napisałem wcześniej dla Android i iOS tworząc klon app-ki w Xamarin. W projekcie portable napisałem klasę:

    public class CircleView: View
    {
        public CircleView()
        {
        }

        public static readonly BindableProperty CircleColorProperty =
            BindableProperty.Create<CircleView, Color>(
                p => p.CircleColor, Color.Red);
       
        public Color CircleColor
        {
            get { return (Color)GetValue(CircleColorProperty); }
            set { SetValue(CircleColorProperty, value); }
        }
    }

Tutaj twórcy Xamarin.Forms dali kolejny upust chęci odróżnienia się od wszystkich innych gatunków XAML mianując propercję bindowalną jako BindableProperty, a nie DependencyProperty. Może to był wyraz szczerości, że jej funkcjonalność nie jest tak potężna jak we frameworkach stworzonych od początku przez Microsoft. Następnie w każdym projekcie na daną platformę stworzyłem sobie renderer, który łączy klasę “na wszystko” z natywną kontrolką. W Android wygląda to tak:

using Xamarin.Forms;
using LightOrganApp.Droid.Renderers;
using Xamarin.Forms.Platform.Android;

[assembly: ExportRenderer(typeof(LightOrganApp.Controls.CircleView), typeof(CircleViewRenderer))]
namespace LightOrganApp.Droid.Renderers
{
    public class CircleViewRenderer: ViewRenderer<LightOrganApp.Controls.CircleView, LightOrganApp.Droid.UI.CircleView>
    {
        public CircleViewRenderer()
        {
        }

        protected override void OnElementChanged(ElementChangedEventArgs<LightOrganApp.Controls.CircleView> e)
        {
            base.OnElementChanged(e);

            if (e.OldElement != null || this.Element == null)
                return;

            var circle = new LightOrganApp.Droid.UI.CircleView(Forms.Context)
            {               
                CircleColor = Element.CircleColor.ToAndroid()               
            };

            SetNativeControl(circle);
        }

        protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (Control == null || Element == null)
                return;

            if (e.PropertyName == LightOrganApp.Controls.CircleView.CircleColorProperty.PropertyName)
            {
                Control.CircleColor = Element.CircleColor.ToAndroid();
            }
        }
    }
}

a w iOS tak:

using Xamarin.Forms;
using LightOrganApp.iOS.Renderers;
using Xamarin.Forms.Platform.iOS;
using UIKit;

[assembly: ExportRenderer(typeof(LightOrganApp.Controls.CircleView), typeof(CircleViewRenderer))]
namespace LightOrganApp.iOS.Renderers
{
    public class CircleViewRenderer: ViewRenderer<LightOrganApp.Controls.CircleView, LightOrganApp.iOS.CircleView>
    {
        protected override void OnElementChanged(ElementChangedEventArgs<LightOrganApp.Controls.CircleView> e)
        {
            base.OnElementChanged(e);

            if (e.OldElement != null || this.Element == null)
                return;

            var circle = new LightOrganApp.iOS.CircleView
            {
                CircleColor = Element.CircleColor.ToUIColor(),
                BackgroundColor = UIColor.Clear           
            };

            SetNativeControl(circle);
        }

        protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (Control == null || Element == null)
                return;

            if (e.PropertyName == LightOrganApp.Controls.CircleView.CircleColorProperty.PropertyName)
            {
                Control.CircleColor = Element.CircleColor.ToUIColor();
            }
        }
    }
}

Po tym wszystkim (nie licząc czasami potrzeby pisania natywnych kontrolek) możemy cieszyć się własną kontrolką w XAML:

<controls:CircleView VerticalOptions="FillAndExpand"
            HorizontalOptions="FillAndExpand" CircleColor="{Binding BassColor}"/>

Aby dopełnić pełni obrazu dodam, że w wersji preview Xamarin.Forms Microsoft wprowadził możliwość używania bezpośrednio w XAML natywnych elementów typowych dla danej platformy bez konieczności pisania klas typu renderer. Znacząco to uprości pisanie interfejsu. Nie testowałem tego jeszcze, ale z pewnością wrócimy do tego tematu.

 

#5 orientacja ekranu i jeszcze renderer

Xamarin.Forms w stosunku do XAML UWP Windows 10, CSS w Web, natywnego Androida czy nawet natywnego iOS brakuje deklaratywnego sposobu definiowania innych wariantów interfejsu użytkownika w zależności od orientacji ekranu, rodzaju urządzenia czy rozmiarów ekranu. Aby obracać światła kolorofonu skorzystałem najpierw z kodu, który zmieniał orientację StackLayout. StackLayout z Xamarin.Forms w przeciwieństwie do StackPanelu z każdego innego XAML ma kilka fajnych właściwości, jak możliwość wypełniania dostępnej przestrzeni (niczym Grid), a także ustawianie odległości między elementami. W dodatku domyślna odległość między elementami nie jest zero jak w StackPanel, tylko wynosi 10!  To duża niespodzianka dla osób wywodzących się z XAML innego niż w Xamarin. Pomysł ze zmianą orientacji działał ładnie na Androidzie, ale na iOS (chyba że tylko na iOS 10?) spotkała mnie przykra niespodzianka. Po obrocie ekranu kółka się … rozciągnęły, tak że urosły i były widoczne zasadniczo dwa z małym ogonkiem, a nie trzy. Czemu nie wiadomo.

Przypomniałem sobie, że na iOS w natywnym ujęciu wyłączałem animacje tranzycji strony przy obrocie ekranu. Ale jak to zrobić w cross-platformowym Xamarin.Forms? Jest na to sposób. Możemy napisać własny renderer strony. Tak zrobiłem teraz w projekcie tylko dla iOS:

using CoreAnimation;
using CoreGraphics;
using UIKit;
using Xamarin.Forms.Platform.iOS;
using LightOrganApp.iOS.Renderers;
using Xamarin.Forms;
using LightOrganApp;

[assembly: ExportRenderer(typeof(MainPage), typeof(MainPageRenderer))]
namespace LightOrganApp.iOS.Renderers
{
    public class MainPageRenderer: PageRenderer
    {
        public override void ViewWillTransitionToSize(CGSize toSize, IUIViewControllerTransitionCoordinator coordinator)
        {
            base.ViewWillTransitionToSize(toSize, coordinator);
            CATransaction.Begin();
            CATransaction.DisableActions = true;

            coordinator.AnimateAlongsideTransition(ctx => { }, ctx => CATransaction.Commit());
        }
    }
}

Ale to nie rozwiązało sprawy. Jak kółka na iOS po obrocie ekranu się rozwalały, tak rozwalały się dalej. Intuicja programisty nie piszącego od wczoraj podpowiedziała mi rozwiązanie. Może by przy obrocie ekranu ładować DataTemplate do ContentControl?  Ale hola. W Xamarin.Forms nie ma… ContentControl. Grr.. Natrafiłem jednak na świetny post ContentPresenter for Xamarin.Forms, który wskazał mi zaktualizowane repozytorium https://github.com/XLabs/Xamarin-Forms-Labs. Z niego wziąłem implementację ContentControl:

   public class ContentControl : ContentView
   {               
       public static readonly BindableProperty ContentTemplateProperty = BindableProperty.Create<ContentControl, DataTemplate>(x => x.ContentTemplate, null, propertyChanged:  OnContentTemplateChanged);

       private static void OnContentTemplateChanged(BindableObject bindable, object oldvalue, object newvalue)
       {
           var cp = (ContentControl)bindable;

           var template = cp.ContentTemplate;
           if (template != null)
           {
               var content = (View)template.CreateContent();
               cp.Content = content;
           }
           else
           {
               cp.Content = null;
           }
       }
      
       public DataTemplate ContentTemplate
       {
           get
           {
               return (DataTemplate)GetValue(ContentTemplateProperty);
           }
           set
           {
               SetValue(ContentTemplateProperty, value);
           }
       }
   }

Czy to jest trudny kod? Nie mógł być częścią Xamarin.Forms? Następnie zdefiniowałem sobie w zasobach MainPage szablony:

    <DataTemplate x:Key="VerticalLights">       
        <StackLayout Spacing="20">            
          <controls:CircleView VerticalOptions="FillAndExpand"
            HorizontalOptions="FillAndExpand" CircleColor="{Binding BassColor}"/>
          <controls:CircleView VerticalOptions="FillAndExpand"
            HorizontalOptions="FillAndExpand" CircleColor="{Binding MidColor}"/>
          <controls:CircleView VerticalOptions="FillAndExpand"
            HorizontalOptions="FillAndExpand" CircleColor="{Binding TrebleColor}"/>             
        </StackLayout>       
      </DataTemplate>
      <DataTemplate x:Key="HorizontalLights">       
        <StackLayout Spacing="20" Orientation="Horizontal">            
          <controls:CircleView VerticalOptions="FillAndExpand"
            HorizontalOptions="FillAndExpand" CircleColor="{Binding BassColor}"/>
          <controls:CircleView VerticalOptions="FillAndExpand"
            HorizontalOptions="FillAndExpand" CircleColor="{Binding MidColor}"/>
          <controls:CircleView VerticalOptions="FillAndExpand"
            HorizontalOptions="FillAndExpand" CircleColor="{Binding TrebleColor}"/>              
        </StackLayout>       
      </DataTemplate>

Przynajmniej DataTemplate wygląda dość standardowo. A teraz kod przełączający szablon po obrocie ekranu:

        private double width = 0;
        private double height = 0;

        …

        protected override void OnSizeAllocated(double width, double height)
        {
            base.OnSizeAllocated(width, height);
            if (width != this.width || height != this.height)
            {
                this.width = width;
                this.height = height;

                if (width > height)
                {
                    lights.ContentTemplate = Resources["HorizontalLights"] as DataTemplate;
                }
                else
                {
                    lights.ContentTemplate = Resources["VerticalLights"] as DataTemplate;                                
                }
            }
        }

      

#6 binding

Jeszcze słowo o data bindingu. Xaml w Xamarin nie byłby sobą, gdyby było jak w innych postaciach Xaml. Zamiast właściwości DataContext mamy BindingContext. Dobrze, że INotifyPropertyChanged jest i działa tak, jak należałoby się tego spodziewać. W moim przypadku steruję kolorami świateł właśnie przez binding. Szczegóły w MainPage.xaml.cs.

 

Morał jest taki, że Xaml Xaml-owi nierówny. Andy Wigley mówił kiedyś, że może ustandaryzują XAML. Przydałoby się! Muszę przyznać, że w obecnej sytuacji znajomość innych odmian XAML czasami zamiast mi pomagać, to mi wręcz przeszkadzała. Czy tak powinno być? Po drugie mimo pisania w XAML czasami potrzebujemy dość często zrobić coś za pomocą natywnego podejścia, czy to przez brak dostępu do ostylowania, brak zachowania znanego z natywnego komponentu czy własna kontrolkę. Upada wtedy nieco atut, że nie trzeba znać natywnych platform, mamy za to największą elastyczność. Dodając fakt, że aplikacja Xamarin.Forms działa (przynajmniej bez wysilonych zabiegów) gołym okiem wolniej od aplikacji z natywnym w pełni interfejsem (w oryginalnych narzędziach i SDK czy też w Xamarin.Android i Xamarin.iOS), ten sposób tworzenia aplikacji mobilnych mniej mnie przekonuje niż pozostałe, z którymi miałem doczynienia. Dodatkowa warstwa abstrakcji powoduje narzut przez potrzebę jej parsowania. Visual Studio 2015 nie jest też dopracowane dla XAML w Xamarin.Forms. Od strony dewelopera może się zdarzyć, że coś się skompiluje, uruchomi, a potem dowiemy się, że obiekt nie ma propercji Margin, Padding czy Background (albo w ogóle się nie dowiemy jak u mnie na iOS, gdzie aplikacja od razu się zamykała). A, zapomniałem dodać, że Margin w ogóle prawie nie istnieje w Xamarin.Forms. Trzeba to obchodzić stosując wszędzie Padding, o ile to możliwe. W rozwojowej wersji Xamarin.Forms mamy podgląd w designerze dla różnych platform. Ja póki co pracuję na stabilnej w czystym edytorze XML. Microsoft ma jeszcze dużo do zrobienia, by korzystanie z XAML w Xamarin.Forms dorównało korzystaniu z XAML w UWP czy WPF. Działają jednak cały czas, oprócz natywnych kontrolek bezpośrednio w XAML pojawiło się też preview kompilowanego bindingu.

Mimo pewnych problemów z Xamarin.Forms, nadal będę się przyglądał tej technologii,  by wyrobić sobie o niej jeszcze pełniejszy obraz. Obecnie kolekcjonerskie “różowe złoto” ma UI w XAML pozwalające na robienie wszystkiego poza konfiguracją, teraz pora na próbę sprzężenia go z natywnym API, by listować muzykę, odtwarzać ją i by światła mrugały. Ale to już w następnym odcinku. Stay tuned!

środa, 7 września 2016

Xamarin.Forms kontra Xamarin odc.1 (toolbar, nawigacja, stylowanie, lokalizacja, ikony aplikacji)

Dobry wieczór, w nowym cyklu tematycznym Xamarin.Forms kontra Xamarin witam serdecznie. Z XAML w Xamarin musiałem się po prostu zmierzyć, to była tylko kwestia czasu.

Z XAML jestem od początku tzn. początku 2005, kiedy były wersje CTP ówczesnego .NET 3.0. Po zachwycie nad WPF miałem wręcz najlepszy dev-okres w czasach Silverlight. Byłem też wielkim entuzjastą Silverlight w Windows Phone 7.x. Z chwilą pojawienia się pierwszego wydania Windows 8 Developer Preview uważnie zapoznałem się z XAML WinRT, śledziłem też rozwój wypadków w Windows Phone 8.x. Na ulepszonym nieco XAML WinRT opierają się z kolei aplikacje uniwersalne XAML w Windows 10.

Z XAML jestem naprawdę długo. Nie chcę popadać w jakąś megalomanię, ale zaczynam chyba skalą czasową doganiać takie osobowości jak Andy Wigley, Charles Petzold, Jessy Liberty czy Pete Brown. Był jeszcze John Papa, ale on na 100% przechcił się na HTML 5, a ja tylko częściowo. Co te osoby dziś wszystkie robią (poza JP)?  Zajmują się platformą Xamarin, a zwłaszcza Xamarin.Forms, który obsługuje XAML. Nie wypadało wręcz tego odpuścić.

Aby nie było zbyt prosto dodam, że mamy doczynienia z kolejna odmiana XAML, z zauważalnymi różnicami w stosunku do wszystkich technologii jakie tutaj dziś wymieniłem. Jakie są to różnice?  A jak ma się to do Xamarin.Android i Xamarin.iOS czy natywnych rozwiązań w oryginale?  To przykładowe pytania, na które będziemy udzielać odpowiedzi. Zrobimy to tworząc jeszcze bardziej kolekcjonerską wersję app-ki z czasów DSP, tym razem o nazwie Light Organ XF / Kolorofon XF (XF od Xamarin Forms!). Nie wiem czemu, ale od początku miałem wizję, że jej kolor będzie w kolorze “różowego złota” . Nawet nadałem jej kryptonim “różowe złoto”. Nie jestem oryginalny, jak wiadomo to jeden z kolorów iPhone (btw dziś zgodnie ze spiskowymi teoriami odbyła się premiera iPhone 7,  jak skończę tego posta wskakuję komentować na twitterze).

Po dość długim wstępie, mogę w końcu powiedzieć zaczynamy!

image

Na razie jesteśmy w fazie początkowej, na github dwa niemal puste ekrany,  póki co na Android i iOS.  Oto jeden z nich:

Screenshot_20160907-205510  IMG_0057

Jak to wygląda w XAML?

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:i18n="clr-namespace:LightOrganApp"
             xmlns:local="clr-namespace:LightOrganApp"
             x:Class="LightOrganApp.MainPage" Title="{i18n:Translate AppName}"
             BackgroundColor="{StaticResource PageBackgroundColor}">
  <ContentPage.ToolbarItems>
    <ToolbarItem Text="{i18n:Translate ActionMediaFiles}" Clicked="OnMediaFilesClicked">     
      <ToolbarItem.Icon>
        <OnPlatform x:TypeArguments="FileImageSource" Android="ic_library_music_white_48dp.png" iOS="Search.png"/>
      </ToolbarItem.Icon>     
    </ToolbarItem>   
  </ContentPage.ToolbarItems> 

</ContentPage>

Zasoby przydatne dla wielu stron zdefiniowałem w App.xaml:

<?xml version="1.0" encoding="utf-8" ?>
<Application xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="LightOrganApp.App">
  <Application.Resources>
    <ResourceDictionary>
      <Color x:Key="PageBackgroundColor">#ff8a80</Color>
      <Style TargetType="NavigationPage">
        <Setter Property="BackgroundColor" Value="{StaticResource PageBackgroundColor}"/>
        <Setter Property="BarBackgroundColor" Value="#e57373"/>
        <Setter Property="BarTextColor" Value="White"/>
      </Style>     
    </ResourceDictionary>   
  </Application.Resources>
</Application>

A code-behind strony?

    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();

            if (Device.OS == TargetPlatform.Android)
            {
                var toolbarItem = new ToolbarItem(AppResources.ActionSettings, null, () => { }, ToolbarItemOrder.Secondary, 0);
                ToolbarItems.Add(toolbarItem);
            }
        }

        async void OnMediaFilesClicked(object sender, EventArgs e)
        {
            var fileListPage = new FileListPage();
                
            await Navigation.PushAsync(fileListPage);
        }
    }

Strona FileListPage jest póki co pusta, tylko z pustym toolbarem. A teraz uwagi. Będzie ich sporo, pewnie nie dam rady podzielić się wszystkim, co mam w głowie, ale traktuję tego posta tylko jako swoisty skrótowy ekspres z ciekawostkami.

#1  XAML

Mamy XAML z roku 2009. Brawo. XAML-e tworzone od początku przez Microsoft nadal bazują na specyfikacji z 2006 (za wyjątkiem WF). Czemu muszę pisać jawnie ResourceDictionary w Resources?  Czemu mam pisać BackgroundColor zamiast Background?  Andy Wigley powiedział pół słowem w Warszawie, że może Microsoft ustandaryzuje XAML w Xamarin. Przydałoby się, bo pewną klątwą nad tym językiem jest, że każdy framework ma swoją własną jego odmianę.

#2  Status bar

Nie pokolorujemy go w XAML, ani nawet w cross-platformowym projekcie!  W Android w projekcie na tę platformę pilnujemy ustawiania:

<item name="colorPrimaryDark">#e57373</item>

w Resources/values/styles.xml. W iOS zdaje się dopilnowałem, by w Info.plist było:

<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleLightContent</string>

#3 Toolbar

Zrobiłem odpowiedni styl do NavigationPage. Musi być do NavigationPage, jak zrobimy do ContentPage nie zadziała!  Widzę kilka mankamentów. Nie uciekając się do natywnych renderingów jestem obowiązywany podawać bitmapowe ikony, a natywnie np. w iOS mogłem podać słowo “search” i to wystarczało. No, ale mam unifikację w XAML. Inna rzecz, to nie udało mi się póki co ostylować koloru popupu dla akcji secondary w przypadku Android. Nawet natywny sposób jaki stosowałem w Xamarin.Android nie chce działać z Xamarin.Forms!!!  O tym, że w XAML nie ma gdzie tego zrobić nawet nie mówię. Jeśli chodzi o Android to kolejne pytanie, co się stało z cieniem toolbara. Xamarin niby wspiera Material Design poprzez FormsAppCompatActivity, ale czemu domyślnie tego cienia nie widać?  Nie mam już siły, uderzę z pytaniami na forum Xamarina.

#4  Nawigacja

Widać fajne nowoczesne podejście z async.  Odpalają się domyślnie jakieś tranzycje między stronami, inne jednak niż w domyślne w moich natywnych rozwiązaniach.

#5  Lokalizacja

Generalnie wykonałem to co na stronie Localizing Xamarin.Forms Apps with RESX Resource Files. Jak w przypadku całego Xamarin.Forms widać, że pewną małą część pozostawia się rozwiązaniom natywnym.

#6  Ikony aplikacji

Podmieniłem generalnie te defaultowe na swoje z wcześniejszych solucji. Bez zaskoczenia, pamiętać tylko że Xamarin.Forms wspiera szerszy zakres wersji systemów niż robiłem to tworząc rozwiązania natywne.

To tyle na dziś. Stay tuned!