Skip to content

Commit 96ccc48

Browse files
committed
Tweaks
1 parent 4cdb492 commit 96ccc48

9 files changed

+55
-85
lines changed

Diff for: README.md

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
A proof of concept for Windows Forms Roslyn analyzers
2+
3+
---
4+
5+
`ControlTabOrderAnalyzer` checks and warns if the programmatic tab order of UI controls on a form or a usercontrol is different from the natural tab order.
6+
7+
![example](img/ControlTabOrderAnalyzer.png)
8+
9+
Related reading:
10+
* "_Order of elements in the UIA tree_" section in [Considerations around the accessibility of a WinForms Store app](https://docs.microsoft.com/en-us/archive/blogs/winuiautomation/considerations-around-the-accessibility-of-a-winforms-store-app)
11+
* "_Order, Order! The keyboard and programmatic order of your controls will make or break the experience for your customers_" section in [Real-world learnings on keyboard accessibility in WinForms and WPF apps](https://www.linkedin.com/pulse/real-world-learnings-keyboard-accessibility-winforms-wpf-guy-barker)

Diff for: img/ControlTabOrderAnalyzer.png

231 KB
Loading

Diff for: src/WindowsForms.Analyzers/ControlTabOrderAnalyzer.cs

+7-7
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,14 @@ private void CodeBlockAction(OperationBlockAnalysisContext context)
9191
if (expressionStatementOperation.Operation is IOperation invocationOperation &&
9292
invocationOperation.Syntax is InvocationExpressionSyntax expressionSyntax)
9393
{
94-
ParseControlAddStatements(context, expressionSyntax);
94+
ParseControlAddStatements(expressionSyntax);
9595
continue;
9696
}
9797

9898
// Look for ".TabIndex = <x>"
9999
if (expressionStatementOperation.Operation is IAssignmentOperation assignmentOperation)
100100
{
101-
ParseTabIndexAssignments(context, (AssignmentExpressionSyntax)assignmentOperation.Syntax);
101+
ParseTabIndexAssignments((AssignmentExpressionSyntax)assignmentOperation.Syntax, context);
102102
continue;
103103
}
104104

@@ -153,7 +153,7 @@ private void CodeBlockAction(OperationBlockAnalysisContext context)
153153
continue;
154154
}
155155

156-
Diagnostic diagnostic = Diagnostic.Create(s_inconsistentTabIndexRule,
156+
var diagnostic = Diagnostic.Create(s_inconsistentTabIndexRule,
157157
location: _controlsAddIndexLocations[key],
158158
key, addIndex, tabIndex);
159159
context.ReportDiagnostic(diagnostic);
@@ -162,7 +162,7 @@ private void CodeBlockAction(OperationBlockAnalysisContext context)
162162
}
163163
}
164164

165-
private void ParseControlAddStatements(OperationBlockAnalysisContext context, InvocationExpressionSyntax expressionSyntax)
165+
private void ParseControlAddStatements(InvocationExpressionSyntax expressionSyntax)
166166
{
167167
if (!expressionSyntax.Expression.ToString().EndsWith(".Controls.Add"))
168168
{
@@ -203,10 +203,10 @@ private void ParseControlAddStatements(OperationBlockAnalysisContext context, In
203203
}
204204

205205
_controlsAddIndex[container].Add(controlName);
206-
_controlsAddIndexLocations[controlName] = Location.Create(syntax.SyntaxTree, syntax.Span);
206+
_controlsAddIndexLocations[controlName] = syntax.Parent.GetLocation(); // Location.Create(syntax.SyntaxTree, syntax.Span);
207207
}
208208

209-
private void ParseTabIndexAssignments(OperationBlockAnalysisContext context, AssignmentExpressionSyntax expressionSyntax)
209+
private void ParseTabIndexAssignments(AssignmentExpressionSyntax expressionSyntax, OperationBlockAnalysisContext context)
210210
{
211211
var propertyNameExpressionSyntax = (MemberAccessExpressionSyntax)expressionSyntax.Left;
212212
SimpleNameSyntax propertyNameSyntax = propertyNameExpressionSyntax.Name;
@@ -226,7 +226,7 @@ private void ParseTabIndexAssignments(OperationBlockAnalysisContext context, Ass
226226
if (expressionSyntax.Right is not LiteralExpressionSyntax propertyValueExpressionSyntax)
227227
{
228228
var diagnostic = Diagnostic.Create(s_nonNumericTabIndexValueRule,
229-
Location.Create(expressionSyntax.Right.SyntaxTree, expressionSyntax.Right.Span),
229+
expressionSyntax.Right.GetLocation(),
230230
controlName,
231231
expressionSyntax.Right.ToString());
232232
context.ReportDiagnostic(diagnostic);

Diff for: src/WindowsForms.CodeFixes/WinFormsAccessibilityCodeFixProvider.cs

+15-2
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,16 @@
99
using Microsoft.CodeAnalysis;
1010
using Microsoft.CodeAnalysis.CodeActions;
1111
using Microsoft.CodeAnalysis.CodeFixes;
12+
using Microsoft.CodeAnalysis.CodeRefactorings;
1213
using Microsoft.CodeAnalysis.CSharp;
1314
using Microsoft.CodeAnalysis.CSharp.Syntax;
1415
using Microsoft.CodeAnalysis.Rename;
1516
using WindowsForms.Analyzers;
1617

1718
namespace WindowsForms
1819
{
19-
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(WinFormsAccessibilityCodeFixProvider)), Shared]
20-
public class WinFormsAccessibilityCodeFixProvider : CodeFixProvider
20+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ControlTabOrderAnalyzerCodeFixProvider)), Shared]
21+
public class ControlTabOrderAnalyzerCodeFixProvider : CodeFixProvider
2122
{
2223
public sealed override ImmutableArray<string> FixableDiagnosticIds
2324
{
@@ -69,4 +70,16 @@ private async Task<Solution> MakeUppercaseAsync(Document document, TypeDeclarati
6970
return newSolution;
7071
}
7172
}
73+
74+
[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = nameof(ControlTabOrderAnalyzerCodeRefactoringProvider)), Shared]
75+
76+
public class ControlTabOrderAnalyzerCodeRefactoringProvider : CodeRefactoringProvider
77+
{
78+
public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
79+
{
80+
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
81+
82+
throw new System.NotImplementedException();
83+
}
84+
}
7285
}

