Monthly Archives: November 2020

Using async/await in an IValueConverter in Xamarin.Forms

Let’s start out with a very simple Setting – we have the following Models:
Product
+ Id : string
+ Name: string
+ Image: Image

Image
+ Id: string
+ Filename: string

In our service layer, we have a function that gets all products, and we want to show them on a Xamarin.Forms page, including each image. The Image-Model has no byte-data or whatsoever included. There is a special ImageCacheService implemented, that uses the Image to get the data either from disk (cache) or from a WebApi. This function is implemented using the async/await-pattern. So we have to use an async-context to get the data.
Here is how the ViewModel and Xaml looks like:

 public class MainPageViewModel
    {
        
        private ProductsService _productsService;
        public MainPageViewModel(ProductsService productsService)
        {
            _productsService = productsService;
            InitAsync();
        }

        private async void InitAsync()
        {
            var products = await _productsService.GetProducts();
            products.ForEach(Products.Add);
        }

        public ObservableCollection<Product> Products { get; } = new ObservableCollection<Product>();
    }
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="AsyncConverter.MainPage">
   <CollectionView>
       <CollectionView.ItemTemplate>
           <DataTemplate>
               <Grid ColumnDefinitions="300,*">
                  <Image Source="" /> <!-- ??? how to bind image -->
                  <Label Grid.Column="1" Text="{Binding Name}" />
               </Grid>
           </DataTemplate>
       </CollectionView.ItemTemplate>
   </CollectionView>
</ContentPage>

So how to solve that problem? We somehow need to call await ImageCacheService.GetImageData(Image) on each Image to retrieve the data. The most common approach is to use ViewModels instead of direct Model binding. But we only want to show the product’s images on a page, we don’t want to change something. So why creating two(?) ViewModels that have no added value?

A more sophisticated approach would be to create an IValueConverter implementation, so we can just bind the Image-Model and the converter does the magic to retrieve the image from database/web/file system or wherever it is stored. However, if you try this implementation, you will quickly reach your limits. IValueConverter has no async pendant, so you can’t use it directly. But how about the following idea:

The converter doesn’t return an ImageSource, but it returns a ViewModel, with a property called something like “Result”. And as soon as the image data is retrieved, it will set the Result property and raise a PropertyChanged event, so that the UI can use it.

The implementation will look something like that:

public abstract class AsyncConverter<T> : IValueConverter
    {
        public abstract Task<T> AsyncConvert(object value, Type targetType, object parameter, CultureInfo culture);

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var res = new GenericAsyncViewModel();
            res.Start(AsyncConvert(value, targetType, parameter, culture));
            return res;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }

        private class GenericAsyncViewModel : INotifyPropertyChanged
        {
            private T _result;
            public event PropertyChangedEventHandler PropertyChanged;

            public T Result
            {
                get => _result;
                set
                {
                    _result = value;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Result)));
                }
            }

            public async void Start(Task<T> task)
            {
                Result = await task;
            }

        }
    }

This is a generic implementation, when we now implement our ImageModelConverter, it looks like that:

 public class ImageModelConverter : AsyncConverter<ImageSource>
    {
        // its a little bit tricky to inject this using IoC, but it's possible
        // but that's not part of this ... so I just create the service here
        public ImageCacheService _imageCacheService = new ImageCacheService();
        public override async Task<ImageSource> AsyncConvert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is Models.Image image)
            {
                var imageData = await _imageCacheService.GetImageData(image);
                return ImageSource.FromStream(() => new MemoryStream(imageData));
            }
            return null;
        }
    }

When we use that Converter, it is slightly a little bit different to our usual procedure, we don’t bind the view’s property directly with the converter, but the BindingContext:

<ContentPage
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:converters="clr-namespace:AsyncConverter.Converters"
    x:Class="AsyncConverter.MainPage">
 <ContentPage.Resources>
     <converters:ImageModelConverter x:Key="ImageModelConverter"/>
 </ContentPage.Resources>
    <CollectionView ItemsSource="{Binding Products}">
        <CollectionView.ItemTemplate>
            <DataTemplate>
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="200" />
                        <ColumnDefinition Width="*" />
                    </Grid.ColumnDefinitions>
                    <Image BindingContext="{Binding Image, Converter={StaticResource ImageModelConverter}}"
                           Source="{Binding Result}" />
                    <Label Grid.Column="1" Text="{Binding Name}" />

                </Grid>
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>
</ContentPage>


If you are interested in all of the source code, feel free to check it out on git
https://github.com/ThomasKison/AsyncConverter

…feel free to modify and use…I appreciate any feedback.

Flyout in Xamarin forms

Creating a Flyout in Xamarin.Forms

Flyouts are a pretty neat control, first introduced in Windows UWP, but there is nothing close in Xamarin.Forms which can be used on all platforms. So I decided to create one.

What is a Flyout?

