private bool HasDownstreamChanges(ITrackingCollection refChangeTracker, ITrackable trackableRef, ITrackable item,
                                              EntityReferenceProperty refProp, bool hasDownstreamChanges)
            {
// Get downstream changes
                IEnumerable <ITrackable> refPropItems   = refChangeTracker.Cast <ITrackable>();
                IEnumerable <ITrackable> refPropChanges = GetChanges(refPropItems);

                // Set flag for downstream changes
                bool hasLocalDownstreamChanges =
                    refPropChanges.Any(t => t.TrackingState != TrackingState.Deleted) ||
                    trackableRef.TrackingState == TrackingState.Added ||
                    trackableRef.TrackingState == TrackingState.Modified;

                // Set ref prop to null if unchanged or deleted
                if (!hasLocalDownstreamChanges &&
                    (trackableRef.TrackingState == TrackingState.Unchanged ||
                     trackableRef.TrackingState == TrackingState.Deleted))
                {
                    EntityInfo(item).RefNavPropUnchanged.Add(refProp.Property);
                    return(hasDownstreamChanges);
                }

                // prevent overwrite of hasDownstreamChanges when return from recursion
                hasDownstreamChanges |= hasLocalDownstreamChanges;
                return(hasDownstreamChanges);
            }
Пример #2
0
        public ITrackingCollection <IRemoteRepositoryModel> GetRepositories(ITrackingCollection <IRemoteRepositoryModel> collection)
        {
            var keyobs = GetUserFromCache()
                         .Select(user => string.Format(CultureInfo.InvariantCulture, "{0}|{1}", CacheIndex.RepoPrefix, user.Login));

            var source = Observable.Defer(() => keyobs
                                          .SelectMany(key =>
                                                      hostCache.GetAndFetchLatestFromIndex(key, () =>
                                                                                           apiClient.GetRepositories()
                                                                                           .Select(RepositoryCacheItem.Create),
                                                                                           item =>
            {
                // this could blow up due to the collection being disposed somewhere else
                try { collection.RemoveItem(Create(item)); }
                catch (ObjectDisposedException) { }
            },
                                                                                           TimeSpan.FromMinutes(5),
                                                                                           TimeSpan.FromDays(1))
                                                      )
                                          .Select(Create)
                                          );

            collection.Listen(source);
            return(collection);
        }
Пример #3
0
        /// <summary>
        /// Recursively enable or disable tracking on trackable entities in an object graph.
        /// </summary>
        /// <param name="item">Trackable object</param>
        /// <param name="enableTracking">Enable or disable change-tracking</param>
        /// <param name="visitationHelper">Circular reference checking helper</param>
        public static void SetTracking(this ITrackable item,
                                       bool enableTracking, ObjectVisitationHelper visitationHelper = null)
        {
            // Iterator entity properties
            foreach (var navProp in item.GetNavigationProperties())
            {
                // Set tracking on 1-1 and M-1 properties
                foreach (var refProp in navProp.AsReferenceProperty())
                {
                    // Get ref prop change tracker
                    ITrackingCollection refChangeTracker = item.GetRefPropertyChangeTracker(refProp.Property.Name);
                    if (refChangeTracker != null)
                    {
                        // Set tracking on ref prop change tracker
                        refChangeTracker.SetTracking(enableTracking, visitationHelper);
                    }
                }

                // Set tracking on 1-M and M-M properties
                foreach (var colProp in navProp.AsCollectionProperty <ITrackingCollection>())
                {
                    colProp.EntityCollection.SetTracking(enableTracking, visitationHelper);
                }
            }
        }
Пример #4
0
        /// <summary>
        /// Determine if an entity is a child of a many-to-many change-tracking collection property.
        /// </summary>
        /// <param name="changeTracker">Change-tracking collection</param>
        /// <returns></returns>
        public static bool IsManyToManyChildCollection(ITrackingCollection changeTracker)
        {
            // Entity is a M-M child if change-tracking collection has a non-null Parent property
            bool isManyToManyChild = changeTracker.Parent != null;

            return(isManyToManyChild);
        }
        public void UpdateStateParametersTest()
        {
            var  target    = new object();
            bool isInvoked = false;
            var  oldState  = EntityState.Unchanged;
            var  newState  = EntityState.Unchanged;

            _transitionManager.ChangeState = (item, state, entityState) =>
            {
                isInvoked = true;
                state.ShouldEqual(oldState);
                entityState.ShouldEqual(newState);
                target.ShouldEqual(item);
                return(entityState);
            };
            ITrackingCollection collection = Create(_transitionManager);

            oldState = EntityState.Detached;
            newState = EntityState.Added;
            collection.UpdateState(target, newState);
            isInvoked.ShouldBeTrue();

            isInvoked = false;
            oldState  = EntityState.Added;
            newState  = EntityState.Modified;
            collection.UpdateState(target, newState);
        }
