Rubber鸭代码检查可能需要很长时间才能完成,所以我决定在后台线程上运行它们,而不是锁定IDE。唯一的缺点是。当检查运行时,IDE不会被锁定,因此用户最终可能会得到与陈旧的解析结果相反的检查结果--这可能意味着陈旧的检查结果(例如,您将Option Explicit添加到没有它的模块中),或者陈旧的令牌定位(例如添加或删除代码行;每个引用位于下面的令牌的检查结果在导航时会被错误地放置)。
但UX是值得的。我认为。
状态标签指示解析发生时..。

当检查正在运行时,...and:

所有结果都同时在网格中填充:

当没有发现任何问题时,就会有一种明显的状态:

这是演示者的代码:
namespace Rubberduck.UI.CodeInspections
{
public class CodeInspectionsDockablePresenter : DockablePresenterBase
{
private CodeInspectionsWindow Control { get { return UserControl as CodeInspectionsWindow; } }
private IEnumerable<VBProjectParseResult> _parseResults;
private IList<ICodeInspectionResult> _results;
private readonly IInspector _inspector;
public CodeInspectionsDockablePresenter(IInspector inspector, VBE vbe, AddIn addin, CodeInspectionsWindow window)
:base(vbe, addin, window)
{
_inspector = inspector;
_inspector.IssuesFound += OnIssuesFound;
_inspector.Reset += OnReset;
_inspector.Parsing += OnParsing;
_inspector.ParseCompleted += OnParseCompleted;
Control.RefreshCodeInspections += OnRefreshCodeInspections;
Control.NavigateCodeIssue += OnNavigateCodeIssue;
Control.QuickFix += OnQuickFix;
Control.CopyResults += OnCopyResultsToClipboard;
}
// indicates that the _parseResults are no longer in sync with the UI
private bool _needsResync;
private void OnParseCompleted(object sender, ParseCompletedEventArgs e)
{
ToggleParsingStatus(false);
if (sender == this)
{
_needsResync = false;
_parseResults = e.ParseResults;
Task.Run(() => RefreshAsync());
}
else
{
_parseResults = e.ParseResults;
_needsResync = true;
}
}
private void OnParsing(object sender, EventArgs e)
{
ToggleParsingStatus();
Control.Invoke((MethodInvoker) delegate
{
Control.EnableRefresh(false);
});
}
private void ToggleParsingStatus(bool isParsing = true)
{
Control.Invoke((MethodInvoker) delegate
{
Control.ToggleParsingStatus(isParsing);
});
}
private void OnCopyResultsToClipboard(object sender, EventArgs e)
{
var results = string.Join("\n", _results.Select(FormatResultForClipboard));
var text = string.Format("Rubberduck Code Inspections - {0}\n{1} issue" + (_results.Count != 1 ? "s" : string.Empty) + " found.\n",
DateTime.Now, _results.Count) + results;
Clipboard.SetText(text);
}
private string FormatResultForClipboard(ICodeInspectionResult result)
{
var module = result.QualifiedSelection.QualifiedName;
return string.Format(
"{0}: {1} - {2}.{3}, line {4}",
result.Severity,
result.Name,
module.Project.Name,
module.Component.Name,
result.QualifiedSelection.Selection.StartLine);
}
private int _issues;
private void OnIssuesFound(object sender, InspectorIssuesFoundEventArg e)
{
Interlocked.Add(ref _issues, e.Issues.Count);
Control.Invoke((MethodInvoker) delegate
{
var newCount = _issues;
Control.SetIssuesStatus(newCount);
});
}
private void OnQuickFix(object sender, QuickFixEventArgs e)
{
e.QuickFix(VBE);
_needsResync = true;
OnRefreshCodeInspections(null, EventArgs.Empty);
}
public override void Show()
{
base.Show();
Task.Run(() => RefreshAsync());
}
private void OnNavigateCodeIssue(object sender, NavigateCodeEventArgs e)
{
try
{
e.QualifiedName.Component.CodeModule.CodePane.SetSelection(e.Selection);
}
catch (COMException)
{
// gulp
}
}
private void OnRefreshCodeInspections(object sender, EventArgs e)
{
Task.Run(() => RefreshAsync()).ContinueWith(t =>
{
Control.SetIssuesStatus(_results.Count, true);
});
}
private async Task RefreshAsync()
{
Control.Invoke((MethodInvoker) delegate
{
Control.EnableRefresh(false);
Control.Cursor = Cursors.WaitCursor;
});
try
{
if (VBE != null)
{
if (_parseResults == null || _needsResync)
{
_inspector.Parse(VBE, this);
return;
}
var parseResults = _parseResults.SingleOrDefault(p => p.Project == VBE.ActiveVBProject);
if (parseResults == null || _needsResync)
{
_inspector.Parse(VBE, this);
return;
}
_results = await _inspector.FindIssuesAsync(parseResults);
Control.Invoke((MethodInvoker) delegate
{
Control.SetContent(_results.Select(item => new CodeInspectionResultGridViewItem(item))
.OrderBy(item => item.Component)
.ThenBy(item => item.Line));
});
}
}
catch (COMException exception)
{
// swallow
}
finally
{
Control.Invoke((MethodInvoker) delegate
{
Control.Cursor = Cursors.Default;
Control.SetIssuesStatus(_issues, true);
Control.EnableRefresh();
});
}
}
private void OnReset(object sender, EventArgs e)
{
_issues = 0;
Control.Invoke((MethodInvoker) delegate
{
Control.SetIssuesStatus(_issues);
Control.InspectionResults.Clear();
});
}
}
}下面是窗口的代码隐藏:
namespace Rubberduck.UI.CodeInspections
{
public partial class CodeInspectionsWindow : UserControl, IDockableUserControl
{
private const string ClassId = "D3B2A683-9856-4246-BDC8-6B0795DC875B";
string IDockableUserControl.ClassId { get { return ClassId; } }
string IDockableUserControl.Caption { get { return "Code Inspections"; } }
public BindingList<CodeInspectionResultGridViewItem> InspectionResults
{
get { return CodeIssuesGridView.DataSource as BindingList<CodeInspectionResultGridViewItem>; }
set { CodeIssuesGridView.DataSource = value; }
}
public CodeInspectionsWindow()
{
InitializeComponent();
RefreshButton.Click += RefreshButtonClicked;
QuickFixButton.ButtonClick += QuickFixButton_Click;
GoButton.Click += GoButton_Click;
PreviousButton.Click += PreviousButton_Click;
NextButton.Click += NextButton_Click;
CopyButton.Click += CopyButton_Click;
var items = new List<CodeInspectionResultGridViewItem>();
CodeIssuesGridView.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
CodeIssuesGridView.DataSource = new BindingList<CodeInspectionResultGridViewItem>(items);
CodeIssuesGridView.AutoResizeColumns();
CodeIssuesGridView.Columns["Issue"].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
CodeIssuesGridView.SelectionChanged += CodeIssuesGridView_SelectionChanged;
CodeIssuesGridView.CellDoubleClick += CodeIssuesGridView_CellDoubleClick;
}
public void ToggleParsingStatus(bool enabled = true)
{
StatusLabel.Image = enabled
? Resources.hourglass
: Resources.exclamation_diamond;
StatusLabel.Text = enabled
? RubberduckUI.Parsing
: RubberduckUI.CodeInspections_Inspecting;
}
public void SetIssuesStatus(int issueCount, bool completed = false)
{
_issueCount = issueCount;
if (issueCount == 0)
{
if (completed)
{
StatusLabel.Image = Resources.tick_circle;
StatusLabel.Text = RubberduckUI.CodeInspections_NoIssues;
}
else
{
StatusLabel.Image = Resources.hourglass;
StatusLabel.Text = RubberduckUI.CodeInspections_Inspecting;
}
}
else
{
if (completed)
{
StatusLabel.Image = Resources.exclamation_diamond;
StatusLabel.Text = string.Format("{0} issue" + (issueCount != 1 ? "s" : string.Empty), issueCount);
}
else
{
StatusLabel.Image = Resources.hourglass;
StatusLabel.Text = string.Format("{0} ({1} issue" + (issueCount != 1 ? "s" : string.Empty) + ")", RubberduckUI.CodeInspections_Inspecting, issueCount);
}
}
}
private int _issueCount;
public void EnableRefresh(bool enabled = true)
{
RefreshButton.Enabled = enabled;
QuickFixButton.Enabled = enabled && _issueCount > 0;
}
public event EventHandler CopyResults;
private void CopyButton_Click(object sender, EventArgs e)
{
var handler = CopyResults;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
}
private void QuickFixButton_Click(object sender, EventArgs e)
{
QuickFixItemClick(QuickFixButton.DropDownItems.Cast<ToolStripMenuItem>().First(item => item.Checked), EventArgs.Empty);
}
private void PreviousButton_Click(object sender, EventArgs e)
{
var previousIssueIndex = (CodeIssuesGridView.SelectedRows[0].Index == 0)
? CodeIssuesGridView.Rows.Count - 1
: CodeIssuesGridView.SelectedRows[0].Index - 1;
CodeIssuesGridView.Rows[previousIssueIndex].Selected = true;
var item = CodeIssuesGridView.Rows[previousIssueIndex].DataBoundItem as CodeInspectionResultGridViewItem;
OnNavigateCodeIssue(item);
}
private void NextButton_Click(object sender, EventArgs e)
{
if (CodeIssuesGridView.Rows.Count == 0)
{
return;
}
var nextIssueIndex = (CodeIssuesGridView.SelectedRows[0].Index == CodeIssuesGridView.Rows.Count - 1)
? 0
: CodeIssuesGridView.SelectedRows[0].Index + 1;
CodeIssuesGridView.Rows[nextIssueIndex].Selected = true;
var item = CodeIssuesGridView.Rows[nextIssueIndex].DataBoundItem as CodeInspectionResultGridViewItem;
OnNavigateCodeIssue(item);
}
private IDictionary<string, Action<VBE>> _availableQuickFixes;
private void CodeIssuesGridView_SelectionChanged(object sender, EventArgs e)
{
var enableNavigation = (CodeIssuesGridView.SelectedRows.Count != 0);
NextButton.Enabled = enableNavigation;
PreviousButton.Enabled = enableNavigation;
GoButton.Enabled = enableNavigation;
CopyButton.Enabled = enableNavigation;
var quickFixMenu = QuickFixButton.DropDownItems;
if (quickFixMenu.Count > 0)
{
foreach (var quickFixButton in quickFixMenu.Cast<ToolStripMenuItem>())
{
quickFixButton.Click -= QuickFixItemClick;
}
}
if (CodeIssuesGridView.SelectedRows.Count > 0)
{
var issue = (CodeInspectionResultGridViewItem) CodeIssuesGridView.SelectedRows[0].DataBoundItem;
_availableQuickFixes = issue.GetInspectionResultItem()
.GetQuickFixes();
var descriptions = _availableQuickFixes.Keys.ToList();
quickFixMenu.Clear();
foreach (var caption in descriptions)
{
var item = (ToolStripMenuItem) quickFixMenu.Add(caption);
if (quickFixMenu.Count > 0)
{
item.CheckOnClick = false;
item.Checked = quickFixMenu.Count == 1;
item.Click += QuickFixItemClick;
}
}
}
QuickFixButton.Enabled = QuickFixButton.HasDropDownItems;
}
public event EventHandler<QuickFixEventArgs> QuickFix;
private void QuickFixItemClick(object sender, EventArgs e)
{
var quickFixButton = (ToolStripMenuItem)sender;
if (QuickFix == null)
{
return;
}
var args = new QuickFixEventArgs(_availableQuickFixes[quickFixButton.Text]);
QuickFix(this, args);
}
public void SetContent(IEnumerable<CodeInspectionResultGridViewItem> inspectionResults)
{
var results = inspectionResults.ToList();
CodeIssuesGridView.DataSource = new BindingList<CodeInspectionResultGridViewItem>(results);
CodeIssuesGridView.Refresh();
}
private void GoButton_Click(object sender, EventArgs e)
{
var issue = CodeIssuesGridView.SelectedRows[0].DataBoundItem as CodeInspectionResultGridViewItem;
OnNavigateCodeIssue(issue);
}
public event EventHandler<NavigateCodeEventArgs> NavigateCodeIssue;
private void CodeIssuesGridView_CellDoubleClick(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex < 0)
{
return;
}
var issue = CodeIssuesGridView.Rows[e.RowIndex].DataBoundItem as CodeInspectionResultGridViewItem;
OnNavigateCodeIssue(issue);
}
private void OnNavigateCodeIssue(CodeInspectionResultGridViewItem item)
{
var handler = NavigateCodeIssue;
if (handler == null)
{
return;
}
var result = item.GetInspectionResultItem();
handler(this, new NavigateCodeEventArgs(result.QualifiedSelection));
}
public event EventHandler RefreshCodeInspections;
private void RefreshButtonClicked(object sender, EventArgs e)
{
var handler = RefreshCodeInspections;
if (handler == null)
{
return;
}
toolStrip1.Refresh();
handler(this, EventArgs.Empty);
}
}
}这东西很管用..。但是肯定有一些问题-- RefreshAsync运行的频率比它应该的要高。
发布于 2015-05-15 19:01:10
一开始,您的事件处理程序命名使我感到困惑。标准的命名约定是OnEventName引发事件。标准处理程序看起来像ObjectName_EventName。
public class ClassWithEvent : IDisposable
{
public event EventHandler SomeEvent;
protected virtual void OnSomeEvent()
{
var e = SomeEvent;
if (e != null)
e(this, EventArgs.Empty);
}
public void Dispose()
{
SomeEvent = null;
}
}
public class ClassListensToEvent
{
private ClassWithEvent myClass;
public ClassListensToEvent()
{
myClass.SomeEvent += myClass_SomeEvent;
}
private void myClass_SomeEvent(object sender, EventArgs eventArgs)
{
}
}我会对RefreshAsync进行一些重大的重构,以及它是如何使用的。我不完全理解您的工作流程,但我希望RefreshAsync可能会被取消。没有意义的刷新和过时的结果。我将使用CancellationTokenSource并将CancellationToken传递给RefreshAsync和Task.Run,如下所示:
Task.Run(() => RefreshAsync(token), token);如果您得到了更新的结果,首先要做的就是调用CancellationTokenSource.Cancel()来停止任何现有的刷新。在RefreshAsync中,您可以优雅地捕捉和吞咽TaskCancelledException。
最后,RefreshAsync不是很异步。您多次使用Invoke,这会阻塞UI线程。理想情况下,RefreshAsync将是异步的,直到完成,然后在结束时(通过BeginInvoke)更新一次GUI。
通过这些更改,您可以取消RefreshAsync,这样就不会在旧数据上浪费周期,也不会在GUI上绘制旧的结果。
发布于 2015-05-15 06:43:03
只是几枪..。
以减少这里的小代码重复
私有void OnParseCompleted(对象发送方,ParseCompletedEventArgs e) { ToggleParsingStatus(false);if (发送方== this) { _needsResync = false;_parseResults = e.ParseResults;Task.Run(() => RefreshAsync());}=={ _parseResults = e.ParseResults;_needsResync = true;}}
你可以把这个重构成
private void OnParseCompleted(object sender, ParseCompletedEventArgs e)
{
ToggleParsingStatus(false);
_parseResults = e.ParseResults;
_needsResync = sender != this
if (!needsResync)
{
Task.Run(() => RefreshAsync());
}
}在private async Task RefreshAsync()方法中,您有更多的“问题”。首先,您应该通过在if (VBE == null)上使用保护子句来减少水平间距,并尽早返回。
下一个问题在这里
parseResults = _parseResults.SingleOrDefault(p => p.Project == VBE.ActiveVBProject);
你能看见吗??对SingleOrDefault()的调用将基于名称返回单个项或默认项。一个项目!=复数!因此,将局部变量重命名为parseResult。
仔细看看这两条if语句
if (\_parseResults == null || \_needsResync) { \_inspector.Parse(VBE, this); return; } var parseResults = \_parseResults.SingleOrDefault(p => p.Project == VBE.ActiveVBProject); if (parseResults == null || \_needsResync) { \_inspector.Parse(VBE, this); return; }
我们看到,在第二个if条件下,我们可以省略|| _needsResync,因为这永远不会是true。
我刚看到了你写十字句的好方法。
发布于 2015-05-15 07:36:30
小小的评论,我会像这样重构OnCopyResultsToClipboard:
private void OnCopyResultsToClipboard(object sender, EventArgs e)
{
var results = string.Join(Environment.NewLine, _results.Select(FormatResultForClipboard));
var plural = _results.Count != 1 ? "s" : string.Empty;
var text = string.Format("Rubberduck Code Inspections - {0}{3}{1} issue{2} found.{3} {4}",
DateTime.Now, _results.Count, plural, Environment.NewLine, results);
Clipboard.SetText(text);
}你在字符串格式的同时使用字符串连接,这让我觉得很奇怪。
https://codereview.stackexchange.com/questions/90798
复制相似问题