Adding Animation to Accordion ListView

So, I just started learning about animation in Xamarin. I read the documentation and want to try implementing it. Because I’m new at this stuff, I wanna start with something simple. I modified one of my old post about accordion list view by adding little animation to it. If you’ve read that post, you will know that when I expand the list view, I changed the down arrow icon to up arrow icon. And when I collapse the list view, I do the otherwise. The icon changing process just happen as it is, without any effect or animation on it. I want to change that. I want to add a rotate animation when the icon changing happen. So, let’s do that.

Image Gesture Recognizer

First thing we need to is switching the trigger of expanding list view event. Back then I used List View Item Tapped event, but now, because I want to rotate the arrow icon, I need to put the trigger in Image view where the arrow icon is. So, I add Image Gesture Recognizer to the Image view. It’s look like this.

animated accordion

With this gesture recognizer, I can get the current Image view being tapped by user so I can handle it properly in code behind. In code behind, all I need to is just rotate the image and call function in view model to expand or collapse the list view. I rotate the image 180 degree and for 0,5 second.

public partial class AccordionCountriesPage : ContentPage
{
    AccordionCountriesViewModel viewModel;
    public AccordionCountriesPage()
    {
        InitializeComponent();
        BindingContext = viewModel = new AccordionCountriesViewModel();
    }

    void Handle_ItemTapped(object sender, ItemTappedEventArgs e)
    {
        //Country mCountry = (Country)e.Item;
        //viewModel.ShowCities(mCountry);
        ListView listView = sender as ListView;
        listView.SelectedItem = null;
    }

    async void Handle_Tapped(object sender, EventArgs e)
    {
        Image image = sender as Image;
        await image.RotateTo(180, 500);
        Grid grid = image.Parent as Grid;
        Label label = grid.Children[0] as Label;
        viewModel.ShowCities(label.Text);
    }
}

A little bit downside from this method is I don’t get the whole Country class like I did when I was using List View Item Tapped. But I’m still able to know which country user tapped by accessing the label which contain the country’s name. By doing this, it means I also need little modification to the view model as well. This is how the view model look like now. I only updated the ShowCities method.

public class AccordionCountriesViewModel : BaseViewModel
{
    private CustomObservableCollection countries;
    public CustomObservableCollection Countries
    {
        get => countries;
        set => SetProperty(ref countries, value);
    }

    public AccordionCountriesViewModel()
    {
        ObservableCollection USACities = new ObservableCollection()
        {
            new City(){ CityName = "Washington DC" },
            new City(){ CityName = "New York" },
            new City(){ CityName = "Los Angeles" }
        };

        ObservableCollection ChinaCities = new ObservableCollection()
        {
            new City(){ CityName = "Beijing" },
            new City(){ CityName = "Shanghai" },
            new City(){ CityName = "Shenzhen" }
        };

        ObservableCollection RussiaCities = new ObservableCollection()
        {
            new City(){ CityName = "Moscow" },
            new City(){ CityName = "St. Peterburg" },
            new City(){ CityName = "Kazan" }
        };

        countries = new CustomObservableCollection()
        {
            new Country(){ CountryName = "USA", IsChildrenVisible = false, Cities = USACities },
            new Country(){ CountryName = "China", IsChildrenVisible = false, Cities = ChinaCities },
            new Country(){ CountryName = "Russia", IsChildrenVisible = false, Cities = RussiaCities },
        };
    }

    public void ShowCities(string countryName)
    {
        Country country = Countries.SingleOrDefault(c => c.CountryName == countryName);
        country.IsChildrenVisible = !country.IsChildrenVisible;
        Countries.ReportItemChange(country);
    }
}

Let’s rotate it!

Nah, now we can run the project again and see how our arrow rotating.

 

Credit :

  • Arrow Icons from FlatIcon, Down Arrow icon by Freepik, Right Arrow icon by GraphicsBay, Up Arrow icon by Hadrien

Binding DateTime and TimeSpan to one property

As we know together, it take different properties to record what user input into DatePicker and TimePicker. DatePicker requires DateTime property, and yet TimePicker needs TimeSpan. And let say we have some kind of application form to submit bunch of data, and one property require the user to input complete set of time, from year to hour but we only have one DateTime property to bind into. Of course we can just add TimeSpan to DateTime and we gonna have complete set of DateTime from our DatePicker and TimePicker. But, what if we got bunch of DateTime property to input, we gonna adding TimeSpan to DateTime multiple times, which is ineffective. So, I create a simple custom control to simplify this problem. Let’s take a look.

XAML File

First, we need to create new content view with XAML on it. In XAML file we gonna need three 2 visible items, the DatePicker and TimePicker, and one invisible item, a DatePicker. The function of two first properties is obvious, to enable user input. But the last invisible DateTime will serve special function. I will explain later after we reach the code behind section, for now let see the code for XAML file

