WordPress-Seite als Xamarin.Forms App – Teil 1

Vor mehr als zwei Jahren habe ich euch bereits WordPressXF vorgestellt. Mein Kollege Thomas Pentenrieder hat eine .NET Library geschrieben, um auf einen WordPress-Blog von einer .NET App zugreifen zu können. Er hat sich dann dann primär um eine  UWP-Version gekümmert, welche die WordPressPCL-Library in Action zeigt. Ich habe mich selbst um eine Xamarin.Forms Version gekümmert. Nun dachte ich mir, dass man doch einmal die Solution updaten könnte, um so die neusten Xamarin.Forms Features verwenden zu können. Da ich jetzt nicht einfacher nur das Projekt updaten wollte, habe ich mir gedacht, dass die Umsetzung in Form von mehreren Blog-Beiträgen passiert und ich euch so zeige, wie eine kleine App entsteht, welche in der Lage ist die Beiträge eines Blogs unter Android und iOS an zu zeigen. Im ersten Teil wollen wir beginnen eine Übersichtsseite zu erstellen, welche die aktuellen Blog-Beiträge lädt und anzeigt, welche beim Scrollen automatisch nachgeladen werden.

Als Ausgangslage dient eine Xamarin.Forms Solution, welche Blank als Template verwendet und Projekte für Android und iOS besitzt. Der erste Schritt ist dann immer das aktualisieren der NuGet-Packages und gleichzeitig das Hinzufügen von ein paar weiteren Packages. In diesem Fall benötigen wir Unity.Container, Unity.ServiceLocation, WordPressPCL und Xamarin.Forms.Visual.Material.

Wir beginnen nun mit der eigentlichen Business-Logik. Dazu wollen wir einen Service schreiben, welcher die Kommunikation mit WordPress übernimmt. Dafür erstellen wir zunächst ein Interface IWordPressService, welches aktuell nur eine Methode zur Verfügung stellt.

public interface IWordPressService
{
    Task<IEnumerable<Post>> GetLatestPostsAsync(int page = 0, 
        int perPage = 20);
}

Die Methode GetLatestPostsAsync soll nun also die letzten Beiträge abrufen. Daher kümmern wir uns nun um die Implementierung des Services:

public class WordPressService : IWordPressService
{
    private readonly WordPressClient _client;

    public WordPressService()
    {
        _client = new WordPressClient(Statics.WordpressUrl);
    }

    public async Task<IEnumerable<Post>> GetLatestPostsAsync(int page = 0, 
        int perPage = 20)
    {
        try
        {
            page++;

            var posts = await _client.Posts.Query(new PostsQueryBuilder
            {
                Page = page,
                PerPage = perPage,
                Embed = true
            });

            return posts;
        }
        catch(Exception ex)
        {
            Debug.WriteLine($"{nameof(WordPressService)} | " +
                $"{nameof(GetLatestPostsAsync)} | {ex}");
        }

        return null;
    }
}

Der Service verwendet intern den WordPressClient aus der WordPressPCL-Library und bekommt als Parameter die notwendige WordPress-URL übergeben, welche ich in einer Klasse Statics ausgelagert habe.

Im nächsten Schritt kümmern wir uns um die ViewModels. Zunächst erstellen wir ein BaseViewModel, welches als Basis-Klasse für alle ViewModels dienen wird und das bekannte Interface INotifyPropertyChanged implementiert und bereits einige Properties zur Verfügung stellt.

public class BaseViewModel : INotifyPropertyChanged
{
    private bool _isLoading;
    public bool IsLoading
    {
        get => _isLoading;
        set { _isLoading = value; OnPropertyChanged(); }
    }

    private bool _isRefreshing;
    public bool IsRefreshing
    {
        get => _isRefreshing;
        set { _isRefreshing = value; OnPropertyChanged(); }
    }


    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] 
        string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Damit können wir jetzt auch schon das ViewModel PostsViewModel erstellen. Dieses bekommt per Dependency Injection das IWordPressService-Interface übergeben und stellt die Property Posts bereit, welche die Beiträge des WordPress-Blogs beinhaltet. Darüber hinaus werden noch zwei Commands bereitgestellt, welche das initiale Laden der ersten Beiträge und auch das Nachladen der Beiträge übernehmen.

public class PostsViewModel : BaseViewModel
{
    private readonly IWordPressService _wordPressService;

    private int _currentPage = -1;

    private ObservableCollection<Post> _posts = new ObservableCollection<Post>();
    public ObservableCollection<Post> Posts
    {
        get => _posts;
        set { _posts = value; OnPropertyChanged(); }
    }

    private bool _isIncrementalLoading;
    public bool IsIncrementalLoading
    {
        get => _isIncrementalLoading;
        set { _isIncrementalLoading = value; OnPropertyChanged(); }
    }

    private AsyncRelayCommand _loadPostsAsyncCommand;
    public AsyncRelayCommand LoadPostsAsyncCommand 
            => _loadPostsAsyncCommand ?? (_loadPostsAsyncCommand 
                = new AsyncRelayCommand(LoadPostsAsync));

    private AsyncRelayCommand _loadMorePostsAsyncCommand;
    public AsyncRelayCommand LoadMorePostsAsyncCommand 
            => _loadMorePostsAsyncCommand ?? (_loadMorePostsAsyncCommand 
                = new AsyncRelayCommand(LoadMorePostsAsync));


    public PostsViewModel(IWordPressService wordPressService)
    {
        _wordPressService = wordPressService;
    }


