twintechs / TwinTechsFormsLib

Apache License 2.0
193 stars 73 forks source link

implement IBoxedCell pt2 #32

Open pfbwitz opened 8 years ago

pfbwitz commented 8 years ago

Accidentally closed issue: https://github.com/twintechs/TwinTechsFormsLib/issues/31

Sorry!

Original issue:

"Hello,

I'm implementing this library on both Android and iOS (same solution/project). It works fine (super smooth!) in my Android project, but on iOS, my list doesn't load (fast)images and whenever I scroll, the app will crash with this message:

System.InvalidOperationException: Implement IBoxedCell on cell renderer: TwinTechs.Controls.NativeCell, TwinTechsLib.iOS, Version=1.0.5858.22559, Culture=neutral, PublicKeyToken=null

Google isn't helping me at all (it's as if IBoxedCell doesn't exist at all). It's working in the sample project, but it will crash everytime I try to run it on iOS."

Your reply:

"Can you give me the code for your cell and list please?"

My reply:

"Hello,

Thank you for your reply. I have two implementations of your fastcell. I'm using it for the complex fastcell I built, however it's easier to explain with the simplecell, since it's the same problem. Here's the xaml

<?xml version="1.0" encoding="utf-8" ?>
<controls1:FastCell xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:cells="clr-namespace:floricodeapp.App.Views.Cells;assembly=floricodeapp.App"
             xmlns:controls1="clr-namespace:TwinTechs.Controls;assembly=TwinTechsForms"
             x:Class="floricodeapp.App.Views.Cells.MyFastCell">
    <cells:IndexedStackLayout x:Name="Container" VerticalOptions="FillAndExpand" Orientation="Horizontal" Spacing="5" Padding="0" HorizontalOptions="FillAndExpand">
    <controls1:FastImage Aspect="AspectFill" x:Name="Image"/>
    <StackLayout Padding="2" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
      <Label x:Name ="Omschrijving" VerticalOptions="Start" HorizontalOptions="StartAndExpand" FontAttributes="Bold" LineBreakMode="TailTruncation" />
      <StackLayout Orientation="Horizontal" VerticalOptions="EndAndExpand" Spacing="0">
        <Label x:Name="VbnCode" />
        <BoxView WidthRequest="8" HeightRequest="1" />
        <Image Source="rhs_kleur" HorizontalOptions="Start" VerticalOptions="Center" x:Name="Rhs1" />
        <Label x:Name="RhsKleur1" />
        <BoxView WidthRequest="8" HeightRequest="1" />
        <Image Source="rhs_kleur" HorizontalOptions="Start" VerticalOptions="Center" x:Name="Rhs2" />
        <Label x:Name="RhsKleur2" />
      </StackLayout>
    </StackLayout>
        <Image x:Name="FavImage" Source="fave.png" HorizontalOptions="EndAndExpand" VerticalOptions="Center" Aspect="AspectFit" />
        <Image x:Name="IsNew" HorizontalOptions="End" VerticalOptions="Start" Source="nieuw.png"/>
        </cells:IndexedStackLayout>
</controls1:FastCell>

The accompanying C#:

class IndexedStackLayout : StackLayout
    {
        public int Index { get; set; }
    }

    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class MyFastCell : FastCell
    {
        protected override void InitializeCell()
        {
            InitializeComponent();

            var item = BindingContext as SearchProductModel;
            var gesture = new TapGestureRecognizer { CommandParameter = item.ParentPage };
            gesture.Tapped += GestureOnTapped;
            Container.GestureRecognizers.Add(gesture);
        }

        protected override void SetupCell(bool isRecycled)
        {
            var item = BindingContext as SearchProductModel;

            if (item != null)
            {
                Container.Index = item.Index;
                Height = Constants.ThumbRenderSize;
                Image.ImageUrl = string.Format(Constants.ThumbnailUrl, item.VbnCode) ?? "";

                if (Device.OS == TargetPlatform.Android)
                {
                    Image.VerticalOptions = LayoutOptions.Center;
                    Image.HorizontalOptions = LayoutOptions.Start;
                    Image.HeightRequest = Constants.ThumbRenderSize;
                    Image.WidthRequest = Constants.ThumbRenderSize;
                }

                Omschrijving.Text= item.Omschrijving;
                Omschrijving.FontSize = Device.GetNamedSize(NamedSize.Small, typeof (Label));

                IsNew.Opacity = item.Opacity;

                Rhs1.BackgroundColor = item.RhsKleur1Color;
                Rhs1.IsVisible = item.Rhs1Visible;

                Rhs2.BackgroundColor = item.RhsKleur2Color;
                Rhs2.IsVisible = item.Rhs2Visible;

                RhsKleur1.TextColor = Constants.DetailTextColor;
                RhsKleur1.Text = item.RhsKleur1;
                RhsKleur2.TextColor = Constants.DetailTextColor;
                RhsKleur2.Text = item.RhsKleur2;

                FavImage.IsVisible = item.IsFavorite;

                VbnCode.Text = item.VbnCodeWithPoundSign;
                VbnCode.TextColor = Constants.DetailTextColor;
            }
        }

        private void GestureOnTapped(object sender, EventArgs eventArgs)
        {
            var s = (IndexedStackLayout)sender;
            var parent = (PeterListView)((TappedEventArgs)eventArgs).Parameter;

            parent.Navigation.PushAsync(new ProductPage(parent.Producten.Select(p => p.VbnCode).ToList(), s.Index, parent.TotalCount, parent.ParentPage, this));
        }
    }