TitledDateTimePickerCode2

As you can see, I have one invisible DatePicker that bind to property called SelectedDateTime, we will use this property to bind whatever Date and Time that user input. One thing you need to take note is how I handle property changed in both visible DatePicker and TimePicker. Whenever user change the date or time, it will also change the our main property, SelectedDateTime.

Another thing you may notice is, I’m using custom Custom Date and Time Picker. It’s actually just custom picker with adjustable Font Size. If you curious, you can take a look at my previous post about adjusting font size of Xamarin Form’s Picker. But of course you also can use the original DatePicker and TimePicker from Xamarin Forms. It will works just fine, you just can’t bind the font size.

Code Behind

Now let’s move to the code behind that XAML file. This file actually is just full of properties you wanna bind to this custom control. You can add more properties that fits your needs, like FontColor maybe, or any other properties. But I wanna focus to how I handle property changes in this file. Like I said before, I use DateSelected and PropertyChanged event handler in DatePicker and TimePicker respectively to update SelectedDateTime. But this method has one weakness. If the SelectedDateTime already has value when its bind, the visible DatePicker and TimePicker won’t show it’s value. Before we go any further, let’s take a look at the code.

public partial class TitledDateTimePicker : ContentView
    {
        public TitledDateTimePicker()
        {
            InitializeComponent();
            LabelTitle.BindingContext = this;
            DatePickerContent.BindingContext = this;
            TimePickerContent.BindingContext = this;
        }

        public static BindableProperty SelectedDateTimeProperty =
            BindableProperty.Create(nameof(SelectedDateTime), typeof(DateTime?), typeof(TitledDateTimePicker), DateTime.Now, BindingMode.TwoWay);

        public DateTime? SelectedDateTime
        {
            get => (DateTime?)GetValue(SelectedDateTimeProperty);
            set => SetValue(SelectedDateTimeProperty, value);
        }

        public static BindableProperty TitleProperty =
            BindableProperty.Create(nameof(Title), typeof(string), typeof(TitledDateTimePicker), null, BindingMode.TwoWay);

        public string Title
        {
            get => (string)GetValue(TitleProperty);
            set => SetValue(TitleProperty, value);
        }

        public static BindableProperty DatetimeProperty =
            BindableProperty.Create(nameof(Datetime), typeof(DateTime), typeof(TitledDateTimePicker), DateTime.Today, BindingMode.TwoWay);

        public DateTime Datetime
        {
            get => (DateTime)GetValue(DatetimeProperty);
            set => SetValue(DatetimeProperty, value);
        }

        public static BindableProperty TimespanProperty =
            BindableProperty.Create(nameof(Timespan), typeof(TimeSpan), typeof(TitledDateTimePicker), DateTime.Today.TimeOfDay, BindingMode.TwoWay);

        public TimeSpan Timespan
        {
            get => (TimeSpan)GetValue(TimespanProperty);
            set => SetValue(TimespanProperty, value);
        }

        public static BindableProperty FontSizeProperty =
            BindableProperty.Create(nameof(FontSize), typeof(int), typeof(TitledDateTimePicker), 12, BindingMode.TwoWay);

        public int FontSize
        {
            get => (int)GetValue(FontSizeProperty);
            set => SetValue(FontSizeProperty, value);
        }

        public static BindableProperty IsEditableProperty =
            BindableProperty.Create(nameof(IsEditable), typeof(bool), typeof(TitledEntry), true, BindingMode.TwoWay);

        public bool IsEditable
        {
            get => (bool)GetValue(IsEditableProperty);
            set => SetValue(IsEditableProperty, value);
        }

        void Handle_DateSelected(object sender, DateChangedEventArgs e)
        {
            UpdateSelectedDatime();
        }

        void Handle_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "Time")
            {
                UpdateSelectedDatime();
            }
        }

        void Handle_BindingContextChanged(object sender, EventArgs e)
        {
            if (SelectedDateTime.HasValue)
                SetDateTime();
        }

        void UpdateSelectedDatime()
        {
            SelectedDateTime = Datetime.Add(Timespan);
        }

        void SetDateTime()
        {
            Datetime = SelectedDateTime.Value;
            Timespan = SelectedDateTime.Value.TimeOfDay;
        }
    }

As you can see at code above, whenever DateSelected and PropertyChanged fired, means user input date or time, it will update SelectedDateTime. In other hand, BindingContextChanged of my invisible DatePicker do the completely opposites. It will update the DateTime and TimeSpan of the visible DatePicker and TimePicker respectively.