Diff for: tests/App/Form1.Designer.cs

+15-14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: tests/App/Form1.resx

+1-60
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,5 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<root>
3-
<!--
4-
Microsoft ResX Schema
5-
6-
Version 2.0
7-
8-
The primary goals of this format is to allow a simple XML format
9-
that is mostly human readable. The generation and parsing of the
10-
various data types are done through the TypeConverter classes
11-
associated with the data types.
12-
13-
Example:
14-
15-
... ado.net/XML headers & schema ...
16-
<resheader name="resmimetype">text/microsoft-resx</resheader>
17-
<resheader name="version">2.0</resheader>
18-
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
19-
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
20-
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
21-
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
22-
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
23-
<value>[base64 mime encoded serialized .NET Framework object]</value>
24-
</data>
25-
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
26-
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
27-
<comment>This is a comment</comment>
28-
</data>
29-
30-
There are any number of "resheader" rows that contain simple
31-
name/value pairs.
32-
33-
Each data row contains a name, and value. The row also contains a
34-
type or mimetype. Type corresponds to a .NET class that support
35-
text/value conversion through the TypeConverter architecture.
36-
Classes that don't support this are serialized and stored with the
37-
mimetype set.
38-
39-
The mimetype is used for serialized objects, and tells the
40-
ResXResourceReader how to depersist the object. This is currently not
41-
extensible. For a given mimetype the value must be set accordingly:
42-
43-
Note - application/x-microsoft.net.object.binary.base64 is the format
44-
that the ResXResourceWriter will generate, however the reader can
45-
read any of the formats listed below.
46-
47-
mimetype: application/x-microsoft.net.object.binary.base64
48-
value : The object must be serialized with
49-
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
50-
: and then encoded with base64 encoding.
51-
52-
mimetype: application/x-microsoft.net.object.soap.base64
53-
value : The object must be serialized with
54-
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
55-
: and then encoded with base64 encoding.
56-
57-
mimetype: application/x-microsoft.net.object.bytearray.base64
58-
value : The object must be serialized into a byte array
59-
: using a System.ComponentModel.TypeConverter
60-
: and then encoded with base64 encoding.
61-
-->
2+
<root>
623
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
634
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
645
<xsd:element name="root" msdata:IsDataSet="true">

Diff for: tests/App/TestApp.csproj

+4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
1313
<OutputItemType>Analyzer</OutputItemType>
1414
</ProjectReference>
15+
<ProjectReference Include="..\..\src\WindowsForms.CodeFixes\WindowsForms.CodeFixes.csproj">
16+
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
17+
<OutputItemType>Analyzer</OutputItemType>
18+
</ProjectReference>
1519
</ItemGroup>
1620

1721
</Project>

Diff for: tests/WindowsForms.Analyzers.Tests/WinFormsAccessibilityTests.InconsistentTabIndexRule.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
using WindowsForms.Analyzers;
77
using VerifyCS = WindowsForms.Test.CSharpCodeFixVerifier<
88
WindowsForms.Analyzers.ControlTabOrderAnalyzer,
9-
WindowsForms.WinFormsAccessibilityCodeFixProvider>;
9+
WindowsForms.ControlTabOrderAnalyzerCodeFixProvider>;
1010

1111
namespace WindowsForms.Test
1212
{

Diff for: tests/WindowsForms.Analyzers.Tests/WinFormsAccessibilityTests.NonNumericTabIndexValueRule.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
using WindowsForms.Analyzers;
77
using VerifyCS = WindowsForms.Test.CSharpCodeFixVerifier<
88
WindowsForms.Analyzers.ControlTabOrderAnalyzer,
9-
WindowsForms.WinFormsAccessibilityCodeFixProvider>;
9+
WindowsForms.ControlTabOrderAnalyzerCodeFixProvider>;
1010

1111
namespace WindowsForms.Test
1212
{

0 commit comments

Comments
 (0)