2

I bound an ObservableCollection to a WPF ListView. The Data from that list comes from a REST-Service. So I get the Data from the REST-Service and put it into the bound ObservableCollection. I call the REST-Service periodically to check for updated data which means that data could be deleted, added or the order of the items can change. How do I reflect those changes into the ObservableCollection? I don't want to replace the ObservableCollection completely every time I get updated data from the REST-Service. It would be far more user-friendly if the ObservableCollection is just changed for the entries that changed in the source data. So when an Item is added in the source data I want to add this item to the ObservableCollection at the exact same position as it is in the source-data (REST-Service). Same for deleted Items and resorted Items. So I want to just update the changed Items and not the whole Collection. Is that possible?

zpete
  • 1,725
  • 2
  • 18
  • 31
  • use OnpropertyChanged event along with the property. – Abin Oct 24 '13 at 06:37
  • What do you mean by user-friendly? Is it more performant or is such API easier to use? In case you have to implement the solution by yourself, it is definitely not easy, therefore, not user-friendly. In case you care about performance, you will have to decide what is the threshold after which replacing the whole collection is better. So implementing evaluation of this decision itself is not user-friendly task either. Until you have something specific aspect in mind, which will benefit from updating the collection item-by-item, just replace it. It is user-friendly enough. – galenus Oct 24 '13 at 07:46
  • With "user-friendly" I mean that the user should not see a refresh of the complete list if only one item changes. So replacing the old collection with the new collection completely is not a very good UI-Expecience for the end-user. – zpete Oct 24 '13 at 08:12
  • There is another concern you have to take into account: how do you find the items you've received are in the collection? Do they implement the IEquitable interface? – galenus Oct 24 '13 at 09:03

5 Answers5

1

The task you're trying to accomplish is not so easy. You have to check each item of new collection against the relative item of the 'old' collection, to see if something changed or not.

Then, for performance reasons, this solution isn't so useful.

The simple solution would be to replace the current collection with the new one created using the data from the service. The pseudo-code would be this:

ObservableCollection<DataItem> baseCollection = new ObservableCollection<DataItem>();

// adding/removing items
ObservableCollection<DataItem> serviceCollection = new ObservableCollection<DataItem>();

// adding/removing items
baseCollection.Clear();

// replacing old collection with the new one
baseCollection = serviceCollection;
pix
  • 1,264
  • 19
  • 32
Alberto Solano
  • 7,972
  • 3
  • 38
  • 61
1

UPDATE: as it seems that there is no standard-way to do this I tried to implement a solution myself. This is absolutely no production code and I might have forgotten a lot of use-cases but maybe this is a start? Here is what I came up with:

public class ObservableCollectionEx<T> : ObservableCollection<T>
{
    public void RecreateCollection( IList<T> newList )
    {
        // nothing changed => do nothing
        if( this.IsEqualToCollection( newList ) ) return;

        // handle deleted items
        IList<T> deletedItems = this.GetDeletedItems( newList );
        if( deletedItems.Count > 0 )
        {
            foreach( T deletedItem in deletedItems )
            {
                this.Remove( deletedItem );
            }
        }

        // handle added items
        IList<T> addedItems = this.GetAddedItems( newList );           
        if( addedItems.Count > 0 )
        {
            foreach( T addedItem in addedItems )
            {
                this.Add( addedItem );
            }
        }

        // equals now? => return
        if( this.IsEqualToCollection( newList ) ) return;

        // resort entries
        for( int index = 0; index < newList.Count; index++ )
        {
            T item = newList[index];
            int indexOfItem = this.IndexOf( item );
            if( indexOfItem != index ) this.Move( indexOfItem, index );
        }
    }

    private IList<T> GetAddedItems( IEnumerable<T> newList )
    {
        IList<T> addedItems = new List<T>();
        foreach( T item in newList )
        {
            if( !this.ContainsItem( item ) ) addedItems.Add( item );
        }
        return addedItems;
    }

