Reputation: 2844
I am having a little Problem with DataBinding to a ListView.
Because I want to have a Listview with MultiSelection I needed to implement a custom class called GenericSelectableItem which stores the Data, and if the cell IsSelected.
First, here is the View Model of the MainPage:
public class MainPageViewModel : BaseViewModel, INotifyPropertyChanged
{
private ObservableCollection<GenericSelectableItem<AudioFile>> _audiofiles = new ObservableCollection<GenericSelectableItem<AudioFile>>();
public ObservableCollection<GenericSelectableItem<AudioFile>> AudioFiles
{
get => _audiofiles ?? new ObservableCollection<GenericSelectableItem<AudioFile>>();
set
{
_audiofiles = value;
PropertyChanged(this, new PropertyChangedEventArgs(nameof(AudioFiles)));
}
}
}
The Xaml for the MainPage:
<!-- The Content -->
<ListView x:Name="listView" Grid.Row="1" HasUnevenRows="true" RowHeight="-1" ItemsSource="{Binding AudioFiles}" ItemSelected="ListView_OnItemSelected">
<ListView.ItemTemplate>
<DataTemplate>
<local:AudioViewCell Audiofile="{Binding Data}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
2 Helper Classes for making a multiselectable ListView:
public class GenericSelectableItem<T> : SelectableItem
{
public GenericSelectableItem(T data)
: base(data)
{
}
public GenericSelectableItem(T data, bool isSelected)
: base(data, isSelected)
{
}
// this is safe as we are just returning the base value
public new T Data
{
get => (T)base.Data;
set => base.Data = value;
}
}
public class SelectableItem : BindableObject
{
public static readonly BindableProperty DataProperty =
BindableProperty.Create(
nameof(Data),
typeof(object),
typeof(SelectableItem),
(object) null);
public static readonly BindableProperty IsSelectedProperty =
BindableProperty.Create(
nameof(IsSelected),
typeof(object),
typeof(SelectableItem),
(object)false);
public SelectableItem(object data)
{
Data = data;
IsSelected = false;
}
public SelectableItem(object data, bool isSelected)
{
Data = data;
IsSelected = isSelected;
}
public object Data
{
get => (object)GetValue(DataProperty);
set => SetValue(DataProperty, value);
}
public bool IsSelected
{
get => (bool)GetValue(IsSelectedProperty);
set => SetValue(IsSelectedProperty, value);
}
}
A Binding Example in AudioViewCell.Xaml:
<Label x:Name="LblFilename" Text="{Binding Filename}"
VerticalTextAlignment="Center"
Style="{StaticResource CellLabel}"/>
The AudioViewCell.cs
public partial class AudioViewCell : ViewCell
{
public static BindableProperty AudiofileProperty = BindableProperty.Create(
propertyName: nameof(Audiofile),
returnType: typeof(AudioFile),
declaringType: typeof(AudioViewCell),
defaultValue: null,
defaultBindingMode: BindingMode.OneWay);
public AudioFile Audiofile
{
get => (AudioFile) GetValue(AudiofileProperty);
set
{
Debug.WriteLine("Audiofile changed");
SetValue(AudiofileProperty, value);
((MenuItemViewModel) BindingContext).Audiofile = value;
}
}
public AudioViewCell()
{
InitializeComponent();
this.BindingContext = new MenuItemViewModel(SlAdditionalData, AwvWaveView);
}
}
And finally the MenuItemViewModel:
public class MenuItemViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private AudioFile _audioFile;
public AudioFile Audiofile
{
get => _audioFile;
set
{
Debug.WriteLine("Setting Audiofile");
_audioFile = value;
PropertyChanged(this, new PropertyChangedEventArgs(nameof(Audiofile)));
PropertyChanged(this, new PropertyChangedEventArgs(nameof(Filename)));
}
}
public string Filename => Audiofile?.Filename;
}
It seems that the Field Data inside GenericSelectableItem is never set so I think there is something wrong with the binding
Does anyone know a better way or why this is not working? Thanks alot for your help!!
Upvotes: 3
Views: 156
Reputation: 5314
TL;DR Version: Taking a deep looking on your cell's and 'cellViewModel's source code I've noticed that there's a confusion on bindings handle on your code. You are treating one BindingContext that you set at the AudioViewCell's constructor but it's overridden by the one set automatically by the ListView (that runs after the constructor). So you stand with a ViewCell rendered with no data.
On this image, I tried to show what's going on with your model:
Notice that yellow circular arrow at left, it's you defining the binding context at the constructor. That's overridden later by the red arrows (set after the listview renders).
To make it works the way you've coded, follow these steps:
MenuItemViewModel
's constructor to receive the "AudioFile" (the MenuItemViewModel class is your real BindingContext);OnBindingContextChanged
method to extract the new one Data
field and send it as a parameter to the constructor of a new instance of MenuItemViewModel
;MenuItemViewModel
as BindingContext of your inner View (It's a StackLayout called slRoot
according to your source code)Here's the steps code:
2:
public class MenuItemViewModel : INotifyPropertyChanged
{
// ...
public void SetAudiofile(AudioFile data)
{
Audiofile = data;
}
// ...
}
3 and 4:
public partial class AudioViewCell : ViewCell
{
// ...
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
// * I'm not sure if it's ok create a new instance to your binding context here, the old one ca be kept on memory due it's subscription. Think about create a method to set just the Audiofile property
slRoot.BindingContext = new MenuItemViewModel( thing, thing, ((GenericSelectableItem<AudioFile>)BindingContext).Data);
}
// ...
}
I've tested and it works, but it's far from an ideal clean solution.
If your intent is to reuse this cell, I think you should expose the properties that can be or not bound, let the need of it says what will be shown. The view cell should only handle visual layout / behavior, don't matter what data is on it.
P.S.: Sorry for my bad English, I hope it can be understandable.
Upvotes: 1