So, when user navigate to a page which use this custom control, and he want to update a DateTime field that has beed set before hand, he will find fields of DatePicker and TimePicker that already set to proper values, because when he navigate to that page, the BindingContextChanged will be triggered. If we don’t do this, the DatePicker will set to default value, which is January 1st, 1990. This why we need that invisible DatePicker, because we need to put that BindingContextChanged event handler somewhere in the code.

How to use it?

Nah, we already have packed up everything we need about Date and Time Selecting in one simple custom control. So, all we need to do is calling it from any pages that need it. This is a simple sample how to do it.

TitledDateTimePickerCode1

 

Sample Code is available in my Github repo

 

SQLite and ORM in Xamarin Forms

It’s about a week ago, I was looking for a complete tutorial about how to implement ORM in SQLite in Xamarin Forms. I did find out that there’s already a library called SQLiteNetExtensions that handle this kind of stuff. It’s also has complete documentation with it. Unfortunately, the documentation is not up to date. I was having difficulty to use that library at first, so I look up for other tutorials on the net. But I couldn’t find one that suit my need, a complete and up to date tutorial. This is actually pretty simple thing, but if you don’t know how to do it properly, it will cost you hours of your life before you can make it works. So, this is how I’ve done it.

Note : Nuget packages you need are SQLite.Net-PCL and SQLiteNetExtenstions

Database Strictures

First thing first. Because we’re talking about ORM, let’s have a simple data structure that represent all kind relationships in ORM; OneToMany, ManyToOne, ManyToMany and OneToOne.

public class Department
{
    [PrimaryKey, AutoIncrement]
    public int ID { get; set; }

    public string DepartmentName { get; set; }

    [OneToMany(CascadeOperations = CascadeOperation.All)]
    public List Employees { get; set; }
}

public class Employee
{
    [PrimaryKey, AutoIncrement]
    public int ID { get; set; }

    public string IDCardNumber { get; set; }
    public string EmployeeName { get; set; }

    [ForeignKey(typeof(Department))]
    public int DepartmentID { get; set; }

    [ManyToOne(CascadeOperations = CascadeOperation.All)]
    public Department Deparment { get; set; }

    [ForeignKey(typeof(EmployeeFamily))]
    public int FamilyID { get; set; }

    [OneToOne(CascadeOperations = CascadeOperation.All)]
    public EmployeeFamily Family { get; set; }

    [ManyToMany(typeof(EmployeeBenefit))]
    public List Benefits { get; set; }
}

public class EmployeeFamily
{
    [PrimaryKey, AutoIncrement]
    public int ID { get; set; }

    public string SpouseName { get; set; }
    public string FirstChildName { get; set; }
    public string SecondChildName { get; set; }

    [ForeignKey(typeof(Employee))]
    public int EmployeeID { get; set; }

    [OneToOne(CascadeOperations = CascadeOperation.All)]
    public Employee Employee { get; set; }
}

public class EmployeeBenefit
{
    [PrimaryKey, AutoIncrement]
    public int ID { get; set; }

    public string BenefitName { get; set; }
    public double BenefitValue { get; set; }

    [ManyToMany(typeof(Employee))]
    public List Employees { get; set; }
}

One thing to take a note is make sure you’re using SQLite and SqLiteExtensions.Attributes instead of SQL.Net.Attributes or it’s not gonna works.

Dependency Services

To implement sqlite to Android and iOS, of course we gonna need dependency service classes. But first let create the interface in our PCL project.

public interface ISQLite
{
    SQLite.SQLiteConnection GetSQLiteConnection();
}

Then we move on to Android project. This is how the dependency service class looks like.

public class SQLiteAndroid : ISQLite
{
    public SQLite.SQLiteConnection GetSQLiteConnection()
    {
        var fileName = "testing.db3";
        var documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
        var path = Path.Combine(documentsPath, fileName);

        var platform = new SQLitePlatformAndroid();
        var connection = new SQLite.SQLiteConnection(path, true);

        return connection;
    }
}

Lastly, we also implement the dependency class to iOS project.

public class SQLiteiOS : ISQLite
{
    public SQLite.Net.SQLiteConnection GetConnection()
    {
        var sqliteFilename = "testing.db3";
        string documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
        string libraryPath = Path.Combine(documentsPath, "..", "Library");
        var path = Path.Combine(libraryPath, sqliteFilename);
        var platform = new SQLitePlatformIOS();
        var conn = new SQLite.Net.SQLiteConnection(platform, path);
        return conn;
    }
}

Data Context

Next, let’s create a class that will handle all things SQLite related. So when we need to do some operations in SQLite, all we need to do just calling the method in this class. Below this is the sample code of the class, but I don’t include all operations possible, just some important operations for example.

public class EmployeeDataContext
{
    private static EmployeeDataContext instance;

