diff --git a/Xamarin.Forms.Build b/Xamarin.Forms.Build index 47582c46134..280bb316e76 160000 --- a/Xamarin.Forms.Build +++ b/Xamarin.Forms.Build @@ -1 +1 @@ -Subproject commit 47582c4613425d20ca454ae7b5256b8a27176fe5 +Subproject commit 280bb316e76e11783f678373a0c8fb4d125abc3f diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue3408.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue3408.cs new file mode 100644 index 00000000000..11e108db74f --- /dev/null +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue3408.cs @@ -0,0 +1,221 @@ +using Xamarin.Forms.CustomAttributes; +using Xamarin.Forms.Internals; +using System; +using System.Collections.Generic; +using System.Linq; +using static Xamarin.Forms.Controls.Issues.Issue3408; + +#if UITEST +using Xamarin.UITest; +using NUnit.Framework; +#endif + +namespace Xamarin.Forms.Controls.Issues +{ + // This may crash for you on Android if you click too many buttons + // https://github.com/xamarin/Xamarin.Forms/issues/3603 + [Preserve(AllMembers = true)] + [Issue(IssueTracker.Github, 3408, "System.ObjectDisposedException: from SwitchCellRenderer when changing ItemSource", PlatformAffected.iOS)] + public class Issue3408 : TestContentPage + { + public static List GetRecommendations(object e) + { + switch (e) + { + case List pc: return pc.First().Recommendations; + case List pc: return pc.First().Recommendations; + default: return null; + } + } + protected override void Init() + { + + var grd = new Grid(); + + var aacountListView = new ListView(); + aacountListView.HasUnevenRows = true; + aacountListView.ItemTemplate = new AccountDetailsDataTemplateSelector(); + aacountListView.BindingContext = new List { new RecommendationsViewModel() }; + + aacountListView.SetBinding(ListView.ItemsSourceProperty, "."); + var btn = new Button + { + Text = "Change Source", + AutomationId = "btn1", + Command = new Command(() => + { + aacountListView.BindingContext = new List { new RecommendationsViewModel2() }; + }) + }; + var btn2 = new Button + { + Text = "Change Property", + AutomationId = "btn2", + Command = new Command(() => + { + + foreach (var item in GetRecommendations(aacountListView.BindingContext)) + { + item.Name = "New Item Name"; + item.IsBusy = !item.IsBusy; + } + + }) + }; + grd.Children.Add(aacountListView); + Grid.SetRow(aacountListView, 0); + grd.Children.Add(btn); + Grid.SetRow(btn, 1); + grd.Children.Add(btn2); + Grid.SetRow(btn2, 2); + Content = grd; + } + +#if UITEST + [Test] + public void Issue3408Test () + { + RunningApp.WaitForElement (q => q.Marked ("btn1")); + RunningApp.WaitForElement (q => q.Marked ("Click to Change")); + RunningApp.Tap(q => q.Marked("btn1")); + RunningApp.WaitForElement(q => q.Marked("This should have changed")); + RunningApp.Tap(q => q.Marked("btn2")); + RunningApp.WaitForElement(q => q.Marked("New Item Name")); + } +#endif + + [Preserve(AllMembers = true)] + public class RecommendationsBaseViewModel : ViewModelBase + { + public string AccountName => $""; + public List Recommendations { get; set; } + } + + [Preserve(AllMembers = true)] + public class RecommendationsViewModel : RecommendationsBaseViewModel + { + public string AccountName => $"Recommendations"; + + public RecommendationsViewModel() + { + Recommendations = new List() + { + new Recommendation(){ Name = "Click to Change"} , + new Recommendation(){ Name = "Recommendations"} , + new Recommendation(){ Name = "Recommendations"} , + }; + } + } + + [Preserve(AllMembers = true)] + public class RecommendationsViewModel2 : RecommendationsBaseViewModel + { + public string AccountName => $"Recommendations 2"; + public RecommendationsViewModel2() + { + Recommendations = new List() + { + new Recommendation(){ Name = "This should have changed"} , + new Recommendation(){ Name = "Recommendations 2"} , + new Recommendation(){ Name = "Recommendations 2", IsBusy = true } , + }; + } + } + + [Preserve(AllMembers = true)] + public class Recommendation : ViewModelBase + { + string _name; + public string Name + { + get { return _name; } + set + { + if (_name == value) + return; + _name = value; + OnPropertyChanged(); + } + } + } + + } + + [Preserve(AllMembers = true)] + public class RecommendationsView : ContentView + { + public RecommendationsView() + { + Grid grd = new Grid(); + var lst = new ListView + { + ItemTemplate = new DataTemplate(() => + { + var swittch = new SwitchCell(); + swittch.SetBinding(SwitchCell.TextProperty, new Binding("Name")); + swittch.SetBinding(SwitchCell.OnProperty, new Binding("IsBusy")); + return swittch; + }) + + }; + + lst.SetBinding(ListView.ItemsSourceProperty, new Binding("Recommendations")); + grd.Children.Add(lst); + Content = grd; + } + + // This work around exists because of this issue + // https://github.com/xamarin/Xamarin.Forms/issues/3602 + object context = null; + protected override void OnBindingContextChanged() + { + base.OnBindingContextChanged(); + if (BindingContext == null) + Device.BeginInvokeOnMainThread(() => BindingContext = context); + else + context = BindingContext; + } + } + + [Preserve(AllMembers = true)] + public class AccountDetailsDataTemplateSelector : DataTemplateSelector + { + public Lazy RecommendationsViewDataTemplate { get; } + public Lazy RecommendationsView { get; } + + public Lazy RecommendationsViewDataTemplate2 { get; } + public Lazy RecommendationsView2 { get; } + + public AccountDetailsDataTemplateSelector() + { + RecommendationsView = new Lazy(() => new ViewCell() { View = new RecommendationsView() }); + RecommendationsViewDataTemplate = new Lazy(() => new DataTemplate(() => RecommendationsView.Value)); + + + RecommendationsView2 = new Lazy(() => new ViewCell() { View = new RecommendationsView() }); + RecommendationsViewDataTemplate2 = new Lazy(() => new DataTemplate(() => RecommendationsView2.Value)); + } + + protected override DataTemplate OnSelectTemplate(object item, BindableObject container) + { + if (item == null) + { + return null; + } + + if (item is RecommendationsViewModel) + { + RecommendationsView.Value.BindingContext = item; + return RecommendationsViewDataTemplate.Value; + } + + if (item is RecommendationsViewModel2) + { + RecommendationsView2.Value.BindingContext = item; + return RecommendationsViewDataTemplate2.Value; + } + + throw new ArgumentException("Invalid ViewModel Type"); + } + } +} diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems index de45b3add29..17a6f5caa2d 100644 --- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems @@ -773,6 +773,7 @@ + diff --git a/Xamarin.Forms.Platform.iOS/Cells/CellTableViewCell.cs b/Xamarin.Forms.Platform.iOS/Cells/CellTableViewCell.cs index ed3a8a0d248..c52606f3e37 100644 --- a/Xamarin.Forms.Platform.iOS/Cells/CellTableViewCell.cs +++ b/Xamarin.Forms.Platform.iOS/Cells/CellTableViewCell.cs @@ -7,7 +7,9 @@ namespace Xamarin.Forms.Platform.iOS public class CellTableViewCell : UITableViewCell, INativeElementView { Cell _cell; + public Action PropertyChanged; + bool _disposed; public CellTableViewCell(UITableViewCellStyle style, string key) : base(style, key) @@ -19,27 +21,27 @@ public Cell Cell get { return _cell; } set { - if (this._cell == value) + if (_cell == value) return; if (_cell != null) + { + _cell.PropertyChanged -= HandlePropertyChanged; Device.BeginInvokeOnMainThread(_cell.SendDisappearing); - - this._cell = value; + } _cell = value; if (_cell != null) + { + _cell.PropertyChanged += HandlePropertyChanged; Device.BeginInvokeOnMainThread(_cell.SendAppearing); + } } } public Element Element => Cell; - public void HandlePropertyChanged(object sender, PropertyChangedEventArgs e) - { - if (PropertyChanged != null) - PropertyChanged(this, e); - } + public void HandlePropertyChanged(object sender, PropertyChangedEventArgs e) => PropertyChanged?.Invoke(sender, e); internal static UITableViewCell GetNativeCell(UITableView tableView, Cell cell, bool recycleCells = false, string templateId = "") { @@ -102,6 +104,11 @@ protected override void Dispose(bool disposing) if (disposing) { PropertyChanged = null; + + if (_cell != null) + { + _cell.PropertyChanged -= HandlePropertyChanged; + } _cell = null; } diff --git a/Xamarin.Forms.Platform.iOS/Cells/EntryCellRenderer.cs b/Xamarin.Forms.Platform.iOS/Cells/EntryCellRenderer.cs index 72e95244494..6db5ed8e154 100644 --- a/Xamarin.Forms.Platform.iOS/Cells/EntryCellRenderer.cs +++ b/Xamarin.Forms.Platform.iOS/Cells/EntryCellRenderer.cs @@ -18,7 +18,7 @@ public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, tvc = new EntryCellTableViewCell(item.GetType().FullName); else { - tvc.Cell.PropertyChanged -= OnCellPropertyChanged; + tvc.PropertyChanged -= HandlePropertyChanged; tvc.TextFieldTextChanged -= OnTextFieldTextChanged; tvc.KeyboardDoneButtonPressed -= OnKeyBoardDoneButtonPressed; } @@ -26,7 +26,7 @@ public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, SetRealCell(item, tvc); tvc.Cell = item; - tvc.Cell.PropertyChanged += OnCellPropertyChanged; + tvc.PropertyChanged += HandlePropertyChanged; tvc.TextFieldTextChanged += OnTextFieldTextChanged; tvc.KeyboardDoneButtonPressed += OnKeyBoardDoneButtonPressed; @@ -44,7 +44,7 @@ public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, return tvc; } - static void OnCellPropertyChanged(object sender, PropertyChangedEventArgs e) + static void HandlePropertyChanged(object sender, PropertyChangedEventArgs e) { var entryCell = (EntryCell)sender; var realCell = (EntryCellTableViewCell)GetRealCell(entryCell); diff --git a/Xamarin.Forms.Platform.iOS/Cells/ImageCellRenderer.cs b/Xamarin.Forms.Platform.iOS/Cells/ImageCellRenderer.cs index 07275030287..171708a62cb 100644 --- a/Xamarin.Forms.Platform.iOS/Cells/ImageCellRenderer.cs +++ b/Xamarin.Forms.Platform.iOS/Cells/ImageCellRenderer.cs @@ -20,12 +20,12 @@ public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, return result; } - protected override void HandlePropertyChanged(object sender, PropertyChangedEventArgs args) + protected override void HandleCellPropertyChanged(object sender, PropertyChangedEventArgs args) { - var tvc = (CellTableViewCell)sender; - var imageCell = (ImageCell)tvc.Cell; + var imageCell = (ImageCell)sender; + var tvc = (CellTableViewCell)GetRealCell(imageCell); - base.HandlePropertyChanged(sender, args); + base.HandleCellPropertyChanged(sender, args); if (args.PropertyName == ImageCell.ImageSourceProperty.PropertyName) SetImage(imageCell, tvc); diff --git a/Xamarin.Forms.Platform.iOS/Cells/SwitchCellRenderer.cs b/Xamarin.Forms.Platform.iOS/Cells/SwitchCellRenderer.cs index c66108f6203..4cf75812212 100644 --- a/Xamarin.Forms.Platform.iOS/Cells/SwitchCellRenderer.cs +++ b/Xamarin.Forms.Platform.iOS/Cells/SwitchCellRenderer.cs @@ -18,7 +18,7 @@ public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, else { uiSwitch = tvc.AccessoryView as UISwitch; - tvc.Cell.PropertyChanged -= OnCellPropertyChanged; + tvc.PropertyChanged -= HandlePropertyChanged; } SetRealCell(item, tvc); @@ -33,7 +33,7 @@ public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, var boolCell = (SwitchCell)item; tvc.Cell = item; - tvc.Cell.PropertyChanged += OnCellPropertyChanged; + tvc.PropertyChanged += HandlePropertyChanged; tvc.AccessoryView = uiSwitch; tvc.TextLabel.Text = boolCell.Text; @@ -48,7 +48,7 @@ public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, return tvc; } - void OnCellPropertyChanged(object sender, PropertyChangedEventArgs e) + void HandlePropertyChanged(object sender, PropertyChangedEventArgs e) { var boolCell = (SwitchCell)sender; var realCell = (CellTableViewCell)GetRealCell(boolCell); diff --git a/Xamarin.Forms.Platform.iOS/Cells/TextCellRenderer.cs b/Xamarin.Forms.Platform.iOS/Cells/TextCellRenderer.cs index 2f5cfdd1c17..b659a737314 100644 --- a/Xamarin.Forms.Platform.iOS/Cells/TextCellRenderer.cs +++ b/Xamarin.Forms.Platform.iOS/Cells/TextCellRenderer.cs @@ -12,15 +12,15 @@ public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, { var textCell = (TextCell)item; - var tvc = reusableCell as CellTableViewCell; - if (tvc == null) + if (!(reusableCell is CellTableViewCell tvc)) tvc = new CellTableViewCell(UITableViewCellStyle.Subtitle, item.GetType().FullName); else - tvc.Cell.PropertyChanged -= tvc.HandlePropertyChanged; + tvc.PropertyChanged -= HandleCellPropertyChanged; + + SetRealCell(item, tvc); tvc.Cell = textCell; - textCell.PropertyChanged += tvc.HandlePropertyChanged; - tvc.PropertyChanged = HandlePropertyChanged; + tvc.PropertyChanged = HandleCellPropertyChanged; tvc.TextLabel.Text = textCell.Text; tvc.DetailTextLabel.Text = textCell.Detail; @@ -36,10 +36,11 @@ public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, return tvc; } - protected virtual void HandlePropertyChanged(object sender, PropertyChangedEventArgs args) + protected virtual void HandleCellPropertyChanged(object sender, PropertyChangedEventArgs args) { - var tvc = (CellTableViewCell)sender; - var textCell = (TextCell)tvc.Cell; + var textCell = (TextCell)sender; + var tvc = (CellTableViewCell)GetRealCell(textCell); + if (args.PropertyName == TextCell.TextProperty.PropertyName) { tvc.TextLabel.Text = ((TextCell)tvc.Cell).Text; @@ -56,6 +57,14 @@ protected virtual void HandlePropertyChanged(object sender, PropertyChangedEvent tvc.DetailTextLabel.TextColor = textCell.DetailColor.ToUIColor(DefaultTextColor); else if (args.PropertyName == Cell.IsEnabledProperty.PropertyName) UpdateIsEnabled(tvc, textCell); + + HandlePropertyChanged(tvc, args); + } + + protected virtual void HandlePropertyChanged(object sender, PropertyChangedEventArgs args) + { + //keeping this method for backwards compatibility + //as the the sender for this method is a CellTableViewCell } static void UpdateIsEnabled(CellTableViewCell cell, TextCell entryCell) @@ -65,4 +74,4 @@ static void UpdateIsEnabled(CellTableViewCell cell, TextCell entryCell) cell.DetailTextLabel.Enabled = entryCell.IsEnabled; } } -} \ No newline at end of file +}