Xamarin.Forms CollectionView View Recycling Issues - c#

So I am using a CollectionView to display a list of entities that, when pressed, toggle between a opened and closed state. I did this by animating the HeightRequest parameter of the parent container of the view that was pressed, then adding whatever views I wanted to show in the view's expanded state. Here is the code snippet for that:
var animate = new Animation(d => this.HeightRequest = d,
this.Bounds.Height, this.Bounds.Height + 300, Easing.CubicOut);
animate.Commit(this, "a", length: 500);
this.layout.Children.Add(this.candidateList);
this.layout.Children.Add(this.openButton);
This works fine, however, if I scroll down the list, I see that there are views that are also expanded even though I did not touch them previously, more so every full page of scrolling later. Some even include the views that I've added to the expanded state, showing incorrect data. I have assumed that this is due to the recycling mechanics of the CollectionView work in order to save on rendering costs, but there must be some way to fix this. Here is the code for relevant views:
var officesList = new CollectionView
{
ItemTemplate = new DataTemplate(typeof(OfficeListView)),
HorizontalOptions = LayoutOptions.FillAndExpand,
ItemsLayout = new LinearItemsLayout(ItemsLayoutOrientation.Vertical)
{
SnapPointsType = SnapPointsType.None,
ItemSpacing = 10,
},
Margin = new Thickness(20, 5),
ItemSizingStrategy = ItemSizingStrategy.MeasureAllItems,
Footer = " ",
FooterTemplate = new DataTemplate(() =>
{
return new StackLayout
{
Margin = new Thickness(20, 10),
Children = {
new LocorumLabels.MediumLabel
{
Text = "x Results | No filters applied" //TODO: Bind these to footer
}
}
};
})
};
The next snippet is the CollectionsView within an expanded "Office":
this.candidateList = new CollectionView
{
ItemTemplate = new DataTemplate(typeof(CandidateDetailView)),
HorizontalOptions = LayoutOptions.FillAndExpand,
ItemsLayout = new LinearItemsLayout(ItemsLayoutOrientation.Vertical)
{
SnapPointsType = SnapPointsType.None,
//ItemSpacing = 10,
},
Margin = new Thickness(10, 5),
ItemSizingStrategy = ItemSizingStrategy.MeasureAllItems,
Footer = " ",
HeightRequest = 300
};
And here is a video showing what is going on:
https://youtu.be/Ltg2o8BwfwY
Hopefully someone can let me know of a solution. Many thanks.

You are right on the cause.
As CollectionView uses DataTemplate you need to set your views and your data in such a way that when recycled the view appears as it should.
The non-obvious part to keep the animation working is to call the animation as of now but when it completes to set the value in data that would alter the view from the state before the animation to the state after the animation.

Related

Xamarin / C# : How to iterate through Array/List DataMember passed to the viewCell without using inner ListView