    public static EmployeeDataContext Instance
    {
        get
        {
            if (instance == null)
            {
                instance = new EmployeeDataContext();
            }
            return instance;
        }
    }

    private SQLite.SQLiteConnection connection;

    private EmployeeDataContext()
    {
        connection = DependencyService.Get().GetSQLiteConnection();
        CreateTableIfNotExist();
    }

    private void CreateTableIfNotExist()
    {

        bool isTableNotExist = false;

        try
        {
            var test = connection.Table().FirstOrDefault();
        }
        catch
        {
            isTableNotExist = true;
        }
if (isTableNotExist)
        {
            connection.CreateTable();
        }

    }

    public List GetAllDepartment()
    {
        return ReadOperations.GetAllWithChildren(connection, recursive: true).ToList();
    }

    public void RefreshDepartment(IList listDepartment)
    {
       WriteOperations.DeleteAll(connection, GetAllDepartment(), true);
       WriteOperations.InsertOrReplaceAllWithChildren(connection, listDepartment, true);
    }

    public void DeleteAllDepartment()
    {
        WriteOperations.DeleteAll(connection, GetAllDepartment(), true);
    }

    public void UpdateDepartment(Department department)
    {
        WriteOperations.UpdateWithChildren(connection, department);
    }

    public void UpdateEmployee(Employee employee)
    {
        WriteOperations.UpdateWithChildren(connection, employee);
    }
}

How to use it

Now, everything is ready, let’s see how to use it. You call the methods from view model or directly from code behind.


List listDepartment = new List();
Department department = new Department();
Employee employee = new Employee();

EmployeeDataContext.Instance.GetAllDepartment();
EmployeeDataContext.Instance.RefreshDepartment(listDepartment);
EmployeeDataContext.Instance.UpdateDepartment(department);
EmployeeDataContext.Instance.UpdateEmployee(employee);

Sample Code is available in my Github repo

 

Making Enum as Bindable Property

This post is actually just an update from my previous post about Entry with title. I’ve made some enhancement to optimize my titled entry. From several changes, I think this enum bindable property is the most important so I decided to make one post discussing about it. But in this post I will also review some other minor changes. So, Let’s begin.

Additional Properties and methods

I added two additional properties. The first is Text, for binding a text to the entry, and Entry keyboard to binding the type of the keyboard. Those two properties are two most essential features of entry that I previously forgot to add. For Text property, it’s just another simple string binding property, but for the keyboard, it’s little bit different. Because entry’s keyboard has several different types that developer can choose depend on what field the entry’s used for. So, I created an enum bindable property to tackle that issue. This is how the code looks like.

public partial class TitledEntry : ContentView
{
    string placeholder = string.Empty;

    public enum KeyboardEnum
    {
        Default,
        Text,
        Chat,
        Url,
        Email,
        Telephone,
        Numeric,
    }

    public TitledEntry()
    {
        InitializeComponent();

        EntryContent.BindingContext = this;
        LabelTitle.BindingContext = this;
        LabelTitle.Text = string.Empty;
    }

    public static BindableProperty PlaceholderProperty =
        BindableProperty.Create(nameof(Placeholder), typeof(string), typeof(TitledEntry), null, BindingMode.TwoWay);

    public string Placeholder
    {
        get => (string)GetValue(PlaceholderProperty);
        set => SetValue(PlaceholderProperty, value);
    }

    public static BindableProperty TextProperty =
        BindableProperty.Create(nameof(Text), typeof(string), typeof(TitledEntry), null, BindingMode.TwoWay);

    public string Text
    {
        get => (string)GetValue(TextProperty);
        set => SetValue(TextProperty, value);
    }

    public static BindableProperty KeyboardProperty =
        BindableProperty.Create(nameof(KeyboardProperty), typeof(KeyboardEnum), typeof(TitledEntry), KeyboardEnum.Default, BindingMode.TwoWay);

    public KeyboardEnum EntryKeyboard
    {
        get => (KeyboardEnum)GetValue(KeyboardProperty);
        set
        {
            SetValue(KeyboardProperty, value);
            SetKeyboard();
        }
    }

    void Handle_Focused(object sender, FocusEventArgs e)
    {
        LabelTitle.Text = Placeholder;

        if (EntryContent.Text == null || EntryContent.Text.Length == 0)
        {
            placeholder = EntryContent.Placeholder;
            EntryContent.Placeholder = string.Empty;
        }

    }

    void Handle_Unfocused(object sender, FocusEventArgs e)
    {
        if (EntryContent.Text == null || EntryContent.Text.Length == 0)
        {
            EntryContent.Placeholder = placeholder;
            LabelTitle.Text = string.Empty;
        }

    }

    void Handle_TextChanged(object sender, TextChangedEventArgs e)
    {
        var entry = sender as Entry;
        if (!string.IsNullOrEmpty(entry.Text))
        {
            LabelTitle.Text = Placeholder;
        }
    }

