0
votes

I am having an issue with list views on in a couple of my Xamarin Forms applications. One form is within a tabbed page setup, the other is a normal content page (different apps)

I have a class like this

public class SomeClass
{
    public string StringOne {get;set;}
    public string StringTwo {get;set;}
    public int IntOne {get;set;}
}

In my Content page, I set up an ObservableCollection and add some data in. I then tell the list that SomeClass is my ItemSource. This produces the ListView correctly on all of my devices.

The problem is that when I change one of the properties, nothing on the ListView changes (so if say I have 3 objects in the Observable and remove one, the list still says 3 - or if I change a property in my second object, the second item on the ListView doesn't change either).

I have also tried to solve the problem by using a standard List and implement INotifyChanged within the class. Again though, the ListView doesn't alter when the List changes.

I know the data has altered as if I make a change to the object, come out and go back in, the data has changed in the UI.

Am I doing something wrong or is this a bug I need to putting into Bugzilla?

2
An ObservableCollection should update when an item is added or removed - but it will not force an update if an item is modified, unless that item implements INotifyPropertyChanged.Jason
So I need both the observable and inotifypropertychanged in order for the list view to update?Nodoid

2 Answers

1
votes

It will not change if you don't bind it and implement INotifyPropertyChanged interface.

Sample Code:

public class ObservableProperty : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string propertyName)
    {
        var handler = PropertyChanged;
        if (handler != null)
            handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

public class SomeClass:ObservableProperty
{
    string stringOne;
    string stringTwo;
    int intOne;
    public string StringOne 
    {
        get{return stringOne;}
        set
        {
            stringOne = value;
            OnPropertyChanged("StringOne");
        }
    }

    public string StringTwo 
    {
        get{ return stringTwo;}
        set
        {
            stringTwo = value;
            OnPropertyChanged("StringTwo");
        }
    }

    public int IntOne 
    {
        get{ return intOne;}
        set
        {
            intOne = value;
            OnPropertyChanged("IntOne");
        }
    }
}

public class MainVM:ObservableProperty
{
    ObservableCollection<SomeClass> items;

    public ObservableCollection<SomeClass> items
    {
        get{return items;}
        set
        {
            items = value;
            OnPropertyChanged("Items");
        }
    }

    public MainVM()
    {
        Items = new ObservableCollection<SomeClass>();

        Items.Add(new SomeClass(){StringOne = "123", StringTwo = "test", IntOne =12});
    }

    public void CallMeForChangingProperty()
    {
        SomeClass item = Items[0];
        item.StringOne = "Test1";
    }
}


public class MainView
{
    public MainView()
    {
        this.BindingContext=  new MainVM()
    }
} 


< ListView ItemsSource="{Binding Items}" RowHeight="120">
 < ListView.ItemTemplate>
   < DataTemplate>
     < ViewCell>
       < ViewCell.View>
        < StackLayout>
            < Label Text= "StringOne" />
            < Label Text= "StringTwo" />
            < Label Text= "IntOne" />
        </ StackLayout>
       </ ViewCell.View>
     </ ViewCell>
   </ DataTemplate>
 </ ListView.ItemTemplate>
</ ListView>
0
votes

Answer given by @eakgul works like a charm for me. I'll attach here what I've implemented, maybe it could help someone.

You have to set INotifyPropertyChanged both, to the ObservableColection and to it's itens.

I have a BaseViewModel with INotifyPropertyChanged as follows:

public class BaseViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
        if (PropertyChanged != null)
        {
            PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    protected void SetProperty<T>(ref T backingField, T value, [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals( backingField, value)) return;
        backingField = value;
        OnPropertyChanged(propertyName);
    }
}

On my BluetoothPage.xaml, first I set bindincontext to my BluetoothPageViewModel.cs and set the ListView ItemsSource and it's binded labels:

<ContentPage.BindingContext>
    <viewmodel:BluetoothPageViewModel/>
</ContentPage.BindingContext>

<ContentPage.Content>
    <StackLayout Padding="5,10">

        <Button x:Name="Scan_Devices_Button"
                Command="{Binding SearchNew_Button_Clicked}"/>
        
        <ListView x:Name="DevicesList"
                  ItemsSource="{Binding BluetoothDevices}"
                  SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
                  IsPullToRefreshEnabled="True">
            
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <StackLayout>
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="*"/>
                                    <ColumnDefinition Width="*"/>
                                </Grid.ColumnDefinitions>
                                <Label Grid.Column="0"
                                       Text="{Binding device.Device.NativeDevice.Name}"/>
                                <Label Grid.Column="1"
                                       Text="{Binding device.Device.NativeDevice.Address, StringFormat='ID: {0}'}"/>
                            </Grid>
                            
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="*"/>
                                    <ColumnDefinition Width="*"/>
                                </Grid.ColumnDefinitions>
                                <Label Grid.Column="0"
                                       Text="{Binding device.Rssi, StringFormat='Power: {0:F2}dbm'}"/> 
                                <Label Grid.Column="1"
                                       Text="{Binding distance, StringFormat='Distance: {0:F2}m'}"/>
                            </Grid>
                        </StackLayout>
                    </ViewCell>
                    
                </DataTemplate>
            </ListView.ItemTemplate>
            
        </ListView>
    </StackLayout>
</ContentPage.Content>

The BluetoothPageViewModel:

public class BluetoothPageViewModel : BaseViewModel
{
    public BluetoothPageViewModel()
    {
        SearchNew_Button_Clicked = new Command(NewDevices_Button_Clicked_Event);
        Scan_Devices_Button_BgColor = "#D6D7D7";
        Scan_Devices_Button_Text = "Scan nearby devices";
    }


    #region Declarations

    public List<IDevice> iDeviceList = new List<IDevice>();
    public ObservableCollection<BluetoothPageModel> _bluetoothDevices = new ObservableCollection<BluetoothPageModel>();
    public BluetoothPageModel _selectedItem;

    public ObservableCollection<BluetoothPageModel> BluetoothDevices
    {
        get { return _bluetoothDevices; }
        set { SetProperty(ref _bluetoothDevices, value); }
    }
    public BluetoothPageModel SelectedItem
    {
        get { return _selectedItem; }
        set { SetProperty(ref _selectedItem, value); }
    }

    public ICommand SearchNew_Button_Clicked { get; private set; }

    #endregion

    #region Functions

    private void NewDevices_Button_Clicked_Event(object obj)
    {
        // discover some devices
        if (!CrossBleAdapter.Current.IsScanning)
        {
            BluetoothDevices.Clear();
            iDeviceList.Clear();
            var scanner = CrossBleAdapter.Current.Scan().Subscribe(scanResult =>
            {
                if (!iDeviceList.Contains(scanResult.Device))
                {
                    iDeviceList.Add(scanResult.Device);
                    Device.BeginInvokeOnMainThread(() =>
                    {
                        BluetoothDevices.Add(new BluetoothPageModel
                        {
                            device = scanResult,
                            distance = Math.Pow(10, ((-68 - scanResult.Rssi) / 31.1474))
                        });
                    });
                }
                else
                {
                    int ind = iDeviceList.IndexOf(scanResult.Device);


                    Device.BeginInvokeOnMainThread(() =>
                    {
                        BluetoothDevices[ind].device = scanResult;
                        BluetoothDevices[ind].distance = Math.Pow(10, ((-68 - scanResult.Rssi) / 31.1474));
                    });
                }
            });
        }
        else
        {
            CrossBleAdapter.Current.StopScan(); //When you want to stop scanning
        }
    }
    #endregion
}

Finally, to be able to update data when you change a property of the BluetoothPageModel class:

public class BluetoothPageModel:BaseViewModel
{
    public IScanResult _device;
    public double _distance;
    public IScanResult device 
    {
        get { return _device; }
        set { SetProperty(ref _device, value); }
    }
    public double distance
    {
        get { return _distance; }
        set { SetProperty(ref _distance, value); }
    }
}

Thanks to eakgul answer I could get it working. Hope it can help someone else.