I've been advised that Xamarin doesn't support nested ListViews.. I trying to build social app, in which the post might have several images and comments.
The feed page will be basically a ListView of the posts. While I cannot use a listView within the post (which is a viewCell), I tried to use Grid for the images instead, and another Grid for Comments. Both Images and Comments can be Lists, Arrays or ObservableCollections in the "post" Class.
Now I need to make foreach loop of this array/list, adding images with source bound to array item. But it seems to me, I couldn't use the data passed to the viewCell in C# (can only be used in Xaml layout).
Anybody has any ideas how to solve this.
[DataContract]
Public class post {
[DataMember]
List<comment> commentContainer {get; set;}
[DataMember]
List<String> imageContainer {get; set;}
}
in Xaml of cellView
<Grid x:Name="imagesGrid" IsVisible="{Binding isImage}" BindingContext="{Binding imageContainer}">
here I need to iterate the list of image in c# if possible - runtime- !
</Grid>
You are right to say that you shouldn't use nested ListViews in Xamarin.Forms, virtualization goes out the window and scrolling/gestures will be hard to handle.
However, i think this design in Xamarin.Forms will be fraught with problems. Xamarin.Forms ListView is a very specific beast and wont play well with very complicated layouts. My gut feeling is you want to do this in Native Views maybe or at the very least design this entirely in code. Anyway that is just my gut feel.
Have it be noted, i would shy away from this design and make it simple for your self by doing like WhatsApp does, and use a Template Selector and break apart multiple images and posts with a DataTemplateSelecto. Then you Don't have to worry about complicated layouts.
However, if you have your heart set on more complicated layouts inside ListView (be warned there be dragons) though you can take a look at this, Cell Appearance As noted i would be more inclined to this entirely in Code apposed to an ad-hock Xaml solution
public class CustomCell : ViewCell
{
public CustomCell()
{
//instantiate each of our views
var image = new Image ();
StackLayout cellWrapper = new StackLayout ();
StackLayout horizontalLayout = new StackLayout ();
Label left = new Label ();
Label right = new Label ();
...
//add views to the view hierarchy
horizontalLayout.Children.Add (image);
horizontalLayout.Children.Add (left);
horizontalLayout.Children.Add (right);
cellWrapper.Children.Add (horizontalLayout);
View = cellWrapper;
}
}
And set it like this
public partial class ImageCellPage : ContentPage
{
public ImageCellPage ()
{
InitializeComponent ();
listView.ItemTemplate = new DataTemplate (typeof(CustomCell));
}
}
Also checkout Grid
You can build one in code like this
var grid = new Grid();
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star)});
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star)});
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star)});
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star)});
var topLeft = new Label { Text = "Top Left" };
var topRight = new Label { Text = "Top Right" };
var bottomLeft = new Label { Text = "Bottom Left" };
var bottomRight = new Label { Text = "Bottom Right" };
grid.Children.Add(topLeft, 0, 0);
grid.Children.Add(topRight, 0, 1);
grid.Children.Add(bottomLeft, 1, 0);
grid.Children.Add(bottomRight, 1, 1);

Create Multiple Labels in Data Template

I have created a data template to use within a list view. This will later be expanded to add more content to each item in this list view. At the moment all the items that are bound to the observable collection are working as expected, except for one.
In each instance of the data template the bound properties are height, RouteName and routeStops. The height and RouteName are working fine but I'm not sure how to bind the routeStops correctly.
For each one of the RouteNames there are multiple stops, so for each data template use there must be one label that has the RouteName and multiple labels for each stop on the route (using routeStops).
I am not entirely sure how to achieve this, I can only seem to bind one stop to one label. I want to create them dynamically to allow for any amount of stops.
So the code behind that creates the data template (Just the constructor):
public MainRoutePageViewDetail(MessagDatabase database)
{
InitializeComponent();
BindingContext = mainroutepageviewmodel = new MainRoutePageViewModel(database,Navigation);
StackLayout mainstack = new StackLayout();
var routelisttemplate = new DataTemplate(() => {
ViewCell viewcell = new ViewCell();
stacklayout = new StackLayout();
stacklayout.SetBinding(StackLayout.HeightRequestProperty,"height");
viewcell.View = stacklayout;
// labels for template
var nameLabel = new Label { FontAttributes = FontAttributes.Bold, BackgroundColor = Color.LightGray };
nameLabel.HorizontalOptions = LayoutOptions.Center;
nameLabel.VerticalOptions = LayoutOptions.Center;
nameLabel.SetBinding(Label.TextProperty, "RouteName");
//inforLabel.SetBinding(Label.TextProperty, "Stops");
stacklayout.Children.Add(nameLabel);
StackLayout nextstack = new StackLayout();
var nameLabel2 = new Label { FontAttributes = FontAttributes.Bold, BackgroundColor = Color.Red };
nameLabel2.HorizontalOptions = LayoutOptions.Center;
nameLabel2.VerticalOptions = LayoutOptions.Center;
nameLabel2.SetBinding(Label.TextProperty, "routeStops");
nextstack.Children.Add(nameLabel2);
stacklayout.Children.Add(nextstack);
return viewcell;
});
ListView listviewofroutes = new ListView();
mainstack.Children.Add(listviewofroutes);
listviewofroutes.SetBinding(ListView.ItemsSourceProperty, "routeLabels");
listviewofroutes.ItemTemplate = routelisttemplate;
listviewofroutes.HasUnevenRows = true;
Content = mainstack;
}// end of constructor
This is bound to an ObservableCollection in the view model. Im going to leave this out as its irrelevant because the bindings work fine.
This calls down to functions in the model that collect data from SQL tables.
The function in the model that collects data:
public List<RouteInfo> getrouteInfo()
{
var DataBaseSelection = _connection.Query<RouteInfoTable>("Select * From [RouteInfoTable]");
List<RouteInfo> dataList = new List<RouteInfo>();
for (var i = 0; i < DataBaseSelection.Count; i++)
{
var DataBaseSelection2 = _connection.Query<RouteStopsTable>("Select StopOnRoute From [RouteStopsTable] WHERE RouteName = ? ",DataBaseSelection[i].RouteName);
dataList.Add(new RouteInfo
{
ID = DataBaseSelection[i].ID,
RouteName = DataBaseSelection[i].RouteName,
Stops = DataBaseSelection[i].Stops,
DayOf = DataBaseSelection[i].DayOf,
IsVisible = DataBaseSelection[i].IsVisible,
routeStops = DataBaseSelection2[i].StopOnRoute,
height = 200
});
}
return dataList;
}
The first table (RouteInfoTable) gets RouteName and some other information and the second table gets the stops on the route using the RouteName as a key. This is all added to a list of RouteInfo instances.
DataBaseSelection2 grabs all of the stops on the route but only one of them displays. I know why this is but I dont know how to display all three.
The Table definitions and class definitions as well as the selections from the tables are not an issue. I have debugged these and they are getting the correct information I just dont know how to display it on the front end in the way I want to. Here is a visual of what I mean if its getting complicated:
The best I can do is one route stop not all three.
An ideas how to achieve this?
Sorry if its complicated.
You can use Grouping in ListView to achieve this visual. Basically in this case you will be defining two DataTemplate(s) -
GroupHeaderTemplate for RouteName
ItemTemplate for StopsOnRoute.