    void SetKeyboard()
    {
        switch (EntryKeyboard)
        {
            case KeyboardEnum.Default:
                EntryContent.Keyboard = Keyboard.Default;
                break;
            case KeyboardEnum.Text:
                EntryContent.Keyboard = Keyboard.Text;
                break;
            case KeyboardEnum.Chat:
                EntryContent.Keyboard = Keyboard.Chat;
                break;
            case KeyboardEnum.Url:
                EntryContent.Keyboard = Keyboard.Url;
                break;
            case KeyboardEnum.Email:
                EntryContent.Keyboard = Keyboard.Email;
                break;
            case KeyboardEnum.Telephone:
                EntryContent.Keyboard = Keyboard.Telephone;
                break;
            case KeyboardEnum.Numeric:
                EntryContent.Keyboard = Keyboard.Numeric;
                break;
            default:
                EntryContent.Keyboard = Keyboard.Default;
                break;
        }
    }
}

As you can see at the code, I create similar enum like the original entry’s keyboard and then I created a bindable property for that enum. And then I create the method SetKeyboard to convert my enum to the real keyboard. I know I can use value converter, but I think it’s more simple this way.

Another method that I added is Text Changed event handling for the entry. I use this method to handle if entry text already has value from beginning.  And for note, I also make a change in xaml side. I turned the stack layout into grid to optimize the rendering performance of the xaml.

BindableEnum1

Sample Code is available in my Github repo

Turning Entry’s Placeholder into Title in Xamarin Forms

I really hope that this title is not misleading. I’m not sure ‘turning’ is the right word, but I don’t know better word to describe what I want to discuss in this post. As we know together, in Android Native app, we can have Entry component whose placeholder will turn into title with a little ‘going up’ animation when user start interact with it. And because you can only have it in Android, not in other platform, Xamarin Forms doesn’t support it, at least not yet. Unfortunately, in my current project I’m already required to create such a thing. So, this is how I do the work around, not perfect yet but not too bad either.

Custom Entry with Title

The first thing I do is creating a custom content view with a stack layout in it. The stack layout contains an Entry and a Label that will serve as Entry’s title. Why stack layout, not Grid? It’s not just for simplicity’s sake. Remember the ‘going up’ animation I discuss earlier? I want to create an effect like that, not as good as the original, but I think it’s good enough. When you hide something in stack layout and something trigger it to appear, it will create a little animation, up or down depend on the position. It also works the other way around, when you hide something, you’ll get similar animation. So, this how I put the Entry and Label on the xaml file.

TitledEntryCode1

Pay the on the Entry. It has event handler for Focused and Unfocused event, that where we gonna do the trick. And the code below is how the code behind looks like.

public partial class TitledEntry : ContentView
{
    string placeholder = string.Empty;

    public TitledEntry()
    {
        InitializeComponent();

        if (EntryContent.Text == null || EntryContent.Text.Length == 0)
        {
            LabelTitle.IsVisible = false;
        }

        EntryContent.BindingContext = this;
        LabelTitle.BindingContext = this;
    }

    public static BindableProperty PlaceholderProperty =
        BindableProperty.Create(nameof(Placeholder), typeof(string), typeof(TitledEntry), null, BindingMode.TwoWay);

    public string Placeholder
    {
        get => (string)GetValue(PlaceholderProperty);
        set => SetValue(PlaceholderProperty, value);
    }

    void Handle_Focused(object sender, FocusEventArgs e)
    {
        LabelTitle.IsVisible = true;

        if (EntryContent.Text == null || EntryContent.Text.Length == 0)
        {
            placeholder = EntryContent.Placeholder;
            EntryContent.Placeholder = string.Empty;
        }

    }

    void Handle_Unfocused(object sender, FocusEventArgs e)
    {
        if (EntryContent.Text == null || EntryContent.Text.Length == 0)
        {
            EntryContent.Placeholder = placeholder;
            LabelTitle.IsVisible = false;
        }

    }
}

So all I do is just giving condition about Entry’s text length and stack layout will do the rest. If there’s one thing you need to remember is setting the binding context of the Entry and Label to current class like I do in the constructor or you won’t be able to bind them from view model.

Let’s use it

Now that we already have our custom entry ready, let see it in action. I create a view model contains 2 strings called Username and Email and I set a view model as the binding context for the class whose xaml file you can see below.

TitledEntryCode2

And if try to run the solution, you’ll have Entries like the real Android’s Entry.

 

Update : Making Enum as Bindable Property

Sample Code is available in my Github repo

 

Get to know about iOS Web Kit Web View