The Viewmodel:

    public class SearchProductKleurModel : BaseModel
    {
        public int VbnCode { get; set; }
        public string RhsCode { get; set; }
        public int R { get; set; }
        public int G { get; set; }
        public int B { get; set; }
    }

    public class SearchProductModel : BaseModel
    {
        public byte[] Icon;

        public PeterListView ParentPage { get; set; }
        public int Index { get; set; }

        private List<SearchProductKleurModel> _kleuren;
        public List<SearchProductKleurModel> Kleuren
        {
            get
            {
                if (_kleuren == null)
                {
                    _kleuren = new List<SearchProductKleurModel>();
                    if (!string.IsNullOrEmpty(RhsKleur))
                    {
                        var arr = RhsKleur.Split(new []{'~'},StringSplitOptions.RemoveEmptyEntries);

                        foreach (var details in arr)
                        {
                            var detail = details.Split('_');
                            int r, g, b;
                            r = g = b = 255;
                            try
                            {
                                if (detail.Count() > 1)
                                {
                                    b = Convert.ToInt32(detail[3]);
                                    g = Convert.ToInt32(detail[2]);
                                    r = Convert.ToInt32(detail[1]);
                                }
                                _kleuren.Add(new SearchProductKleurModel
                                {
                                    VbnCode = VbnCode,
                                    B = b,
                                    G = g,
                                    R = r,
                                    RhsCode = detail[0]
                                });
                            }
                            catch (Exception ex)
                            {
                                //throw ex;
                            }
                        }
                    }

                }
                return _kleuren;
            }
        }

        public Color RhsKleur1Color
        {
            get
            {
                var s = Color.White;
                if (Kleuren.Any())
                {
                    var k = Kleuren.First();
                    s = Color.FromRgb(k.R, k.G, k.B);
                }
                return s;
            }
        }

        public Color RhsKleur2Color
        {
            get
            {
                var s = Color.White;
                if (Kleuren.Count > 1)
                {
                    var k = Kleuren.ElementAt(1);
                    s = Color.FromRgb(k.R, k.G, k.B);
                }
                return s;
            }
        }

        public string RhsKleur1
        {
            get
            {
                var s = string.Empty;
                if (Kleuren.Any())
                {
                    s = Kleuren.First().RhsCode;
                }
                return s;
            }
        }

        public string RhsKleur2
        {
            get
            {
                var s = string.Empty;
                if (Kleuren.Count > 1)
                {
                    s = Kleuren.ElementAt(1).RhsCode;
                }
                return s;
            }
        }

        private string _omschrijving;
        public string Omschrijving
        {
            get { return _omschrijving; }
            set
            {
                _omschrijving = value;
                OnPropertyChanged();
            }
        }

        private int _vbnCode;
        public int VbnCode
        {
            get { return _vbnCode; }
            set
            {
                _vbnCode = value;
                OnPropertyChanged();
            }
        }

        public string VbnCodeWithPoundSign
        {
            get { return "#" + _vbnCode; }
        }

        private string _rhskleur;
        public string RhsKleur
        {
            get { return _rhskleur; }
            set
            {
                _rhskleur = value;
                OnPropertyChanged();
            }
        }

        private bool _isnieuw;
        public bool IsNieuw
        {
            get { return _isnieuw; }
            set
            {
                _isnieuw = value;
                OnPropertyChanged();
            }
        }

        public string Thumbnail { get; set; }
        private IWebServiceHelper _wsHelper;
        public ImageSource ImageSource
        {
            get
            {
                return new UriImageSource{ CachingEnabled = false, Uri=new Uri(Thumbnail)};
//                return ImageSource.FromUri(new Uri(Thumbnail));
            }
        }

        public int Opacity
        {
            get { return IsNieuw ? 1 : 0; }
        }

        public bool Rhs1Visible
        {
            get { return Kleuren.Any(); }
        }

        public bool Rhs2Visible
        {
            get { return Kleuren.Any() && Kleuren.Count > 1; }
        }

        private bool _isFavorite;
        public bool IsFavorite
        {
            get { return _isFavorite; }
            set
            {
                _isFavorite = value;
                OnPropertyChanged();
            }
        }

        public override string ToString()
        {
            return Omschrijving;
        }
    }

