提交 fb02f5ff 编写于 作者: H Heejae Chang

fixed todo list race as well

上级 8a266801
......@@ -128,7 +128,6 @@
<InternalsVisibleToTypeScript Include="Microsoft.VisualStudio.LanguageServices.TypeScript" />
<InternalsVisibleToTypeScript Include="Microsoft.Test.Apex.VisualStudio" />
<InternalsVisibleToTypeScript Include="Roslyn.Services.Editor.TypeScript.UnitTests" />
<InternalsVisibleToMoq Include="DynamicProxyGenAssembly2" />
</ItemGroup>
<ItemGroup>
<Compile Include="CommandArgs.cs" />
......@@ -271,7 +270,7 @@
<Compile Include="Implementation\Suggestions\FixMultipleSuggestedAction.cs" />
<Compile Include="Implementation\TodoComment\ITodoListProvider.cs" />
<Compile Include="Implementation\TodoComment\TodoItem.cs" />
<Compile Include="Implementation\TodoComment\TodoListEventArgs.cs" />
<Compile Include="Implementation\TodoComment\TodoItemsUpdatedArgs.cs" />
<Compile Include="Host\ICallHierarchyPresenter.cs" />
<Compile Include="Host\IPreviewPaneService.cs" />
<Compile Include="Host\INavigableItemsPresenter.cs" />
......
......@@ -5,6 +5,7 @@
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Common;
using Microsoft.CodeAnalysis.Editor.Shared.Options;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
......@@ -150,6 +151,14 @@ public ImmutableArray<TodoItem> GetTodoItems(Workspace workspace, DocumentId id,
return existingData.Items;
}
public IEnumerable<UpdatedEventArgs> GetTodoItemsUpdatedEventArgs(Workspace workspace, CancellationToken cancellationToken)
{
foreach (var documentId in _state.GetDocumentIds())
{
yield return new UpdatedEventArgs(Tuple.Create(this, documentId), workspace, documentId.ProjectId, documentId);
}
}
private static bool CheckVersions(Document document, VersionStamp textVersion, VersionStamp syntaxVersion, Data existingData)
{
// first check full version to see whether we can reuse data in same session, if we can't, check timestamp only version to see whether
......
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
using Microsoft.CodeAnalysis.Common;
namespace Microsoft.CodeAnalysis.Editor
{
......@@ -17,8 +19,13 @@ internal interface ITodoListProvider
/// When an event handler is newly added, this event will fire for the currently available todo items and then
/// afterward for any changes since.
/// </summary>
event EventHandler<TodoListEventArgs> TodoListUpdated;
event EventHandler<TodoItemsUpdatedArgs> TodoListUpdated;
ImmutableArray<TodoItem> GetTodoItems(Workspace workspace, DocumentId documentId, CancellationToken cancellationToken);
/// <summary>
/// Get current UpdatedEventArgs stored in ITodoListProvider
/// </summary>
IEnumerable<UpdatedEventArgs> GetTodoItemsUpdatedEventArgs(Workspace workspace, CancellationToken cancellationToken);
}
}
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Runtime.CompilerServices;
using System.Threading;
using Microsoft.CodeAnalysis.Common;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.SolutionCrawler;
......@@ -35,11 +37,11 @@ internal void RaiseTaskListUpdated(object id, Workspace workspace, Solution solu
var handler = this.TodoListUpdated;
if (handler != null)
{
handler(this, new TodoListEventArgs(Tuple.Create(this, id), workspace, solution, projectId, documentId, items));
handler(this, new TodoItemsUpdatedArgs(Tuple.Create(this, id), workspace, solution, projectId, documentId, items));
}
}
public event EventHandler<TodoListEventArgs> TodoListUpdated;
public event EventHandler<TodoItemsUpdatedArgs> TodoListUpdated;
public ImmutableArray<TodoItem> GetTodoItems(Workspace workspace, DocumentId documentId, CancellationToken cancellationToken)
{
......@@ -58,6 +60,17 @@ public ImmutableArray<TodoItem> GetTodoItems(Workspace workspace, DocumentId doc
return analyzer.GetTodoItems(workspace, document.Id, cancellationToken);
}
public IEnumerable<UpdatedEventArgs> GetTodoItemsUpdatedEventArgs(Workspace workspace, CancellationToken cancellationToken)
{
var analyzer = TryGetAnalyzer(workspace);
if (analyzer == null)
{
return ImmutableArray<UpdatedEventArgs>.Empty;
}
return analyzer.GetTodoItemsUpdatedEventArgs(workspace, cancellationToken);
}
private TodoCommentIncrementalAnalyzer TryGetAnalyzer(Workspace workspace)
{
TodoCommentIncrementalAnalyzer analyzer;
......
......@@ -84,6 +84,11 @@ protected override void WriteTo(Stream stream, Data data, CancellationToken canc
}
}
public ImmutableArray<DocumentId> GetDocumentIds()
{
return DataCache.Keys.ToImmutableArrayOrEmpty();
}
public ImmutableArray<TodoItem> GetItems_TestingOnly(DocumentId documentId)
{
Data data;
......
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Immutable;
namespace Microsoft.CodeAnalysis.Editor
{
internal class TodoListEventArgs : EventArgs
{
/// <summary>
/// The identity of task item group.
/// </summary>
public object Id { get; }
/// <summary>
/// Workspace this task items are associated with
/// </summary>
public Workspace Workspace { get; }
/// <summary>
/// Solution this task items are associated with
/// </summary>
public Solution Solution { get; }
/// <summary>
/// projectId this task items are associated with
/// </summary>
public ProjectId ProjectId { get; }
/// <summary>
/// documentId this task items are associated with
/// </summary>
public DocumentId DocumentId { get; }
/// <summary>
/// The task items associated with the ID.
/// </summary>
public ImmutableArray<TodoItem> TodoItems { get; }
public TodoListEventArgs(
object id, Workspace workspace, Solution solution, ProjectId projectId, DocumentId documentId, ImmutableArray<TodoItem> todoItems)
{
this.Id = id;
this.Workspace = workspace;
this.Solution = solution;
this.ProjectId = projectId;
this.DocumentId = documentId;
this.TodoItems = todoItems;
}
}
}
......@@ -2,16 +2,31 @@
using System;
namespace Microsoft.CodeAnalysis.Diagnostics
namespace Microsoft.CodeAnalysis.Common
{
internal class DiagnosticsArgs : EventArgs
internal class UpdatedEventArgs : EventArgs
{
/// <summary>
/// The identity of update group.
/// </summary>
public object Id { get; }
/// <summary>
/// Workspace this update is associated with
/// </summary>
public Workspace Workspace { get; }
/// <summary>
/// projectId this update is associated with
/// </summary>
public ProjectId ProjectId { get; }
/// <summary>
/// documentId this update is associated with
/// </summary>
public DocumentId DocumentId { get; }
public DiagnosticsArgs(object id, Workspace workspace, ProjectId projectId, DocumentId documentId)
public UpdatedEventArgs(object id, Workspace workspace, ProjectId projectId, DocumentId documentId)
{
this.Id = id;
this.Workspace = workspace;
......
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.CodeAnalysis.Common;
namespace Microsoft.CodeAnalysis.Diagnostics
{
/// <summary>
/// Base type of a type that is used as <see cref="DiagnosticsArgs.Id"/> for live diagnostic
/// Base type of a type that is used as <see cref="UpdatedEventArgs.Id"/> for live diagnostic
/// </summary>
internal class AnalyzerUpdateArgsId : BuildToolId.Base<DiagnosticAnalyzer>, ISupportLiveUpdate
{
......
......@@ -7,6 +7,7 @@
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Common;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Roslyn.Utilities;
......@@ -201,7 +202,7 @@ private static IEnumerable<DiagnosticData> FilterSuppressedDiagnostics(IEnumerab
}
}
public IEnumerable<DiagnosticsArgs> GetDiagnosticsArgs(Workspace workspace, ProjectId projectId, DocumentId documentId, CancellationToken cancellationToken)
public IEnumerable<UpdatedEventArgs> GetDiagnosticsUpdatedEventArgs(Workspace workspace, ProjectId projectId, DocumentId documentId, CancellationToken cancellationToken)
{
foreach (var source in _updateSources)
{
......@@ -213,7 +214,7 @@ public IEnumerable<DiagnosticsArgs> GetDiagnosticsArgs(Workspace workspace, Proj
foreach (var data in list.Object)
{
yield return new DiagnosticsArgs(data.Id, data.Workspace, data.ProjectId, data.DocumentId);
yield return new UpdatedEventArgs(data.Id, data.Workspace, data.ProjectId, data.DocumentId);
}
}
}
......@@ -294,12 +295,12 @@ private struct Data : IEquatable<Data>
public readonly object Id;
public readonly ImmutableArray<DiagnosticData> Diagnostics;
public Data(DiagnosticsArgs args) :
public Data(UpdatedEventArgs args) :
this(args, ImmutableArray<DiagnosticData>.Empty)
{
}
public Data(DiagnosticsArgs args, ImmutableArray<DiagnosticData> diagnostics)
public Data(UpdatedEventArgs args, ImmutableArray<DiagnosticData> diagnostics)
{
this.Workspace = args.Workspace;
this.ProjectId = args.ProjectId;
......
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Immutable;
using Microsoft.CodeAnalysis.Common;
namespace Microsoft.CodeAnalysis.Diagnostics
{
internal class DiagnosticsUpdatedArgs : DiagnosticsArgs
internal class DiagnosticsUpdatedArgs : UpdatedEventArgs
{
public Solution Solution { get; }
public ImmutableArray<DiagnosticData> Diagnostics { get; }
......
......@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.CodeAnalysis.Common;
namespace Microsoft.CodeAnalysis.Diagnostics
{
......@@ -19,8 +20,8 @@ internal interface IDiagnosticService
IEnumerable<DiagnosticData> GetDiagnostics(Workspace workspace, ProjectId projectId, DocumentId documentId, object id, bool includeSuppressedDiagnostics, CancellationToken cancellationToken);
/// <summary>
/// Get current DiagnosticsArgs stored in IDiagnosticUpdateSource
/// Get current UpdatedEventArgs stored in IDiagnosticUpdateSource
/// </summary>
IEnumerable<DiagnosticsArgs> GetDiagnosticsArgs(Workspace workspace, ProjectId projectId, DocumentId documentId, CancellationToken cancellationToken);
IEnumerable<UpdatedEventArgs> GetDiagnosticsUpdatedEventArgs(Workspace workspace, ProjectId projectId, DocumentId documentId, CancellationToken cancellationToken);
}
}
\ No newline at end of file
......@@ -162,7 +162,7 @@
<Compile Include="Diagnostics\AbstractHostDiagnosticUpdateSource.cs" />
<Compile Include="Diagnostics\AnalyzerUpdateArgsId.cs" />
<Compile Include="Diagnostics\DiagnosticService_UpdateSourceRegistrationService.cs" />
<Compile Include="Diagnostics\DiagnosticsArgs.cs" />
<Compile Include="Common\UpdatedEventArgs.cs" />
<Compile Include="Diagnostics\Extensions.cs" />
<Compile Include="Diagnostics\HostDiagnosticAnalyzerPackage.cs" />
<Compile Include="Diagnostics\IDiagnosticUpdateSourceRegistrationService.cs" />
......
......@@ -8,6 +8,7 @@
using System.Windows;
using System.Windows.Controls;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Common;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Diagnostics.EngineV1;
using Microsoft.CodeAnalysis.Editor.Implementation.Diagnostics;
......@@ -40,16 +41,15 @@ protected class LiveTableDataSource : AbstractRoslynTableDataSource<DiagnosticDa
_tracker = new OpenDocumentTracker<DiagnosticData>(_workspace);
_diagnosticService = diagnosticService;
_diagnosticService.DiagnosticsUpdated += OnDiagnosticsUpdated;
PopulateInitialData(workspace, diagnosticService);
_diagnosticService.DiagnosticsUpdated += OnDiagnosticsUpdated;
}
public override string DisplayName => ServicesVSResources.DiagnosticsTableSourceName;
public override string SourceTypeIdentifier => StandardTableDataSources.ErrorTableDataSource;
public override string Identifier => _identifier;
public override object GetItemKey(object data) => ((DiagnosticsArgs)data).Id;
public override object GetItemKey(object data) => ((UpdatedEventArgs)data).Id;
public override ImmutableArray<TableItem<DiagnosticData>> Deduplicate(IEnumerable<IList<TableItem<DiagnosticData>>> groupedItems)
{
......@@ -102,12 +102,12 @@ protected override object GetOrUpdateAggregationKey(object data)
private bool CheckAggregateKey(AggregatedKey key, DiagnosticsUpdatedArgs args)
{
if (key == null || args == null)
if (key == null)
{
return true;
}
if (args.DocumentId == null || args.Solution == null)
if (args?.DocumentId == null || args?.Solution == null)
{
return true;
}
......@@ -119,20 +119,15 @@ private bool CheckAggregateKey(AggregatedKey key, DiagnosticsUpdatedArgs args)
private object CreateAggregationKey(object data)
{
var args = data as DiagnosticsUpdatedArgs;
if (args == null)
{
return ((DiagnosticsArgs)data).Id;
}
if (args.DocumentId == null || args.Solution == null)
if (args?.DocumentId == null || args?.Solution == null)
{
return args.Id;
return GetItemKey(data);
}
var argumentKey = args.Id as DiagnosticIncrementalAnalyzer.ArgumentKey;
if (argumentKey == null)
{
return args.Id;
return GetItemKey(data);
}
var documents = args.Solution.GetRelatedDocumentIds(args.DocumentId);
......@@ -141,7 +136,7 @@ private object CreateAggregationKey(object data)
private void PopulateInitialData(Workspace workspace, IDiagnosticService diagnosticService)
{
foreach (var args in diagnosticService.GetDiagnosticsArgs(workspace, projectId: null, documentId: null, cancellationToken: CancellationToken.None))
foreach (var args in diagnosticService.GetDiagnosticsUpdatedEventArgs(workspace, projectId: null, documentId: null, cancellationToken: CancellationToken.None))
{
OnDataAddedOrChanged(args);
}
......@@ -172,7 +167,7 @@ private void OnDiagnosticsUpdated(object sender, DiagnosticsUpdatedArgs e)
public override AbstractTableEntriesSource<DiagnosticData> CreateTableEntriesSource(object data)
{
var item = (DiagnosticsArgs)data;
var item = (UpdatedEventArgs)data;
return new TableEntriesSource(this, item.Workspace, item.ProjectId, item.DocumentId, item.Id);
}
......
......@@ -6,6 +6,7 @@
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Common;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServices.Implementation.Diagnostics;
......@@ -76,14 +77,17 @@ private class TableDataSource : AbstractRoslynTableDataSource<TodoItem>
{
_workspace = workspace;
_identifier = identifier;
_todoListProvider = todoListProvider;
_todoListProvider.TodoListUpdated += OnTodoListUpdated;
PopulateInitialData(workspace, _todoListProvider);
}
public override string DisplayName => ServicesVSResources.TodoTableSourceName;
public override string SourceTypeIdentifier => StandardTableDataSources.CommentTableDataSource;
public override string Identifier => _identifier;
public override object GetItemKey(object data) => ((TodoListEventArgs)data).DocumentId;
public override object GetItemKey(object data) => ((UpdatedEventArgs)data).DocumentId;
protected override object GetOrUpdateAggregationKey(object data)
{
......@@ -95,7 +99,12 @@ protected override object GetOrUpdateAggregationKey(object data)
return key;
}
if (!CheckAggregateKey((ImmutableArray<DocumentId>)key, (TodoListEventArgs)data))
if (!(key is ImmutableArray<DocumentId>))
{
return key;
}
if (!CheckAggregateKey((ImmutableArray<DocumentId>)key, data as TodoItemsUpdatedArgs))
{
RemoveStaledData(data);
......@@ -106,9 +115,9 @@ protected override object GetOrUpdateAggregationKey(object data)
return key;
}
private bool CheckAggregateKey(ImmutableArray<DocumentId> key, TodoListEventArgs args)
private bool CheckAggregateKey(ImmutableArray<DocumentId> key, TodoItemsUpdatedArgs args)
{
if (args.DocumentId == null || args.Solution == null)
if (args?.DocumentId == null || args?.Solution == null)
{
return true;
}
......@@ -119,10 +128,10 @@ private bool CheckAggregateKey(ImmutableArray<DocumentId> key, TodoListEventArgs
private object CreateAggregationKey(object data)
{
var args = (TodoListEventArgs)data;
if (args.Solution == null)
var args = data as TodoItemsUpdatedArgs;
if (args?.Solution == null)
{
return args.DocumentId;
return GetItemKey(data);
}
return args.Solution.GetRelatedDocumentIds(args.DocumentId);
......@@ -149,7 +158,15 @@ private static IEnumerable<TableItem<TodoItem>> Order(IEnumerable<TableItem<Todo
.ThenBy(d => d.Primary.OriginalColumn);
}
private void OnTodoListUpdated(object sender, TodoListEventArgs e)
private void PopulateInitialData(Workspace workspace, ITodoListProvider todoListService)
{
foreach (var args in todoListService.GetTodoItemsUpdatedEventArgs(workspace, cancellationToken: CancellationToken.None))
{
OnDataAddedOrChanged(args);
}
}
private void OnTodoListUpdated(object sender, TodoItemsUpdatedArgs e)
{
if (_workspace != e.Workspace)
{
......@@ -169,7 +186,7 @@ private void OnTodoListUpdated(object sender, TodoListEventArgs e)
public override AbstractTableEntriesSource<TodoItem> CreateTableEntriesSource(object data)
{
var item = (TodoListEventArgs)data;
var item = (UpdatedEventArgs)data;
return new TableEntriesSource(this, item.Workspace, item.DocumentId);
}
......
......@@ -5,6 +5,7 @@ Imports System.Threading
Imports System.Windows
Imports System.Windows.Controls
Imports Microsoft.CodeAnalysis
Imports Microsoft.CodeAnalysis.Common
Imports Microsoft.CodeAnalysis.Diagnostics
Imports Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces
Imports Microsoft.CodeAnalysis.Options
......@@ -546,16 +547,16 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics
Return diagnostics
End Function
Public Function GetDiagnosticsArgs(workspace As Workspace, projectId As ProjectId, documentId As DocumentId, cancellationToken As CancellationToken) As IEnumerable(Of DiagnosticsArgs) Implements IDiagnosticService.GetDiagnosticsArgs
Public Function GetDiagnosticsArgs(workspace As Workspace, projectId As ProjectId, documentId As DocumentId, cancellationToken As CancellationToken) As IEnumerable(Of UpdatedEventArgs) Implements IDiagnosticService.GetDiagnosticsUpdatedEventArgs
Assert.NotNull(workspace)
Dim diagnosticsArgs As IEnumerable(Of DiagnosticsArgs)
Dim diagnosticsArgs As IEnumerable(Of UpdatedEventArgs)
If documentId IsNot Nothing Then
diagnosticsArgs = Items.Where(Function(t) t.DocumentId Is documentId) _
.Select(
Function(t)
Return New DiagnosticsArgs(
Return New UpdatedEventArgs(
New ErrorId(Me, If(CObj(t.DocumentId), t.ProjectId)),
t.Workspace, t.ProjectId, t.DocumentId)
End Function).ToImmutableArrayOrEmpty()
......@@ -563,14 +564,14 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics
diagnosticsArgs = Items.Where(Function(t) t.ProjectId Is projectId) _
.Select(
Function(t)
Return New DiagnosticsArgs(
Return New UpdatedEventArgs(
New ErrorId(Me, If(CObj(t.DocumentId), t.ProjectId)),
t.Workspace, t.ProjectId, t.DocumentId)
End Function).ToImmutableArrayOrEmpty()
Else
diagnosticsArgs = Items.Select(
Function(t)
Return New DiagnosticsArgs(
Return New UpdatedEventArgs(
New ErrorId(Me, If(CObj(t.DocumentId), t.ProjectId)),
t.Workspace, t.ProjectId, t.DocumentId)
End Function).ToImmutableArrayOrEmpty()
......
......@@ -3,6 +3,7 @@
Imports System.Collections.Immutable
Imports System.Threading
Imports Microsoft.CodeAnalysis
Imports Microsoft.CodeAnalysis.Common
Imports Microsoft.CodeAnalysis.Editor
Imports Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces
Imports Microsoft.VisualStudio.LanguageServices.Implementation.TableDataSource
......@@ -316,7 +317,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics
Me.Items = items
End Sub
Public Event TodoListUpdated As EventHandler(Of TodoListEventArgs) Implements ITodoListProvider.TodoListUpdated
Public Event TodoListUpdated As EventHandler(Of TodoItemsUpdatedArgs) Implements ITodoListProvider.TodoListUpdated
Public Function GetTodoItems(workspace As Workspace, documentId As DocumentId, cancellationToken As CancellationToken) As ImmutableArray(Of TodoItem) Implements ITodoListProvider.GetTodoItems
Assert.NotNull(workspace)
......@@ -325,17 +326,21 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics
Return Items.Where(Function(t) t.DocumentId Is documentId).ToImmutableArrayOrEmpty()
End Function
Public Function GetTodoItemsUpdatedEventArgs(workspace As Workspace, cancellationToken As CancellationToken) As IEnumerable(Of UpdatedEventArgs) Implements ITodoListProvider.GetTodoItemsUpdatedEventArgs
Return Items.Select(Function(t) New UpdatedEventArgs(Tuple.Create(Me, t.DocumentId), t.Workspace, t.DocumentId.ProjectId, t.DocumentId)).ToImmutableArrayOrEmpty()
End Function
Public Sub RaiseTodoListUpdated(workspace As Workspace)
Dim map = Items.Where(Function(t) t.Workspace Is workspace).ToLookup(Function(t) t.DocumentId)
For Each group In map
RaiseEvent TodoListUpdated(Me, New TodoListEventArgs(
RaiseEvent TodoListUpdated(Me, New TodoItemsUpdatedArgs(
Tuple.Create(Me, group.Key), workspace, workspace.CurrentSolution, group.Key.ProjectId, group.Key, group.ToImmutableArrayOrEmpty()))
Next
End Sub
Public Sub RaiseClearTodoListUpdated(workspace As Workspace, documentId As DocumentId)
RaiseEvent TodoListUpdated(Me, New TodoListEventArgs(
RaiseEvent TodoListUpdated(Me, New TodoItemsUpdatedArgs(
Tuple.Create(Me, documentId), workspace, workspace.CurrentSolution, documentId.ProjectId, documentId, ImmutableArray(Of TodoItem).Empty))
End Sub
End Class
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册