Recently I’ve been informed that I want to display Web View on iOS application, I actually have 3 options. The first, the most common one, using UI Web View. I’ve been using it in all my project whenever I need  to display a web view, whether Swift project or Xamarin. Second one is Web Kit Web View or WK Web View in short. WK Web View has similar functionalities and methods like UI Web View but it designed to overcome UI Web View weakness when compiling Java Script. Third is Safari View Controller. It can be said that you put Safari Browser inside your app. The purpose is your user doesn’t need to navigate outside your app when you want to display a web. It’s about maintaining user engagement to your app. I already posted twice about UI Web View and not so much I can discuss about Safari, so in this post I want to discuss about WK Web View and how to migrate from UI Web View to WK Web View in Xamarin Forms.

Hybrid Web View

If you look up about Hybrid Web View on internet, the first link you get will directly navigate you to Xamarin documentation about how to use WK Web View. For starter I will follow through the example that documentation provides. So, here my code for my Hybrid WK Web View.

public class HybridWebKitWebView : View
{
    Action action;
    public static readonly BindableProperty UriProperty = BindableProperty.Create(
      propertyName: "Uri",
      returnType: typeof(string),
      declaringType: typeof(HybridWebKitWebView),
      defaultValue: default(string));

    public string Uri
    {
        get { return (string)GetValue(UriProperty); }
        set { SetValue(UriProperty, value); }
    }

    public void RegisterAction(Action callback)
    {
        action = callback;
    }

    public void Cleanup()
    {
        action = null;
    }

    public void InvokeAction(string data)
    {
        if (action == null || data == null)
        {
            return;
        }
       action.Invoke(data);
    }
}

Renderer and Delegate

Still from documentation, here’s the code for WK Web View Renderer, It’s pretty much the same, I just get rid some codes because I wanna load from internet not local content.

public class HybridWKWebViewRenderer: ViewRenderer
{
    WKUserContentController userController;

    protected override void OnElementChanged(ElementChangedEventArgs e)
    {
        base.OnElementChanged(e);

        if (Control == null)
        {
            userController = new WKUserContentController();

            var config = new WKWebViewConfiguration { UserContentController = userController };
            var webView = new WKWebView(Frame, config);
            webView.NavigationDelegate = new HybridWKWebViewDelegate(this);
            SetNativeControl(webView);
        }
        if (e.OldElement != null)
        {
            var hybridWebView = e.OldElement as HybridWebKitWebView;
            hybridWebView.Cleanup();
        }
        if (e.NewElement != null)
        {
            Control.LoadRequest(new NSUrlRequest(new NSUrl(Element.Uri)));
        }
    }
}

Next is the Delegate class. The delegate class of WK Web View is actually pretty much the same with Delegate class for UI Web View. So, if you want to migrate from UI Web View to WK Web View, it won’t be too difficult. It just has different class names. Here’s some important methods and their ‘new name’ in WK Web View

  • LoadStarted became DidStartProvisionalNavigation
  • LoadFailed became DidFailProvisionalNavigation
  • LoadingFinished became DidFinishNavigation
  • ShouldStartLoad became DecidePolicy

They don’t just have different names but different parameters as well, so here’s the example of empty Delegate Class of WK Web View.

public class HybridWKWebViewDelegate: WKNavigationDelegate
{
    HybridWKWebViewRenderer hybridWebViewRenderer;
    public HybridWKWebViewDelegate(HybridWKWebViewRenderer _webViewRenderer = null)
    {
        hybridWebViewRenderer = _webViewRenderer ?? new HybridWKWebViewRenderer();
    }

    [Export("webView:didStartProvisionalNavigation:")]
    public override void DidStartProvisionalNavigation(WKWebView webView, WKNavigation navigation)
    {

    }

    [Export("webView:didFailProvisionalNavigation:")]
    public override void DidFailProvisionalNavigation(WKWebView webView, WKNavigation navigation, NSError error)
    {

    }

    [Export("webView:didFinishNavigation:")]
    public override void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
    {

    }

    [Export("webView:decidePolicyForNavigationAction:decisionHandler:")]
    public override void DecidePolicy(WKWebView webView, WKNavigationAction navigationAction, Action decisionHandler)
    {

    }
}

Decide Policy

Let’s dive little bit deeper about DecidePolicy Method because it’s works little bit different compare to ShouldStartLoad method. When you load a web page in WK Web View, this method is the first one who get triggered. In this method, you can decide whether you want to allow the web view to load the web page or not.

But to handle that kind of event, first you need to know what Navigation Type that the web page using. There’re six of them and you need to handle the navigation activities based on them. Here’s the simple sample of it.

