Xamarin Forms CollectionView ScrollTo Not working on Load - xaml

I Have XAML form using a CollectionView. I bind the view to a collection of date objects. I set the SelectedItem in Code Behind which is working as expected. I then try to use ScrollTo in code behind however it always displays the first item in the collection once the page is loaded. After the page loads I have wired an image to fire the ScrollTo event and it works as expected. Below is my code. Thanks in advance for any help!
'''
for (int i = 0; i < 31 ; i++)
{
ScheduleDate date = new ScheduleDate();
date._Date = dt.AddDays(i);
colDates.Add(date);
}
cvDate.ItemsSource = colDates;
cvDate.SelectedItem = colDates[10];
cvDate.ScrollTo(cvDate.SelectedItem, null, ScrollToPosition.Start, true)
'''
XAML Form
'''
<CollectionView x:Name="cvDate" Grid.Row="0" Grid.Column="1"
SelectionMode="Single" >
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Horizontal" />
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<Label Text="{Binding Description}"
FontAttributes="Bold"
FontSize="Large"
HorizontalOptions="Center"
VerticalOptions="Center"
/>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
'''
Edit:
I wired up the OnCollectionViewScrolled event and when the following line of code is executed from OnAppearing()
cvDate.ScrollTo(cvDate.SelectedItem, null, ScrollToPosition.Start, false);
OnCollectionViewScrolled is NOT being called. However when I execute the same line of code from a button click event OnCollectionViewScrolled is being fired.

I am not sure what is your Description and _Date. I make a simple example for your reference.
Trigger the ScrollTo in OnAppearing override method:
protected override void OnAppearing()
{
cvDate.ScrollTo(cvDate.SelectedItem, null, ScrollToPosition.Start, false);
}
Trigger the ScrollTo with button click method:
private void Button_Clicked(object sender, EventArgs e)
{
cvDate.ScrollTo(cvDate.SelectedItem, null, ScrollToPosition.Start, false);
}
The whole code:
public partial class Page4 : ContentPage
{
public ObservableCollection<ScheduleDate> colDates { get; set; }
public Page4()
{
InitializeComponent();
colDates = new ObservableCollection<ScheduleDate>();
for (int i = 0; i < 31; i++)
{
ScheduleDate date = new ScheduleDate();
date.Description = i.ToString();
colDates.Add(date);
}
cvDate.ItemsSource = colDates;
cvDate.SelectedItem = colDates[10];
}
protected override void OnAppearing()
{
cvDate.ScrollTo(cvDate.SelectedItem, null, ScrollToPosition.Start, false);
}
private void Button_Clicked(object sender, EventArgs e)
{
//cvDate.ScrollTo(cvDate.SelectedItem, null, ScrollToPosition.Start, false);
}
}
public class ScheduleDate
{
public string Description { get; set; }
}
Screenshot:
Update:
<ContentPage.Content>
<StackLayout>
<CollectionView
x:Name="cvDate"
ItemsSource="{Binding colDates}"
SelectionMode="Single">
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Horizontal" />
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<Label
FontAttributes="Bold"
FontSize="Large"
HorizontalOptions="Center"
Text="{Binding Description}"
VerticalOptions="Center" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<Button Clicked="Button_Clicked"></Button>
</StackLayout>
</ContentPage.Content>

I believe that at least on UWP you can't scroll to an element until it's actually been loaded by the UI.
In that case you could work around this with a CollectionView custom renderer.
You can override the ItemsViewRenderer.ScrollTo function and postpone the scroll until the target element has been loaded. To know when that happens you can register to the ListViewBase.ContainerContentChanging event.

Related

Code Behind Xamarin forms launch a function in another page