How to Remove a Xamarin.Forms TableView and Re-Create It Without Application Crashing

I have a StackLayout in Xamarin.Forms 1.4.2, and from code-behind I am trying to create a table view, and then just for a test I have a button to re-create the TableView in the StackLayout.
If I use the following, that uses a standard TextCell, then I can re-create the TableView as many times as I wish without the application crashing:-
This is on Windows Phone btw:-
TableView tableView1 = new TableView()
{
Intent = TableIntent.Form,
VerticalOptions = LayoutOptions.FillAndExpand
};
myStackLayoutControl.Children.Add(tableView1);
tableView1 = new TableRoot()
{
new TableSection("Section 1")
{
new TextCell()
{
Text = "Text1",
Detail = "Text2"
}
}
};
However when I change the TextCell to a ViewCell such as:-
new ViewCell()
{
View = new Label()
{
Text="hello",
HorizontalOptions = LayoutOptions.FillAndExpand,
VerticalOptions = LayoutOptions.FillAndExpand
},
Height = 100
}
It will display the TableView correctly on the first execution.
However clicking a button that clears down the TableView and executes the same code to re-create it will run the same code, however straight after it has finished the function it will crash the entire application with an Application_UnhandledException being caught!
The exception caught is:-
MeasureOverride of element 'Xamarin.Forms.Platform.WinPhone.ViewToRendererConverter+WrapperControl' should not return PositiveInfinity or Nan as its DesiredSize.
Does anyone have any ideas how to resolve this?

Refreshing OxyPlot graphs doesn't work

I have a Xamarin forms application which has an event that runs every second and fetches data from a Characteristic. In this event I create the whole layout, set the data and update the view like this:
public void UpdateDisplay (ICharacteristic c) {
//Lots of irrelevant code above this.
foreach (Accelerometer item in acceloReadings)
{
counter += 1;
//Y-graph data
DataPoint itemPointY = new DataPoint();
itemPointY.X = counter;
itemPointY.Y = item.yAxes;
PointsY.Add(itemPointY);
}
yAs = new PlotModel("Variations Y-Axis");
yAs.PlotType = PlotType.XY;
yAs.Axes.Add(new LinearAxis(AxisPosition.Left, -270, 270));
y = new LineSeries();
//Custom color
y.Color = OxyColor.FromArgb(255, 154, 200, 253);
y.ItemsSource = PointsY;
yAs.Series.Add(y);
opvY = new OxyPlotView
{
WidthRequest = 100,
HeightRequest = 100,
BackgroundColor = Color.Aqua
};
opvY.Model = yAs;
Content = new StackLayout
{
opvY,
}
};
}
That code fills up all the models etc and I then want to update my graphs in the same method. I've tried a few ways:
//After all the data is re-loaded and added I want to refresh my graphs.
xAs.InvalidatePlot(true);
opvX.Model.InvalidatePlot(true);
opvX.InvalidateDisplay();
However none of these have any succes. When you look at the official docs you can see clearly it states:
"To update the plot, you must do one of the following alternatives:
Change the Model property of the PlotView control
Call Invalidate on the PlotView control
Call Invalidate on the PlotModel"
While I've tried all these ways none of them update my graphs/view. When I manually click and resize the graph I can see the data is refreshed. Eventually I want to create the graphs just once in the main method and then update the graphs with the InvalidateDisplay.
So, what am I doing wrong?!

