Update display of one item in a ListView's ObservableCollection - xamarin.forms

I have a ListView which is bound to an ObservableCollection.
Is there a way to update a single cell whenever a property of a SomeModel item changed, without reloading the ListView by changing the ObservableCollection?
(Question is copied from https://forums.xamarin.com/discussion/40084/update-item-properties-in-a-listviews-observablecollection, as is my answer there.)

As I can see you are trying to use MVVM as a pattern for your Xamarin.Forms app. You are already using the ObservableCollection for displaying a list of the data. When a new item is added or removed from collection UI will be refreshed accordingly and that is because the ObserverbleCollection is implementing INotifyCollectionChanged.
What you want to achieve with this question is next behaviour, when you want to change the particular value for the item in the collection and update the UI the best and simplest way to achieve that is to implement INotifyPropertyChanged for a model of the item from your collection.
Bellow, I have a simple demo example on how to achieve that, your answer is working as I can see but I am sure this example would be nicer for you to use it.
I have simple Button with command and ListView which holds my collection data.
Here is my page, SimpleMvvmExamplePage.xaml:
<StackLayout>
<Button Text="Set status"
Command="{Binding SetStatusCommand}"
Margin="6"/>
<ListView ItemsSource="{Binding Cars}"
HasUnevenRows="True">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Vertical"
Margin="8">
<Label Text="{Binding Name}"
FontAttributes="Bold" />
<StackLayout Orientation="Horizontal">
<Label Text="Seen?"
VerticalOptions="Center"/>
<CheckBox IsChecked="{Binding Seen}"
Margin="8,0,0,0"
VerticalOptions="Center"
IsEnabled="False" />
</StackLayout>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
The basic idea from this demo is to change the value of the property Seen and set value for the CheckBox when the user clicks on that Button above the ListView.
This is my Car.cs class.
public class Car : INotifyPropertyChanged
{
private string name;
public string Name
{
get { return name; }
set
{
name = value;
OnPropertyChanged();
}
}
private bool seen;
public bool Seen
{
get { return seen; }
set
{
seen = value;
OnPropertyChanged();
}
}
// Make base class for this logic, something like BindableBase
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
In the full demo example which is on my Github, I am using my BindableBase class where I handle raising the INotifyPropertyChanged when some property value is changed with this SetProperty method in the setter of the props.
You can find the implementation here: https://github.com/almirvuk/Theatrum/tree/master/Theatrum.Mobile/Theatrum.Mobile
The last thing to show is my ViewModel for this page, and inside of the ViewModel, I will change the value of Seen property to True for items in the collection when the user clicks on the Button above the ListView. Here is my SimpleMvvmExamplePageViewModel.cs
public class SimpleMvvmExamplePageViewModel
{
public ObservableCollection<Car> Cars { get; set; }
public ICommand SetStatusCommand { get; private set; }
public SimpleMvvmExamplePageViewModel()
{
// Set simple dummy data for our ObservableCollection of Cars
Cars = new ObservableCollection<Car>()
{
new Car()
{
Name = "Audi R8",
Seen = false
},
new Car()
{
Name = "BMW M5",
Seen = false
},
new Car()
{
Name = "Ferrari 430 Scuderia",
Seen = false
},
new Car()
{
Name = "Lamborghini Veneno",
Seen = false
},
new Car()
{
Name = "Mercedes-AMG GT R",
Seen = false
}
};
SetStatusCommand = new Command(SetStatus);
}
private void SetStatus()
{
Car selectedCar = Cars.Where(c => c.Seen == false)
.FirstOrDefault();
if (selectedCar != null)
{
// Change the value and update UI automatically
selectedCar.Seen = true;
}
}
}
This code will help us to achieve this kind of behaviour: When the user clicks on the Button we will change value of the property of the item from collection and UI will be refreshed, checkbox value will be checked.
The final result of this demo could be seen on this gif bellow.
P.S. I could combine this with ItemTapped event from ListView but I wanted to make this very simple so this example is like this.
Hope this was helpful for you, wishing you lots of luck with coding!

Any UI associated with a model item will be refreshed, if replace the item with itself, in the Observable Collection.
Details:
In ViewModel, given property:
public ObservableCollection<Item> Items { get; set; } = new ObservableCollection<Item>();
Where Item is your model class.
After adding some items (not shown), suppose you want to cause item "item" to refresh itself:
public void RefreshMe(Item item)
{
// Replace the item with itself.
Items[Items.IndexOf(item)] = item;
}
NOTE: The above code assumes "item" is known to be in "Items". If this is not known, test that IndexOf returns >= 0 before performing the replacement.
In my case, I had a DataTemplateSelector on the collection, and the item was changed in such a way that a different template was required. (Specifically, clicking on the item toggled it between collapsed view and expanded/detailed view, by the TemplateSelector reading an IsExpanded property from the model item.)
NOTE: tested with a CollectionView, but AFAIK will also work with the older ListView class.
Tested on iOS and Android.
Technical Note:
This replacement of an item presumably triggers a Replace NotifyCollectionChangedEvent, with newItems and oldItems both containing only item.

Related

How to pass data using PopAsync?

I have the following situation:
I have an app, where I want to create notes using a special page for it. I have VM with ObservableCollection NoteItems with all the notes. When I want to create a new note, I add a new note to this ObservableCollection and pass this new note using BindingContext
var editingPage = new EditingPage();
editingPage.BindingContext = NoteItems[NoteItems.Count - 1].Text;
Application.Current.MainPage.Navigation.PushAsync(editingPage);
In the editing page I have an Entry field
<Entry
x:Name="EdtingPageEntryField"
Text="{Binding Text}"
Placeholder="Enter a note"
/>
, which is Binding his text with the Text parameter of a NoteItem.
The problem is that if I change the text in entry field, it does not automatically apply to a Text parameter of a NoteItem. That is why I want to pass this text when I close this EditingPage(go back to the MainPage). So the question is, how can I pass this Text parameter to a NoteItem element from NoteItems ObservableCollection, which is located in VM.
UPD. The value of NoteItem, which is located in NoteItems does not change
UPD2. I was wrong about the value of NoteItem, it has been changed, but the new value does not display on MainPage, that is why I used INotifyPropertyChanged, but it did not work.
Here is Note Item class
public class NoteItem
{
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private string text;
public string Text
{
get { return text; }
set
{
if(text != value)
{
text = value;
NotifyPropertyChanged();
}
}
}
And MainPage.xaml:
<ContentPage.BindingContext>
<local:NoteItemViewModel/>
</ContentPage.BindingContext>
<FlexLayout>
<ListView ItemsSource="{Binding NoteItems}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Label Text="{Binding Text}"/>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</FlexLayout>
'''
in order to dynamically update the UI, your model must implement INotifyPropertyChanged
public class NoteItem : INotifyPropertyChanged
{
...
}
simply adding a PropertyChanged method is not the same as implementing INotifyPropertyChanged. You must add the interface to the class definition so that the binding mechanism knows that your class has implemented it

Entry controls in a CollectionView aren't updating the source they're bound to (Xamarin.Forms, UWP)

I'm creating an educational app for learning terminology... using UWP via Xamarin.Forms for the data entry. This view is for creating lists of terms and saving them to my database. In this case, we're making a list for bones of the body. Here's what it looks like:
Pretty simple UI. Every time I click "Add New Item(s)" it creates a "<*New Item*>" Entry at the top of my CollectionView. I overwrite "<*New Item*>" with the word I want, and then click the "Save" button.
The "Add New Item(s)" button works like a champ, but the "Save" button is sick. After clicking "Save" the first time, it sends the list to my json database just fine. When I inspect the database I see "<*New Item*>" along with the rest of the list, like so:
"name": "Bones of the Body, Common",
"choiceFormat": "6x1",
"items": [
"<*New Item*>",
"Ankle Bone",
"Arm Bone",
"Breastbone",
"Cheek Bone",
"Collarbone",
"Forearm Bone (Large)",
"Forearm Bone (Small)",
"Hammer",
"Hard Palate (back)",
"Heel Bone",
"Hip Bone",
"Knee Cap",
"Leg Bone (Large)",
"Leg Bone (Small)",
"Lower Jaw",
"Rib",
"Rib Cage",
"Shoulder Blade",
"Spine",
"Stirrup",
"Tailbone",
"Thigh Bone",
"Tongue Bone",
"Upper Jaw"
],
"spellable": true
Now in my UI, I click into the top Entry for "<*New Item*>" and rename it to "Anvil" like so...
Then when I click save, the database list stays the same... it still says "<*New Item*>" and not "Anvil".
So I dropped a breakpoint in the save function and the debugger's telling me that here's where the issue is... the ObservableCollection that's the source for my CollectionView (named OCItems) doesn't say "Anvil", it's stuck on "<*New Item*>". (The location of the breakpoint is noted in my ViewModel code below.)
My other controls (Choice Format and Spellable) bind, update, and save to the database just fine. What's going on with the Entry controls in my CollectionView? Why don't they update when I replace the text in them? Note that both {Binding}s are TwoWay.
Here's the relevant code:
XAML
...
<Button Text="Add New Item(s)" Grid.Row="2" Grid.Column="0" Command="{Binding AddItemSlot}" /><Button Text="Save" Grid.Row="2" Grid.Column="2" Command="{Binding SaveAnswerSet}" />
<CollectionView x:Name="MyCollectionView" Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3" ItemsSource="{Binding OCItems, Mode=TwoWay}" >
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="x:String">
<Entry Text="{Binding Mode=TwoWay}" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
...
ViewModel
...
public ObservableCollection<string> OCItems { get; set; }
public ICommand AddItemSlot { get; set; }
public ICommand SaveAnswerSet { get; set; }
...
...
// (in constuctor)
AddItemSlot = new Command(() =>
{
OCItems.Insert(0, "<*New Item*>");
});
SaveAnswerSet = new Command(() =>
{
App.Database.Save(OCItems); // BREAKPOINT
});
...
Thanks for your consideration.
Kind regards,
David
Entry controls in a CollectionView aren't updating the source they're bound to
The problem is string does not implement INotifyCollectionChanged interface, so it will not notify the item change after modified. For this scenario, we suggest make a class to wrap string type and implement INotifyCollectionChanged interface. And use ObservableCollection to replace ObservableCollection.
public class StringWrap : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string PropertyName = null)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
}
}
private string _content;
public string Content
{
get
{
return _content;
}
set
{
_content = value;
OnPropertyChanged();
}
}
}

How can i Refresh a object?

I have a property representing an actor
private Actor _actor;
public Actor Actor
{
get => _actor;
set
{
if (_actor != value) {
_actor = value;
OnPropertyChanged("Actor");
}
}
}
and a list, with a checkmark that depends on the state of Actor. When I click over the label the state of Actor shall change the checkmark
private async void OnSelectionAsync(object item)
{
Actor = item;
but I cannot see the changes in my ListView, why?
Edit 1:
in my list, i am binding the actor Text="{Binding Actor.id} to send my converter and to change the item check
<Label IsVisible="False" x:Name="dTd" Text="{Binding Actor.id}" />
<ListView ItemsSource="{Binding Actors}">
...
<Image Source="" IsVisible="{Binding id , Converter={StaticResource MyList}, ConverterParameter={x:Reference dTd}}"/>
As far as I can tell, you are using the text of your label to determine whether an actor is selected or not. Anyway, you are not binding to that text in any way, but you are using the Label itself as a binding parameter. I do not know for sure, but it seems to me as if "binding" the value this way does not work as intended by you, because change notifications are not subscribed to this way. Anyway, when you refresh that list, the bindings are refreshed and the converter is used with the new value. Since ConverterParameter is not a bindable property I doubt that there is any chance to use it this way.
What I'd do is to either extend your Actor class or create a new ActorViewModel class with the property
public bool IsSelected
{
get => isSelected;
set
{
if (value == isSelected)
{
return;
}
isSelected = value;
OnPropertyChanged(nameof(IsSelected));
}
}
From your ListView you can now bind to that property
<Image Source="..." IsVisible="{Binding IsSelected}"/>
All you have to do now is setting the property accordingly
private async void OnSelectionAsync(object item)
{
Actor = item as Actor;
foreach(var actor in Actors)
{
Actor.IsSelected = actor.id == Actor.id;
}
}
this way, the change should reflect in the ListView.

Can't get parent binding context from data template created in code

I'm creating a data template in code in a view model and binding it to a ListView like so
<ListView
x:Name="MainList
ItemTemplate="{Binding JobListTemplate}"
I have a button in the template that binds to a command in the view model.
Previously the data template was hard coded in the page XAML and the button looked like this
<Button
Text="Click me"
Command="{Binding Path=BindingContext.ClickMeCommand, Source={x:Reference MainList}}"
CommandParameter="{Binding .}" />
All worked well.
Now the button is created in code like this
var button = new Button();
button.Text = "Click me;
button.SetBinding(Button.CommandProperty, new Binding("BindingContext.ClickMeCommand",
BindingMode.Default, null, null, null,
new ReferenceExtension { Name = "MainList" }));
and I can't seem to get the parent (list view) binding context for the command properly bind to the vm.
I have read some posts that suggest it can be an issue if the template is not in the same file as the page.
Any ideas?
This was quite complex and took parts from various SO answers and Xamarin forum posts so I thought I'd post the answer.
First I created a ViewCell class where I assembled the view cell in code. Note the _favouriteButton variable.
public class JobListViewCell : ViewCell
{
private Button _favouriteButton;
public JobListViewCell()
{
BuildViewCell();
}
I created the button and bound the command parameter property - this will pass the list view item
_favouriteButton = new Button();
_favouriteButton.SetDynamicResource(VisualElement.StyleProperty, "IconButtonSolid");
_favouriteButton.Text = FontAwesomeIcons.Heart;
_favouriteButton.SetBinding(Button.CommandParameterProperty, new Binding("."));
I created a bindable property called ParentBindingContext - this will allow me to update the binding context on the buttons command
//
// ParentBindingContext
//
public static readonly BindableProperty ParentBindingContextProperty =
BindableProperty.Create(nameof(ParentBindingContext), typeof(object),
typeof(JobListViewCell), "", BindingMode.OneWay, null,
OnParentBindingContextPropertyChanged);
private static void OnParentBindingContextPropertyChanged(BindableObject bindable, object oldvalue, object newvalue)
{
if (bindable is JobListViewCell control)
{
control._favouriteButton?.SetBinding(Button.CommandProperty,
new Binding("ToggleIsFavouriteCommand",
BindingMode.Default, null, null, null,
newvalue));
}
}
public object ParentBindingContext
{
get => (object)GetValue(ParentBindingContextProperty);
set => SetValue(ParentBindingContextProperty, value);
}
Finally, in my page XAML I added the view cell and bound ParentBindingContext to the list view.
<ListView
Grid.Row="1"
ItemsSource="{Binding Jobs}"
SelectedItem="{Binding SelectedJob, Mode=TwoWay}"
x:Name="MainList">
<ListView.ItemTemplate>
<DataTemplate>
<viewCells:JobListViewCell
ParentBindingContext="{Binding BindingContext, Source={x:Reference MainList}}">
</viewCells:JobListViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Here is the command in the view model. The list view item containing the button that was pressed is passed in.
private Command _toggleIsFavouriteCommand;
public Command ToggleIsFavouriteCommand => _toggleIsFavouriteCommand ??= new Command<JobMaster>(ToggleIsFavourite);
private void ToggleIsFavourite(JobMaster jobMaster)
{
jobMaster.IsFavourite = !jobMaster.IsFavourite;
}
Hope this helps someone.
Your setting the source reference is wrong. Refer to this link Data Binding.
var button = new Button
{
Text = "Click me",
};
button.SetBinding(Button.CommandProperty, new Binding("BindingContext.ClickMeCommand",source: listView));

Is there any control for achieving truncate the label & show the full text when click the arrow

I have developed the xamarin forms project. I need to truncate the text as default then show the remaining text once the button clicked. Again the text need to be truncated when click the button. It is like "See more" & "See less" functionality. Please anyone suggest me how to achieve this. I have added the screenshot for further reference.
This is how it should look when expanded:
And this is how it should look like when collapsed:
First of all I think you have the carets wrong. The downward caret is usually the one for an expanded control and vice versa. At the end it's up to you, but you should stick to common UX idioms unless you have a very good reason not to.
If one line of preview text would suffice there is quite an easy solution for your issue: You can set the LineBreakMode of the label when your view is clicked:
XAML:
<?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="Test_SO.ExpansibleControl">
<ContentView.GestureRecognizers>
<TapGestureRecognizer Command="{Binding ClickCommand}" />
</ContentView.GestureRecognizers>
<ContentView.Content>
<Grid RowSpacing="10" Padding="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<Label Text="{Binding Text}" LineBreakMode="{Binding LineBreakMode}" HorizontalOptions="StartAndExpand" />
<Label Grid.Column="1" Text="{Binding CaretCharacter}" HorizontalOptions="End" />
</Grid>
</ContentView.Content>
</ContentView>
Just a short outline of what I've done
Added a Grid to ExpansibleControl
Within the grid there is a Label for the text
The Text is bound to the Text property of a viewmodel (see below)
The LineBreakMode is bound to the LineBreakMode of a viewmodel
Furthermore a second Label is added for the caret
This would be an image in a real app, but for the purpose of demonstration a Label will suffice
The Text is bound to the CaretCharacter property of our viewmodel
Furthermore I've added a TapGestureRecognizer and bound its Command to the ClickCommand property of out viewmodel.
Viewmodel
As stated above, the views in our ExpansibleControl are bound to a viewmodel. Here is the respective code
public class ExpansibleControlViewModel : INotifyPropertyChanged
{
private char _caretCharacter;
private bool _isCollapsed;
private LineBreakMode _lineBreakMode;
private string _text;
public ExpansibleControlViewModel()
{
ClickCommand = new Command(OnClick);
}
public event PropertyChangedEventHandler PropertyChanged;
public char CaretCharacter
{
get => _caretCharacter;
private set
{
if (_caretCharacter == value)
{
return;
}
_caretCharacter = value;
OnPropertyChanged();
}
}
public Command ClickCommand { get; private set; }
public bool IsCollapsed
{
get => _isCollapsed;
set
{
_isCollapsed = value;
if (_isCollapsed)
{
LineBreakMode = LineBreakMode.TailTruncation;
CaretCharacter = '<';
}
else
{
LineBreakMode = LineBreakMode.WordWrap;
CaretCharacter = 'v';
}
}
}
public LineBreakMode LineBreakMode
{
get => _lineBreakMode;
private set
{
if (_lineBreakMode == value)
{
return;
}
_lineBreakMode = value;
OnPropertyChanged();
}
}
public string Text
{
get
{
return _text;
}
set
{
if (_text == value)
{
return;
}
_text = value;
OnPropertyChanged();
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void OnClick()
{
ToggleIsCollapsed();
}
private void ToggleIsCollapsed()
{
IsCollapsed = !IsCollapsed;
}
}
The viewmodel implements INotifyPropertyChanged to inform the view that properties have changed (otherwise the bound views would not be updated when a value changes). Due to CallerMemberNameAttribute the name of the property that calls OnPropertyChanged is automatically passed in the propertyName parameter when the optional parameter is omitted.
IsCollapsed:bool is rather a helper, to simplify the code and reveal intention. When the callback for ClickCommand is called, IsCollapsed is inverted. When it has changed, we set the LineBreakMode and the CaretCharacter to the according values. The setters of those in turn call OnPropertyChanged to raise the PropertyChanged event and the UI updates. By setting the LineBreakMode we "tell" the label either to truncate the text or to break text at words (there are other options).
What if you need a larger portion of text?
There are some options you can go with here.
You could for example fix the LineBreakMode to WordWrap and set the HeightRequest of the Label to a value >0 to show just a portion of the text and to -1 to show the whole text. In that case you could overlay a gradient from transparent to your background color, to fade out text from top to bottom, as a hint that there is more text to show.
Another option is to manipulate the text itself. You could have a writeable property OriginalText and a read-only property Text and just truncate the text "by hand".
Anyway, for both options you will have to take care that it scales across platforms, which may or may not be an easy endeavour.
With latest Xamarin Forms 3.3 there is a new attribute "MaxLines" for Labels, see https://blog.verslu.is/xamarin/xamarin-forms-xamarin/maxlines-label-xamarin-forms/ . This should help if you need to display on multiple lines.

Resources