I have 2 ContentView in MyWordPage.Xaml which are MyWordListView and AddWordsView
My MyWordPage.Xaml looks like this :
<ContentView x:Name="MyWordListView" >
<CollectionView x:Name="ListOfWords" IsVisible="False"
SelectionMode="Single" >
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Vertical" />
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout >
<Label Text="{Binding .}" />
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ContentView>
<ContentView IsVisible="False" x:Name="AddWordsView" />
<pv:PancakeView HorizontalOptions="End" VerticalOptions="End" Margin="0,0,10,150" Padding="10" CornerRadius="10">
<Image HeightRequest="30" WidthRequest="30" Aspect="AspectFit" />
<pv:PancakeView.GestureRecognizers>
<TapGestureRecognizer Tapped="OnChangeViewButton" />
</pv:PancakeView.GestureRecognizers>
</pv:PancakeView>
<Grid/>
If MyWordListView is visible, AddWordsView is not visible and vise versa.
The App starts with MyWordListPage visible and MyWordPage.xaml.cs looks like this:
public MyWordPage()
{
InitializeComponent();
// My second content view AddWordsView take content form a content page
// this content page name is MyAddWordPage
AddWordsView.Content = new MyAddWordPage().Content;
//My list of words
ListOfWords.ItemsSource = new List<string>()
{
"New York",
"London",
"Mumbai",
"Chicago"
};
}
//I navigate between the 2 View With an Overlay Button that make
//each one of View visible thanks to a boolean
bool ViewChange=false;
void OnChangeViewButton(System.Object sender, System.EventArgs e)
{
if (ViewChange==false)
{
AddWordsView.IsVisible=true;
MyWordListView.IsVisible=false;
ViewChange=!ViewChange;
}
else
{
AddWordsView.IsVisible=false;
MyWordListView.IsVisible=true;
ViewChange=!ViewChange;
}
}
OnUpdateMyList()
{
// Here I do things to refresh my list
}
MyAddWordPage.xaml.cs looks like this :
public MyWordPage()
{
InitializeComponent();
}
void OnInsertWord(System.Object sender, System.EventArgs e)
{
}
What I would like to do :
In MyAddWordPage.xaml.cs when clicking on a button to launch the function OnInsertWord() I would like to launch the function OnUpdateMyList() in MyWordPage.xaml.cs in order to refresh My collectionView in MyWordListView
Thanks for your help
According to your requirement, you can achieve this by overriding OnAppearing method.
The OnAppearing method is executed after the ContentPage is laid out, but just before it becomes visible.So, you can rebind the list to the collectionview in this method. Therefore, this is a good place to set the content of Xamarin.Forms views.
Here is the code in MyWordPage.xaml.cs:
public MainPage()
{
InitializeComponent();
// My second content view AddWordsView take content form a content page
// this content page name is MyAddWordPage
AddWordsView.Content = new MyAddWordPage().Content;
//My list of words
}
//Create a list
List<string> list = new List<string>()
{
"New York",
"London",
"Mumbai",
"Chicago"
};
protected override void OnAppearing()
{
base.OnAppearing();
ListOfWords.ItemsSource = list;
}
//I navigate between the 2 View With an Overlay Button that make
//each one of View visible thanks to a boolean
bool ViewChange = false;
void OnChangeViewButton(System.Object sender, System.EventArgs e)
{
if (ViewChange == false)
{
AddWordsView.IsVisible = true;
MyWordListView.IsVisible = false;
ViewChange = !ViewChange;
}
else
{
AddWordsView.IsVisible = false;
MyWordListView.IsVisible = true;
ViewChange = !ViewChange;
}
}
}
Here is the code in MyWordPage.Xaml:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="App5.MainPage">
<StackLayout>
<StackLayout x:Name="MyWordListView">
<CollectionView x:Name="ListOfWords" SelectionMode="Single" >
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Vertical" />
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout >
<Label Text="{Binding .}" />
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</StackLayout>
<ContentView IsVisible="False" x:Name="AddWordsView" >
<Label Text="Here is the text."></Label>
</ContentView>
<StackLayout>
<Button Clicked="OnChangeViewButton"></Button>
</StackLayout>
</StackLayout>
</ContentPage>

Why doesnt my image bind until I save my contentpage unless I save and reload