Common class for WPF Containers (Canvas, Grid, etc)

I am creating a method that is invoked by a button. The method then adds a canvas to the button's parent container control. So, e.g., the button is on a grid. Then the method creates a canvas that is shown just below the button. But I have 2 issues:
How can I get a reference to the button's parent container?
Is there a class for container controls? I don't care if the button is in a grid, a canvas, a Stackpanel, etc. So I am looking for an interface that all types of contianers implement or a class that they inherit.
The second aspect is more important as I could pass a reference to the container manually.
EDIT:
It should look like this (minus the colors, those are only to show the different elements.
The red canvas is supposed to pop up to handle a confirmation. Maybe even with a nice animation. My idea was to create a class that can be invoked similar to this:
MyPopup popup = new MyPopup("Are you sure?", "Yes", "No", delegateFirstButton, delegateSecondButton);
popup.Show();
My code so far is not yet a class but only a method. The text parts are hard coded for the moment. The marked line needs to be more flexible and is the reason for my question.
public void ShowPopup(Control senderControl)
{
//I need to have a parameter that accepts all containers instead of this line:
this.myGrid.Children.Add(popup);
Border border = new Border();
popup.Children.Add(border);
border.Margin = new Thickness() { Top = 10 };
border.Child= text;
text.Text = "Are you sure?";
text.HorizontalAlignment = System.Windows.HorizontalAlignment.Center;
popup.SizeChanged += delegate { border.Width = popup.ActualWidth; };
popup.Children.Add(btn1);
btn1.Content = "Yes";
btn1.Height = 22;
btn1.Padding = new Thickness(10, 0, 10, 0);
btn1.Margin = new Thickness() { Left = 15, Top = 35 };
popup.Children.Add(btn2);
btn2.Content = "No";
btn2.Height = 22;
btn2.Padding = new Thickness(10, 0, 10, 0);
btn1.SizeChanged += delegate { btn2.Margin = new Thickness() { Left = 30 + btn1.ActualWidth, Top = 35 }; };
popup.Height = 70;
btn2.SizeChanged += delegate
{
popup.Width = 45 + btn1.ActualWidth + btn2.ActualWidth;
updatePositions(senderControl);
};
popup.Background = Brushes.Red;
popup.VerticalAlignment = System.Windows.VerticalAlignment.Top;
popup.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
}
public void updatePositions(Control senderControl)
{
Point location = senderControl.TranslatePoint(new Point(0, 0), this.myGrid);
popup.Margin = new Thickness()
{
Left = location.X + (senderControl.ActualWidth / 2) - (popup.Width / 2),
Top = location.Y + senderControl.ActualHeight + 15
};
}
Sounds like you're choosing the hard way to do that.
If you need a popup, then use a Popup.
Otherwise, if for whatever reasons you don't want to use that, You'd better place a grid somewhere near the root of your XAML, and ALWAYS use that as a container:
<Window>
<Grid x:Name="MainUI"/>
<Grid x:Name="PopupContainer"/>
</Window>
Otherwise, you'll almost invariably encounter Z-Index problems (if you follow your current approach).
Also, it's a really bad idea to create all that UI stuff in code. Either encapsulate your Yes/No dialog in a UserControl or create a proper Template for that.
As I said before, avoid at all costs creating/manipulating UI elements in code, as it creates a lot of maintainability issues.

Categories