The listview is simple custom control I made to enable infinite scrolling. It's simple and tiny, but does the job as well as examples I found elsewhere with a lot more code and complexity:

 public class InfiniteListView<T> : ListView
    {
        public bool CurrentlyAdding;

        public InfiniteListView()
            : base(ListViewCachingStrategy.RecycleElement)
        {
            ItemAppearing += (sender, args) =>
            {
                var s = ItemsSource.Cast<T>().ToList();
                if (CurrentlyAdding || !s.Any()) 
                    return;

                if (s.IndexOf((T)args.Item) == s.Count - 1)
                    OnEndReached(null);
            };
        }

        public event EventHandler EndReached;
        private void OnEndReached(EventArgs e)
        {
            var handler = EndReached;
            if (handler != null)
                handler(this, e);
        }

        public void SelectItem(int index)
        {
            try
            {
                ScrollTo(ItemsSource.Cast<T>().ElementAt(index), ScrollToPosition.Center, false);
            }
            catch
            {
            }
        }
    }

The listview isn't directly place on a page, but rather in a contentview, embedded on a page:

  public void LoadList()
        {
            InfiniteList.Clear();
            InfiniteList = new PeterListView(this, this is FavorietenPage);
            _listHolder.Content = InfiniteList;
            InfiniteList.Reset();
        }

The contentview which holds the list is a xaml-file. The list is embedded like this:

<controls:InfiniteListView x:Name="listView" x:TypeArguments="model:SearchProductModel" />

The list is bound to data with an Init()-method:

private void Init(HomePage parentPage)
        {
            ParentPage = parentPage;
            Producten = new ObservableCollection<SearchProductModel>();
            LoadData();
            if(Device.OS == TargetPlatform.Android)
                listView.ItemTemplate = new DataTemplate(typeof(MyFastCell));
            else
                listView.ItemTemplate = new DataTemplate(typeof(MyCell));

            listView.ItemsSource = Producten;
            listView.EndReached += (sender, args) =>
            {
                if (Producten.Count < TotalCount)
                {
                    listView.CurrentlyAdding = true;
                    AddProducts(Producten.Count);
                }
            };
            listView.ItemTapped  += ListViewOnItemTapped;
            listView.ItemSelected += (sender, args) => listView.SelectedItem = null; 

            BindingContext = this;
        }

The LoadData()- and AddProducts()-methods simply add data to the ObservableCollection.

public ObservableCollection<SearchProductModel> Producten { get; set; }

I hope this is enough information. Now I'm forced to utilize the regular Viewcells on iOS, making the Android-version of the app perform much better. (iOS takes 3-5 seconds to load a new dataset from the observablecollection)"

pfbwitz commented 8 years ago

iboxedcell

arielbh commented 8 years ago

Having the same issue.

pfbwitz commented 8 years ago

Fixed it for me!

I changed the constructor of my listview as such:

  public InfiniteListView()
            //: base(ListViewCachingStrategy.RecycleElement)
        {

Inheriting the default constructor of the ListView fixed the issue for me :)

arielbh commented 8 years ago

@pfbwitz wait, you removed the RecycleElement strategy from your ListView?

pfbwitz commented 8 years ago

Yep. I did exactly that. I reckon the fast cell renderer manages it's own recycling and perhaps in conflicted with the Xamarin Forms cachingstrategy.

arielbh commented 8 years ago

I've started reviewing @georgejecook's code. I'm still unconvinced this is the way to go. I hope @georgejecook will drop by and explain it for us as he is done on many occasions via forum and posts :)