    private async Task LoadPostsAsync()
    {
        try
        {
            IsRefreshing = true;

            _currentPage = 0;

            Posts.Clear();

            var posts = await _wordPressService.GetLatestPostsAsync(_currentPage, 
                Statics.PageSize);
            Posts.AddRange(posts);
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"{nameof(PostsViewModel)} | " +
                $"{nameof(LoadPostsAsync)} | {ex}");
        }
        finally
        {
            IsRefreshing = false;
        }
    }

    private async Task LoadMorePostsAsync()
    {
        if (IsIncrementalLoading)
            return;

        try
        {
            IsIncrementalLoading = true;

            _currentPage++;

            var posts = await _wordPressService.GetLatestPostsAsync(_currentPage, 
                Statics.PageSize);

            if (posts == null)
                return;

            Posts.AddRange(posts);
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"{nameof(PostsViewModel)} | " +
                $"{nameof(LoadMorePostsAsync)} | {ex}");
        }
        finally
        {
            IsIncrementalLoading = false;
        }
    }
}

Damit können wir nun beginnen die UI zu bauen. Dazu erstellen wir zunächst ein Control, das PostControl. Dieses zeigt auf der späteren Übersichtseite einen einzelnen Blog-Beitrag an. Dazu gehören ein Bild, das Veröffentlichungsdatum, der Titel und ein kurzes Ausschnitt aus dem Beitrag.

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XFWordPress.Controls.PostControl"
             x:Name="Root">

    <ContentView.Content>
        <Frame CornerRadius="8">
            <Grid RowDefinitions="Auto,Auto,Auto,Auto">

                <Image Source="{Binding Embedded, 
                    Converter={StaticResource EmbeddedToFeaturedImageConverter}, 
                    Source={x:Reference Root}}"
                       Grid.Row="0" />

                <Label Text="{Binding Date, 
                    StringFormat='{0:g}', 
                    Source={x:Reference Root}}"
                       Style="{StaticResource PostDateLabelStyle}"
                       Grid.Row="1" />

                <Label Text="{Binding Title, 
                    Converter={StaticResource HtmlStringToDecodedStringConverter}, 
                    Source={x:Reference Root}}"
                       Style="{StaticResource PostTitleLabelStyle}"
                       Grid.Row="2" />

                <Label Text="{Binding Excerpt, 
                    Converter={StaticResource HtmlStringToDecodedStringConverter}, 
                    Source={x:Reference Root}}"
                       Grid.Row="3" />
            </Grid>
        </Frame>
    </ContentView.Content>
</ContentView>

Wie man sieht, kommen hier zwei Converter zum Einsatz. Zum einen wird das Bild aus den Daten extrahiert und angezeigt und zum anderen werden der Titel und der Ausschnitt von den HTML-Tags befreit. An dieser Stelle werde ich nicht die Converter aufführen und auch nicht die Bindable Properties, welche notwendig sind. Aber keine Sorge, denn am Ende des Beitrags gibt es den vollständigen Code als Download.

Abschließend kümmern wir uns nun um die eigentliche Page, welche die Beiträge anzeigt. Als Basis-Control kommt hier eine CollectionView zum Einsatz. Diese kümmert sich auch gleich um das automatische Nachladen der Artikel, wenn der Nutzer nach unten scrollt. Um die gesamte Liste zu aktualisieren, verwenden wir eine RefreshView. Diese muss per Pull-To-Refresh auch am Anfang ausgeführt werden, um die Artikel initial zu laden.

<?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:markupextensions="clr-namespace:XFWordPress.MarkupExtensions"
             xmlns:controls="clr-namespace:XFWordPress.Controls"
             x:Class="XFWordPress.Views.PostsOverviewPage"
             Title="{markupextensions:Translate PostsOverviewPageTitle}">

    <ContentPage.Content>
        <RefreshView Command="{Binding LoadPostsAsyncCommand}"
                     IsRefreshing="{Binding IsRefreshing}">
            <CollectionView ItemsSource="{Binding Posts}"
                            RemainingItemsThresholdReachedCommand="{Binding 
                                LoadMorePostsAsyncCommand}"
                            RemainingItemsThreshold="3">
                <CollectionView.ItemsLayout>
                    <GridItemsLayout Orientation="Vertical"
                                     VerticalItemSpacing="12" />
                </CollectionView.ItemsLayout>
                <CollectionView.ItemTemplate>
                    <DataTemplate>
                        <controls:PostControl Embedded="{Binding Embedded}"
                                              Date="{Binding Date}"
                                              Title="{Binding Title.Rendered}"
                                              Excerpt="{Binding Excerpt.Rendered}" />
                    </DataTemplate>
                </CollectionView.ItemTemplate>
            </CollectionView>
        </RefreshView>
    </ContentPage.Content>
</ContentPage>

Damit ist das Grundgerüst bereits umgesetzt und wir können die App ausprobieren. Wenn Ihr selbst die App mit eurem eigenen WordPress-Blog ausprobieren wollt, dann müsst ihr einfach nur die URL in der Statics-Klasse austauschen. Der folgende Screenshot zeigt die bisherige App unter Android.

Nun möchte ich euch natürlich den aktuellen Stand des Projektes noch als Download anbieten, so dass ihr selbst beginnen könnt euren WordPress-Blog in eine eigene App zu verwandeln.

Im nächsten Beitrag der Serie werden wir einen SplashScreen integrieren und bereits beim ersten Aufruf der App die ersten Beiträge automatisch laden. Seid also auf den nächsten Beitrag gespannt.

SCRCPY: Bildschirm von Android-Device auf Desktop spiegeln Wie bin ich eigentlich zum Programmieren gekommen? XF: Custom Control – Initials View