INotifyPropertyChanged Xamarin string value just disappears - xamarin

I have a Xamarin.Forms project with a ListView populated with an ObservableCollection. The class (object) that is each item in the ObservableCollection implements INotifyPropertyChanged. A Color property toggles fine in the UI but a string property disappears and never returns.
I get the initial values from a webservice but then just do a completely static change of the values, for debugging and I still can't figure it out.
At the top of the my ContentPage class I have this:
public ObservableCollection<GroceryItem> oc;
After my webservice has returned with the data I put the data in the ObservableCollection and make that, the ItemsSource for the listview. Like this:
lvGroceries.ItemsSource = oc;
All that works great.
The XAML
<ListView x:Name="lvGroceries" ItemTapped="GroceryPdsItemTapped" >
<ListView.ItemTemplate >
<DataTemplate>
<ViewCell>
<AbsoluteLayout VerticalOptions="Fill">
<Label Text="{Binding GroceryName}" AbsoluteLayout.LayoutBounds="0,0,200,40" ></Label>
<Label Text="{Binding strHomeLoc}" AbsoluteLayout.LayoutBounds="200,0,100,40" ></Label>
<Label Text="{Binding isNeeded}" AbsoluteLayout.LayoutBounds="300,0,50,40" ></Label>
<Label Text="someText" BackgroundColor="{Binding myBackgroundColor}" AbsoluteLayout.LayoutBounds="350,0,50,40" ></Label>
</AbsoluteLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
The class - GroceryItem
public class GroceryItem : INotifyPropertyChanged
{
public GroceryItem() { }
public event PropertyChangedEventHandler PropertyChanged;
private string privateIsNeeded;
public string isNeeded
{
get { return privateIsNeeded; }
set
{
privateIsNeeded = value;
OnPropertyChanged();
}
}
private Color theColor;
public Color myBackgroundColor
{
get { return theColor; }
set
{
theColor = value;
OnPropertyChanged();
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
The click handler. I grab an item from the ObservableCollection and change the two properties.
public void GroceryPdsItemTapped(object obj, ItemTappedEventArgs e)
{
if (e.Item == null)
{
return;
}
var g = ((GroceryItem)e.Item);
foreach (var gr in oc)
{
if (gr.GroceryId == "27769")
{ // the UI changes because the myBackgroundColor property in the GroceryItem class is watching for a value change
gr.myBackgroundColor = (gr.myBackgroundColor == Color.Yellow) ? Color.Blue : Color.Yellow;
gr.isNeeded = (gr.isNeeded == "true" || gr.isNeeded == "blah" || gr.isNeeded == "false") ? "notblah" : "blah";
}
}
}
The Color toggles fine in the UI but the isNeeded string value disappears on the first tap and never re-appears
Ideas?

A couple issues here. First is that you need to make your ObservableCollection run OnPropertyChanged() when it has been changed like so:
private ObservableCollection<GroceryItem> _oc;
public ObservableCollection<GroceryItem> oc {
get { return _oc ?? (_oc = new ObservableCollection<GroceryItem>()); }
set {
if(_oc != value) {
_oc = value;
OnPropertyChanged();
}
}
}
Now you should really have all of this in a ViewModel but since you do not, you need to set your ContentPage as the BindingContext from within your code-behind like this:
public partial class MyGroceryPage : ContentPage {
public MyGroceryPage() { BindingContext = this; }
}
You also need to be binding your ObservableCollection to your ListView.ItemSource instead of assigning it. That looks like this:
<ListView ItemSource="{Binding oc}"/>
If you do the above and then you go into your code behind and execute lvGroceries.ItemsSource = oc; then that would overwrite the binding that you did in your XAML, so do not do that. Instead, when you get data from your web service, you would just assign it to your existing ObservableCollection:
public async Task GetGroceryData() {
List<GroceryItem> myData = await GroceryService.GetGroceriesAsync();
oc = new ObservableCollection<GroceryItem>(myData);
}
Try all of that first and if your items are still not updating you might want to try removing them from your ObservableCollection, changing the properties, then adding them back in:
public void GroceryPdsItemTapped(object obj, ItemTappedEventArgs e) {
if (e.Item == null) { return; }
var g = ((GroceryItem)e.Item);
foreach (var gr in oc.ToList()) {
if (gr.GroceryId == "27769") { // the UI changes because the myBackgroundColor property in the GroceryItem class is watching for a value change
oc.Remove(gr);
gr.myBackgroundColor = (gr.myBackgroundColor == Color.Yellow) ? Color.Blue : Color.Yellow;
gr.isNeeded = (gr.isNeeded == "true" || gr.isNeeded == "blah" || gr.isNeeded == "false") ? "notblah" : "blah";
oc.Add(gr);
}
}
}

Related

Xamarin Forms: How to Change the textcolor of Collectionview SelectedItem?

I have a CarouselPage having 5 children and every child has a horizontal collection view. When selecting an item in Collectionview or swiping the pages, I need to give a different text color and need to add an underline for the selected item. I have tried like below:
CarouselHomePage.cs
public partial class CarouselHomePage : CarouselPage
{
public List<Activity> activityList { get; set; }
public CarouselHomePage()
{
InitializeComponent();
activityList = new List<Activity>();
AddActivities();
MessagingCenter.Subscribe<App, string>((App)Xamarin.Forms.Application.Current, "child", (s, child) =>
{
CurrentPage = Children[Int32.Parse(child)];
});
}
private void AddActivities()
{
activityList.Add(new Activity() { Title = "PageNumber1" });
activityList.Add(new Activity() { Title = "PageNumber2" });
activityList.Add(new Activity() { Title = "PageNumber3" });
activityList.Add(new Activity() { Title = "PageNumber4" });
activityList.Add(new Activity() { Title = "PageNumber5" });
AddChild(activityList);
}
public void AddChild(List<Activity> activityList)
{
this.Children.Add(new PageNumber1(activityList));
this.Children.Add(new PageNumber2(activityList));
this.Children.Add(new PageNumber3(activityList));
this.Children.Add(new PageNumber4(activityList));
this.Children.Add(new PageNumber5(activityList));
}
}
Activity.cs
public class Activity
{
public string Title { get; set; }
public bool visibility { get; set; }
public bool Visibility
{
set
{
if (value != null)
{
visibility = value;
NotifyPropertyChanged();
}
}
get
{
return visibility;
}
}
private Color textColor;
public Color TextColor
{
set
{
if (value != null)
{
textColor = value;
NotifyPropertyChanged();
}
}
get
{
return textColor;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
PageNumber1.xaml
<ContentPage.Content>
<StackLayout Orientation="Vertical">
<CollectionView
SelectionMode="Single"
x:Name="ActivityList"
Margin="5,10,5,10"
SelectionChanged="TagItemTapped"
ItemsLayout="HorizontalList">
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout
Orientation="Vertical"
Margin="15">
<Label
TextColor="{Binding TextColor}"
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center"
Text="{Binding Title}">
<Label.FontSize>
<OnIdiom x:TypeArguments="x:Double">
<OnIdiom.Phone>18</OnIdiom.Phone>
<OnIdiom.Tablet>27</OnIdiom.Tablet>
<OnIdiom.Desktop>18</OnIdiom.Desktop>
</OnIdiom>
</Label.FontSize>
</Label>
<BoxView
HeightRequest="2"
IsVisible="{Binding Visibility}"
BackgroundColor="{Binding TextColor}"
HorizontalOptions="CenterAndExpand"
VerticalOptions="Start"/>
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
<CollectionView.HeightRequest>
<OnIdiom x:TypeArguments="x:Double">
<OnIdiom.Phone>30</OnIdiom.Phone>
<OnIdiom.Tablet>60</OnIdiom.Tablet>
<OnIdiom.Desktop>30</OnIdiom.Desktop>
</OnIdiom>
</CollectionView.HeightRequest>
</CollectionView>
<Label Text="Welcome to PageNumber1"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand" />
</StackLayout>
</ContentPage.Content>
PageNumber1.xaml.cs
public partial class PageNumber1 : ContentPage
{
public PageNumber1(List<Activity> activityList)
{
InitializeComponent();
if (activityList == null)
{
ActivityList.IsVisible = false;
}
else
{
for (int i = 0; i < activityList.Count; i++)
{
if (activityList[i].Title == "PageNumber1")
{
activityList[i].TextColor = Color.FromHex("#26b4d8");
activityList[i].Visibility = true;
}
else
{
activityList[i].TextColor = Color.Gray;
activityList[i].Visibility = false;
}
}
ActivityList.ItemsSource = activityList;
}
}
public void TagItemTapped(object sender, SelectionChangedEventArgs e)
{
var selectedItem = (e.CurrentSelection.FirstOrDefault() as Activity);
if (selectedItem != null)
{
string childnumber = "";
if (selectedItem.Title == "PageNumber1")
{
childnumber = "0";
}
else if (selectedItem.Title == "PageNumber2")
{
childnumber = "1";
}
else if (selectedItem.Title == "PageNumber3")
{
childnumber = "2";
}
else if (selectedItem.Title == "PageNumber4")
{
childnumber = "3";
}
else if (selectedItem.Title == "PageNumber5")
{
childnumber = "4";
}
MessagingCenter.Send<App, string>((App)Xamarin.Forms.Application.Current, "child", childnumber);
}
}
}
I have added the same code on all the other child pages with the corresponding title in the if statement. But the selected page title color is not working and underline is not showing.
Screenshot:
Also if I select the last item in the collectionview, I need to scroll the collection on the last child to the last item. For this I have used ScrollTo feature of Collectioview. But that is also not working.
protected override void OnAppearing()
{
ActivityList.ScrollTo(4);
}
The above code will work if I manually swipe the pages. When directly tap the collectionview item, the scrolling is not working.
I have uploaded a sample project here.
About underline not showing , the reason is HeightRequest of CollectionView setted too small with 30 .
Modify that to above 35 , it will show correcttly . Such as :
<CollectionView.HeightRequest>
<OnIdiom x:TypeArguments="x:Double">
<OnIdiom.Phone>40</OnIdiom.Phone>
<OnIdiom.Tablet>60</OnIdiom.Tablet>
<OnIdiom.Desktop>30</OnIdiom.Desktop>
</OnIdiom>
</CollectionView.HeightRequest>
The effect :
About selected problem , this is the sample project here .

How to set/get the properties Grial Repeater?

refer to http://docs.grialkit.com/repeater.html, it got a property
call 'SelectedItem', i want to know the code in detail how to set or get it ?
Please help, thanks!
This is how you will get the SelectedItem, I am assuming you are following the MVVM approach.
XAML:-
<grial:Repeater Spacing="8"
Padding="10"
ScrollPadding="10"
ItemsSource="{Binding Items}"
SelectedItem= "{Binding SelectedItemObject}">
<grial:Repeater.ItemTemplate>
<!--DEFAULT ITEM TEMPLATE-->
</grial:Repeater.ItemTemplate>
</grial:Repeater>
ViewModel:
private ModelClass _selectedItemObject;
public ModelClass SelectedItemObject
{
get { return _selectedItemObject; }
set
{
_selectedItemObject = value;
OnPropertyChanged("SelectedItemObject");
OnItemSelected(SelectedItemObject);
}
}
private List<ModelClass> _items;
public List<ModelClass> Items
{
get{ return _items;}
set
{
_items = value;
OnPropertyChanged("Items");
}
}
private void OnItemSelected(ModelClass selectedItem)
{
if (selectedItem == null)
return;
// Perform your logic her on selection
}

Button not disabled on page load in Xamarin.Forms

I have a button in my page. I want to disable this button on page load, but its not disabled.
XAML:
<Button IsEnabled="{Binding IsEnabledSaveBtn,Mode=TwoWay}" Text="Save" Command="{Binding SaveItem}" />
ViewModel:
bool _isEnabledSaveBtn = false;
public bool IsEnabledSaveBtn
{
get { return _isEnabledSaveBtn; }
set
{
_isEnabledSaveBtn = value;
OnPropertyChanged();
}
}
Inside ViewModel Constructor:
public CreateDiscountViewModel(INavigation navigation)
{
Navigation = navigation;
IsEnabledSaveBtn=False;
}
I'm also validating data on TextChange event and it's working fine. But I want to disable this button on page load.
How can I solve this?
You need the following changes:
In your XAML you do not need two-way binding :
<Button IsEnabled="{Binding IsEnabledSaveBtn}" Text="Save" Command="{Binding SaveItem}" />
Your Property should look something like this:
private bool _isEnabledSaveBtn;
public bool IsEnabledSaveBtn
{
get { return _isEnabledSaveBtn; }
set
{
_isEnabledSaveBtn = value;
OnPropertyChanged(nameof(IsEnabledSaveBtn));
}
}
And in your ViewModel constructor set the value:
public CreateDiscountViewModel(INavigation navigation)
{
Navigation = navigation;
IsEnabledSaveBtn = false;
}
Something seems to be wrong with your OnPropertyChanged event.
Try the following, it will work.
is Button Enabled:
bool _isEnabledSaveBtn = false;
public bool IsEnabledSaveBtn
{
get { return _isEnabledSaveBtn; }
set
{
_isEnabledSaveBtn = value;
OnPropertyChanged("IsEnabledSaveBtn");
}
}
OnPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
and in your contructor
IsEnabledSaveBtn = false;

Where to set bindings for a custom cell with customer renderer if I am using a DataTemplateSelector?

I have a DataTemplateSelector that selects between two different cells. On Android, this template picks cells that are defined as Android xml files. I can confirm that the template selector is working because I have two different color circles showing, and the colors are correct. But my data is not being bound and I am not sure why. I think I am not setting the binding somewhere, but I am not sure where/how to do that.
Here is my page that includes the ListViewwith the DataTemplateSelector. I set the ItemsSourcehere, but I never set the bindings for the different parts of the list items. That is where I do not know what to do.
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyApp.Pages.Routines.TopLevelRoutinesPage"
xmlns:statics="clr-namespace:MyApp.Statics;assembly=MyApp"
xmlns:controls="clr-namespace:MyApp.Controls;assembly=MyApp">
<ContentPage.Resources>
<ResourceDictionary>
<controls:RoutinesDataTemplateSelector x:Key="RoutinesDataTemplateSelector"></controls:RoutinesDataTemplateSelector>
</ResourceDictionary>
</ContentPage.Resources>
<ContentPage.Content>
<StackLayout VerticalOptions="FillAndExpand"
HorizontalOptions="FillAndExpand"
Orientation="Vertical"
Spacing="0">
<ListView ItemsSource="{Binding SelectedRoutineTree}"
ItemTemplate="{StaticResource RoutinesDataTemplateSelector}"
x:Name="RoutinesView"
ItemSelected="RoutineClicked"
Margin ="0, 8, 0, 0">
</ListView>
</StackLayout>
</ContentPage.Content>
</ContentPage>
The code-behind:
using MyApp.ViewModels;
using MyCloudContracts.DTOs;
using System;
using System.Linq;
using Xamarin.Forms;
namespace MyApp.Pages.Routines
{
public partial class TopLevelRoutinesPage : ContentPage
{
private TopLevelRoutinesViewModel _viewModel;
private string _projCompName;
public TopLevelRoutinesPage(Guid docId, bool fromCompany, string projCompName)
{
InitializeComponent();
_projCompName = projCompName;
Title = _projCompName;
_viewModel = new TopLevelRoutinesViewModel(docId, fromCompany);
BindingContext = _viewModel;
if (Device.OS == TargetPlatform.Android)
RoutinesView.SeparatorVisibility = SeparatorVisibility.None;
}
private async void RoutineClicked(object sender, SelectedItemChangedEventArgs e)
{
//since this is also called when an item is deselected, return if set to null
if (e.SelectedItem == null)
return;
var selectedRoutine = (PublishedDocumentFragmentDTO)e.SelectedItem;
var fragId = selectedRoutine.FragmentId;
var title = selectedRoutine.Title;
var blobIdStr = selectedRoutine.BlobId;
var blobId = new Guid(blobIdStr);
if (selectedRoutine.Children.Any())
{
var routineTree = _viewModel.SelectedRoutineTree;
var subroutinesPage = new SubroutinesPage(routineTree, fragId, title, blobId, _projCompName);
await Navigation.PushAsync(subroutinesPage);
}
else
{
var routinePage = new RoutinePage(title, blobId);
await Navigation.PushAsync(routinePage);
}
//take away selected background
((ListView)sender).SelectedItem = null;
}
}
}
The DataTemplateSelector
using MyApp.Pages.Routines.CustomCells;
using MyCloudContracts.DTOs;
using Xamarin.Forms;
namespace MyApp.Controls
{
class RoutinesDataTemplateSelector : DataTemplateSelector
{
private readonly DataTemplate _folderDataTemplate;
private readonly DataTemplate _routineDataTemplate;
public RoutinesDataTemplateSelector()
{
_folderDataTemplate = new DataTemplate(typeof(FolderViewCell));
_routineDataTemplate = new DataTemplate(typeof(RoutineViewCell));
}
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
{
var chooser = item as PublishedDocumentFragmentDTO;
if (chooser == null)
return null;
else if (chooser.Children.Length == 0)
{
return _routineDataTemplate;
}
else
{
return _folderDataTemplate;
}
}
}
}
And an example of one of my custom ViewCells. I think this is where I am wrong, but I am not sure why. I make the properties, but I do not know how to set them properly.
using Xamarin.Forms;
namespace MyApp.Pages.Routines.CustomCells
{
public class RoutineViewCell : ViewCell
{
public static readonly BindableProperty TitleProperty =
BindableProperty.Create("Title", typeof(string), typeof(RoutineViewCell), "");
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
}
}
Thanks for the help :)
I found the answer. I needed to override OnBindingContextChanged() in the custom cell file. My working code looks like this now:
using Xamarin.Forms;
namespace MyApp.Pages.Routines.CustomCells
{
public class RoutineViewCell : ViewCell
{
public static readonly BindableProperty TitleProperty =
BindableProperty.Create("Title", typeof(string), typeof(RoutineViewCell), "");
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
protected override void OnBindingContextChanged()
{
this.SetBinding(TitleProperty, "Title");
base.OnBindingContextChanged();
}
}
}

Async loading of images

I want to use best way to show images in my PanoramaPage. I download one page and shows it´s information a then I want to async load another page with images. So I am using HttpWebRequest and I get response. Everything is okay and hope this is best way for these. So I create my GaleryViewModel and for all images at page I add url to my class.
And there is a problem. I can´t see images in view. This i my view:
<ListBox ItemsSource="{Binding Images}" x:Name="listImages" Height="652" Canvas.Top="80">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="0,0,0,17">
<Image Height="100" Width="100" Margin="12,0,9,0" >
<Image.Source>
<BitmapImage UriSource="{Binding ImgURL}" CreateOptions="BackgroundCreation"/>
</Image.Source>
</Image>
<TextBlock Text="{Binding LineOne}" TextWrapping="Wrap" Style="{StaticResource PhoneTextExtraLargeStyle}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
This is content of my WebResponse event handler:
MovieExt movie = this.DataContext as MovieExt;
HtmlDocument doc = new HtmlDocument();
doc.LoadHtml(response);
var photos = from ti in doc.DocumentNode.Descendants("div")
where ti.Attributes["class"] != null && ti.Attributes["class"].Value == "photo"
select ti;
Regex rgx = new Regex("http://[0-9a-zA-Z_./]+");
foreach (var photo in photos)
{
GaleryViewModel fotka = new GaleryViewModel();
string style = photo.Attributes["style"].Value;
MatchCollection matches = rgx.Matches(style);
if (matches.Count > 0)
{
foreach (Match match in matches)
fotka.ImgURL = match.Value;
}
fotka.LineOne = "Test";
movie.Images.Add(fotka);
}
this.DataContext = movie;
this.listImages.ItemsSource = movie.Images;
and for all GaleryViewModel and MovieExt:
public class GaleryViewModel : INotifyPropertyChanged
{
private string _imgUrl;
public string ImgURL
{
get
{
return _imgUrl;
}
set
{
if (value != _imgUrl)
{
_imgUrl = value;
NotifyPropertyChanged("ImgURL");
}
}
}
private string _lineOne;
public string LineOne
{
get
{
return _lineOne;
}
set
{
if (value != _lineOne)
{
_lineOne = value;
NotifyPropertyChanged("LineOne");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (null != handler)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
public class MovieExt
{
public string Title { get; set; }
public string Year { get; set; }
public string Url { get; set; }
...
public List<GaleryViewModel> Images { get; set; }
...
}
I am not sure what I am doing wrong but I think that is something with binding. Thanks for help
Looking at this it looks like you haven't notified that the MovieExt.Images property has changed. Without doing this your ListBox won't update its Items. To do this MovieExt will also need to support INotifyPropertyChanged.
You have to convert Uri to BitmapImage, because BitmapImage UriSource is a stream. Please take a look at this: Image UriSource and Data Binding
Replace loading method (just for test) to:
MovieExt movie = new MovieExt();
movie.Images = new List<GaleryViewModel>();
GaleryViewModel fotka1 = new GaleryViewModel();
fotka1.LineOne = "line1";
fotka1.ImgURL = "http://proservice.kiev.ua/wp-content/uploads/2011/10/apple-logo.jpg";
GaleryViewModel fotka2 = new GaleryViewModel();
fotka2.LineOne = "line2";
fotka2.ImgURL = "http://www.mykhailenko.com/blog/wp-content/uploads/2010/12/apple-logo-history-2.png";
movie.Images.Add(fotka1);
movie.Images.Add(fotka2);
listImages.ItemsSource = movie.Images;
and it works
I guess the problem is in this line:
if (matches.Count > 0)
You have no matches, so your Url is empty
Can you provide us data your service return to debug code in your context?
Also, why you need loop for this assignment?
foreach (Match match in matches)
fotka.ImgURL = match.Value;

Resources