Search code examples
c#listviewuwpobservablecollectionxbind

UWP Convert IEnumerable to ObservableCollection (x:Bind)


Okay, so I have a IEnumerable collection which I x:Bind into a ListView.

While the app is running I want whenever this IEnumerable list changes, my listview to be updated as well. This is not the case even when using INotifyPropertyChanged, so I decided to move towards converting the IEnumerable to an ObservableCollection.

I found out that the cast that exists is:

myCollection = new ObservableCollection<object>(the_list);

That would work fine in a different problem, but in my case I am x:Bind this to the listview and each time I run the method with the above code a new reference is created and the bind will not work (It doesn't work). Is that correct ? If yes how can I work around this ? If not, what am I doing wrong ? Thank you.

Currently my code looks like:

public ObservableCollection<IMessage> MessageList;

private async Task SetMessages()
{
    MessageList = new ObservableCollection<IMessage>(await channel.GetMessagesAsync(NumOfMessages).Flatten());
}

What Worked for me:

Thanks solely to the Marian Dolinský's answer everything works now. I present some for of my code to make it more clear What I've done was:

class ChatViewModel : INotifyPropertyChanged
{
    //Number of messages to have in list at once.
    private int NumOfMessages = 20;

    //Called when a user clicks on a channel.
    public async Task SelectChannel(object sender, ItemClickEventArgs e)
    {
        //Set the channel to clicked item
        channel = (SocketTextChannel)e.ClickedItem;
        //Receive the messages in the list.
        IEnumerable<IMessage> data = await channel.GetMessagesAsync(NumOfMessages).Flatten();
        //Clear the observablecollection from any possible previous messages (if for example you select another channel).
        messageList.Clear();
        //Loop and add all messages to the observablecollection
        foreach (var item in data)
        {
            messageList.Add(item);
        }
    }

    //The event handler when a message is received.
    private async Task Message_Received(SocketMessage arg)
    {
        //Calls to add message only if the message received is of interest.
        if (arg.Channel == channel)
        {
            //Sets the thread equal to the UI thread.
            await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
             {
                //Adds the new message, removes the oldest to keep always 20.
                 messageList.Add(arg);
                 messageList.Remove(messageList[0]);
             });
        }
    }

    private ObservableCollection<IMessage> messageList = new ObservableCollection<IMessage>();

    public ObservableCollection<IMessage> MessageList
    {
        get
        {
            return messageList;
        }
    }
}

And here is the code in XAML:

<Page.DataContext>
    <vm:ChatViewModel x:Name="ChatViewModel"/>
</Page.DataContext>

                    <ListView VerticalContentAlignment="Bottom" ItemsSource="{x:Bind ChatViewModel.MessageList, Mode=OneWay}" SelectionMode="None" ItemTemplate="{StaticResource MessageListDataTemplate}">
                        <ListView.ItemContainerStyle>
                            <Style TargetType="ListViewItem">
                                <Setter Property="HorizontalContentAlignment" Value="Stretch" />
                                <Setter Property="Margin" Value="0,0,5,5"/>
                            </Style>
                        </ListView.ItemContainerStyle>
                    </ListView>

Thanks again for your help :)


Solution

  • It's not updated because of how the ObservableCollection works - it notifies only about changes inside the collection but when you are assigning the MessageList in your SetMessages method, you're creating new instance of ObservableCollection and the ListView.Source is pointing to the original bound instance of ObservableCollection.

    Also, I see that MessageList is not a property, but field. Fields doesn't work with bindings.

    You have four options how to achieve updating of the ListView:

    1) You could synchronize the existing MessageList with new data:

    private async Task SetMessages()
    {
        IEnumerable newData = await channel.GetMessagesAsync(20).Flatten();
    
        // Assuming MessageList is not null here you will sync newData with MessageList
    }
    


    2) If you are not using the MessageList collection anywhere else, you can set the ItemsSource property of ListView directly from code:

    private async Task SetMessages()
    {
         YourListView.ItemsSource = await channel.GetMessagesAsync(20).Flatten();
    }
    


    3) Since you are using the x:Bind expression, you can call Bindings.Update(); method everytime you want to refresh any binding on the page:

    private async Task SetMessages()
    {
        MessageList = new ObservableCollection<IMessage>(await channel.GetMessagesAsync(20).Flatten());
        Bindings.Update();
    }
    


    4) You could implement INotifyPropertyChanged on the page or create DependencyProperty for the MessageList and then bind it with Mode set to OneWay:

    <ListView ItemsSource="{x:Bind MessageList, Mode=OneWay}">
    

    Personally I'd not recommend the fourth option. The best option would be to sync the data since ListView automatically animates adding and removing the ListViewItems.

    EDIT:

    I think the problem is in these two lines:

    messageList.Add(arg);
    messageList.Remove(messageList[NumOfMessages - 1]);
    

    since new items are added at the end of the collection, so you are removing the item you added last time. For deleting the oldest item, you should use messageList.RemoveAt(0); to remove the item at the first position.