Пример #6
0
        /// <summary>
        /// Gets a collection of Pull Requests. If you want to refresh existing data, pass a collection in
        /// </summary>
        /// <param name="repo"></param>
        /// <param name="collection"></param>
        /// <returns></returns>
        public ITrackingCollection <IPullRequestModel> GetPullRequests(ILocalRepositoryModel repo,
                                                                       ITrackingCollection <IPullRequestModel> collection)
        {
            // Since the api to list pull requests returns all the data for each pr, cache each pr in its own entry
            // and also cache an index that contains all the keys for each pr. This way we can fetch prs in bulk
            // but also individually without duplicating information. We store things in a custom observable collection
            // that checks whether an item is being updated (coming from the live stream after being retrieved from cache)
            // and replaces it instead of appending, so items get refreshed in-place as they come in.

            var keyobs = GetUserFromCache()
                         .Select(user => string.Format(CultureInfo.InvariantCulture, "{0}|{1}:{2}", CacheIndex.PRPrefix, user.Login, repo.Name));

            var source = Observable.Defer(() => keyobs
                                          .SelectMany(key =>
                                                      hostCache.GetAndFetchLatestFromIndex(key, () =>
                                                                                           apiClient.GetPullRequestsForRepository(repo.CloneUrl.Owner, repo.CloneUrl.RepositoryName)
                                                                                           .Select(PullRequestCacheItem.Create),
                                                                                           item =>
            {
                // this could blow up due to the collection being disposed somewhere else
                try { collection.RemoveItem(Create(item)); }
                catch (ObjectDisposedException) { }
            },
                                                                                           TimeSpan.Zero,
                                                                                           TimeSpan.FromDays(7))
                                                      )
                                          .Select(Create)
                                          );

            collection.Listen(source);
            return(collection);
        }
Пример #7
0
        /// <summary>
        /// Recursively enable or disable tracking on trackable entities in an object graph.
        /// </summary>
        /// <param name="item">Trackable object</param>
        /// <param name="enableTracking">Enable or disable change-tracking</param>
        /// <param name="visitationHelper">Circular reference checking helper</param>
        /// <param name="oneToManyOnly">True if tracking should be set only for OneToMany relations</param>
        public static void SetTracking(this ITrackable item, bool enableTracking,
                                       ObjectVisitationHelper visitationHelper = null, bool oneToManyOnly = false)
        {
            // Iterator entity properties
            foreach (var navProp in item.GetNavigationProperties())
            {
                // Skip if 1-M only
                if (!oneToManyOnly)
                {
                    // Set tracking on 1-1 and M-1 properties
                    foreach (var refProp in navProp.AsReferenceProperty())
                    {
                        // Get ref prop change tracker
                        ITrackingCollection refChangeTracker = item.GetRefPropertyChangeTracker(refProp.Property.Name);
                        if (refChangeTracker != null)
                        {
                            // Set tracking on ref prop change tracker
                            refChangeTracker.SetTracking(enableTracking, visitationHelper, oneToManyOnly);
                        }
                    }
                }

                // Set tracking on 1-M and M-M properties (if not 1-M only)
                foreach (var colProp in navProp.AsCollectionProperty <ITrackingCollection>())
                {
                    bool isOneToMany = !IsManyToManyChildCollection(colProp.EntityCollection);
                    if (!oneToManyOnly || isOneToMany)
                    {
                        colProp.EntityCollection.SetTracking(enableTracking, visitationHelper, oneToManyOnly);
                    }
                }
            }
        }
        /// <summary>
        /// Recursively remove items marked as deleted.
        /// </summary>
        /// <param name="changeTracker">Change-tracking collection</param>
        /// <param name="visitationHelper">Circular reference checking helper</param>
        public static void RemoveRestoredDeletes(this ITrackingCollection changeTracker, ObjectVisitationHelper visitationHelper = null)
        {
            ObjectVisitationHelper.EnsureCreated(ref visitationHelper);

            // Iterate items in change-tracking collection
            var items = changeTracker as IList;

            if (items == null)
            {
                return;
            }
            var count = items.Count;

            for (int i = count - 1; i > -1; i--)
            {
                // Get trackable item
                var item = items[i] as ITrackable;
                if (item == null)
                {
                    continue;
                }

                // Prevent endless recursion
                if (visitationHelper.TryVisit(item))
                {
                    // Iterate entity properties
                    foreach (var navProp in item.GetNavigationProperties())
                    {
                        // Process 1-1 and M-1 properties
                        foreach (var refProp in navProp.AsReferenceProperty())
                        {
                            // Get changed ref prop
                            ITrackingCollection refChangeTracker = item.GetRefPropertyChangeTracker(refProp.Property.Name);

                            // Remove deletes on rep prop
                            if (refChangeTracker != null)
                            {
                                refChangeTracker.RemoveRestoredDeletes(visitationHelper);
                            }
                        }

                        // Process 1-M and M-M properties
                        foreach (var colProp in navProp.AsCollectionProperty <ITrackingCollection>())
                        {
                            colProp.EntityCollection.RemoveRestoredDeletes(visitationHelper);
                        }
                    }
                }

                // Remove item if marked as deleted
                if (item.TrackingState == TrackingState.Deleted)
                {
                    var isTracking = changeTracker.Tracking;
                    changeTracker.InternalTracking = false;
                    items.RemoveAt(i);
                    changeTracker.InternalTracking = isTracking;
                }
            }
        }