I'm currently trying to bind a few properties when I click a button and it pushes a new page.
Starting from the top, this is how my app is setup
public App()
{
InitializeComponent();
MainPage = new NavigationPage(new MainPage());
}
I have a "MainPage" which is essentially the first page that shows when starting the app.
In my MainPage.xaml
I've set the BindingContext to the MainViewModel
<ContentPage.BindingContext>
<viewModels:MainViewModel/>
</ContentPage.BindingContext>
And I also have a button which has it's Command bound to a Command in my MainViewModel
<Button Text="New Goal"
HeightRequest="50" WidthRequest="100"
TextColor="White"
Margin="10"
CornerRadius="4"
Command="{Binding NewGoalCommand}">
<Button.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#8FDF70" Offset="0.1" />
<GradientStop Color="#1DBE95" Offset="1.0" />
</LinearGradientBrush>
</Button.Background>
</Button>
The MainViewModel is pretty straightforward. It has a Command property which I initialize in the constructor
public ObservableCollection<Item> Items { get; set; } = new ObservableCollection<Item>();
public Command NewGoalCommand { get; set; }
public MainViewModel()
{
NewGoalCommand = new Command(() => ShowNewGoalPage());
}
private async void ShowNewGoalPage()
{
await Application.Current.MainPage.Navigation.PushAsync(new NewGoal(SelectedItem));
}
NewGoal.xaml
As you can see in the code it's invoking NewGoal which is my second page which shows up when I click the button, this page does show up when I click the button which to me, indicates that the binding was successful.
The same goes for this page, I'm setting the BindingContext to my other ViewModel which is responsible for it's corresponding view, like so
<ContentPage.BindingContext>
<viewModel:NewGoalViewModel/>
</ContentPage.BindingContext>
And I've also added components which are going to bind to it's corresponding property so that when I click "Save" it adds that item to the collection inside the MainViewModel
<ContentPage.Content>
<StackLayout Spacing="0">
<Image WidthRequest="50" HeightRequest="50"
Margin="10"
Source="{Binding ItemModel.ImageSource}"/>
<StackLayout Margin="20,0,20,0"
Spacing="0">
<Label Text="Title"/>
<Entry />
</StackLayout>
<StackLayout Margin="20,20,20,0"
Spacing="0">
<Label Text="Description"/>
<Entry />
</StackLayout>
<StackLayout Margin="20,20,20,0"
Spacing="0">
<Label Text="Type"/>
<Picker ItemsSource="{Binding ItemModel.Type}" />
</StackLayout>
<StackLayout Margin="20,20,20,0"
Spacing="0">
<Label Text="Price"/>
<Entry Keyboard="Numeric"/>
</StackLayout>
<Button Command="{Binding SaveCommand}"
Text="Save"
VerticalOptions="End">
</Button>
</StackLayout>
</ContentPage.Content>
And here is the NewGoalViewModel
class NewGoalViewModel : MainViewModel
{
public Item ItemModel { get; set; }
public Command SaveCommand { get; set; }
public NewGoalViewModel()
{
ItemModel = new Item();
ItemModel.Title = "Title";
ItemModel.Description = "Description";
ItemModel.Type = SavingsType.Other;
ItemModel.Price = 19.00f;
ItemModel.ImageSource = "cash.jpg";
SaveCommand = new Command(() => AddGoal());
}
private void AddGoal()
{
Items.Add(new Item { Title = "Rainy Day", Type = SavingsType.Other, Price = 100.00, ImageSource = "cash.jpg" });
}
}
The issue
So when I start the app and I click the first button, it takes me to the next page.
when I land on that page, it should show me an Image at the top. It's bound to a property which I've assigned in the constructor ItemModel.ImageSource = "cash.jpg";
The issue is however is that it doesnt actually bind, resulting in the image not showing until I save the NewGoal.xaml page and it reloads. Once it's done reloading it shows the image.
Try modifying
private Item itemModel { get; set; }
public Item ItemModel
{
get { return itemModel ; }
set
{
itemModel = value;
OnPropertyChanged();
}
}
and inherit your ViewModel to : INotifyPropertyChanged
and add this to implement
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
var changed = PropertyChanged;
if (changed == null)
return;
changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

Binding IsVisible to property toggled by Switch