[Export("webView:decidePolicyForNavigationAction:decisionHandler:")]
public override void DecidePolicy(WKWebView webView, WKNavigationAction navigationAction, Action decisionHandler)
{
    switch (navigationAction.NavigationType)
    {
        case WKNavigationType.BackForward:
            decisionHandler(WKNavigationActionPolicy.Allow);
            break;
        case WKNavigationType.FormResubmitted:
            decisionHandler(WKNavigationActionPolicy.Cancel);
            break;
        case WKNavigationType.FormSubmitted:
            decisionHandler(WKNavigationActionPolicy.Allow);
            break;
        case WKNavigationType.LinkActivated:
            decisionHandler(WKNavigationActionPolicy.Cancel);
            break;
        case WKNavigationType.Other:
            decisionHandler(WKNavigationActionPolicy.Allow);
            break;
        case WKNavigationType.Reload:
            decisionHandler(WKNavigationActionPolicy.Cancel);
            break;
    }
}

From the names, I believe you can guess what kind Navigation Type are them. The only one which little bit unclear maybe the Other type. Well, so far I only meet this type twice. First, when the web view load the url that I set for the first time. So, everything will be started from this type before the web view navigate to other page. The second one I will discuss in the next section.

Handle Button Click Event

For example how to use Delegate class in WK Web View, I will use example of what I’ve done in UI Web View. First is about handling button click event. Here’s how I did it using WK Web View Delegate class. The navigation type I used is Other.

[Export("webView:didFinishNavigation:")]
public override void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
{
    // find the particular button
    string funcurl = string.Empty;
    funcurl = "var btn1 =  document.getElementsByClassName('btn-primary')[1]; if(btn1 != null){btn1.addEventListener('click', function() { window.location = \"dosomething\"; }); }" +
              "var btn2 =  document.getElementsByClassName('btn-primary')[2]; if(btn2 != null){reject.addEventListener('click', function() { window.location = \"dosomethingelse\"; }); }";

    webView.EvaluateJavaScript("javascript: r(function(){" + funcurl + "});", null);
}

[Export("webView:decidePolicyForNavigationAction:decisionHandler:")]
public override void DecidePolicy(WKWebView webView, WKNavigationAction navigationAction, Action decisionHandler)
{
    switch (navigationAction.NavigationType)
    {

        case WKNavigationType.Other:

            var request = navigationAction.Request;

            if (request.Url.ToString().Contains("dosomething"))
            {
                // do something here
                decisionHandler(WKNavigationActionPolicy.Cancel);
            }
            else if (request.Url.ToString().Contains("dosomethingelse"))
            {
                // do something else here
                decisionHandler(WKNavigationActionPolicy.Cancel);
            }
            break;
        default:
            decisionHandler(WKNavigationActionPolicy.Allow);
            break;
    }
}

Adding Authorization Header

Second example is about Adding Authorization Header. I think adding this header suppose to in every type of navigation. But, in this example I will just add it in two most common navigation types, Other and LinkActivated. Link Activated is navigation type when user clink a link in a web page. So, here’s how it’s done.

[Export("webView:decidePolicyForNavigationAction:decisionHandler:")]
public override void DecidePolicy(WKWebView webView, WKNavigationAction navigationAction, Action decisionHandler)
{
    switch (navigationAction.NavigationType)
    {
        case WKNavigationType.LinkActivated:
            var request = navigationAction.Request;

            if (!request.Headers.ContainsKey(new NSString("Authorization")))
            {
                var copy = request.MutableCopy() as NSMutableUrlRequest;
                var token = "";

                NSMutableDictionary dic = new NSMutableDictionary();
                dic.Add(new NSString("Authorization"), new NSString("Bearer " + token));
                copy.Headers = dic;

                string currentUrl = request.Url.ToString();

                webView.LoadRequest(copy);
                decisionHandler(WKNavigationActionPolicy.Cancel);
            }
            else
            {
                decisionHandler(WKNavigationActionPolicy.Allow);
            }

            break;

        case WKNavigationType.Other:

           request = navigationAction.Request;

            if (!request.Headers.ContainsKey(new NSString("Authorization")))
            {
                var copy = request.MutableCopy() as NSMutableUrlRequest;
                var token = "";

                NSMutableDictionary dic = new NSMutableDictionary();
                dic.Add(new NSString("Authorization"), new NSString("Bearer " + token));
                copy.Headers = dic;

                string currentUrl = request.Url.ToString();

                webView.LoadRequest(copy);
                decisionHandler(WKNavigationActionPolicy.Cancel);
            }
            else
            {
                decisionHandler(WKNavigationActionPolicy.Allow);
            }

             break;
        default:
            decisionHandler(WKNavigationActionPolicy.Allow);
            break;
    }

}

 

Accordion ListView in Xamarin Forms

Accordion ListView or Expanded ListView, a list view that can show another list view inside its list view item, is one of UI components that Xamarin Forms doesn’t have it by default. So, in this post, I want to discuss one of the simplest way to create an accordion list view look alike in Xamarin Forms.