Пример #9
0
        // Fired when an item in the collection has changed
        private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (Tracking)
            {
                var entity = sender as ITrackable;
                if (entity == null)
                {
                    return;
                }

                // Enable tracking on reference properties
#if SILVERLIGHT || NET40
                var prop = entity.GetType().GetProperty(e.PropertyName);
                if (prop != null && typeof(ITrackable).IsAssignableFrom(prop.PropertyType))
#else
                var prop = entity.GetType().GetTypeInfo().GetDeclaredProperty(e.PropertyName);
                if (prop != null && typeof(ITrackable).GetTypeInfo().IsAssignableFrom(prop.PropertyType.GetTypeInfo()))
#endif
                {
                    ITrackingCollection refPropChangeTracker = entity.GetRefPropertyChangeTracker(e.PropertyName);
                    if (refPropChangeTracker != null)
                    {
                        refPropChangeTracker.Tracking = Tracking;
                    }
                    return;
                }

                if (e.PropertyName != Constants.TrackingProperties.TrackingState &&
                    e.PropertyName != Constants.TrackingProperties.ModifiedProperties &&
                    !ExcludedProperties.Contains(e.PropertyName))
                {
                    // If unchanged mark item as modified, fire EntityChanged event
                    if (entity.TrackingState == TrackingState.Unchanged)
                    {
                        entity.TrackingState = TrackingState.Modified;
                        if (EntityChanged != null)
                        {
                            EntityChanged(this, EventArgs.Empty);
                        }
                    }

                    // Add prop to modified props, and fire EntityChanged event
                    if (entity.TrackingState == TrackingState.Unchanged ||
                        entity.TrackingState == TrackingState.Modified)
                    {
                        if (entity.ModifiedProperties == null)
                        {
                            entity.ModifiedProperties = new List <string>();
                        }
                        if (!entity.ModifiedProperties.Contains(e.PropertyName))
                        {
                            entity.ModifiedProperties.Add(e.PropertyName);
                        }
                    }
                }
            }
        }
Пример #10
0
        /// <summary>
        /// Restore items marked as deleted.
        /// </summary>
        /// <param name="changeTracker">Change-tracking collection</param>
        /// <param name="visitationHelper">Circular reference checking helper</param>
        public static void RestoreDeletes(this ITrackingCollection changeTracker, ObjectVisitationHelper visitationHelper = null)
        {
            ObjectVisitationHelper.EnsureCreated(ref visitationHelper);

            // Get cached deletes
            var removedDeletes = changeTracker.GetChanges(true).Cast <ITrackable>().ToList();

            // Restore deleted items
            if (removedDeletes.Any())
            {
                var isTracking = changeTracker.Tracking;
                changeTracker.Tracking = false;
                foreach (var delete in removedDeletes)
                {
                    var items = changeTracker as IList;
                    if (items != null && !items.Contains(delete))
                    {
                        items.Add(delete);
                    }
                }
                changeTracker.Tracking = isTracking;
            }

            foreach (var item in changeTracker.Cast <ITrackable>())
            {
                // Prevent endless recursion
                if (!visitationHelper.TryVisit(item))
                {
                    continue;
                }

                // Iterate entity properties
                foreach (var navProp in item.GetNavigationProperties())
                {
                    // Process 1-1 and M-1 properties
                    foreach (var refProp in navProp.AsReferenceProperty())
                    {
                        // Get changed ref prop
                        ITrackingCollection refChangeTracker = item.GetRefPropertyChangeTracker(refProp.Property.Name);

                        // Restore deletes on rep prop
                        if (refChangeTracker != null)
                        {
                            refChangeTracker.RestoreDeletes(visitationHelper);
                        }
                    }

                    // Process 1-M and M-M properties
                    foreach (var colProp in navProp.AsCollectionProperty <ITrackingCollection>())
                    {
                        colProp.EntityCollection.RestoreDeletes(visitationHelper);
                    }
                }
            }
        }