from the Windows UWP documentation (https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.flyout?view=winrt-19041)

“Use a Flyout control for collecting information, for displaying more info, or for warnings and confirmations. Unlike a dialog, a Flyout does not create a separate window, and does not block other user interaction.”

So a Flyout can be attached to whatever control you want, and it will pop up as soon as the item is tapped. There are several use cases it easy fits in:
– show more details in a specific context
– ask for confirmation
– ask for more details
– …

How will the XAML look like?

<Label Text="Hello who?">
   <FlyoutBase.AttachedFlyout>
      <Flyout BackgroundColor="Red">
          <Flyout.DataTemplate>
               <DataTemplate>
                  <Label Text="Hello World!"/>
               </DataTemplate>
          </Flyout.DataTemplate>
      </FLyout>
   </FlyoutBase.AttachedFlyout>
</Label>

Looks easy to use, but still needs to be implemented.

Main Challenge: Create a layer over the current content for our Flyout

First we need to add a layer over the whole content of the page, so we can freely place a Flyout on the screen over all other controls. We can achieve that by going up the whole visual tree, find the ContentPage and kinda inject a Grid into it. A Grid is pretty easy to use, and it has the ability to have several Children that arrange in layers one over each other. We need to do this step, because we know nothing about the Layout that is used by the programmer, that created that Page.

FlyoutBase.AttachedFlyout is a AttachedProperty that we can attach to whatever control we need. After the Flyout is attached, we just need to add the TapGesture to that control.

internal class FlyoutRootGrid : Grid {}    // just a specific type, to remind it
public class FlyoutBase
    {
        #region attachedflyout property
        public static readonly BindableProperty AttachedFlyoutProperty =
            BindableProperty.CreateAttached("AttachedFlyout", typeof(Flyout), typeof(FlyoutBase), null, propertyChanged: OnAttachedFlyoutChanged);



        public static Flyout GetAttachedFlyout(BindableObject view)
        {
            return (Flyout)view.GetValue(AttachedFlyoutProperty);
        }

        public static void SetAttachedFlyout(BindableObject view, Flyout value)
        {
            view.SetValue(AttachedFlyoutProperty, value);
        }
        #endregion

        // called whenever a flyout is attached to a view
        private static async void OnAttachedFlyoutChanged(BindableObject bindable, object oldValue, object newValue)
        {
            if (bindable is View view)
            {
                await Task.Delay(1000); // we wait a little, so the view is attached to the visual tree
                // this is very weak, we need to think about a better solution here
                AttachFlyout(view);
            }
        }



        private static void AttachFlyout(View view)
        {
            // find the FlyoutRoot
            var flyoutRootGrid = CreateOrFindRootGrid(view);
            // and now attach the flyout to it as child
            var flyout = AttachFlyoutToRoot(flyoutRootGrid, view);
            // register the desture to the visual
            view.GestureRecognizers.Add(
                new TapGestureRecognizer
                {
                    NumberOfTapsRequired = 1,
                    Command = new Command(obj => ToggleFlyoutIsVisible(flyout, view))
                });
        }

        // will go through the visual tree up to the content page
        // remove the content
        // add a "FlyotRootGrid"
        // and set the removed content to it as first child
        private static FlyoutRootGrid CreateOrFindRootGrid(VisualElement view)
        {
            var flyOutRootGrid = VisualTreeHelper.FindParentElement<FlyoutRootGrid>(view);
            // maybe we already have such a control in the visual tree (perhaps we use more than one flyout)
            // so we can reuse it
            if (flyOutRootGrid == null)
            {
                // otherwise attach iot to root
                var parentPage = VisualTreeHelper.FindParentPage<ContentPage>(view);
                flyOutRootGrid = new FlyoutRootGrid();
                var oldContent = parentPage.Content;
                parentPage.Content = flyOutRootGrid;
                flyOutRootGrid.Children.Add(oldContent);
            }
            return flyOutRootGrid;
        }

        private static Flyout AttachFlyoutToRoot(FlyoutRootGrid flyoutRootGrid, View view)
        {
            // get the flyout from the attached property
            var flyout = GetAttachedFlyout(view);
            // not visible yet
            flyout.IsVisible = false;
            // add it to the root grid
            flyoutRootGrid.Children.Add(flyout);
            return flyout;
        }

        private static void ToggleFlyoutIsVisible(Flyout flyout, View view)
        {
            // align flout to the view that has been clicked
            // set IsVisible to the flyout
        }

    }

Now that the Flyout is added to the VisualTree of the ContentPage, we just need to handle the fade in and out of the Flyout. We also have to take care of placing the Flyout correctly on the screen. Therefore we need to find the screen coordinates of the tapped view and position the Flyout accordingly.