    private IList<T> GetDeletedItems( IEnumerable<T> newList )
    {
        IList<T> deletedItems = new List<T>();
        foreach( var item in this.Items )
        {
            if( !newList.Contains( item ) ) deletedItems.Add( item );
        }
        return deletedItems;
    }

    private bool IsEqualToCollection( IList<T> newList )
    {   
        // diffent number of items => collection differs
        if( this.Items.Count != newList.Count ) return false;

        for( int i = 0; i < this.Items.Count; i++ )
        {
            if( !this.Items[i].Equals( newList[i] ) ) return false;
        }
        return true;
    }

    private bool ContainsItem( object value )
    {
        foreach( var item in this.Items )
        {
            if( value.Equals( item ) ) return true;
        }
        return false;
    }
}

The Method "RecreateCollection" is the method to call to "sync" the updated List from Datasource (newList) into the existing ObservableCollection. I am sure the resorting is done wrong so maybe someone can help me out on this one? Also worth mentioning: the Items in the Collections have to override EqualsTo in order to compare the objects by content and not by reference.

zpete
  • 1,725
  • 2
  • 18
  • 31
  • Don't worry about it being 'production' code. You've got to do whatever you have to do to get the job done. I'm sure you can get your approach working, and it will probably be just fine, but there are some ways of doing this using the built-in functionality available in .Net. My solution is an example, though not very beautiful. Something along the line of an observable collection of objects that themselves send notifications is entirely possible. My answer, and the link provided, utilize this. Still your approach should produce a handy class for the future once you get the sorting sorted. – ouflak Oct 29 '13 at 08:25
0

I've struggled with this myself and I'm afraid I haven't found an especially elegant solution. If someone here posts a better solution, I am likewise interested. But what I've done so far is basically have an ObservableCollection of ObservableCollections.

So the declaration looks like:

      ObservableCollection<ObservableCollection<string>> collectionThatUpdatesAtAppropriatePosition = new 
ObservableCollection<ObservableCollection<string>>();

Now the idea here is that the ObservableCollection in its own particular position in the list always only ever has one data value at its zero[0] location, and thus represents the data. So an example update would look like:

    if (this.collectionThatUpdatesAtAppropriatePosition.Count > 0)
    {
        this.collectionThatUpdatesAtAppropriatePosition[0].RemoveAt(0);
        this.collectionThatUpdatesAtAppropriatePosition[0].Add(yourData);
    }

I know it isn't pretty. I wonder if there isn't something that couldn't be better tried with NotificationObjects. In theory, I would think anything that implements INotifyPropertyChanged should do. But it does work. Good luck. I will be keeping an eye on this question to see if anybody else comes up with something more sophisticated.

Community
  • 1
  • 1
ouflak
  • 2,458
  • 10
  • 44
  • 49
0

Here is my implementation of an ObservableCollection Extension, to add an UpdateCollection method with any IEnumerable

using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Linq;

namespace MyApp.Extensions
{
    /// <summary>
    /// Observable collection extension.
    /// </summary>
    public static class ObservableCollectionExtension
    {

        /// <summary>
        /// Replaces the collection without destroy it
        /// Note that we don't Clear() and repopulate collection to avoid and UI winking
        /// </summary>
        /// <param name="collection">Collection.</param>
        /// <param name="newCollection">New collection.</param>
        /// <typeparam name="T">The 1st type parameter.</typeparam>
        public static void UpdateCollection<T>(this ObservableCollection<T> collection, IEnumerable<T> newCollection)
        {
            IEnumerator<T> newCollectionEnumerator = newCollection.GetEnumerator();
            IEnumerator<T> collectionEnumerator = collection.GetEnumerator();

            Collection<T> itemsToDelete = new Collection<T>();
            while( collectionEnumerator.MoveNext())
            {
                T item = collectionEnumerator.Current;

                // Store item to delete (we can't do it while parse collection.
                if( !newCollection.Contains(item)){
                    itemsToDelete.Add(item);
                }
            }

            // Handle item to delete.
            foreach( T itemToDelete in itemsToDelete){
                collection.Remove(itemToDelete);
            }

            var i = 0;
            while (newCollectionEnumerator.MoveNext())
            {
                T item = newCollectionEnumerator.Current;

                // Handle new item.
                if (!collection.Contains(item)){
                    collection.Insert(i, item);
                }

                // Handle existing item, move at the good index.
                if (collection.Contains(item)){
                    int oldIndex = collection.IndexOf(item);
                    collection.Move(oldIndex, i);
                }

                i++;
            }
        }
    }
}