Пример #11
0
        // Fired when an item in the collection has changed
        private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (Tracking)
            {
                var entity = sender as ITrackable;
                if (entity == null)
                {
                    return;
                }

                // Enable tracking on reference properties
                var prop = PortableReflectionHelper.Instance.GetProperty(entity.GetType(), e.PropertyName);
                if (prop != null && PortableReflectionHelper.Instance.IsAssignable(typeof(ITrackable), prop.PropertyType))
                {
                    ITrackingCollection refPropChangeTracker = entity.GetRefPropertyChangeTracker(e.PropertyName);
                    if (refPropChangeTracker != null)
                    {
                        refPropChangeTracker.Tracking = Tracking;
                    }
                    return;
                }

                if (e.PropertyName != Constants.TrackingProperties.TrackingState &&
                    e.PropertyName != Constants.TrackingProperties.ModifiedProperties &&
                    !ExcludedProperties.Contains(e.PropertyName))
                {
                    // If unchanged mark item as modified, fire EntityChanged event
                    if (entity.TrackingState == TrackingState.Unchanged)
                    {
                        entity.TrackingState = TrackingState.Modified;
                        if (EntityChanged != null)
                        {
                            EntityChanged(this, EventArgs.Empty);
                        }
                    }

                    // Add prop to modified props, and fire EntityChanged event
                    if (entity.TrackingState == TrackingState.Unchanged ||
                        entity.TrackingState == TrackingState.Modified)
                    {
                        if (entity.ModifiedProperties == null)
                        {
                            entity.ModifiedProperties = new HashSet <string>();
                        }
                        entity.ModifiedProperties.Add(e.PropertyName);
                    }
                }
            }
        }
 private static void SetHandler(this ITrackingCollection trackableCollection, bool handle,
                                EventHandler entityChanged)
 {
     if (entityChanged == null)
     {
         return;
     }
     if (handle)
     {
         trackableCollection.EntityChanged += entityChanged;
     }
     else
     {
         trackableCollection.EntityChanged -= entityChanged;
     }
 }
        public ProductWorkspaceViewModel(IRepository repository, IMessagePresenter messagePresenter,
            IToastPresenter toastPresenter)
        {
            Should.NotBeNull(repository, "repository");
            Should.NotBeNull(messagePresenter, "messagePresenter");
            Should.NotBeNull(toastPresenter, "toastPresenter");
            _repository = repository;
            _messagePresenter = messagePresenter;
            _toastPresenter = toastPresenter;
            _trackingCollection = new TrackingCollection(new CompositeEqualityComparer().AddComparer(ProductModel.KeyComparer));

            SaveChangesCommand = RelayCommandBase.FromAsyncHandler(SaveChanges, CanSaveChanges, this);
            AddProductCommand = RelayCommandBase.FromAsyncHandler(AddProduct);
            EditProductCommand = RelayCommandBase.FromAsyncHandler<ProductModel>(EditProduct, CanEditProduct, this);
            RemoveProductCommand = RelayCommandBase.FromAsyncHandler<ProductModel>(RemoveProduct, CanRemoveProduct, this);
            DisplayName = UiResources.ProductWorkspaceName;
        }
        public ProductWorkspaceViewModel(IRepository repository, IMessagePresenter messagePresenter,
                                         IToastPresenter toastPresenter)
        {
            Should.NotBeNull(repository, "repository");
            Should.NotBeNull(messagePresenter, "messagePresenter");
            Should.NotBeNull(toastPresenter, "toastPresenter");
            _repository         = repository;
            _messagePresenter   = messagePresenter;
            _toastPresenter     = toastPresenter;
            _trackingCollection = new TrackingCollection();

            SaveChangesCommand   = new RelayCommand(SaveChanges, CanSaveChanges, this);
            AddProductCommand    = new RelayCommand(AddProduct);
            EditProductCommand   = new RelayCommand <ProductModel>(EditProduct, CanEditProduct, this);
            RemoveProductCommand = new RelayCommand <ProductModel>(RemoveProduct, CanRemoveProduct, this);
            DisplayName          = UiResources.ProductWorkspaceName;
        }
        public ProductWorkspaceViewModel(IRepository repository, IMessagePresenter messagePresenter,
                                         IToastPresenter toastPresenter)
        {
            Should.NotBeNull(repository, "repository");
            Should.NotBeNull(messagePresenter, "messagePresenter");
            Should.NotBeNull(toastPresenter, "toastPresenter");
            _repository         = repository;
            _messagePresenter   = messagePresenter;
            _toastPresenter     = toastPresenter;
            _trackingCollection = new TrackingCollection(new CompositeEqualityComparer().AddComparer(ProductModel.KeyComparer));

            SaveChangesCommand   = RelayCommandBase.FromAsyncHandler(SaveChanges, CanSaveChanges, this);
            AddProductCommand    = RelayCommandBase.FromAsyncHandler(AddProduct);
            EditProductCommand   = RelayCommandBase.FromAsyncHandler <ProductModel>(EditProduct, CanEditProduct, this);
            RemoveProductCommand = RelayCommandBase.FromAsyncHandler <ProductModel>(RemoveProduct, CanRemoveProduct, this);
            DisplayName          = UiResources.ProductWorkspaceName;
        }
        public async void LoadState(IDataContext state)
        {
            ITrackingCollection trackingCollection = state.GetData(StateConstant);

            if (trackingCollection == null)
            {
                return;
            }
            IList <IEntityStateEntry> changes = trackingCollection.GetChanges();
            await _initializedTask;

            _trackingCollection.UpdateStates(changes);
            foreach (IEntityStateEntry change in changes)
            {
                var          oldItem     = (ProductModel)change.Entity;
                ProductModel currentItem = GridViewModel.OriginalItemsSource.FirstOrDefault(model => model.Id == oldItem.Id);
                switch (change.State)
                {
                case EntityState.Added:
                    GridViewModel.OriginalItemsSource.Add(oldItem);
                    break;

                case EntityState.Modified:
                    if (currentItem != null)
                    {
                        GridViewModel.OriginalItemsSource.Remove(currentItem);
                    }
                    GridViewModel.OriginalItemsSource.Add(oldItem);
                    break;

                case EntityState.Deleted:
                    if (currentItem != null)
                    {
                        GridViewModel.OriginalItemsSource.Remove(currentItem);
                    }
                    break;
                }
            }
            this.OnPropertyChanged(() => v => v.HasChanges);
        }
            /// <summary>
            /// Get entities that have been added, modified or deleted, including trackable
            /// reference and child entities.
            /// </summary>
            /// <param name="items">Collection of ITrackable objects</param>
            /// <returns>Collection containing only added, modified or deleted entities</returns>
            private IEnumerable <ITrackable> GetChanges(IEnumerable <ITrackable> items)
            {
                // Prevent endless recursion by collection
                if (!visitationHelper.TryVisit(items))
                {
                    yield break;
                }

                // Prevent endless recursion by item
                items = items.Where(i => visitationHelper.TryVisit(i)).ToList();

                // Iterate items in change-tracking collection
                foreach (ITrackable item in items)
                {
                    // Downstream changes flag
                    bool hasDownstreamChanges = false;

                    // Iterate entity properties
                    foreach (var navProp in item.GetNavigationProperties())
                    {
                        // Process 1-1 and M-1 properties
                        foreach (var refProp in navProp.AsReferenceProperty())
                        {
                            ITrackable trackableRef = refProp.EntityReference;

                            // if already visited and unchanged, set to null
                            if (visitationHelper.IsVisited(trackableRef))
                            {
                                if ((trackableRef.TrackingState == TrackingState.Unchanged ||
                                     trackableRef.TrackingState == TrackingState.Deleted))
                                {
                                    EntityInfo(item).RefNavPropUnchanged.Add(refProp.Property);
                                }
                                continue;
                            }

                            // Get changed ref prop
                            ITrackingCollection refChangeTracker = item.GetRefPropertyChangeTracker(refProp.Property.Name);
                            if (refChangeTracker != null)
                            {
                                // Get downstream changes
                                IEnumerable <ITrackable> refPropItems   = refChangeTracker.Cast <ITrackable>();
                                IEnumerable <ITrackable> refPropChanges = GetChanges(refPropItems);

                                // Set flag for downstream changes
                                bool hasLocalDownstreamChanges =
                                    refPropChanges.Any(t => t.TrackingState != TrackingState.Deleted) ||
                                    trackableRef.TrackingState == TrackingState.Added ||
                                    trackableRef.TrackingState == TrackingState.Modified;

                                // Set ref prop to null if unchanged or deleted
                                if (!hasLocalDownstreamChanges &&
                                    (trackableRef.TrackingState == TrackingState.Unchanged ||
                                     trackableRef.TrackingState == TrackingState.Deleted))
                                {
                                    EntityInfo(item).RefNavPropUnchanged.Add(refProp.Property);
                                    continue;
                                }

                                // prevent overwrite of hasDownstreamChanges when return from recursion
                                hasDownstreamChanges |= hasLocalDownstreamChanges;
                            }
                        }

                        // Process 1-M and M-M properties
                        foreach (var colProp in navProp.AsCollectionProperty <IList>())
                        {
                            // Get changes on child collection
                            var trackingItems = colProp.EntityCollection;
                            if (trackingItems.Count > 0)
                            {
                                // Continue recursion if trackable hasn't been visited
                                if (!visitationHelper.IsVisited(trackingItems))
                                {
                                    // Get changes on child collection
                                    var trackingCollChanges = new HashSet <ITrackable>(
                                        GetChanges(trackingItems.Cast <ITrackable>()),
                                        ObjectReferenceEqualityComparer <ITrackable> .Default);

                                    // Set flag for downstream changes
                                    hasDownstreamChanges |= trackingCollChanges.Any();

                                    // Memorize only changed items of collection
                                    EntityInfo(item).ColNavPropChangedEntities[colProp.Property] = trackingCollChanges;
                                }
                            }
                        }
                    }

                    // Return item if it has changes
                    if (hasDownstreamChanges || item.TrackingState != TrackingState.Unchanged)
                    {
                        yield return(item);
                    }
                }
            }