I have a Switch bound to a property of an element in a List. I want to bind IsVisible of a button to the same property, but the button's visibility is not changed when the property is changed by the Switch. What am I missing?
XAML:
<StackLayout>
<ListView HorizontalOptions="FillAndExpand" ItemsSource="{Binding EquipmentList}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Horizontal">
<Label Text="{Binding Name}" />
<Switch IsToggled="{Binding State}" />
<Button
Command="{Binding BindingContext.DoCommand, Source={x:Reference TestPage}}"
CommandParameter="{Binding .}"
IsVisible="{Binding State}"
Text="Click" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
ViewModel:
private Command<Equipment> _doCommand;
public Command<Equipment> DoCommand => _doCommand ??
(_doCommand = new Command<Equipment>((Equipment obj) => HandleEquipment(obj)));
// Outputs correct Name and State of the list item
private void HandleEquipment(Equipment obj)
{
System.Diagnostics.Debug.WriteLine(obj.Name + ", " + obj.State);
}
Model:
class Equipment
{
public int Id { get; set; }
public string Name { get; set; }
public bool State { get; set; }
public Equipment(int Id, string Name, bool State)
{
this.Id = Id;
this.Name = Name;
this.State = State;
}
}
As Gerald wrote in his first comment: You have to implement the INotifyPropertyChanged interface on your Equipment model (and not just in the ViewModel).
Without this implementation, the elements in the view have no chance to know, that the state changed (in your case the button).
Implementation:
public class Equipment: INotifyPropertyChanged
{
public bool State
{
get => _state;
set =>
{
_state = value;
OnPropertyChanged();
}
}
private bool _state;
// OTHER PROPERTIES
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
The call of the method OnPropertyChanged() is important. The IsVisible property of the button recognizes the change and updates his value.
Instead of binding two things to a property, why not have the single item bound (i.e. the switch) and use XAML to show or hide the button:
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibility" />
</Window.Resources>
<StackLayout>
<ListView HorizontalOptions="FillAndExpand" ItemsSource="{Binding EquipmentList}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Horizontal">
<Label Text="{Binding Name}" />
<Switch Name="toggleSwitch" IsToggled="{Binding State}" />
<Button
Command="{Binding BindingContext.DoCommand, Source={x:Reference TestPage}}"
CommandParameter="{Binding .}"
IsVisible="{Binding ElementName=toggleSwitch, Path=IsToggled, Converter={StaticResource BooleanToVisibilityConverter}"
Text="Click" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
It may not be a Window that your StackLayout is in, but if you place a BooleanToVisibilityConverter in your Resources section you'll then be able to use it in your XAML file.
This will mean that if the property name changes in the future you only have one place you need to update in the user interface and you're also using the power of the XAML language.
Also as correctly pointed out by everyone, you need to implement INotifyPropertyChanged in the model in order for the Switch to be updated too.

Nested ListView is not working in xamarin forms

Nested list view is not working, I have a list which contains another list in it. To show it in View I am using Nested listview; But the code is not working,and i am not able to identify on where it went wrong... Below is my code
Main Page
<ContentPage.Content>
<StackLayout>
<ListView x:Name="outerListview" ItemsSource="{Binding lst}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell x:Name="outerListviewCell">
<ViewCell.View>
<ContentView>
<Label Text="{Binding ItemName}"/>
<StackLayout>
<ListView ItemsSource="{Binding ItemList}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell x:Name="InnerListviewCell">
<Grid>
<Label Text="{Binding stockQty}"/>
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentView>
</ViewCell.View>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage.Content>
ViewModel
public MainPageViewModel()
{
lst = new ObservableCollection<A>()
{
new A()
{
ItemName="Item1", ItemList=new ObservableCollection<ItemDetails>()
{
new ItemDetails() { stockQty="2"},
new ItemDetails(){ stockQty="3"}
}
},
new A()
{
ItemName="Item2", ItemList=new ObservableCollection<ItemDetails>()
{
new ItemDetails() { stockQty="3"},
new ItemDetails(){ stockQty="4"}
}
}
};
}
Model ( Class A and Class Itemdetails)
class A:INotifyPropertyChanged
{
public A()
{
ItemName = string.Empty;
ItemList = new ObservableCollection<ItemDetails>();
}
private string _ItemName;
public string ItemName
{
get { return _ItemName; }
set { _ItemName = value; OnPropertyChanged(); }
}
private ObservableCollection<ItemDetails> _itemlist;
public ObservableCollection<ItemDetails> ItemList
{
get { return _itemlist; }
set { _itemlist = value; OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
if (PropertyChanged != null)
PropertyChanged(this,
new PropertyChangedEventArgs(propertyName));
}
}
class ItemDetails:INotifyPropertyChanged
{
private string _stockQty;
public string stockQty
{
get { return _stockQty; }
set { _stockQty = value; OnPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
if (PropertyChanged != null)
PropertyChanged(this,
new PropertyChangedEventArgs(propertyName));
}
}
When I Run the above code I am getting below output in screen
2
3
What is expected actually is
Item1
2
3
Item2
3
4
What is wrong in above code? could anyone can help me?
Nesting Listview inside another Listview is not a good idea and it is not a supported on Xamarin.Forms.
ListView is very "sensitive" and it can easialy cause problems with scrolling and of course there are problems with poor performance of your app.
So I strongly recommend you to rethink about your layout and take a look at Grouping with ListView more about it here, maybe you can achieve what you want with Grouping.
After checking your code , I found something need to modify in your code.
In order to show the ItemName , you should wrap Label inside StackLayout .
In order to get Uneven Row, you should set listview.HasUnevenRows = true.
Modify your code as below:
<ContentPage.Content>
<ListView x:Name="outerListview" HasUnevenRows="True" ItemsSource="{Binding lst}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell x:Name="outerListviewCell">
<ViewCell.View>
<ContentView>
<StackLayout>
<Label Text="{Binding ItemName}"/>
<ListView ItemsSource="{Binding ItemList}" RowHeight="20">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell x:Name="InnerListviewCell">
<Grid>
<Label Text="{Binding stockQty}"/>
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentView>
</ViewCell.View>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentPage.Content>
That is absolutely wrong. You must use one ListView with IsGroupingEnabled set to True.
Follow instuctions here to make it work correct: https://xamarinhelp.com/xamarin-forms-listview-grouping/

Two ways Binding between a Slider and Entry

I would like to bind an Entry with a Slider and vice versa. I wrote something like this:
<Entry x:Name="myEntry" Text="{Binding Value, Mode=TwoWay}" BindingContext="{x:Reference slider}"/>
<Slider x:Name="slider" Maximum="100" Minimum="0" BindingContext="{x:Reference myEntry}"/>
When I use the slider, the value in the entry is updated, but when I put manually some value in the Entry, the value append a 0 or change to 0. What can be the problem. I am working on android.
You should bind both your Slider and Entry to string/ integer in a backing View Model.
class MyViewModel
{
private int _sliderValue;
public string EntryText
{
get => _sliderValue.ToString();
set => SetProperty(ref _sliderValue, int.Parse(value) );
}
public int SliderValue
{
get => _sliderValue;
set => (ref _sliderValue, value);
}
}
And in the view
<Entry Text="{Binding EntryText}" />
<Slider Value="{Binding SliderValue}" />
More MVVM Info
Fresh MVVM for Xamarin
Caliburn Micro for Xamarin
please refer the following xaml code
<Frame HorizontalOptions="FillAndExpand" VerticalOptions="StartAndExpand">
<StackLayout>
<Entry Text="{Binding Path=Value}"
FontSize="18"
x:Name="label"
BindingContext="{x:Reference Name=slider}"/>
<Slider x:Name="slider"
Maximum="1500"
VerticalOptions="CenterAndExpand" />
</StackLayout>
</Frame>
The same can be achieved by using the view model please refer the code
Xaml
<Frame HorizontalOptions="FillAndExpand" VerticalOptions="StartAndExpand">
<StackLayout>
<Entry x:Name="NameEntry" Placeholder="Enter Name" Text="{Binding Forename,Mode=TwoWay}" />
<Slider Value="{Binding Forename}" Minimum="0" Maximum="10"/>
</StackLayout>
</Frame>
------------------- now see the model in a c# file-----------------------------------
public partial class MainPage : ContentPage
{
public class DetailsViewModel : INotifyPropertyChanged
{
int forename;
public int Forename
{
get
{
return forename;
}
set
{
if (forename != value)
{
forename = value;
OnPropertyChanged ("Forename");
}
}
}
protected virtual void OnPropertyChanged(string propertyName)
{
var changed = PropertyChanged;
if (changed != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public MainPage()
{
InitializeComponent();
BindingContext = new DetailsViewModel();
}
}