private static void ToggleFlyoutIsVisible(Flyout flyout, View view)
{
    bool setVisible = !flyout.IsVisible; // checkout the flyout is visible
    if (flyout.Content == null &amp;&amp; flyout.DataTemplate != null)
    { // when we yet have no content -> create it from datatemplate
        flyout.Content = flyout.DataTemplate.CreateContent() as View;
    }
    if (setVisible)
    {
        flyout.AlignFlyout(view);
    }
    flyout.PlayAnimation(setVisible, view);
}

I don’t just want to set the IsVisible property. Let’s do something fancy and add an animation. But first we need to find the coordinates of the tapped control. I found a solution here (https://forums.xamarin.com/discussion/66386/how-to-get-the-coordinates-where-there-is-a-control-on-the-screen) and adjusted it a little to fit my needs. The VisualTreeHelper also includes the FindParentPage-method from a previous blog post.

public static class VisualTreeHelper
    {
        public static T FindParentPage<T>(Element view)
            where T:Page
        {
            return FindParentElement<T>(view);
        }

        public static T FindParentElement<T>(Element view, Func<T,bool> predicate = null)
            where T:Element
        {
           
            if (view is T element)
            {
                if (predicate == null)
                {
                    return element;
                }
                if (predicate(element))
                {
                    return element;
                }
            }
            if (view.Parent == null)
            {
                return null;
            }
            return FindParentElement<T>(view.Parent, predicate);
        }

        public static (double X, double Y) GetScreenCoordinates(this VisualElement view)
        {
            // A view's default X- and Y-coordinates are LOCAL with respect to the boundaries of its parent,
            // and NOT with respect to the screen. This method calculates the SCREEN coordinates of a view.
            // The coordinates returned refer to the top left corner of the view.
            var screenCoordinateX = view.X;
            var screenCoordinateY = view.Y;

            var parent = (VisualElement)view.Parent;
            while (parent != null &amp;&amp; parent is VisualElement)
            {
                screenCoordinateX += parent.X;
                screenCoordinateY += parent.Y;
                parent = parent.Parent as VisualElement;
            }
            return (screenCoordinateX, screenCoordinateY);
        }
    }

Now we can put all together and implement the Flyout

public class Flyout : ContentView
    {
        public enum AnimationType { Fade, SlideVertical }
        public Flyout()
        {
            HorizontalOptions = LayoutOptions.Start;
            VerticalOptions = LayoutOptions.Start;
        }

        public AnimationType Animation { get; set; }

        public DataTemplate DataTemplate { get; set; }


        double _targetHeight = -1;
        internal async void PlayAnimation(bool setVisible, View view)
        {
            switch (Animation)
            {
                case AnimationType.Fade:
                    if (setVisible)
                    {
                        Opacity = 0;
                        IsVisible = true;
                        await this.FadeTo(1, 250);
                    }
                    else
                    {
                        await this.FadeTo(0, 250);
                        IsVisible = false;
                    }
                    break;
                case AnimationType.SlideVertical:
                    if (setVisible)
                    {
                        if (_targetHeight == -1)
                        {
                            _targetHeight = Height == -1 ? HeightRequest : Height;
                        }
                        HeightRequest = 0;
                        Content.Opacity = 0;

                        IsVisible = true;
                        var animation = new Animation(d => HeightRequest = d, 0, _targetHeight);
                        animation.Commit(this, "Flyout");
                        await Task.Delay(100);
                        await Content.FadeTo(1, 55);

                    }
                    else
                    {
                        await Content.FadeTo(0, 55);
                        var animation = new Animation(d => HeightRequest = d, Height, 0);
                        animation.Commit(this, "Flyout2");
                        await Task.Delay(250);
                        IsVisible = false;
                    }
                    break;
            }
        }

        internal void AlignFlyout(View view)
        {
            var coords = view.GetScreenCoordinates();
            Margin = new Thickness(coords.X, coords.Y + view.Height, 0, 0);
        }

    }

As always you can use and extend the code, however you like. It’s not complete yet, feel free to fix bugs or extend it.

How to find the root Page of a specific Control in Xamarin.Forms

Actually it’s very easy to achieve this. Just traverse the visual tree upwards to find the root.

 public static class VisualTreeHelper
    {
        public static ContentPage FindParentPage(Element view)
        {
            if (view is ContentPage page)
            {
                return page;
            }
            return FindParentPage(view.Parent);
        }
    }

This does what we intended to do, but… we can do better than that!
Maybe we don’t want to just find the (Content)Page, how about finding the root grid, or a parent of a specific type. We just need to take that function and convert it to a generic function, that’s all, and we are way more flexible.

public static class VisualTreeHelper
    {
        public static T FindParentPage<T>(Element view)
            where T:Page
        {
            return FindParentElement<T>(view);
        }

        public static T FindParentElement<T>(Element view)
            where T:Element
        {
           
            if (view is T page)
            {
                return page;
            }
            if (view.Parent == null)
            {
                return null;
            }
            return FindParentElement<T>(view.Parent);
        }
    }