Пример #18
0
        public ITrackingCollection<IRemoteRepositoryModel> GetRepositories(ITrackingCollection<IRemoteRepositoryModel> collection)
        {
            var keyobs = GetUserFromCache()
                .Select(user => string.Format(CultureInfo.InvariantCulture, "{0}|{1}", CacheIndex.RepoPrefix, user.Login));

            var source = Observable.Defer(() => keyobs
                .SelectMany(key =>
                    hostCache.GetAndFetchLatestFromIndex(key, () =>
                        apiClient.GetRepositories()
                                 .Select(RepositoryCacheItem.Create),
                        item =>
                        {
                            // this could blow up due to the collection being disposed somewhere else
                            try { collection.RemoveItem(Create(item)); }
                            catch (ObjectDisposedException) { }
                        },
                        TimeSpan.FromMinutes(5),
                        TimeSpan.FromDays(1))
                )
                .Select(Create)
            );

            collection.Listen(source);
            return collection;
        }
Пример #19
0
        /// <summary>
        /// Gets a collection of Pull Requests. If you want to refresh existing data, pass a collection in
        /// </summary>
        /// <param name="repo"></param>
        /// <param name="collection"></param>
        /// <returns></returns>
        public ITrackingCollection<IPullRequestModel> GetPullRequests(ILocalRepositoryModel repo,
            ITrackingCollection<IPullRequestModel> collection)
        {
            // Since the api to list pull requests returns all the data for each pr, cache each pr in its own entry
            // and also cache an index that contains all the keys for each pr. This way we can fetch prs in bulk
            // but also individually without duplicating information. We store things in a custom observable collection
            // that checks whether an item is being updated (coming from the live stream after being retrieved from cache)
            // and replaces it instead of appending, so items get refreshed in-place as they come in.

            var keyobs = GetUserFromCache()
                .Select(user => string.Format(CultureInfo.InvariantCulture, "{0}|{1}:{2}", CacheIndex.PRPrefix, user.Login, repo.Name));

            var source = Observable.Defer(() => keyobs
                .SelectMany(key =>
                    hostCache.GetAndFetchLatestFromIndex(key, () =>
                        apiClient.GetPullRequestsForRepository(repo.CloneUrl.Owner, repo.CloneUrl.RepositoryName)
                                 .Select(PullRequestCacheItem.Create),
                        item =>
                        {
                            // this could blow up due to the collection being disposed somewhere else
                            try { collection.RemoveItem(Create(item)); }
                            catch (ObjectDisposedException) { }
                        },
                        TimeSpan.Zero,
                        TimeSpan.FromDays(7))
                )
                .Select(Create)
            );

            collection.Listen(source);
            return collection;
        }
        private static void MergeChanges(this ITrackingCollection originalChangeTracker,
                                         IEnumerable <ITrackable> updatedItems, ObjectVisitationHelper visitationHelper, bool isTrackableRef = false)
        {
            ObjectVisitationHelper.EnsureCreated(ref visitationHelper);

            // Process each updated item
            foreach (var updatedItem in updatedItems)
            {
                // Prevent endless recursion
                if (!visitationHelper.TryVisit(updatedItem))
                {
                    continue;
                }

                // Get matching orig item
                var origItem = originalChangeTracker.Cast <ITrackable>()
                               .GetEquatableItem(updatedItem, isTrackableRef);
                if (origItem == null)
                {
                    continue;
                }

                // Back fill entity identity on trackable ref
                if (isTrackableRef)
                {
                    var origItemIdentifiable = (IIdentifiable)origItem;
                    origItemIdentifiable.SetEntityIdentifier();
                    ((IIdentifiable)updatedItem).SetEntityIdentifier(origItemIdentifiable);
                }

                // Iterate entity properties
                foreach (var navProp in updatedItem.GetNavigationProperties())
                {
                    // Set to 1-1 and M-1 properties
                    foreach (var refProp in navProp.AsReferenceProperty())
                    {
                        ITrackingCollection refPropChangeTracker = origItem.GetRefPropertyChangeTracker(navProp.Property.Name);
                        if (refPropChangeTracker != null)
                        {
                            refPropChangeTracker.MergeChanges(new[] { refProp.EntityReference }, visitationHelper, true);
                        }
                    }

                    // Set 1-M and M-M properties
                    foreach (var colProp in navProp.AsCollectionProperty())
                    {
                        var origItemsChangeTracker = origItem.GetEntityCollectionProperty <ITrackingCollection>(navProp.Property).EntityCollection;
                        if (origItemsChangeTracker != null)
                        {
                            // Merge changes into trackable children
                            origItemsChangeTracker.MergeChanges(colProp.EntityCollection, visitationHelper);
                        }
                    }
                }

                // Set properties on orig item
                origItem.SetEntityProperties(updatedItem, originalChangeTracker);

                // Accept changes
                origItem.AcceptChanges();
            }

            // Remove cached deletes
            originalChangeTracker.RemoveCachedDeletes();
        }
            /// <summary>
            /// Get entities that have been added, modified or deleted, including trackable
            /// reference and child entities.
            /// </summary>
            /// <param name="items">Collection of ITrackable objects</param>
            /// <returns>Collection containing only added, modified or deleted entities</returns>
            private IEnumerable <ITrackable> GetChanges(IEnumerable <ITrackable> items)
            {
                // Prevent endless recursion by collection
                if (!visitationHelper.TryVisit(items))
                {
                    yield break;
                }

                // Prevent endless recursion by item
                items = items.Where(i => visitationHelper.TryVisit(i)).ToList();

                // Iterate items in change-tracking collection
                foreach (ITrackable item in items)
                {
                    // Downstream changes flag
                    bool hasDownstreamChanges = false;

                    // Iterate entity properties
                    foreach (var navProp in item.GetNavigationProperties())
                    {
                        // Process 1-1 and M-1 properties
                        foreach (var refProp in navProp.AsReferenceProperty())
                        {
                            ITrackable trackableRef = refProp.EntityReference;

                            // if already visited and unchanged, set to null
                            if (visitationHelper.IsVisited(trackableRef))
                            {
                                if ((trackableRef.TrackingState == TrackingState.Unchanged ||
                                     trackableRef.TrackingState == TrackingState.Deleted))
                                {
                                    EntityInfo(item).RefNavPropUnchanged.Add(refProp.Property);
                                }
                                continue;
                            }

                            // Get changed ref prop
                            ITrackingCollection refChangeTracker = trackableRef.GetRefPropertyChangeTracker(refProp.Property.Name);//item.GetRefPropertyChangeTracker(refProp.Property.Name);
                            if (refChangeTracker != null)
                            {
                                hasDownstreamChanges = HasDownstreamChanges(refChangeTracker, trackableRef, item, refProp, hasDownstreamChanges);
                            }
                        }

                        // Process 1-M and M-M properties
                        foreach (var colProp in navProp.AsCollectionProperty <IList>())
                        {
                            // Get changes on child collection
                            var trackingItems = colProp.EntityCollection;
                            if (trackingItems.Count > 0)
                            {
                                // Continue recursion if trackable hasn't been visited
                                if (!visitationHelper.IsVisited(trackingItems))
                                {
                                    hasDownstreamChanges = GetChangesOnChildCollection(trackingItems, hasDownstreamChanges, item, colProp);
                                }
                            }
                        }
                    }

                    // Return item if it has changes
                    if (hasDownstreamChanges || item.TrackingState != TrackingState.Unchanged)
                    {
                        yield return(item);
                    }
                }
            }
        public static ObservableCollection <T> CreateListenerCollection <T>(this ITrackingCollection <T> tcol,
                                                                            IList <T> stickieItemsOnTop = null)
            where T : ICopyable <T>
        {
            var col = new ObservableCollection <T>();

            tcol.CollectionChanged += (s, e) =>
            {
                var offset = 0;
                if (stickieItemsOnTop != null)
                {
                    foreach (var item in stickieItemsOnTop)
                    {
                        if (col.Contains(item))
                        {
                            offset++;
                        }
                    }
                }

                if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Move)
                {
                    for (int i = 0, oldIdx = e.OldStartingIndex, newIdx = e.NewStartingIndex;
                         i < e.OldItems.Count; i++, oldIdx++, newIdx++)
                    {
                        col.Move(oldIdx + offset, newIdx + offset);
                    }
                }
                else if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
                {
                    foreach (T item in e.NewItems)
                    {
                        col.Add(item);
                    }
                }
                else if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
                {
                    foreach (T item in e.OldItems)
                    {
                        col.Remove(item);
                    }
                }
                else if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Replace)
                {
                    for (int i = 0, idx = e.OldStartingIndex;
                         i < e.OldItems.Count; i++, idx++)
                    {
                        col[idx + offset] = (T)e.NewItems[i];
                    }
                }
                else if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Reset)
                {
                    col.Clear();
                    if (stickieItemsOnTop != null)
                    {
                        foreach (var item in stickieItemsOnTop)
                        {
                            col.Add(item);
                        }
                    }
                }
            };
            return(col);
        }
        private static void SetEntityProperties(this ITrackable targetItem, ITrackable sourceItem,
                                                ITrackingCollection changeTracker)
        {
            // List of 'prop.SetValue' actions
            var actions = new List <Action>();

            // Iterate simple properties
#if SILVERLIGHT || NET40
            foreach (var prop in targetItem.GetType().GetProperties().Where(p => p.CanWrite)
#else
            foreach (var prop in targetItem.GetType().GetTypeInfo().DeclaredProperties
                     .Where(p => p.CanWrite && !p.GetMethod.IsPrivate)
#endif
                     .Except(targetItem.GetNavigationProperties(false).Select(np => np.Property)))
            {
                // Skip tracking properties
                if (prop.Name == Constants.TrackingProperties.TrackingState ||
                    prop.Name == Constants.TrackingProperties.ModifiedProperties)
                {
                    continue;
                }

                // Get source item prop value
                object sourceValue = prop.GetValue(sourceItem, null);
                object targetValue = prop.GetValue(targetItem, null);

                // Continue if source is null or source and target equal
                if (sourceValue == null || sourceValue.Equals(targetValue))
                {
                    continue;
                }

                // Deferred 'SetValue'
                actions.Add(() => prop.SetValue(targetItem, sourceValue, null));
            }

            // Iterate entity reference properties (skip collections)
            foreach (var refProp in targetItem
                     .GetNavigationProperties(false)
                     .OfReferenceType()
                     .Where(np => np.Property.CanWrite))
            {
                ITrackable targetValue = refProp.EntityReference;

                // Skip non-null trackable
                if (targetValue != null)
                {
                    continue;
                }

                ITrackable sourceValue = sourceItem.GetEntityReferenceProperty(refProp.Property).EntityReference;

                // Continue if source is null
                if (sourceValue == null)
                {
                    continue;
                }

                // Deferred 'SetValue'
                actions.Add(() => refProp.Property.SetValue(targetItem, sourceValue, null));
            }

            // Nothing to do?
            if (!actions.Any())
            {
                return;
            }

            // Turn off change-tracking
            bool isTracking = changeTracker.Tracking;
            changeTracker.Tracking = false;

            // Set target item prop value
            foreach (var action in actions)
            {
                action();
            }

            // Reset change-tracking
            changeTracker.Tracking = isTracking;
        }