Alessandro Del Sole's Blog

/* A programming space about Microsoft® .NET® */
posts - 159, comments - 0, trackbacks - 0

My Links

News

Your host

This is me! This space is about Microsoft® .NET® and Microsoft® Visual Basic development. Enjoy! :-)

These postings are provided 'AS IS' for entertainment purposes only with absolutely no warranty expressed or implied and confer no rights.

Microsoft MVP

My MVP Profile

I'm a VB!

Watch my interview in Seattle

My new book on VB 2015!

Pre-order VB 2015 Unleashed Pre-order my new book "Visual Basic 2015 Unleashed". Click for more info!

My new book on LightSwitch!

Visual Studio LightSwitch Unleashed My book "Visual Studio LightSwitch Unleashed" is available. Click the cover!

Your visits

Follow me on Twitter!

CodePlex download Download my open-source projects from CodePlex!

Article Categories

Archives

Post Categories

.NET Framework

Blogroll

Help Authoring

Microsoft & MSDN

Setup & Deployment

Visual Basic 2005/2008/2010

venerdì 2 settembre 2016

Xamarin.Forms: Enrich the UI with calendars and busy indicators with Syncfusion Essential Studio

As you might know, Syncfusion offers the very popular Essential Studio suite of controls for a huge number of development platforms, including Windows desktop, mobile, and Web.

One thing you might not know, is that Syncfusion is offering individuals and small businesses free access to the Essential Studio for free via the Community License. You can read more about this offer in this page.

Among the others, this offer includes Essential Studio for Xamarin. So, in this blog post I'm going to show how to use a couple controls in a Xamarin.Form app for Android, iOS, and Windows. More specifically, I'll show how to create a cross-platform news reader for the popular Channel9 web site enriching the UI with the BusyIndicator and Calendar controls. I assume you are a bit familiar with creating a Xamarin.Forms app and with LINQ, so I won't cover this in details and I will focus more thoroughly on implementing Syncfusion controls, as well as discussing some goodies in Xamarin.Forms that you might not be familiar with, such as pull-to-refresh.

Having that said, create a new blank Xamarin.Forms app called Channel9Browser using the Portable Class Library template.

The goal
The final result we want to reach is shown in the following figures, both based on the UWP app version:





The Calendar control will be used to filter the list of videos based on the selected date.

The source code for the sample app can be downloaded from https://github.com/alessandrodelsole/sfchannel9browser.

Quick intro
As an introduction, I will first describe the most important characteristics of the BusyIndicator and Calendar controls. The reason is that I want you to already know about these information before I discuss the ViewModel later.
The BusyIndicator control differs from the Xamarin's ActivityIndicator in that it exposes an AnimationType property, which allows to select among a number of different animations for different scenarios, and it also allows customizing the indicator's size via two properties, ViewBoxWidth and ViewBoxHeight. It is so simple to use and very attractive at the same time. The Calendar control is also extremely easy to use and configure. Each calendar event is represented by an instance of the CalendarInlineEvent class, which provides properties that map any possible event information, such as start/end time, subject, location, and even a different color for each event. Also, Calendar exposes a property called CalendarEventCollection, which you populate with a list of CalendarInlineEvent objects. Because this collection inherits from ObservableCollection<CalendarInlineEvent>, it can be easily data-bound to the Calendar control in order to show a list of events with no effort.

Adding references
Before we do anything else, we must add references to the libraries that offer the controls. These are located under C:\Program Files (x86)\Syncfusion\Essential Studio\version_number\Xamarin\lib, where version_number is the Essential Studio version you have installed on your machine. Notice how there are subfolders containing platform-specific version of the required libraries. For the PCL project, select and add the Syncfusion.SfBusyIndicator.XForms.dll and Syncfusion.SfCalendar.XForms.dll libraries from the pcl subfolder. For each platform-specific project you want to target, add the related references you find in the proper subfolders. For instance, for the Android project you can find the Syncfusion.SfBusyIndicator.Android.dll, Syncfusion.SfBusyIndicator.XForms.Android.dll, and Syncfusion.SfBusyIndicator.XForms.dll libraries under the android subfolder. Select the same libraries for the other platform-specific projects, changing the platform of course, and repeat the steps for the SfCalendar control.

Defining the model
The sample app has the goal of downloading news from the Channel9 web site in the form of RSS feeds. Every item in the news feed must be represented with a class that we call Item and that is defined as follows:

public class Item : INotifyPropertyChanged 
{
     private string title;
     public string Title
     {
         get
         {
             return title;
         }
         set
         {
             title = value;
             OnPropertyChanged(nameof(Title));
         }
     }
     private string link;
     public string Link
     {
         get
         {
             return link;
         }
         set
         {
             link = value;
             OnPropertyChanged(nameof(Link));
         }
     }
     private string author;
     public string Author
     {
         get
         {
             return author;
         }
         set
         {
             author = value;
             OnPropertyChanged(nameof(Author));
         }
     }
     private DateTime pubDate;
     public DateTime PubDate
     {
         get
         {
             return pubDate;
         }
         set
         {
             pubDate = value;
             OnPropertyChanged(nameof(PubDate));
         }
     }
     private string thumbnail;
     public string Thumbnail
     {
         get { return thumbnail; }
         set
         {
             thumbnail = value;
             OnPropertyChanged(nameof(Thumbnail));
         }
     }
     public event PropertyChangedEventHandler PropertyChanged;
     private void OnPropertyChanged(string propertyName)
     {
         this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
     }
}


The INotifyPropertyChanged interface implementation is required to raise notifications every time a property value changes. Do not forget to add a using System.ComponentModel directive. Notice the Thumbnail property, that will store the link of a thumbnail that Channel9 provides for each video. The next step is defining a class that downloads the news from the Internet and returns a collection of Item objects. This class is called ItemService, defined as follows (I'll make the necessary considerations later):

public static class ItemService 
{
     public static string FeedUri = "https://channel9.msdn.com/Blogs/MVP-VisualStudio-Dev/RSS";
     private static XNamespace mediaNS = XNamespace.Get("http://search.yahoo.com/mrss/");
     private static string GetThumbnail(XElement node)
     {
         var images = node.Descendants(mediaNS + "thumbnail");
         string imageUrl;
         switch(Device.Idiom)
         {
             case TargetIdiom.Phone:
                 imageUrl = images.FirstOrDefault().Attribute("url").Value;
                 break;
             default:
                 if (images.Count() > 1)
                 {
                     imageUrl = images.Skip(1).FirstOrDefault().Attribute("url").Value;
                 }
                 else
                 {
                     imageUrl = images.FirstOrDefault().Attribute("url").Value;
                 }
                 break;
          }
         return imageUrl;
     }

     // Query the RSS feed with LINQ and return an IEnumerable of Item
     public static async Task<IEnumerable<Item>> QueryRssAsync(CancellationToken token)
     {
         try
         {
             var client = new HttpClient();
             var data = await client.GetAsync(new Uri(FeedUri), token);
             var actualData = await data.Content.ReadAsStringAsync();
             var doc = XDocument.Parse(actualData);
             var dcNS = XNamespace.Get("http://purl.org/dc/elements/1.1/");
             var query = (from video in doc.Descendants("item")
                          select new Item
                          {
                              Title = video.Element("title").Value,
                              Author = video.Element(dcNS + "creator").Value,
                              Link = video.Element("link").Value,
                              Thumbnail = GetThumbnail(video),
                              PubDate = DateTime.Parse(video.Element("pubDate").Value,
                                  System.Globalization.CultureInfo.InvariantCulture)
                          });
             return query;
         }
         catch (OperationCanceledException)
         {
             return null;
         }
         catch (Exception ex)
         {
             return null;
         }
     }
 }

Following is a list of useful considerations:

  • the FeedUrl field contains the URL for the news feed;
  • Channel9's news feed contains a list of thumbnails, in different resolutions, for each video. For this reason, a special method called GetThumbnail retrieves the appropriate thumbnail resolution according to the device type the app is running on. The device type can be retrieved using the Idiom property from the Xamarin.Forms.Device class.
  • The QueryRssAsync method returns a collection of Item objects that will be used shortly.

The ViewModel

Typically, a ViewModel is a class that connects the model to the user interface. I'm not implementing the MVVM pattern here because I can't assume all of you know about it, but I'll be declaring a class that exposes objects to the user interface the proper way. This is also the place where we get started with some objects from Syncfusion. Our ViewModel must expose a collection of Item objects to the user interface, but it will also expose a collection of calendar events, each representing the publication date of a video on Channel9 and that will be data-bound to the Calendar control for filtering videos based on the selected date. The Syncfusion.SfCalendar.XForms namespace exposes the CalendarEventCollection class, which inherits from ObservableCollection<CalendarInlineEvent>. Following is the code for the ViewModel class, and I'll make some considerations later:

public class ItemViewModel : INotifyPropertyChanged 
{
     private bool isBusy;
     public bool IsBusy
     {
         get
         {
             return isBusy;
         }
         set
         {
             isBusy = value;
             OnPropertyChanged();
         }
     }
     public ObservableCollection<Item> Items { getset; }
     public ObservableCollection<Item> FilteredItems { getset; }
     public CalendarEventCollection VideoDates { getset; }
     public ItemViewModel()
     {
         this.Items = new ObservableCollection<Item>();
         this.VideoDates = new CalendarEventCollection();
     }
     public void FilterByDate(DateTime date)
     {
         this.FilteredItems = new ObservableCollection<Item>(this.Items.Where(i => i.PubDate.Date == date.Date));
     }
     public event PropertyChangedEventHandler PropertyChanged;
     public void OnPropertyChanged([CallerMemberName]string name = "") => PropertyChanged?.Invoke(thisnew PropertyChangedEventArgs(name));
     public async Task InitializeAsync()
     {
         if (IsBusy)
             return;
         IsBusy = true;
         try
         {
             var result = await ItemService.QueryRssAsync(new System.Threading.CancellationToken());
             if (result != null)
             {
                 this.Items = null;
                 this.Items = new ObservableCollection<Item>(result);
                 this.VideoDates.Clear();
                 foreach (var item in this.Items)
                 {
                     var videoEvent = new CalendarInlineEvent();
                     videoEvent.StartTime = item.PubDate;
                     videoEvent.EndTime = item.PubDate;
                     videoEvent.Subject = item.Title;
                     videoEvent.Color = Color.Maroon;
                     this.VideoDates.Add(videoEvent);
                 }
             }
         }
         catch (Exception)
         {
             return;
         }
         finally
         {
             IsBusy = false;
         }
     }
 }
Now a list of key concepts in this class:

  • it implements the INotifyPropertyChanged interface to notify callers of any changes in property values;
  • it exposes the IsBusy property that will be data-bound to the user interface to make it look busy while loading data;
  • it exposes the InitializeAsync method, which is responsible for downloading the list of news from the Internet and creating a proper collection;
  • it exposes a property called VideoDates, of type CalendarEventCollection, which stores the list of publication dates and that will be data-bound to the Calendar control
  • VideoDates is populated with instances of the CalendarInlineEvent class. This is done with a for..each loop over the collection of news returned from Channel9, where each contains the publication date;
  • for each calendar event, you can specify the start/end time, the subject, and the color. Of course, you can customize events further by specifying other information such as the location.


The App class
The App class is the place where we declare a variable of type ItemViewModel that will be shared to all pages. The code to add is simply the following:

internal static ItemViewModel viewModel;
public App()
{
     InitializeComponent();
     viewModel = new ItemViewModel();
     MainPage = new NavigationPage(new MainPage());
}

The User Interface

The sample app is made of four pages: the main page, a page for viewing the original content on Channel9, a page for selecting dates, and a page that shows a list of filtered videos. Let's start with the main page, whose code requires adding the XML namespaces for Syncfusion's controls at the top and that looks like the following:

<?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:local="clr-namespace:Channel9Browser"
              xmlns:busyindicator="clr-namespace:Syncfusion.SfBusyIndicator.XForms;assembly=Syncfusion.SfBusyIndicator.XForms"
              x:Class="Channel9Browser.MainPage">
   <StackLayout>
     <busyindicator:SfBusyIndicator x:Name="busyindicator" AnimationType="HorizontalPulsingBox" IsBusy="False" IsVisible="False" />
     <Button x:Name="FilterButton" Text="Filter by date" Clicked="FilterButton_Clicked" />
     <ListView x:Name="RssView" ItemsSource="{Binding Items}" IsPullToRefreshEnabled="True"
               Refreshing="RssView_Refreshing"
               IsRefreshing="{Binding IsBusy, Mode=OneWay}"
                ItemTapped="RssView_ItemTapped" >
       <ListView.SeparatorColor>
         <OnPlatform x:TypeArguments="Color" iOS="Transparent"/>
       </ListView.SeparatorColor>
       <ListView.ItemTemplate>
         <DataTemplate>
           <ViewCell>
             <StackLayout Padding="2">
               <Image Source="{Binding Thumbnail}" Aspect="AspectFit" />
               <Label Text="{Binding Title}" TextColor="#f35e20" />
               <Label Text="{Binding Author}" />
               <Label Text="{Binding PubDate}" TextColor="#503026" />
             </StackLayout>
           </ViewCell>
         </DataTemplate>
       </ListView.ItemTemplate>
     </ListView>
   </StackLayout>
 </ContentPage>
Key points:

  • the BusyIndicator control provides the IsBusy property, which enables the control animation when True;
  • its AnimationType property allows you to select among a number of built-in animations, such as HorizontalPulsingBox, Globe, Battery, Print. Check out the documentation for a comprehensive list of supported animations and figures. In this case I'm using the HorizontalPulsingBox, but there are some animations that are specific to some scenarios (e.g. Print).
  • you can assign the ViewBoxWidth and ViewBoxHeight properties with a fixed value in order to make it larger or smaller according to your needs.
  • the ListView control offers the pull-to-refresh capability with the IsPullToRefreshEnabled property. When True, you can handle the Refreshing event and make the UI look busy with the IsRefreshing property (in this case it's bound to the same-named property in the ViewModel).

Also, notice how the controls inside the ListView's data template are data-bound to properties of the Item class. In the C# code-behind, we have to load contents and respond to events. This is the code (key points at the end):

public partial class MainPage : ContentPage 
{
     public MainPage()
     {
         InitializeComponent();
         this.BindingContext = App.viewModel;
         switch(Device.Idiom)
         {
             case TargetIdiom.Desktop:
                 this.RssView.RowHeight = 288;
                 break;
             default:
                 this.RssView.RowHeight = 123;
                 break;
         }
     }
     private async Task LoadDataAsync()
     {
         this.busyindicator.IsVisible = true;
         this.busyindicator.IsBusy = true;
         await App.viewModel.InitializeAsync();
         this.RssView.ItemsSource = App.viewModel.Items;
         this.busyindicator.IsVisible = false;
         this.busyindicator.IsBusy = false;
     }

     private async void RssView_Refreshing(object sender, EventArgs e)
     {
         await LoadDataAsync();
     }
     protected async override void OnAppearing()
     {
         base.OnAppearing();
         await LoadDataAsync();
     }
     private async void RssView_ItemTapped(object sender, ItemTappedEventArgs e)
     {
         var selected = e.Item as Item;
         if (selected != null)
             await App.Current.MainPage.Navigation.PushAsync(new WebContentPage(selected.Link));
     }
     private void FilterButton_Clicked(object sender, EventArgs e)
     {
         App.Current.MainPage.Navigation.PushAsync(new CalendarPage());
     }


Notice how the Device.Idiom property allows to detect the device type the app is running on; if this is a desktop machine (typically Windows), we resize the ListView rows to make them a bit larger. The key point here is the LoadDataAsync method, not only because it downloads data from the Internet but because it enables and disables the BusyIndicator control by simply setting the IsVisible and IsBusy properties. The Refreshing event handler is invoked when the pull-to-refresh gesture is intercepted, while the ListView.ItemTapped and Button.Clicked event handlers are respectively invoked when the user taps an item in the collection of news and when the user wants to filter news by date. The second page is called WebContentPage.xaml and displays the original Web page on Channel 9. Both the UI and the code-behind are very simple and use the WebView control:

<?xml version="1.0" encoding="utf-8" ?>
 <ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
              xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
              x:Class="Channel9Browser.Pages.WebContentPage">
   <WebView x:Name="WebView1"/>
 </ContentPage>

public partial class WebContentPage : ContentPage
{
     string link;
     public WebContentPage(string link)
     {
         InitializeComponent();
         this.link = link;
     }
     protected override void OnAppearing()
     {
         base.OnAppearing();
         WebView1.Source = link;
     }
 }
The third page is called CalendarPage.xaml and uses the Calendar control. Let's see the code first, then let's make some considerations:

<?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:calendar="clr-namespace:Syncfusion.SfCalendar.XForms;assembly=Syncfusion.SfCalendar.XForms"
              x:Class="Channel9Browser.Pages.CalendarPage">
   <StackLayout>
     <Label Text="Select a date"/>
     <calendar:SfCalendar x:Name="calendar" VerticalOptions="FillAndExpand"
                HorizontalOptions="FillAndExpand"
                 SelectionMode="SingleSelection"
                 ViewMode="MonthView"
                 ShowInlineEvents="true"
                 DataSource="{Binding}"/>
     <Button x:Name="GoButton" Text="Go!" Clicked="GoButton_Clicked"/>
   </StackLayout>
 </ContentPage>
There is a new XML namespace called calendar, that points to the proper namespace in the Syncfusion's library. The Calendar control has a number of interesting properties, such as SelectionMode, ViewMode, and ShowInLineEvents. The latter is very important because, when True, it allows to display each CalendarInlineEvent when a date is selected. ViewMode is self-explanatory and supports MonthView and YearView values. SelectionMode allows to specify whether a user can select multiple dates, in this case we just allow SingleSelection for the sake of simplicity. The Calendar control is populated at runtime and via data-binding by assigning the DataSource property with an instance of the CalendarEventCollection class, in our case exposed by the ViewModel. The interesting thing is that you can interact with dates in a variety of ways; one is retrieving the selected date when the calendar is tapped, which is useful if we want to pass the date to another page. This is accomplished by handling the OnCalendarTapped event, which we do in the code-behind file as well as binding the CalendarEventCollection to the UI:

public partial class CalendarPage : ContentPage 
{
     private DateTime selectedDate;
     public CalendarPage()
     {
         InitializeComponent();
         this.calendar.OnCalendarTapped += Calendar_OnCalendarTapped;
         this.BindingContext = App.viewModel;
         this.calendar.BindingContext = App.viewModel.VideoDates;
     }
     private void Calendar_OnCalendarTapped(object sender, CalendarTappedEventArgs args)
     {
         SfCalendar calendar = args.Calendar;
         selectedDate = args.datetime;
     }
     private async void GoButton_Clicked(object sender, EventArgs e)
     {
         await App.Current.MainPage.Navigation.PushAsync(new FilteredVideosPage(selectedDate));
     }
 }

The CalendarTappedEventArgs class provides, among the other, a property called Calendar that represents the instance of the Calendar control; the latter exposes another property called datetime and that represents the selected date. This is passed to the last page, called FilteredVideosPage.xaml. This is similar to the main page, but provides a simplified UI in that it does not need to refresh and download the news feed, and it looks like this:

<?xml version="1.0" encoding="utf-8" ?> 
<
ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Channel9Browser.Pages.FilteredVideosPage">
   <StackLayout>
     <ListView x:Name="RssView" ItemsSource="{Binding FilteredItems}" IsPullToRefreshEnabled="True"
               ItemTapped="RssView_ItemTapped">
       <ListView.SeparatorColor>
         <OnPlatform x:TypeArguments="Color" iOS="Transparent"/>
       </ListView.SeparatorColor>
       <ListView.ItemTemplate>
         <DataTemplate>
           <ViewCell>
             <StackLayout Padding="2">
               <Image Source="{Binding Thumbnail}" />
               <Label Text="{Binding Title}" TextColor="#f35e20"/>
               <Label Text="{Binding Author}"/>
               <Label Text="{Binding PubDate}" TextColor="#503026"/>
             </StackLayout>
           </ViewCell>
         </DataTemplate>
       </ListView.ItemTemplate>
     </ListView>
   </StackLayout>
 </ContentPage>

This time the binding is against the FilteredItems collection from the ViewModel. At the code-behind level, the code is very similar to the main page but it does not support pull-to-refresh and it must invoke the FilterByDate method in the ViewModel. This is required for two reasons: it performs a LINQ query to filter the news by date and it ensures filtering is performed by comparing only the date part of a DateTime object. This is the code:

public partial class FilteredVideosPage : ContentPage 
{
     private DateTime filterDate;
     public FilteredVideosPage(DateTime filterDate)
     {
         InitializeComponent();
         switch (Device.Idiom)
         {
             case TargetIdiom.Desktop:
                 this.RssView.RowHeight = 288;
                 break;
             default:
                 this.RssView.RowHeight = 123;
                 break;
         }
         this.filterDate = filterDate;
         App.viewModel.FilterByDate(filterDate);
         this.BindingContext = App.viewModel;
     }
     private async void RssView_ItemTapped(object sender, ItemTappedEventArgs e)
     {
         var selected = e.Item as Item;
       if (selected != null)
             await App.Current.MainPage.Navigation.PushAsync(new WebContentPage(selected.Link));
     }
     protected override void OnAppearing()
     {
         base.OnAppearing();
         this.RssView.ItemsSource = App.viewModel.FilteredItems;
     }
}

Testing the App
You can now select your favorite platform and test the sample app using these nice controls from Syncfusion. Do not forget to check the developer documentation and the full list of available products in the Syncfusion web site.

Alessandro

posted @ lunedì 1 gennaio 0001 00:00 | Feedback (0) | Filed Under [ Xamarin ]

Powered by:
Powered By Subtext Powered By ASP.NET