KentBoogaart

Badges

Third Anniversary5 LikesSecond AnniversaryFirst AnniversaryName Dropper10 CommentsFirst Comment

About

Username
KentBoogaart
Location
AU
Joined
Visits
51
Last Active
Roles
Member
Points
50
Badges
7

KentBoogaart · Kent Boogaart · ✭✭

About

Username
KentBoogaart
Location
AU
Joined
Visits
51
Last Active
Roles
Member
Points
50
Badges
7
  • Improved life cycle support

    Summary

    As one of the authors of ReactiveUI, I have found that Xamarin.Forms does not provide the requisite view life cycle hooks to enable the framework to manage the activation and deactivation of resources in response to the comings and goings of views.

    Specifically, I have found these problems:

    • Page.Appearing actually fires in response to ViewDidAppear on iOS. One of ReactiveUI's benefits is its code-based, type-safe binding infrastructure. Appearing is the only hook we can use to get stuff on the screen, but it fires too late on iOS, so there is a brief period where the screen is not populated with data. I have already attempted to start a discussion here, but it seemed to stagnate.
    • Given a View, there is no way to tell when that View appears and disappears from the screen. There is no View.Appearing or View.Disappearing events as there is for Page. It might seem appropriate to therefore search up the visual tree for the Page and hook into that. However, this is inefficient (but perhaps that's inevitable) and there is no means of knowing when the Page itself changes. There is no PageChanged event, and PropertyChanged does not fire in response to the Page changing.

    API Changes

    • Page.Appearing should fire in response to ViewWillAppear on iOS. Moreover, there should be Page.Appeared and Page.Disappeared counterparts. Yes, this is potentially breaking to people who are hooked into Appearing and rely on it firing in response to ViewDidAppear on iOS. However, that behavior has always been incorrect and the fix for these people is literally to change Appearing to Appeared (or hold off on the XF upgrade).
    • Ideally, add Appearing, Appeared, Disappearing, Disappeared to View and have them Just Work. Failing that, ensure there is a hook by which framework authors can know the hosting Page for a View has changed. Perhaps a specific PageChanged event, or just ensuring PropertyChanged works as expected in this scenario.

    Intended Use Case

    The use case here is to give framework authors what they need to facilitate the creation of self-sufficient, standalone, encapsulated components by framework consumers. I specifically work on ReactiveUI, but other framework authors will/have run into the same issues.

    ModEdit - Spec incoming ASAP

  • Re: XForms needs an ItemsControl

    @EricGrover I hope you don't mind, but I took your code and improved it. Changes include:

    • removed the generic parameter. It doesn't add any value and makes the control harder to consume in XAML
    • made the ItemsSource property of type IEnumerable instead of restricting to ObservableCollection<T>
    • assumed created views are of type View, never ViewCell. The latter doesn't really make sense because no events like Appearing and Disappearing will be raised, so "supporting" ViewCell will break such expectations
    • bug fixes, including:
      • detaching from old collections where relevant
      • raises ItemCreated for items already in the new collection

    Here's the code:

        using System;
        using System.Collections;
        using System.Collections.Specialized;
        using Xamarin.Forms;
    
        public delegate void RepeaterViewItemAddedEventHandler(object sender, RepeaterViewItemAddedEventArgs args);
    
        // in lieu of an actual Xamarin Forms ItemsControl, this is a heavily modified version of code from https://forums.xamarin.com/discussion/21635/xforms-needs-an-itemscontrol
        public class RepeaterView : StackLayout
        {
            public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create<RepeaterView, IEnumerable>(
                p => p.ItemsSource,
                null,
                BindingMode.OneWay,
                propertyChanged: ItemsChanged);
    
            public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create<RepeaterView, DataTemplate>(
                p => p.ItemTemplate,
                default(DataTemplate));
    
            public event RepeaterViewItemAddedEventHandler ItemCreated;
    
            public IEnumerable ItemsSource
            {
                get { return (IEnumerable)GetValue(ItemsSourceProperty); }
                set { SetValue(ItemsSourceProperty, value); }
            }
    
            public DataTemplate ItemTemplate
            {
                get { return (DataTemplate)GetValue(ItemTemplateProperty); }
                set { SetValue(ItemTemplateProperty, value); }
            }
    
            private static void ItemsChanged(BindableObject bindable, IEnumerable oldValue, IEnumerable newValue)
            {
                var control = (RepeaterView)bindable;
                var oldObservableCollection = oldValue as INotifyCollectionChanged;
    
                if (oldObservableCollection != null)
                {
                    oldObservableCollection.CollectionChanged -= control.OnItemsSourceCollectionChanged;
                }
    
                var newObservableCollection = newValue as INotifyCollectionChanged;
    
                if (newObservableCollection != null)
                {
                    newObservableCollection.CollectionChanged += control.OnItemsSourceCollectionChanged;
                }
    
                control.Children.Clear();
    
                if (newValue != null)
                {
                    foreach (var item in newValue)
                    {
                        var view = control.CreateChildViewFor(item);
                        control.Children.Add(view);
                        control.OnItemCreated(view);
                    }
                }
    
                control.UpdateChildrenLayout();
                control.InvalidateLayout();
            }
    
            protected virtual void OnItemCreated(View view) =>
                this.ItemCreated?.Invoke(this, new RepeaterViewItemAddedEventArgs(view, view.BindingContext));
    
            private void OnItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
            {
                var invalidate = false;
    
                if (e.OldItems != null)
                {
                    this.Children.RemoveAt(e.OldStartingIndex);
                    invalidate = true;
                }
    
                if (e.NewItems != null)
                {
                    for (var i = 0; i < e.NewItems.Count; ++i)
                    {
                        var item = e.NewItems[i];
                        var view = this.CreateChildViewFor(item);
    
                        this.Children.Insert(i + e.NewStartingIndex, view);
                        OnItemCreated(view);
                    }
    
                    invalidate = true;
                }
    
                if (invalidate)
                {
                    this.UpdateChildrenLayout();
                    this.InvalidateLayout();
                }
            }
    
            private View CreateChildViewFor(object item)
            {
                this.ItemTemplate.SetValue(BindableObject.BindingContextProperty, item);
                return (View)this.ItemTemplate.CreateContent();
            }
        }
    
        public class RepeaterViewItemAddedEventArgs : EventArgs
        {
            private readonly View view;
            private readonly object model;
    
            public RepeaterViewItemAddedEventArgs(View view, object model)
            {
                this.view = view;
                this.model = model;
            }
    
            public View View => this.view;
    
            public object Model => this.model;
        }