I would like to slightly modify the code generated when creating a maui project to implement the following
add an object to Meetings in MainPage.xaml.cs when the button is clicked
display the contents of that Meetings
I wrote the following code for this purpose, but there is no change in the output content. One possible reason for this is that adding data to the object does not re-render the screen. How can I solve this problem?
Views/MainPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:App.Views"
x:Class="App.Views.MainPage">
<ScrollView>
<VerticalStackLayout
Spacing="25"
Padding="30,0"
VerticalOptions="Center">
<Image
Source="dotnet_bot.png"
SemanticProperties.Description="Cute dot net bot waving hi to you!"
HeightRequest="200"
HorizontalOptions="Center" />
<Label
Text="Hello, World!"
SemanticProperties.HeadingLevel="Level1"
FontSize="32"
HorizontalOptions="Center" />
<Label
Text="Welcome to .NET Multi-platform App UI"
SemanticProperties.HeadingLevel="Level2"
SemanticProperties.Description="Welcome to dot net Multi platform App U I"
FontSize="18"
HorizontalOptions="Center" />
<Button
x:Name="CounterBtn"
Text="Click me"
SemanticProperties.Hint="Counts the number of times you click"
Clicked="OnCounterClicked"
<ListView ItemsSource="{Binding Meetings}" />
</VerticalStackLayout>
</ScrollView>
</ContentPage>
Views/MainPage.xaml.cs
namespace App.Views;
using App.Models;
public partial class MainPage : ContentPage
{
int count = 0;
public MainPage()
{
InitializeComponent();
BindingContext = new Models.AllMeetings();
}
private void OnCounterClicked(object sender, EventArgs e)
{
count++;
if (count == 1)
CounterBtn.Text = $"Clicked {count} time";
else
CounterBtn.Text = $"Clicked {count} times";
SemanticScreenReader.Announce(CounterBtn.Text);
((Models.AllMeetings)BindingContext).Meetings.Add(new Models.Meeting() { Name = "foo" });
}
}
Modes/AllMeetings
namespace App.Models;
internal class AllMeetings
{
public List<Meeting> Meetings { get; set; }
}
Models/Meetings.cs
namespace App.Models;
internal class Meeting
{
public string Name { get; set; }
}
Updates
Models/AllMeetings.cs
using System.Collections.ObjectModel;
namespace ailia_speech_gui.Models;
internal class AllMeetings
{
public ObservableCollection<Meeting> Meetings { get; set; }
public void Add_Meeting(Meeting meeting)
{
this.Meetings.Add(meeting);
}
}
I made a demo on my side. You can refer to my demo to change your project.
Here is the code in my Model named Products.cs:
namespace ListViewDelete.Models
{
public class Products
{
public string Name
{
get; set;
}
public double Price
{
get; set;
}
}
}
Then you need to create a viewmodel to realize the delete and add method and create the ObservableCollection to load the data.
Here is the code in my ViewModel:
namespace ListViewDelete.ViewModels
{
internal class ProductsViewModels
{
public ObservableCollection<Products> Products
{
get; set;
}
public Command<Products> RemoveCommand
{
get
{
return new Command<Products>((Product) => {
Products.Remove(Product);
});
}
}
public Command<Products> AddCommand
{
get
{
return new Command<Products>((Product) => {
Products.Add(Product);
});
}
}
public ProductsViewModels()
{
Products = new ObservableCollection<Products> {
new Products {
Name = "name1",
Price = 100
},
new Products {
Name = "name2",
Price = 100
},
new Products {
Name = "name3",
Price = 100
}
};
}
}
}
Last, you need to create the ListView or the CollectionView in the MainPage.xaml. Here is the code in the MainPage.xaml:
<StackLayout>
<Button Text="add" Clicked="Button_Clicked"></Button>
<CollectionView ItemsSource="{Binding Products}">
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout>
<Label Text="{Binding Name}" />
<Label Text="{Binding Price}" />
<Button Text="Remove" Clicked="Remove_Clicked" />
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</StackLayout>
Here is the code in MainPage.xaml.cs:
namespace ListViewDelete
{
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
// bind the viewmodel to the Mainpage
BindingContext = new ProductsViewModels();
}
//delete the item from the observablecollection
public void Remove_Clicked(object sender, EventArgs e)
{
var button = sender as Button;
var product = button.BindingContext as Products;
var vm = BindingContext as ProductsViewModels;
vm.RemoveCommand.Execute(product);
}
//add the new item to the observablecollection
private void Button_Clicked(object sender, EventArgs e)
{
var product = new Products()
{
Name =" new name",
Price = 100
};
var vm = BindingContext as ProductsViewModels;
vm.AddCommand.Execute(product);
}
}
}
Meeting collection must be somewhere initialized before calling any operation on collestion (be it on property level or in constructor):
public class AllMeetings
{
public ObservableCollection<Meeting> Meetings { get; } = new ObservableCollection<Meeting>();
public void Add_Meeting(Meeting meeting)
{
this.Meetings.Add(meeting);
}
}
And ListView must have some data template to tell UI how data should be presented:
<ListView ItemsSource="{Binding Meetings}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Label Text="{Binding Name}"/>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
I can't share a binding name here is the code
I can't call the binding name, I just share a string, please could someone help me on how to call the binding name?
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="XF_ListViewDetails_MVVM.Views.ListDetailPage"
Title="Lanches"
BackgroundColor="Maroon">
<ContentPage.Content>
<ListView ItemsSource="{Binding Lanches}"
HasUnevenRows="True"
SeparatorVisibility="None"
>
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Grid Padding="10" RowSpacing="10" ColumnSpacing="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label x:Name="Nome666"
Grid.Column="1"
Text="{Binding Nome}"
FontSize="Medium"
TextColor="White"
VerticalOptions="End"/>
<Button x:Name="Button666"
Text="Compartilhar Texto"
Clicked="ButtonClicked"
/>
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentPage.Content>
</ContentPage>
here is the code in cs
using System;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
using XF_ListViewDetails_MVVM.ViewModels;
using Xamarin.Essentials;
using System.Threading.Tasks;
namespace XF_ListViewDetails_MVVM.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class ListDetailPage : ContentPage
{
public ListDetailPage(string Nome, string Ingredientes, string fonte)
{
InitializeComponent();
BindingContext = new ListDetailPageViewModel();
}
async void ButtonClicked(object sender, System.EventArgs args)
{
await CompartilharTexto(Nome666.Text);
}
public async Task CompartilharTexto(string texto)
{
await Share.RequestAsync(new ShareTextRequest
{
Text = texto,
Title = "Compartilhando Texto"
});
}
}}
the code does not share I need to know how to call the binding name on the home page, it does not run in the model only on the home page
you can get the value of the selected row from the binding context
async void ButtonClicked(object sender, System.EventArgs args)
{
var btn = (Button)sender;
var item = (YourClassNameGoesHere)btn.BindingContext;
await CompartilharTexto(item.Nome);
}
namespace XF_ListViewDetails_MVVM.Models
{
public class ListModel
{
public string Nome { get; set; }
public string Detalhe { get; set; }
public string Imagem { get; set; }
public string Ingredientes { get; set; }
}
}
public class ListDetailPageViewModel
{
public ObservableCollection<ListModel> Lanches { get; set; }
public ListDetailPageViewModel()
{
Lanches = new ObservableCollection<ListModel>();
Lanches.Add(new ListModel
{
Nome = "Cheese Burger Completo",
});
Lanches.Add(new ListModel
{
Nome = "Cheese Salada Completo",
});
Lanches.Add(new ListModel
{
Nome = "Cheese Burger com Ovo ",
});
}
}
}
I have already created all of the controls, and have tested that the controls are added as needed without using the binding.
The issue is as soon as I add the binding which is supposed to determine which control to add, the controls stop working as needed.
The list view control will be populated from a collection of a class, which will have an indicator field to determine which control needs to be loaded. The list view contains a 2nd user control which basically acts as a placeholder for the correct control, it has a bindable property of type text which is set to determine the correct control to be loaded.
Here is the XAML Code for the list view control
<ContentView.Content>
<StackLayout>
<Label Text="Binding Control Type"/>
<Entry x:Name="cntName"/>
<ListView x:Name="GroupedView" GroupDisplayBinding="{Binding Title}" HasUnevenRows="True" GroupShortNameBinding="{Binding ShortName}" IsGroupingEnabled="True">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ViewCell.ContextActions>
<MenuItem Text="Add Comment"/>
<MenuItem Text="Add Attachment"/>
</ViewCell.ContextActions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*"/>
<ColumnDefinition Width="7*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="3*"/>
<RowDefinition Height="7*"/>
</Grid.RowDefinitions>
<Label Text="{Binding QUESTION_ID}" Grid.Row="0" Grid.Column="0" VerticalTextAlignment="Center" FontSize="Medium"/>
<Label Text="{Binding QUESTION_DETAILS}" Grid.Row="1" Grid.Column="0" VerticalTextAlignment="Center" FontSize="Medium"/>
<con:ucListViewControls ControlType="{Binding QUESTION_ANSWERCONTROL}" Grid.Row="1" Grid.Column="1"/>
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.GroupHeaderTemplate>
<DataTemplate>
<ViewCell>
<ViewCell.ContextActions>
<MenuItem Text="Add Comment"/>
<MenuItem Text="Add Attachment"/>
</ViewCell.ContextActions>
<StackLayout Orientation="Horizontal" Padding="5,5,5,5" BackgroundColor="#E2F5F9">
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Command="{Binding Source={x:Reference this}, Path=Tapped}" CommandParameter="{Binding .}"/>
</StackLayout.GestureRecognizers>
<Button Image="{Binding StateIcon}" BackgroundColor="Transparent" BorderColor="Transparent" BorderWidth="0"/>
<Label Text="{Binding Title}" TextColor="#005569" FontSize="15" VerticalOptions="Center"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.GroupHeaderTemplate>
</ListView>
</StackLayout>
</ContentView.Content>
And the code behind for the control
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class ucExpandibleListView : ContentView
{
private ObservableCollection<dbQuestionGroup> _allGroups;
private ObservableCollection<dbQuestionGroup> _expandedGroups;
public ucExpandibleListView()
{
InitializeComponent();
Tapped = new Command(x => HeaderTapped(x));
_allGroups = new ObservableCollection<dbQuestionGroup>()
{
new dbQuestionGroup("Category 1", "C1", false)
{
new dbQuestionModel() { QUESTION_ID = 1, QUESTION_DETAILS = "Testing Question 1", QUESTION_ANSWERCONTROL = "RBL" },
new dbQuestionModel() { QUESTION_ID = 2, QUESTION_DETAILS = "Testing Question 2", QUESTION_ANSWERCONTROL = "" }
}
};
UpdateListContent();
}
private void UpdateListContent()
{
_expandedGroups = new ObservableCollection<dbQuestionGroup>();
foreach (dbQuestionGroup group in _allGroups)
{
dbQuestionGroup newGroup = new dbQuestionGroup(group.Title, group.ShortName, group.Expanded);
newGroup.QuestionCount = group.Count;
if (group.Expanded)
{
foreach (dbQuestionModel question in group)
{
newGroup.Add(question);
}
}
_expandedGroups.Add(newGroup);
}
GroupedView.ItemsSource = _expandedGroups;
}
public Command Tapped { get; set; }
private void HeaderTapped(object group)
{
var groupCat = (dbQuestionGroup)group;
int selectedIndex = _expandedGroups.IndexOf(groupCat);
if (groupCat.Expanded)
{
_allGroups[selectedIndex].Expanded = false;
}
else
{
_allGroups.ToList().ForEach(x => x.Expanded = false);
_allGroups[selectedIndex].Expanded = !_allGroups[selectedIndex].Expanded;
}
UpdateListContent();
}
}
Here is the XAML Code for the placeholder control
<ContentView.Content>
<StackLayout x:Name="stkPlaceholder">
</StackLayout>
</ContentView.Content>
And the code behind for the placeholder control
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class ucListViewControls : ContentView, INotifyPropertyChanged
{
public ucListViewControls()
{
InitializeComponent();
}
#region Control Attributes
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string info)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(info));
}
#endregion
#region Bindable Properties
public static readonly BindableProperty ControlTypeProperty = BindableProperty.Create(nameof(ControlType), typeof(string), typeof(ucListViewControls));
public string ControlType
{
get
{
return (string)GetValue(ControlTypeProperty);
}
set
{
SetValue(ControlTypeProperty, value);
AddControl();
NotifyPropertyChanged("ControlType");
}
}
#endregion
public void AddControl()
{
switch (ControlType)
{
case "RBL":
ucRadiobuttons radiobuttons = new ucRadiobuttons();
radiobuttons.lblTitle1 = "Yes";
radiobuttons.lblTitle2 = "No";
radiobuttons.lblTitle3 = "N/A";
radiobuttons.OnColor1 = Color.Green;
radiobuttons.OnColor2 = Color.Red;
radiobuttons.OnColor3 = Color.Transparent;
stkPlaceholder.Children.Add(radiobuttons);
break;
default:
Entry placeholder = new Entry();
stkPlaceholder.Children.Add(placeholder);
break;
}
}
}
I have tested that the controls are added without the binding, which works perfectly.
I have tried to rewrite the bindable property multiple times in case I missed something, I also could not find any post relating to something similar which wouldve helped me.
Any clues?
The first screenshot shows the expected output, and the seconds screenshots shows what happens when the binding is applied.
dbQuestionModel:
using System.Collections.Generic;
namespace PivotMobile_BusinessLayer.Models
{
public class dbQuestionModel
{
public int QUESTION_PK { get; set; }
public int QUESTION_ID { get; set; }
public string QUESTION_CATEGORY { get; set; }
public string QUESTION_DETAILS { get; set; }
public string QUESTION_TYPE { get; set; }
public string QUESTION_ANSWERCONTROL { get; set; }
public string QUESTION_COMMENT { get; set; }
public List<string> QUESTION_ATTACHMENTS { get; set; }
}
}
dbQuestionGroup:
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace PivotMobile_BusinessLayer.Models
{
public class dbQuestionGroup : ObservableCollection<dbQuestionModel>, INotifyPropertyChanged
{
public static ObservableCollection<dbQuestionGroup> All { private set; get; }
private bool _expanded;
public string Title { get; set; }
public string ShortName { get; set; }
public bool Expanded
{
get
{
return _expanded;
}
set
{
if (_expanded != value)
{
_expanded = value;
OnPropertyChanged("Expanded");
OnPropertyChanged("StateIcon");
}
}
}
public string StateIcon
{
get
{
return Expanded ? "expanded_blue.png" : "collapsed_blue.png";
}
}
public int QuestionCount { get; set; }
public dbQuestionGroup(string title, string shortName, bool expanded = true)
{
Title = title;
ShortName = shortName;
Expanded = expanded;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
ListView Page 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="PivotMobile.Views.ObservationsView"
xmlns:con="clr-namespace:PivotMobile.Controls">
<ContentPage.Content>
<StackLayout Margin="5">
<con:ucExpandibleListView/>
</StackLayout>
</ContentPage.Content>
</ContentPage>
ListView Page Code Behind:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace PivotMobile.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class ObservationsView : ContentPage
{
public ObservationsView ()
{
InitializeComponent ();
}
}
}
I'm very new to Xamarin and C#. So if What I am asking is rookie I apologize. But I have scoured the interwebz and Stack Overflow looking for why what I am doing isn't working and can't figure it out. As far as I can tell it should be working fine but maybe/hopefully I'm just missing something simple.
I'm using MVVM (mostly) and I have a ListView made up of objects called MobileBoardUser. That List View is set up like this
<ListView
ItemsSource="{Binding BoardUsers}"
HasUnevenRows="True"
ItemSelected="StatusBoardPageListView_ItemSelected" >
<ListView.ItemTemplate >
<DataTemplate>
<ViewCell>
//then the various StackLayout and Label objects etc.
In the code behind I am trying to use the ItemSelected method to pass the selected Item into a new page where all of it's properties will be displayed.
private void StatusBoardPageListView_ItemSelected(object sender, SelectedItemChangedEventArgs e)
{
if (e.SelectedItem == null)
{
return;
}
MobileBoardUser userSelected = e.SelectedItem as MobileBoardUser;
Navigation.PushAsync(new BoardUserPage(userSelected));
}
The BoardUserPage Code Behind looks like this
using EIOBoardMobile.Model;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace EIOBoardMobile.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class BoardUserPage : ContentPage
{
public class UserProp
{
public string userprop { get; set; }
}
public class UserValue
{
public string uservalue { get; set; }
}
public ObservableCollection<UserProp> SelectedUserProps { get; set; } = new ObservableCollection<UserProp>();
public ObservableCollection<UserValue> SelectedUserValues { get; set; } = new ObservableCollection<UserValue>();
public BoardUserPage(MobileBoardUser selectedUser)
{
InitializeComponent();
BindingContext = this;
MobileBoardUser shownUser = selectedUser;
foreach (var prop in shownUser.GetType().GetProperties())
{
if (prop.GetType() == typeof(String))
{
UserProp NewUserProp = new UserProp
{
userprop = prop.Name.ToString()
};
SelectedUserProps.Add(NewUserProp);
}
}
foreach (var prop in shownUser.GetType().GetProperties())
{
if (prop.GetType() == typeof(String))
{
UserValue NewUserValue = new UserValue
{
uservalue = prop.GetValue(shownUser, null).ToString()
};
SelectedUserValues.Add(NewUserValue);
}
}
}
}
}
As you can see I have created two lists of objects, one to represent the property names and one to represent the actual values of those properties so they can be used in the xaml. In production these will be dynamic so it is important I be able to do it this way. To this end the BoardUserPage xaml looks like this
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:EIOBoardMobile.Views"
x:Class="EIOBoardMobile.Views.BoardUserPage">
<ContentPage.Content>
<StackLayout Padding="20">
<ListView
ItemsSource="{Binding SelectedUserProps}"
HasUnevenRows="True" >
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Vertical" >
<Label Text="{Binding userprop}" HorizontalOptions="StartAndExpand" TextColor="Black" />
<ListView ItemsSource="{Binding SelectedUserValues}" HorizontalOptions="EndAndExpand" >
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Label Text="{Binding uservalue}" HorizontalOptions="EndAndExpand" TextColor="Blue" />
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage.Content>
</ContentPage>
All of this compiles and I get no unhandled exceptions or run time errors. The code behind that passes the SelectedItem as MobileBoardUser into the new page works to navigate to BoarduserPage but when I get there the page is empty and doing nothing.
What have I done wrong?
Ok after some trial and error I was actually able to figure this out. I had to make some changes to the code. The typeof statements were not constructed properly. For the SelectedUserProps I was getting the typeof the property rather the value. So I had to change that. Also the nested ListView inside another ListView was causing exceptions and failing to generate. Passing e.SelectedItem after casting actually DID work. It was the foreach comparison statements that were causing me grief. So the major changes I made were to the BoardUserPage code behind and the BoardUserPage xaml. Here are those changes. Primarily using one ObservableCollection instead of two (hence now only one foreach statement and correcting the type comparison so that I was comparing values rather than the properties themselves to typeof(String). Here is the code behind
using EIOBoardMobile.Model;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace EIOBoardMobile.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class BoardUserPage : ContentPage
{
public class UserProp
{
public string userprop { get; set; }
public string uservalue { get; set; }
}
public ObservableCollection<UserProp> SelectedUserProps { get; set; } = new ObservableCollection<UserProp>();
public BoardUserPage(MobileBoardUser selectedUser)
{
InitializeComponent();
BindingContext = this;
foreach (var prop in selectedUser.GetType().GetProperties())
{
if (prop.GetValue(selectedUser).GetType() == typeof(String))
{
UserProp NewUserProp = new UserProp
{
userprop = prop.Name.ToString(),
uservalue = prop.GetValue(selectedUser).ToString()
};
SelectedUserProps.Add(NewUserProp);
}
}
}
}
}
and here is the View (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"
xmlns:local="clr-namespace:EIOBoardMobile.Views"
x:Class="EIOBoardMobile.Views.BoardUserPage">
<StackLayout Padding="20" >
<ListView
x:Name="Parent"
ItemsSource="{Binding SelectedUserProps}"
HasUnevenRows="True" >
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Vertical" Padding="10" HeightRequest="100">
<Label Text="{Binding userprop}" HorizontalOptions="StartAndExpand" VerticalOptions="StartAndExpand" TextColor="Black" />
<Label Text="{Binding uservalue}" HorizontalOptions="EndAndExpand" VerticalOptions="EndAndExpand" TextColor="Blue" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage>
This displays the property names on the left and values on the right of only those properties which are strings. This was necessary to avoid displaying IDs and othe integer based key values from the database that would just be meaningless clutter to end users.
I simply changed my ListView to use grouping, but now I can't use ScrollTo anymore.
I have create a simple app, so you can see the problem.
The XAML-page looks like (I am not using XAML in my app at the moment, but I will in an upcoming version).
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:ScrollListExample"
x:Class="ScrollListExample.ProjectPage">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ListView x:Name="ProjectsListView" HasUnevenRows="True" IsGroupingEnabled="True" ItemsSource="{Binding Projects}">
<ListView.GroupHeaderTemplate>
<DataTemplate>
<ViewCell>
<Label Text="{Binding Path=Key}" />
</ViewCell>
</DataTemplate>
</ListView.GroupHeaderTemplate>
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Grid.Row="0" LineBreakMode="TailTruncation" Text="{Binding Path=ProjectName}" />
<Label Grid.Column="0" Grid.Row="1" Text="{Binding Path=ProjectReference, StringFormat='Sag: {0}'}" />
<Label Grid.Column="0" Grid.Row="2" Text="{Binding Path=CustomerName}" />
<Label Grid.Column="0" Grid.Row="3" Text="{Binding Path=FullAddress}" />
<Label Grid.Column="1" Grid.Row="0" Text="{Binding Path=StartTime}" />
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</ContentPage>
And the code-behind file for the example looks like this
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class ProjectPage : ContentPage
{
public ProjectPage()
{
InitializeComponent();
BindingContext = new ProjectsViewModel();
}
protected override void OnAppearing()
{
base.OnAppearing();
Acr.UserDialogs.UserDialogs.Instance.ShowLoading();
var projects = Newtonsoft.Json.JsonConvert.DeserializeObject<IList<ProjectDto>>("[{\"ProjectName\":\"Test sag\",\"ProjectReference\":\"10072\",\"CustomerName\":\"Test firma\",\"FullAddress\":\"Testvej 3\",\"StartDate\":\"2017-02-02T00:00:00\",\"StartTime\":\"\"},{\"ProjectName\":\"aaa\",\"ProjectReference\":\"10077\",\"CustomerName\":\"Test firma\",\"FullAddress\":\"Testvej 12\",\"StartDate\":\"2017-02-08T00:00:00\",\"StartTime\":\"\"},{\"ProjectName\":\"Test\",\"ProjectReference\":\"10082\",\"CustomerName\":\"Test firma\",\"FullAddress\":\"Testvej 50\",\"StartDate\":\"2017-02-16T00:00:00\",\"StartTime\":\"\"},{\"ProjectName\":\"Test\",\"ProjectReference\":\"10085\",\"CustomerName\":\"Testvej boligselskab\",\"FullAddress\":\"Testvej 14\",\"StartDate\":\"2017-02-24T00:00:00\",\"StartTime\":\"\"},{\"ProjectName\":\"Test\",\"ProjectReference\":\"10086\",\"CustomerName\":\"Testing\",\"FullAddress\":\"Testevej 14\",\"StartDate\":\"2017-02-27T00:00:00\",\"StartTime\":\"\"},{\"ProjectName\":\"Test1\",\"ProjectReference\":\"10087\",\"CustomerName\":\"Plejecenter testlyst\",\"FullAddress\":\"Testlystvej 11\",\"StartDate\":\"2017-02-27T00:00:00\",\"StartTime\":\"\"},{\"ProjectName\":\"Test2\",\"ProjectReference\":\"10088\",\"CustomerName\":\"Charlie\",\"FullAddress\":\"Testvej 50\",\"StartDate\":\"2017-02-27T00:00:00\",\"StartTime\":\"\"},{\"ProjectName\":\"Test\",\"ProjectReference\":\"10089\",\"CustomerName\":\"Standard Debitor\",\"FullAddress\":\"[Mangler]\",\"StartDate\":\"2017-03-16T00:00:00\",\"StartTime\":\"\"},{\"ProjectName\":\"Test\",\"ProjectReference\":\"10090\",\"CustomerName\":\"Standard Debitor\",\"FullAddress\":\"[Mangler]\",\"StartDate\":\"2017-03-16T00:00:00\",\"StartTime\":\"\"},{\"ProjectName\":\"Test\",\"ProjectReference\":\"10091\",\"CustomerName\":\"Standard Debitor\",\"FullAddress\":\"[Mangler]\",\"StartDate\":\"2017-03-16T00:00:00\",\"StartTime\":\"\"},{\"ProjectName\":\"Test\",\"ProjectReference\":\"10092\",\"CustomerName\":\"Tester\",\"FullAddress\":\"Testvej 11\",\"StartDate\":\"2017-03-16T00:00:00\",\"StartTime\":\"\"},{\"ProjectName\":\"Test\",\"ProjectReference\":\"10093\",\"CustomerName\":\"Plejehjemmet test\",\"FullAddress\":\"Testvej 90\",\"StartDate\":\"2017-03-16T00:00:00\",\"StartTime\":\"\"},{\"ProjectName\":\"Test\",\"ProjectReference\":\"10094\",\"CustomerName\":\"Plejehjemmet test\",\"FullAddress\":\"Testvej 90\",\"StartDate\":\"2017-03-16T00:00:00\",\"StartTime\":\"\"}]");
var viewModel = BindingContext as ProjectsViewModel;
if (viewModel != null)
viewModel.OriginalProjects = projects;
Acr.UserDialogs.UserDialogs.Instance.ShowLoading("Loading");
Task.Delay(5000).ContinueWith((x) =>
{
Device.BeginInvokeOnMainThread(Acr.UserDialogs.UserDialogs.Instance.HideLoading);
Search();
});
}
private void Search(string inputVal = null)
{
var viewModel = BindingContext as ProjectsViewModel;
if (viewModel != null)
{
var projects = viewModel.OriginalProjects.Where(p => !string.IsNullOrEmpty(inputVal) ? p.ProjectName.Contains(inputVal) : true);
var orderedProjects = projects.OrderBy(p => p.StartDate);
Device.BeginInvokeOnMainThread(() =>
{
foreach (ProjectDto project in orderedProjects)
{
var coll = viewModel.Projects.FirstOrDefault(c => c.Key == project.StartDate);
if (coll == null)
viewModel.Projects.Add(coll = new ObservableCollectionWithDateKey { Key = project.StartDate });
coll.Add(project);
}
var group = viewModel.Projects.LastOrDefault();
if (group != null)
ProjectsListView.ScrollTo(group.First(), group.Key, ScrollToPosition.Start, false);
});
}
}
}
class ProjectsViewModel : INotifyPropertyChanged
{
private ObservableCollection<ObservableCollectionWithDateKey> _projects;
public event PropertyChangedEventHandler PropertyChanged;
public IEnumerable<ProjectDto> OriginalProjects { get; set; }
public ObservableCollection<ObservableCollectionWithDateKey> Projects
{
get { return _projects; }
set
{
_projects = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Projects)));
}
}
public ProjectsViewModel()
{
Projects = new ObservableCollection<ObservableCollectionWithDateKey>();
}
}
public class ProjectDto : INotifyPropertyChanged
{
public string ProjectName { get; set; }
public string ProjectReference { get; set; }
public string CustomerName { get; set; }
public string FullAddress { get; set; }
public DateTime StartDate { get; set; }
public string StartTime { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
}
class ObservableCollectionWithDateKey : ObservableCollection<ProjectDto>
{
public DateTime Key { get; set; }
}
I use Task.Delay(5000) to simulate a response from the server, but I do not think, it matters.
UPDATE
I figured out, the problem was in my ScrollTo-call, where ScrollTo(group.First(), group.Key, ScrollToPosition.Start, false); was called with the Key instead of just the group.
If you create the grouping first (without adding it to the ViewModel), you have to find the correct model in the ViewModel afterwards. As it otherwise does not find the correct ObservableCollection
I have tested your code and reproduced your issue. The problem is you have passed the wrong parameter to ScrollTo method.
ProjectsListView.ScrollTo(group.First(), group.Key, ScrollToPosition.Start, false);
The group parameter of ScrollTo method is the group from your ListView.ItemsSource. But your passed a group.Key. So the method will not be excited as expect. Please modify the code like following.
Device.BeginInvokeOnMainThread(() =>
{
foreach (ProjectDto project in orderedProjects)
{
var coll = viewModel.Projects.FirstOrDefault(c => c.Key == project.StartDate);
if (coll == null)
viewModel.Projects.Add(coll = new ObservableCollectionWithDateKey { Key = project.StartDate });
coll.Add(project);
}
var group = viewModel.Projects.Last();
if (group != null)
ProjectsListView.ScrollTo(group.First(), group, ScrollToPosition.Start, false);
});