Custom Observable Collection

One property that we need the most to create accordion list view is visibility property with boolean data type. This property will determine wether the children list view is visible or not. So, when user tap to an item in list view, that property have to change. And because it’s only a property in one of the list view items, we gonna need custom observable collection that support that condition. Because regular observable collection won’t be support it. The property will change, but it won’t affect the UI view. I have discussed this custom observable collection in first post, and we gonna need it again in this case.


public class CustomObservableCollection : ObservableCollection
{
    public CustomObservableCollection() { }
    public CustomObservableCollection(IEnumerable items) : this()
    {
        foreach (var item in items)
           this.Add(item);
    }
public void ReportItemChange(T item)
    {
        NotifyCollectionChangedEventArgs args =
        new NotifyCollectionChangedEventArgs(
        NotifyCollectionChangedAction.Replace,
        item,
        item,
        IndexOf(item));
        OnCollectionChanged(args);
    }
}

<span id="mce_SELREST_start" style="overflow:hidden;line-height:0;">&#65279;</span>

This custom observable collection add a method called ReportItemChange that will trigger CollectionEventChanged. So, when we change the visibility property, the UI view will be notified.

Entities

So, for example in this post, I will create list of countries which have list of cities on every country. So, these are entities that I use in this example.


public class City
{
    public string CityName { get; set; }
}

public class Country
{
    public string CountryName { get; set; }
    public bool IsChildrenVisible { get; set; }
    public string ArrowIconSource
    {
        get
        {
           if (IsChildrenVisible)
              return "uparrow.png";
           else
              return "downarrow.png";
        }
    }
    public ObservableCollection Cities { get; set; }
    public int ChildrenRowHeightRequest
    {
        get
        {
            if (Cities != null)
                return Cities.Count * 50;
            else
                return 0;
        }
    }
}

Country, as parent entity, has a visibility property called IsChildrenVisible that will determine whether the cities are showed or not. It also has ChildrenRowHeightRequest property to determine height request for Cities list view.

View Model

After entities, we move on to the view model. In view model we create list of countries that will be bind to the view. As a default, we will set the IsChildrenVisible property to false. And in this view model, we also have method called ShowCities that update that property.


public class AccordionCountriesViewModel : BaseViewModel
{
    private CustomObservableCollection countries;
    public CustomObservableCollection Countries
    {
       get => countries;
       set => SetProperty(ref countries, value);
    }

    public AccordionCountriesViewModel()
    {
       ObservableCollection USACities = new ObservableCollection()
       {
           new City(){ CityName = "Washington DC" },
           new City(){ CityName = "New York" },
           new City(){ CityName = "Los Angeles" }
       };

       ObservableCollection ChinaCities = new ObservableCollection()
       {
           new City(){ CityName = "Beijing" },
           new City(){ CityName = "Shanghai" },
           new City(){ CityName = "Shenzhen" }
       };

       ObservableCollection RussiaCities = new ObservableCollection()
       {
          new City(){ CityName = "Moscow" },
          new City(){ CityName = "St. Peterburg" },
          new City(){ CityName = "Kazan" }
       };

       countries = new CustomObservableCollection()
       {
          new Country(){ CountryName = "USA", IsChildrenVisible = false, Cities = USACities },
          new Country(){ CountryName = "China", IsChildrenVisible = false, Cities = ChinaCities },
          new Country(){ CountryName = "Russia", IsChildrenVisible = false, Cities = RussiaCities },
       };
   }

   public void ShowCities(Country mCountry)
   {
      Country country = Countries.SingleOrDefault(c => c.CountryName == mCountry.CountryName);
      country.IsChildrenVisible = !country.IsChildrenVisible;
      Countries.ReportItemChange(country);
   }
}

Let’s try it out

All preparations are complete. We will create a view which has list view inside another list view. And then we user tap the list view item, it will call method ShowCities in view model.

Screen Shot 2018-02-22 at 6.25.00 AM
And this is how we bind the view model and handle the item tapped event.


public partial class AccordionCountriesPage : ContentPage
{
    AccordionCountriesViewModel viewModel;
    public AccordionCountriesPage()
    {
         InitializeComponent();
         BindingContext = viewModel = new AccordionCountriesViewModel();
    }

    void Handle_ItemTapped(object sender, ItemTappedEventArgs e)
    {
        Country mCountry = (Country)e.Item;
        viewModel.ShowCities(mCountry);
    }
}

Now you can try and run the code.

Credit :

  • Arrow Icons from FlatIcon, Down Arrow icon by Freepik, Right Arrow icon by GraphicsBay, Up Arrow icon by Hadrien

 

UPDATE : Adding Animation to Accordion List View