I have observable collection with some items.
/// <summary>
/// The <see cref="Items" /> property's name.
/// </summary>
public const string ItemsPropertyName = "Items";
private ObservableCollection<SomeItem> _items = new ObservableCollection<BrandItem>();
/// <summary>
/// Gets the Items property.
/// </summary>
public ObservableCollection<SomeItem> Items
{
get
{
return _items;
}
set
{
if (_items == value)
{
return;
}
_items = value;
// Update bindings, no broadcast
RaisePropertyChanged(ItemsPropertyName);
}
}
and also pagedCollectionView because I have to group items in datagrid
public const string ItemGroupedPropertyName = "ItemGrouped";
private PagedCollectionView _itemsGrouped;
/// <summary>
/// Gets the ItemSpecificationsGrouped property.
/// </summary>
public PagedCollectionView ItemSpecificationsGrouped
{
get { return _itemsGrouped; }
set
{
if (_itemsGrouped == value)
{
return;
}
_itemsGrouped = value;
// Update bindings, no broadcast
RaisePropertyChanged(ItemGroupedPropertyName);
}
}
#endregion
in viewmodel constructor I set
ItemGrouped = new PagedCollectionView(Items);
ItemGrouped.GroupDescriptions.Add(new PropertyGroupDescription("GroupName"));
and in view a have datagrid that bind ItemsGrouped
<data:DataGrid ItemsSource="{Binding ItemsGrouped}" AutoGenerateColumns="False">
<data:DataGrid.Columns >
<data:DataGridTextColumn IsReadOnly="True"
Binding="{Binding ItemAttribut1}" Width="*"/>
<data:DataGridTextColumn IsReadOnly="True"
Binding="{Binding Attribute2}" Width="*" />
</data:DataGrid.Columns>
</data:DataGrid>
when i change items in Items (clear and add new) after many times I have a memory leak.. When I remove ItemsSource everything is fine.. So I know that PagedCollectionView causing memory leak but i don't know why. Any idea, please? Or another solution to group items inside datagrid by some property in collection.. Thank you!!
The issue is the paged collection view hooked up the the NotifyPropertyChanged from your RaisePropertyChanged(ItemsPropertyName); and never releases the event hook... I solved this because I did not need grouping by returning an ICollectionView. When you bind the grid to an ObservableCollection the datagrid will create the PagedCollectionView for you. When you bind to ICollectionView the grid will use the ICollectionView without creating the PagedCollectionView. Hope this helps...
public ICollectionView UserAdjustments
{
get { return _userAdjustmentsViewSource.View; }
}
private void SetCollection(List<UserAdjustment> adjustments)
{
if(_userAdjustmentsViewSource == null)
{
_userAdjustmentsViewSource = new CollectionViewSource();
}
_userAdjustmentsViewSource.Source = adjustments;
RaisePropertyChanged("UserAdjustments");
}
Related
Xamarin Forms Android Autosize Label TextCompat pre android 8 doesn't autosize text
I unfortunately do not have a high enough rep to comment on anyones post.
I was trying some things out and came across the post linked which got me very close to the solution after experimenting with other posts. I am also trying to autosize text within an app, but inside of an MVVM Master Detail project. If I enter values directly in the Droid renderer it works as expected, but that defeats the purpose when I have fonts of all sizes needed.
I have already made sure my return type is correct.
The code behind is initialized prior to the get value.
The fields are public.
There are no other issues by plugging in numeric values instead of bindable properties.
I am not receiving any values from the view. I would assume the view has not been created yet but the code behind has initialized. I am pretty sure I have done everything mostly right but I mostly deal with stock Xamarin so expanding functionality is still pretty new to me. All help is appreciated.
Custom Control (edit: changed default value from default(int) to an integer value to get rid of exception)
/// <summary>Auto scale label font size class.</summary>
public class AutoSizeLabel : Label
{
/// <summary>Minimum font size property.</summary>
public static readonly BindableProperty MinimumFontSizeProperty = BindableProperty.Create(
propertyName: nameof(MinimumFontSize),
returnType: typeof(int),
declaringType: typeof(AutoSizeLabel),
defaultValue: 17);
/// <summary>Maximum font size property.</summary>
public static readonly BindableProperty MaximumFontSizeProperty = BindableProperty.Create(
propertyName: nameof(MaximumFontSize),
returnType: typeof(int),
declaringType: typeof(AutoSizeLabel),
defaultValue: 24);
/// <summary>Gets or sets minimum font size.</summary>
public int MinimumFontSize
{
get
{
return (int)this.GetValue(MinimumFontSizeProperty);
}
set
{
this.SetValue(MinimumFontSizeProperty, value);
}
}
/// <summary>Gets or sets maximum font size.</summary>
public int MaximumFontSize
{
get
{
return (int)this.GetValue(MaximumFontSizeProperty);
}
set
{
this.SetValue(MaximumFontSizeProperty, value);
}
}
}
Droid Renderer
public class AutoSizeLabelRenderer : LabelRenderer
{
protected override bool ManageNativeControlLifetime => false;
protected override void Dispose(bool disposing)
{
Control.RemoveFromParent();
base.Dispose(disposing);
}
private AutoSizeLabel bindingValue = new AutoSizeLabel();
private AppCompatTextView appCompatTextView;
public AutoSizeLabelRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
{
base.OnElementChanged(e);
if (e.NewElement == null || !(e.NewElement is AutoSizeLabel autoLabel) || Control == null) { return; }
//v8 and above supported natively, no need for the extra stuff below.
if (DeviceInfo.Version.Major >= 8)
{
Control?.SetAutoSizeTextTypeUniformWithConfiguration(bindingValue.MinimumFontSize, bindingValue.MaximumFontSize, 2, (int)ComplexUnitType.Sp);
return;
}
appCompatTextView = new AppCompatTextView(Context);
appCompatTextView.SetTextColor(Element.TextColor.ToAndroid());
appCompatTextView.SetMaxLines(1);
appCompatTextView.SetBindingContext(autoLabel.BindingContext);SetNativeControl(appCompatTextView);
TextViewCompat.SetAutoSizeTextTypeUniformWithConfiguration(Control, bindingValue.MinimumFontSize, bindingValue.MaximumFontSize, 2, (int)ComplexUnitType.Sp);
}
}
XAML Call
<renderer:AutoSizeLabel MinimumFontSize="17"
MaximumFontSize="24"
Style="{StaticResource SomeStyle}"
Text="{Binding SomeText}">
<Label.GestureRecognizers>
<TapGestureRecognizer Command="{Binding SomeCommand}"></TapGestureRecognizer>
</Label.GestureRecognizers>
</renderer:AutoSizeLabel>
This line is unnecessary.
private AutoSizeLabel bindingValue = new AutoSizeLabel();
Instead reference autoLabel. Alternatively I changed the check to
if (e.NewElement == null || Control == null) { return; }
and cast in the following line using
var autoSizeLabel = e.NewElement as AutoSizeLabel;
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.
I have a picker control which is optional field and the user can set the selected item of picker to be empty if he wants.
Is it possible to have Select as the additional option in itemsource of xamarin forms picker control.
Code for Custom picker:
<controls:CustomPicker
Grid.Row="1"
Grid.Column="1"
Margin="0,5,0,0"
Image="Downarrow"
ItemDisplayBinding="{Binding abb}"
ItemsSource="{Binding StateList}"
Placeholder="Select"
SelectedIndex="{Binding StateSelectedIndex}"
Style="{StaticResource Key=PickerHeight}" />
StateSelectedIndex = -1;
I tried setting selected index = -1. That works only when nothing is selected in the picker control, but once if an item is selected from the picker, then option "Select" can not be chosen (disappears).
I tried referring below url Default value for Picker but this did not help.
Any help is appreciated.
Solution 1:
You should set the binding mode of SelectedIndex
SelectedIndex="{Binding StateSelectedIndex,Mode=TwoWay}"
Solution 2:
You could binding the value of SelectedItem in ViewModel .
public class YourViewModel: INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public ObservableCollection<string> MyItems { get; set; }
private string selectItem;
public string SelectItem
{
get
{
return selectItem;
}
set
{
if(value!=null)
{
selectItem = value;
NotifyPropertyChanged("SelectItem");
int SelectIndex = MyItems.IndexOf(value);
// this will been invoked when user change the select , do some thing you want
}
}
}
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.
Not sure if I formatted the question appropriately, please let me know if I did not. But I am trying to simply bind a background color to a value in my viewcell. I have this working, actually. The issue is when I update a value, I don't see the change in background color. The implementation is a bit complicated, but here's my code.
ViewCell (OnBindingContextChanged)
...
ShowReadOverlay.SetBinding(Xamarin.Forms.VisualElement.BackgroundColorProperty, new Xamarin.Forms.Binding(".", Xamarin.Forms.BindingMode.TwoWay, new XamarinMobile.Converters.GridCellBackgroundColorConverter(), null, null, null));
...
So essentially I just build my layout. I decided to only post the relevant code that sets the binding in my OnBindingContextChanged method. If anyone needs any other code I'd be glad to add it, just don't know if it's relevant. My ViewCell class is a simple class that just inherits ViewCell.
Here's my converter:
public class GridCellBackgroundColorConverter : Xamarin.Forms.IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
try
{
var cell = (XamarinMobile.ViewModels.GridCellViewModel)value;
if(cell.HasRead)
{
//return with shadow
return Xamarin.Forms.Color.FromRgba(0,0,0,0.6);
} else
{
//return no shadow
return Xamarin.Forms.Color.FromRgba(0, 0, 0, 0.0);
}
} catch(System.Exception ex)
{
return null;
}
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
#endregion
}
Simple. It works. Now here's the tricky part. So the grid I'm describing, is a listview that contains cells of stories. A user will click on an image which will take them to a story page. When the user is in the story page, they can either go back to the grid to go to another story, or swipe left or right and they can get to another story that way. When a user goes to a story page from our grid, then the cell gets updated fine. BUT if a user swipes to another story NOT from the grid, that's where my issue is. In my story page I have logic that iterates through the grid cells, and finds the story you're currently on (the story you swiped to) and sees if it's in the grid, if it's in the grid, I update the cell's HasRead property. As such:
//find the cell in the grid (if exists)
ViewModels.GridCellViewModel cell = App.GridCells.Where(x => x.StoryId == App.Story.StoryId).FirstOrDefault();
if (cell != null)
{
cell.HasRead = true;
}
This works but... it doesn't trigger the value converter to change the property. What am I doing wrong? How can I get it so that I can update a property, and have it trigger my value converter?
My guess is that you're converter isn't triggering because you've technically bound to the viewcell itself, not the HasRead property. When you set HasRead, it will (assuming it's implementing INotifyPropertyChanged) fire a PropertyChangedEvent which would trigger the binding and call the value converter. However, since your binding is pointing to the viewcell itself, it will only trigger when that changes and ignore property changes elsewhere on that object.
A possible solution is to change the binding to point to HasRead (instead of '.'), and update your converter to expect the boolean directly rather than taking in a viewcell. This would be a better practice for a converter regardless.
That said, this is not really following the mvvm pattern that is generally recommended for xamarin forms apps. My suggestion would be to have a viewmodel that has a property that holds your story models (wrapped in their own StoryViewModels if you need logic there) and make sure the VM and Model classes implement INotifyPropertyChanged. Make the VM the datacontext for the page, bind the list to your listview source and your listview itemtemplate contents will bind to each individual story. Each story can have a HasRead property that binds to the background color via your updated converter.
Like this:
<ContentPage
x:Class="Stack_Stories.MainPage"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Stack_Stories">
<ContentPage.BindingContext>
<local:StoriesViewModel x:Name="VM" />
</ContentPage.BindingContext>
<ContentPage.Resources>
<ResourceDictionary>
<local:StoryReadBackgroundColorConverter x:Key="HasReadColor" />
</ResourceDictionary>
</ContentPage.Resources>
<ListView ItemsSource="{Binding Stories}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Grid x:Name="StoryGrid" BackgroundColor="{Binding HasRead, Converter={StaticResource HasReadColor}}">
<Button Command="{Binding ToggleReadCommand}" Text="{Binding Name}" />
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
public class StoryViewModel : INotifyPropertyChanged
{
private string _name = "";
public string Name
{
get { return _name; }
set { _name = value; OnPropertyChanged(); }
}
private bool _hasRead = false;
public bool HasRead
{
get { return _hasRead; }
set { _hasRead = value; OnPropertyChanged(); }
}
private Command _toggleRead;
public Command ToggleReadCommand
{
get
{
return _toggleRead
?? (_toggleRead = new Command(() => HasRead = !HasRead));
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class StoriesViewModel : INotifyPropertyChanged
{
public StoriesViewModel()
{
// add sample stories
Stories.Add(new StoryViewModel { Name = "First Story" });
Stories.Add(new StoryViewModel { Name = "Second Story", HasRead=true });
}
private ObservableCollection<StoryViewModel> _stories = new ObservableCollection<StoryViewModel>();
public ObservableCollection<StoryViewModel> Stories
{
get { return _stories; }
set { _stories = value; OnPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class StoryReadBackgroundColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (!(value is bool)) return null;
return (bool)value ? Color.FromRgba(0, 0, 0, 0.6) : Color.FromRgba(0, 0, 0, 0.0);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}