Usage :

using MyApp.Extensions;

var _refreshedCollection = /// You data refreshing stuff            
MyObservableExistingCollection.UpdateCollection(_refreshedCollection);

Hope it will help someone. Any optimization are welcome !

j-guyon
  • 1,822
  • 1
  • 19
  • 31
0

This is the GET method I am using inside a ObservableCollection of Devices to async retrieve a Json object collection, using REST, and merging the result into the existing ObservableCollection that could also in use by UI DataGrids & etc. using caller.BeginInvoke(), not production tested but seems to work fine so far.

public class tbDevices
{
    public tbDevices()
    {
        this.Items = new ObservableCollection<tbDevice>();
    }

    public ObservableCollection<tbDevice> Items { get; }

    public async Task<IRestResponse> GET(Control caller, int limit = 0, int offset = 0, int timeout = 10000)
    {
        return await Task.Run(() =>
        {
            try
            {
                IRestResponse response = null;

                var request = new RestRequest(Globals.restDevices, Method.GET, DataFormat.Json);
                if (limit > 0)
                {
                    request.AddParameter("limit", limit);
                }
                if (offset > 0)
                {
                    request.AddParameter("offset", offset);
                }
                request.Timeout = timeout;

                try
                {
                    var client = new RestClient(Globals.apiProtocol + Globals.apiServer + ":" + Globals.apiPort);
                    client.Authenticator = new HttpBasicAuthenticator(Globals.User.email.Trim(), Globals.User.password.Trim());
                    response = client.Execute(request);
                }
                catch (Exception err)
                {
                    throw new System.InvalidOperationException(err.Message, response.ErrorException);
                }

                if (response.ResponseStatus != ResponseStatus.Completed)
                {
                    throw new System.InvalidOperationException("O servidor informou erro HTTP " + (int)response.StatusCode + ": " + response.ErrorMessage, response.ErrorException);
                }

                // Will do a one-by-one data refresh to preserve sfDataGrid UI from flashing
                List<tbDevice> result_objects_list = null;
                try
                {
                    result_objects_list = JsonConvert.DeserializeObject<List<tbDevice>>(response.Content);
                }
                catch (Exception err)
                {
                    throw new System.InvalidOperationException("Não foi possível decodificar a resposta do servidor: " + err.Message);
                }

                // Convert to Dictionary for faster DELETE loop
                Dictionary<string, tbDevice> result_objects_dic = result_objects_list.ToDictionary(x => x.id, x => x);

                // Async update this collection as this may be a UI cross-thread call affecting Controls that use this as datasource
                caller?.BeginInvoke((MethodInvoker)delegate ()
                {
                    // DELETE devices NOT in current_devices 
                    for (int i = this.Items.Count - 1; i > -1; i--)
                    {
                        result_objects_dic.TryGetValue(this.Items[i].id, out tbDevice found);
                        if (found == null)
                        {
                            this.Items.RemoveAt(i);
                        }
                    }

                    // UPDATE/INSERT local devices
                    foreach (var obj in result_objects_dic)
                    {
                        tbDevice found = this.Items.FirstOrDefault(f => f.id == obj.Key);
                        if (found == null)
                        {
                            this.Items.Add(obj.Value);
                        }
                        else
                        {
                            found.Merge(obj.Value);
                        }
                    }
                });

                return response;
            }
            catch (Exception)
            {
                throw; // This preserves the stack trace
            }
        });
    }
}
macedo123
  • 84
  • 1
  • 2