/// <summary> /// ReferencesGraphProvider supports tracking changes. /// ProjectContextChanged gets fired everytime dependencies change. TrackChangesAsync updates /// ExpandedGraphContexts as necessary to reflect changes. /// </summary> private void ProjectContextProvider_ProjectContextChanged(object sender, ProjectContextEventArgs e) { ThreadHelper.JoinableTaskFactory.RunAsync(async() => { await TrackChangesAsync(e.Context).ConfigureAwait(false); }); }
/// <summary> /// When project context changed event received from any context, send a global level /// "context changed" event, to notify <see cref="DependenciesGraphProvider"/>. /// </summary> internal void OnProjectContextChanged(object sender, ProjectContextEventArgs e) { var context = e.Context; if (context == null) { return; } ProjectContextChanged?.Invoke(this, e); }
/// <summary> /// Property ExpandedGraphContexts remembers graph expanded or checked so far. /// Each context represents one level in the graph, i.e. a node and its first level dependencies /// Tracking changes over all expanded contexts ensures that all levels are processed /// and updated when there are any changes in nodes data. /// </summary> internal async Task TrackChangesAsync(ProjectContextEventArgs updatedProjectContext) { foreach (var graphContext in ExpandedGraphContexts.ToList()) { try { await TrackChangesOnGraphContextAsync(graphContext, updatedProjectContext).ConfigureAwait(false); } finally { // Calling OnCompleted ensures that the changes are reflected in UI graphContext.OnCompleted(); } } }
/// <summary> /// When a given project context is unloaded, remove it form the cache and unregister event handlers /// </summary> internal void OnProjectContextUnloaded(object sender, ProjectContextEventArgs e) { var context = e.Context; if (context == null) { return; } ProjectContextUnloaded?.Invoke(this, e); // Remove context for the unloaded project from the cache ProjectContexts.TryRemove(context.ProjectFilePath, out IDependenciesGraphProjectContext removedContext); context.ProjectContextChanged -= OnProjectContextChanged; context.ProjectContextUnloaded -= OnProjectContextUnloaded; }
/// <summary> /// ReferencesGraphProvider supports tracking changes. /// ProjectContextChanged gets fired everytime dependencies change. TrackChangesAsync updates /// ExpandedGraphContexts as necessary to reflect changes. /// </summary> private void ProjectContextProvider_ProjectContextChanged(object sender, ProjectContextEventArgs e) { lock (_changedContextsQueueLock) { _changedContextsQueue[e.Context.ProjectFilePath] = e; // schedule new track changes request in the queue if (_trackChangesTask == null || _trackChangesTask.IsCompleted) { _trackChangesTask = RunTrackChangesAsync(); } else { _trackChangesTask = _trackChangesTask.ContinueWith(t => RunTrackChangesAsync(), TaskScheduler.Default); } } }
/// <summary> /// Tries to apply changes to corresponding node in given GraphContext for given changes /// </summary> private async Task TrackChangesOnGraphContextAsync(IGraphContext graphContext, ProjectContextEventArgs changes) { var updatedProjectContext = changes.Context; foreach (var inputGraphNode in graphContext.InputNodes.ToList()) { var existingNodeInfo = inputGraphNode.GetValue <IDependencyNode>( DependenciesGraphSchema.DependencyNodeProperty); if (existingNodeInfo == null) { continue; } var projectPath = GetPartialValueFromGraphNodeId(inputGraphNode.Id, CodeGraphNodeIdName.Assembly); if (string.IsNullOrEmpty(projectPath)) { continue; } if (!ShouldTrackChangesForNode(existingNodeInfo, projectPath, updatedProjectContext.ProjectFilePath)) { continue; } var subTreeProvider = await GetSubTreeProviderAsync(graphContext, inputGraphNode, projectPath, existingNodeInfo.Id).ConfigureAwait(false); if (subTreeProvider == null) { continue; } var updatedNode = subTreeProvider.GetDependencyNode(existingNodeInfo.Id); if (updatedNode == null) { continue; } var newNode = DependencyNode.Clone(existingNodeInfo); bool anyChanges = AnyChangesToTrack(newNode, updatedNode, changes.Diff, out IEnumerable <IDependencyNode> nodesToAdd, out IEnumerable <IDependencyNode> nodesToRemove); if (!anyChanges) { continue; } // register providers for new nodes outside of scope using (it can not have await there) await RegisterProviders(graphContext, projectPath, nodesToAdd).ConfigureAwait(false); using (var scope = new GraphTransactionScope()) { foreach (var nodeToRemove in nodesToRemove) { RemoveGraphNode(graphContext, projectPath, nodeToRemove, inputGraphNode); newNode.RemoveChild(nodeToRemove); } foreach (var nodeToAdd in nodesToAdd) { AddGraphNode(graphContext, projectPath, null, inputGraphNode, nodeToAdd); newNode.AddChild(nodeToAdd); } // Update the node info saved on the 'inputNode' inputGraphNode.SetValue(DependenciesGraphSchema.DependencyNodeProperty, newNode); scope.Complete(); } } }
public async Task DependenciesGraphProvider_TrackChangesAsync_WithContextProject() { var projectPath = @"c:\myproject\project.csproj"; var contextProjectPath = @"c:\mycontextproject\project.csproj"; var nodeIdString = @"file:///[MyProvider;MyNodeItemSpec]"; var inputNode = IGraphContextFactory.CreateNode(projectPath, nodeIdString); var existingNode = IDependencyNodeFactory.FromJson(@" { ""Id"": { ""ProviderType"": ""MyProvider"", ""ItemSpec"": ""MyNodeItemSpec"" } }", DependencyNode.DependsOnOtherProviders); existingNode.Id.ContextProject = contextProjectPath; var existingChildNode = IDependencyNodeFactory.FromJson(@" { ""Id"": { ""ProviderType"": ""MyProvider"", ""ItemSpec"": ""MyChildNodeItemSpecExisting"" } }"); var existingRefreshedNode = IDependencyNodeFactory.FromJson(@" { ""Id"": { ""ProviderType"": ""MyProvider"", ""ItemSpec"": ""MyNodeItemSpec"" } }", DependencyNode.DependsOnOtherProviders); existingRefreshedNode.Id.ContextProject = contextProjectPath; var newChildNode = IDependencyNodeFactory.FromJson(@" { ""Id"": { ""ProviderType"": ""MyProvider"", ""ItemSpec"": ""MyChildNodeItemSpecNew"" } }"); existingNode.AddChild(existingChildNode); existingRefreshedNode.AddChild(newChildNode); var diff = IDependenciesChangeDiffFactory.Implement(new[] { newChildNode }, new IDependencyNode[] { }, new IDependencyNode[] { }); var mockProvider = new IProjectDependenciesSubTreeProviderMock(); mockProvider.AddTestDependencyNodes(new[] { existingRefreshedNode }); inputNode.SetValue(DependenciesGraphSchema.DependencyNodeProperty, existingNode); inputNode.SetValue(DependenciesGraphSchema.ProviderProperty, mockProvider); var outputNodes = new HashSet <GraphNode>(); var mockGraphContext = IGraphContextFactory.ImplementTrackChanges(inputNode, outputNodes); var updatedProjectContext = IDependenciesGraphProjectContextProviderFactory.ImplementProjectContext(contextProjectPath); var provider = new TestableDependenciesGraphProvider(IDependenciesGraphProjectContextProviderFactory.Create(), Mock.Of <SVsServiceProvider>(), new IProjectThreadingServiceMock().JoinableTaskContext); provider.AddExpandedGraphContext(mockGraphContext); var changes = new ProjectContextEventArgs(updatedProjectContext, diff); // Act await provider.TrackChangesAsync(changes); // Assert Assert.Equal(1, outputNodes.Count); var outputNode = outputNodes.First(); var outputDependency = outputNode.GetValue <IDependencyNode>(DependenciesGraphSchema.DependencyNodeProperty); Assert.Equal(newChildNode.Id, outputDependency.Id); var childProjectPath = outputNode.Id.GetNestedValueByName <Uri>(CodeGraphNodeIdName.Assembly); Assert.Equal(projectPath.Replace('\\', '/'), childProjectPath.AbsolutePath); }
public async Task DependenciesGraphProvider_TrackChangesAsync_InvalidNodeData( string projectPath, bool existingNodeSpecified, bool updatedNodeProvided) { var nodeIdString = @"file:///[MyProvider;MyNodeItemSpec]"; var inputNode = IGraphContextFactory.CreateNode(projectPath, nodeIdString); var existingNode = IDependencyNodeFactory.FromJson(@" { ""Id"": { ""ProviderType"": ""MyProvider"", ""ItemSpec"": ""MyNodeItemSpec"" } }"); var existingChildNode = IDependencyNodeFactory.FromJson(@" { ""Id"": { ""ProviderType"": ""MyProvider"", ""ItemSpec"": ""MyChildNodeItemSpecExisting"" } }"); var existingRefreshedNode = IDependencyNodeFactory.FromJson(@" { ""Id"": { ""ProviderType"": ""MyProvider"", ""ItemSpec"": ""MyNodeItemSpec"" } }"); var newChildNode = IDependencyNodeFactory.FromJson(@" { ""Id"": { ""ProviderType"": ""MyProvider"", ""ItemSpec"": ""MyChildNodeItemSpecNew"" } }"); existingNode.AddChild(existingChildNode); existingRefreshedNode.AddChild(newChildNode); var mockProvider = new IProjectDependenciesSubTreeProviderMock(); if (updatedNodeProvided) { mockProvider.AddTestDependencyNodes(new[] { existingRefreshedNode }); } if (existingNodeSpecified) { inputNode.SetValue(DependenciesGraphSchema.DependencyNodeProperty, existingNode); } inputNode.SetValue(DependenciesGraphSchema.ProviderProperty, mockProvider); var outputNodes = new HashSet <GraphNode>(); var mockGraphContext = IGraphContextFactory.ImplementTrackChanges(inputNode, outputNodes); var updatedProjectContext = IDependenciesGraphProjectContextProviderFactory.ImplementProjectContext(projectPath); var provider = new TestableDependenciesGraphProvider(IDependenciesGraphProjectContextProviderFactory.Create(), Mock.Of <SVsServiceProvider>(), new IProjectThreadingServiceMock().JoinableTaskContext); provider.AddExpandedGraphContext(mockGraphContext); var changes = new ProjectContextEventArgs(updatedProjectContext); // Act await provider.TrackChangesAsync(changes); // Assert Assert.Equal(0, outputNodes.Count); }