I'm trying to search how to create a custom picker on Xamarin but I have no idea how to do it.
Here is what I want to do
I don't even know if I need to install a nuget package. Please help and thanks.
As mentioned by #Skalpel02, you need to sub-class the Picker class and implement the corresponding Renderers in each platform. There, you have the ability to interact with native APIs of the platform.
This could be implemented by custom renderer.
First,a custom Picker control can be created by subclassing the Picker control, as shown in the following code:
public class BorderlessPicker : Picker
{
public BorderlessPicker() : base()
{
}
}
Second:Create the Custom Renderer on each Platform,Override the OnElementChanged method and write logic to customize the control,then Add an ExportRenderer attribute to the custom renderer class to specify that it will be used.
In Android:
[assembly: ExportRenderer(typeof(BorderlessPicker), typeof(BordlessPickerRenderer))]
namespace AppPicker01.Droid
{
public class BordlessPickerRenderer : PickerRenderer
{
public BordlessPickerRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Picker> e)
{
base.OnElementChanged(e);
if (e.OldElement == null)
{
Control.Background = null;
}
}
}
}
In iOS:
[assembly: ExportRenderer(typeof(BorderlessPicker), typeof(BorderlessPickerRenderer))]
namespace AppPicker01.iOS
{
public class BorderlessPickerRenderer : PickerRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Picker> e)
{
base.OnElementChanged(e);
if (Control == null)
{
return;
}
Control.Layer.BorderWidth = 0;
Control.BorderStyle = UITextBorderStyle.None;
}
}
}
Last but not least, consume the custom picker control in Xaml:
<apppicker01:BorderlessPicker Title="Select a color" ItemsSource="{Binding ColorNames}" SelectedItem="{Binding SelectedColorName, Mode=TwoWay}" />
Screenshot:
MS official docs link:
https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/custom-renderer/
You can easily create your own control that doesn't need a renderer and works on iOS, Android, and UWP. Here my solution.
You have to create a View "PickerCustom" for the control
<?xml version="1.0" encoding="UTF-8"?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="___YOURCLASS"
xmlns:xmleditor="clr-namespace:XmlEditor" HorizontalOptions="FillAndExpand" BackgroundColor="#ddd">
<StackLayout x:Name="stack" Orientation="Horizontal" HorizontalOptions="FillAndExpand" Margin="1" BackgroundColor="#fff" Padding="5">
<Label Text="{Binding TextValue}" Margin="0" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"/>
<ImageButton BackgroundColor="#ffffff" Source="dropdown.png" x:Name="img" WidthRequest="20"></ImageButton>
<Entry WidthRequest="0"></Entry>
</StackLayout>
</ContentView>
with this code behind
public partial class PickerCustom : ContentView
{
public PickerCustom()
{
InitializeComponent();
Items = new ObservableCollection<CustomItem>();
SelectedIndex = -1;
BindingContext = this;
TapGestureRecognizer tap0 = new TapGestureRecognizer();
tap0.Tapped += (sender, e) =>
{
img.Focus();
PickerCustomList pcl = new PickerCustomList();
pcl.Items = this.Items;
App.Current.MainPage.Navigation.PushModalAsync(pcl);
MessagingCenter.Subscribe<PickerCustomList>(this, "finish", (sender1) =>
{
MessagingCenter.Unsubscribe<PickerCustomList>(this, "finish");
img.Focus();
if(((PickerCustomList)sender1).SelectedIndex != -1)
{
SelectedIndex = ((PickerCustomList)sender1).SelectedIndex;
}
});
};
GestureManager.AddGesture(stack, tap0);
}
string _textvalue = "";
public string TextValue
{
get
{
return _textvalue;
}
set
{
_textvalue = value;
OnPropertyChanged();
}
}
public ObservableCollection<CustomItem> Items { get; set; }
int _selectedIndex = 0;
public int SelectedIndex
{
get
{
return _selectedIndex;
}
set
{
_selectedIndex = value;
if(_selectedIndex>= Items.Count)
{
_selectedIndex = -1;
} else if (_selectedIndex != -1)
{
TextValue = Items[SelectedIndex].Name;
}
else
{
TextValue = "";
}
OnPropertyChanged();
}
}
}
public class CustomItem
{
public CustomItem(string _name)
{
name = _name;
}
private string name;
public string Name
{
get { return name; }
set { name = value; }
}
}
And a View "PickerCustomList" for the choice
<?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="___YOURCLASS" BackgroundColor="#66aaaaaa"
x:Name="ContentPage1" Padding="30,100,30,100" >
<ListView x:Name="ContactsList" ItemsSource="{Binding Items}" IsVisible="True"
VerticalOptions="Start" HorizontalOptions="Center"
BackgroundColor="Transparent" HasUnevenRows="True">
<ListView.Header HorizontalOptions="FillAndExpand">
<StackLayout Orientation="Horizontal" HorizontalOptions="FillAndExpand" BackgroundColor="#f0f0f0" >
<ImageButton Source="close.png" WidthRequest="20" Clicked="Button_Clicked" Margin="10,5,10,5" BackgroundColor="Transparent"></ImageButton>
</StackLayout>
</ListView.Header>
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell Tapped="ViewCell_Tapped" >
<StackLayout BackgroundColor="#ffffff">
<Label Text="{Binding Name}" Padding="10"></Label>
<ContentView HeightRequest="1" BackgroundColor="#666"></ContentView>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentPage>
with this code behind
public partial class PickerCustomList : ContentPage
{
public int SelectedIndex = -1;
ObservableCollection<CustomItem> myItems= new ObservableCollection<CustomItem>();
public ObservableCollection<CustomItem> Items
{
get { return myItems; }
set {
myItems = value;
OnPropertyChanged();
}
}
public PickerCustomList()
{
InitializeComponent();
BindingContext = this;
}
private void Button_Clicked(object sender, EventArgs e)
{
SelectedIndex = -1;
App.Current.MainPage.Navigation.PopModalAsync();
MessagingCenter.Send<PickerCustomList>(this, "finish");
}
private void ViewCell_Tapped(object sender, EventArgs e)
{
SelectedIndex = Items.IndexOf(((CustomItem)((ViewCell)sender).BindingContext));
App.Current.MainPage.Navigation.PopModalAsync();
MessagingCenter.Send<PickerCustomList>(this, "finish");
}
}
Related
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'm trying to make CollectionView in Shell but it's not updating.
I have one view model connected to Page and AppShell but when I update Collection view only page is updationg.
`public class AppShellViewModel : INotifyPropertyChanged
{
public Command Load { get; }
public ObservableCollection<ListData> _lists { get; set; }
public ObservableCollection<ListData> Lists
{
get { return _lists; }
set
{
_lists = value;
OnPropertyChanged();
}
}
public AppShellViewModel()
{
Lists = new ObservableCollection<ListData>()
{
new ListData(){id=0,name="test",UserId=0},
new ListData(){id=1,name="test1",UserId=1},
new ListData(){id=2,name="test2",UserId=2},
new ListData(){id=3,name="test3",UserId=3},
new ListData(){id=4,name="test4",UserId=4}
};
Load = new Command(async () => await GetUserLists());
}
async Task GetUserLists()
{
for (int i = 5; i < 15; i++)
{
Lists.Add(new ListData {id=i, name=$"test{ i }", UserId=i });
}
}
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}`
Then i have App Shell Collection View
`<Shell.FlyoutContent>
<StackLayout BackgroundColor="#34495e">
<Label Text="YOUR LISTS" FontSize="50" />
<CollectionView ItemsSource="{Binding Lists}" >
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout Padding="10" x:DataType="model:ListData">
<Label Text="{Binding name}"
LineBreakMode="NoWrap"
FontSize="13" />
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</StackLayout>
</Shell.FlyoutContent>`
And There is Page CollectionView
`<?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="ToDoApp.Views.AboutPage"
xmlns:model="clr-namespace:ToDoApp.Models">
<StackLayout>
<Button Text="Load" Command="{Binding Load}"/>
<Label Text="{Binding error}"/>
<CollectionView ItemsSource="{Binding Lists}">
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout Padding="10" x:DataType="model:ListData">
<Label Text="{Binding name}"
LineBreakMode="NoWrap"
FontSize="13" />
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</StackLayout>
</ContentPage>`
before update it looks like this
Page before update
Shell before update
And after update the only what changed is content page and shell is the same as before
Page after update
Shell after update
Related to Jason's comment.
WON'T CHANGE TOGETHER
NOT the same instance - BindingContexts similar to these:
// In AppShell.xaml.cs.
public AppShell()
{
InitializeComponent();
BindingContext = new AppShellViewModel();
}
// In AboutPage.xaml.cs.
public AboutPage()
{
InitializeComponent();
BindingContext = new AppShellViewModel();
}
GOOD (SHARED BETWEEN TWO PLACES)
BindingContexts are SAME instance:
// In AppShellViewModel.cs.
public class AppShellViewModel ...
{
private static AppShellViewModel _it;
public static AppShellViewModel It
{
get {
if (_it == null)
_it = new AppShellViewModel();
return _it;
}
}
}
// In AppShell.xaml.cs.
public AppShell()
{
InitializeComponent();
BindingContext = AppShellViewModel.It;
}
// In AboutPage.xaml.cs.
public AboutPage()
{
InitializeComponent();
BindingContext = AppShellViewModel.It;
}
I have a problem with Binding Data to a Picker in Xamarin Forms and hpe somebody can help me. I have a ContentPage which holds a picker. The Itemssource for that Picker is queried async from a web service. Also, the view (or rather, the ViewModel) is passed a seleted item. For whatever reason, setting the itemssource breaks the binding of the SelectedItem property.
Here is my ViewModel -
public class ExerciseViewModel:BaseViewModel
{
private ApiServices apiService = new ApiServices();
private Exercise exercise;
public Exercise Exercise
{
get => exercise;
set
{
exercise = value;
OnPropertyChanged();
}
}
private List<ExerciseCategory> exerciseCategories = new List<ExerciseCategory>();
public List<ExerciseCategory> ExerciseCategories
{
get => exerciseCategories;
set
{
exerciseCategories = value;
OnPropertyChanged();
}
}
public ExerciseViewModel()
{
GetCategoriesCommand.Execute(null);
Exercise = new Exercise() { Name = "Neue Übung", Category = ExerciseCategories.FirstOrDefault() };
}
public ExerciseViewModel(Exercise ex)
{
Exercise = ex;
GetCategoriesCommand.Execute(null);
}
public ICommand GetCategoriesCommand
{
get
{
return new Command(async () =>
{
ExerciseCategories = await apiService.GetExerciseCategories();
});
}
}
public ICommand AddExerciseCommand
{
get
{
return new Command(async () =>
{
Exercise.Id = await apiService.AddExercise(Exercise);
});
}
}
}
This is the Entity in question - the necessary operators are overloaded, INotifyPropertyChanged is implemented in the BaseClass -
public class ExerciseCategory:BaseClass
{
private string name;
private int id;
[Key]
public int Id
{
get => id;
set
{
id = value;
OnPropertyChanged();
}
}
public string Name
{
get => name;
set
{
name = value;
OnPropertyChanged();
}
}
public override bool Equals(object obj)
{
var other = (obj as ExerciseCategory);
if (other is null)
return false;
return this == other;
}
public static bool operator !=(ExerciseCategory e1, ExerciseCategory e2)
{
return !(e1 == e2);
}
public static bool operator ==(ExerciseCategory e1, ExerciseCategory e2)
{
if (e1.Id == e2.Id)
if (e1.Name == e2.Name)
return true;
return false;
}
}
This is the page's CodeBehind:
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class NewExercisePage : ContentPage
{
public NewExercisePage(ExerciseViewModel viewModel, bool controlsLocked = false)
{
try
{
this.BindingContext = viewModel;
InitializeComponent();
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
}
private void UpdateWebView(object sender, TextChangedEventArgs e)
{
Uri uriResult;
bool result = Uri.TryCreate(e.NewTextValue, UriKind.Absolute, out uriResult)
&& (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);
if (result)
exerciseVideoViewer.Source = e.NewTextValue;
}
}
Finally, here is the 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:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="PerformanceTM.Views.NewExercisePage">
<ContentPage.Content>
<StackLayout>
<WebView x:Name="exerciseVideoViewer" HeightRequest="200" WidthRequest="200"></WebView>
<Grid x:Name="LayoutGrid">
<Label Text="Kategorie" Grid.Column="0" Grid.Row="0"/>
<Picker x:Name="CategoryPicker" ItemsSource="{Binding ExerciseCategories}" ItemDisplayBinding="{Binding Name}" SelectedItem="{Binding Exercise.Category}" Grid.Column="1" Grid.Row="0"/>
<Label Text="Name" Grid.Column="0" Grid.Row="1"/>
<Entry Text="{Binding Exercise.Name}" Grid.Column="1" Grid.Row="1"/>
<Label Text="Video URL" Grid.Column="0" Grid.Row="2"/>
<Entry Text="{Binding Exercise.VideoUrl}" Grid.Column="1" Grid.Row="2" TextChanged="UpdateWebView"/>
<Label Text="Beschreibung" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="3"/>
<Editor Text="{Binding Exercise.Description}" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="4"/>
</Grid>
<Button Text="Speichern" Command="{Binding AddExerciseCommand}"/>
<Label Text="{Binding Exercise.Category.Name}"/>
</StackLayout>
</ContentPage.Content>
</ContentPage>
When I select an ExerciseCategory from the CategoryPicker, Binding works. However, the ExerciseCategory that is prvided to the ViewModel via the "ex" parameter does not result in the proper Category being selected in the Picker.
Since the Categories are not (necessarily) present by the time I call InitializeComponent, I suspect this disconnection between the Exercise.Category and the Picker.SelectedItem comes from that late binding. Still, I cannot really figure out how to fix that. Any help is appreciated.
public class Zicker : INotifyPropertyChanged
{
public class MyClass
{
public string HeyName { get; set; }
public string HeySurname { get; set; }
public int HeyAge { get; set; }
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged([CallerMemberName] string name = null)
{
if (PropertyChanged != null)
{
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(name));
}
}
private ObservableCollection<MyClass> _yourList = new ObservableCollection<MyClass>();
public ObservableCollection<MyClass> YourList
{
get
{
return _yourList;
}
set
{
_yourList = value;
RaisePropertyChanged("YourList");
RaisePropertyChanged("BindMeLabel");
}
}
public int BindMeLabel
{
get { return _yourList.Sum(a => a.HeyAge); }
}
public void WonCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
RaisePropertyChanged("BindMeLabel");
}
public List<string> heresamplenames = new List<string> { "Mohamed", "Zaran", "Ivan" };
public List<string> heresamplesurnames = new List<string> { "Pakou", "Simmone", "Zagoev" };
public List<int> heresampleages = new List<int> { 17,33,50 };
public Zicker()
{
ObservableCollection<MyClass> vs = new ObservableCollection<MyClass>();
for (int i = 0; i < 3; i++)
{ vs.Add(new MyClass { HeyName = heresamplenames[i], HeySurname = heresamplesurnames[i], HeyAge = heresampleages[i] }); }
YourList = vs; YourList.CollectionChanged += WonCollectionChanged;
}
}
<ContentPage.Content>
<StackLayout Orientation="Vertical" HorizontalOptions="Center" VerticalOptions="Center">
<ContentView HorizontalOptions="Fill" VerticalOptions="Fill">
<ListView HorizontalOptions="Center" VerticalOptions="Center" HasUnevenRows="True" ItemsSource="{Binding YourList}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ViewCell.View>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"></ColumnDefinition>
<ColumnDefinition Width="1*"></ColumnDefinition>
<ColumnDefinition Width="1*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Label HorizontalTextAlignment="Center" VerticalTextAlignment="Center" Text="{Binding Path=HeyName}" Grid.Column="0" FontSize="12" TextColor="Black"></Label>
<Label HorizontalTextAlignment="Center" VerticalTextAlignment="Center" Text="{Binding Path=HeySurname}" FontSize="12" TextColor="Black" Grid.Column="1"/>
<Entry HorizontalTextAlignment="Center" VerticalOptions="Center" Text="{Binding Path=HeyAge}" FontSize="12" Keyboard="Numeric" TextColor="Black" Grid.Column="2"/>
</Grid>
</ViewCell.View>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentView>
<Label Text="{Binding BindMeLabel}" HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand" FontSize="40" TextColor="Black"></Label>
</StackLayout>
</ContentPage.Content>
public MainPage()
{
InitializeComponent();
BindingContext = new Zicker();
}
My Problem: In this List, there are three names, surnames, and ages. At the bottom, there is also a label which should be shown as the sum of Ages collection.
When the UI is starting, Label is working well. But, if I try to change any Ages entries, there is a big problem with the binding label.
I want to use MVVM structure but due to this problem, label binding is working just start up.
If you are updating the HeyName property, binding is not updating because the class MyClass does not implement INotifyPropertyChanged.
Try to replace the MyClass class with this code:
public class MyClass : INotifyPropertyChanged
{
private string name;
private string surname;
private int age;
public string HeyName
{
get => name;
set
{
name = value;
RaisePropertyChanged("HeyName");
}
}
public string HeySurname
{
get => surname;
set
{
surname = value;
RaisePropertyChanged("HeySurname");
}
}
public int HeyAge
{
get => age;
set
{
age = value;
RaisePropertyChanged("HeyAge");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged([CallerMemberName] string name = null)
{
if (PropertyChanged != null)
{
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(name));
}
}
}
EDIT:
Sorry, the CollectionChanged is not called when you update the HeyAge property, because it is called only if the collection is changed, but not when a property of an item in the collection changes.
Try to add the OnAgeChanged event into the class MyClass and call it when the HeyAge property change:
public class MyClass : INotifyPropertyChanged
{
public event EventHandler OnAgeChanged;
public int HeyAge
{
get => age;
set
{
age = value;
RaisePropertyChanged("HeyAge");
OnAgeChanged?.Invoke(this, EventArgs.Empty);
}
}
...
...
Then, when you add a new MyClass object into the collection, register the event in the ViewModel like this:
public Zicker()
{
ObservableCollection<MyClass> vs = new ObservableCollection<MyClass>();
for (int i = 0; i < 3; i++)
{
var test = new MyClass()
{
HeyName = heresamplenames[i],
HeySurname = heresamplesurnames[i],
HeyAge = heresampleages[i],
};
test.OnAgeChanged += Test_OnAgeChanged;
vs.Add(test);
}
YourList = vs;
YourList.CollectionChanged += WonCollectionChanged;
}
private void Test_OnAgeChanged(object sender, EventArgs e)
{
RaisePropertyChanged("BindMeLabel");
}
Note that the WonCollectionChanged it's not necessary any more.
Note also that the variable vs is not needed, you can work directly into the YourList object instead.
Using Visual Studio 2015 proff,
My LoginViewModel Class (Portable Class Library)
public class LoginViewModel : INotifyPropertyChanged, INotifyCollectionChanged
{
LoginPage page;
private ObservableCollection<Employees> _employeeList;
private string _loginName;
public ObservableCollection<Employees> EmployeeList
{
get { return _employeeList; }
set
{
_employeeList = value;
OnPropertyChanged();
OnCollectionChanged(NotifyCollectionChangedAction.Reset);
}
}
public string LoginName
{
get { return _loginName; }
set
{
_loginName = value;
if (_loginName != null)
{
OnPropertyChanged();
}
}
}
public LoginViewModel(LoginPage parent)
{
page = parent;
}
public async void GetEmployees()
{
var loginService = new LoginService();
EmployeeList = await loginService.GetEmployeesAsync();
}
public event PropertyChangedEventHandler PropertyChanged;
public event NotifyCollectionChangedEventHandler CollectionChanged;
protected virtual void OnCollectionChanged( NotifyCollectionChangedAction action)
{
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(action));
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
My LoginPage.xaml (Portable Class Library)
<?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="ScannerApp.Views.LoginPage"
xmlns:ViewModels="clr-namespace:ScannerApp.ViewModels;assembly=ScannerApp">
<StackLayout Orientation="Vertical">
<Label Text="Please Login"
VerticalOptions="Start"
HorizontalTextAlignment="Center"
IsVisible="true"
FontSize="Large"
FontAttributes="Bold" />
<ListView x:Name="mylist" ItemsSource="{Binding EmployeeList}"
HasUnevenRows="True" SelectedItem="{Binding LoginName}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Vertical" Padding="12,6">
<Label Text="{Binding Name}"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
My LoginPage.xaml.cs Class (Portable Class Library)
public partial class LoginPage : ContentPage
{
public LoginPage()
{
InitializeComponent();
BindingContext = new LoginViewModel(this);
}
protected override void OnAppearing()
{
base.OnAppearing();
LoginViewModel model = new LoginViewModel(this);
model.GetEmployees();
BindingContext = model;
}
public ListView MyList
{
get
{
return mylist;
}
}
}
Question
I get a list of employees, the list on the front end renders this. The user then Selects a name from the list, At this point I would like to detect this and then navigate to my different page.
Currently my property is not hit, I'm wondering if this has anything to do with my Binding on the code behind "OnAppearing"? but I'm not sure.
While what you have may work there are a few tweaks I would suggest.
No need to set your BindingContext in your constructor and in OnAppearing(). Just make your LoginViewModel a class level private property in your code-behind and only assign it to your BindingContext in your constructor. Then call GetEmployees() in OnAppearing().
Also, you should make GetEmployees() return a Task, in order to await as far up the chain as possible.
ViewModel:
....
public async Task GetEmployees()
{
var loginService = new LoginService();
EmployeeList = await loginService.GetEmployeesAsync();
}
....
Code-behind:
public partial class LoginPage : ContentPage
{
private LoginViewModel _model;
public LoginPage()
{
InitializeComponent();
BindingContext = _model = new LoginViewModel(this);
}
protected override async void OnAppearing() //Notice I changed this to async and can now await the GetEmployees() call
{
base.OnAppearing();
await _model.GetEmployees();
}
public ListView MyList
{
get
{
return mylist;
}
}
private async void OnItemSelected(object sender, SelectedItemChangedEventArgs e) {
if (e.SelectedItem == null) return;
await Navigation.PushAsync(new MenuPage());
}
}
XAML:
<!-- Adding OnItemSelected in XAML below -->
<ListView x:Name="mylist"
ItemsSource="{Binding EmployeeList}"
HasUnevenRows="True"
SelectedItem="{Binding LoginName}"
ItemSelected="OnItemSelected">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Vertical" Padding="12,6">
<Label Text="{Binding Name}"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
mylist.ItemSelected += async (sender, e) => {
if (e.SelectedItem == null) return;
await Navigation.PushAsync(new MenuPage());
};
This works, had to set the page to a nav wrap in the App.cs, then apply this event handler to the OnAppearing method.