Skip to content

Commit b97b68c

Browse files
committed
Refactor event to routed event
Fix #367
1 parent c562aa0 commit b97b68c

3 files changed

Lines changed: 317 additions & 4 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
namespace WpfAnalyzers.Test.Refactorings;
2+
3+
using Gu.Roslyn.Asserts;
4+
using NUnit.Framework;
5+
using WpfAnalyzers.Refactorings;
6+
7+
public static class EventRefactoringTests
8+
{
9+
private static readonly EventRefactoring Refactoring = new();
10+
11+
[Test]
12+
public static void Event()
13+
{
14+
var before = @"
15+
namespace N
16+
{
17+
using System.Windows;
18+
using System.Windows.Controls;
19+
20+
public class C : Control
21+
{
22+
public event RoutedEventHandler? ↓ValueChanged;
23+
}
24+
}";
25+
26+
var after = @"
27+
namespace N
28+
{
29+
using System.Windows;
30+
using System.Windows.Controls;
31+
32+
public class C : Control
33+
{
34+
public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent(
35+
nameof(ValueChanged),
36+
RoutingStrategy.Direct,
37+
typeof(RoutedEventHandler),
38+
typeof(C));
39+
40+
public event RoutedEventHandler? ValueChanged
41+
{
42+
add => this.AddHandler(ValueChangedEvent, value);
43+
remove => this.RemoveHandler(ValueChangedEvent, value);
44+
}
45+
}
46+
}";
47+
RoslynAssert.Refactoring(Refactoring, before, after);
48+
}
49+
}
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
namespace WpfAnalyzers.Refactorings;
2+
3+
using System.Composition;
4+
using System.Threading.Tasks;
5+
6+
using Gu.Roslyn.AnalyzerExtensions;
7+
using Gu.Roslyn.CodeFixExtensions;
8+
9+
using Microsoft.CodeAnalysis;
10+
using Microsoft.CodeAnalysis.CodeActions;
11+
using Microsoft.CodeAnalysis.CodeRefactorings;
12+
using Microsoft.CodeAnalysis.CSharp;
13+
using Microsoft.CodeAnalysis.CSharp.Syntax;
14+
15+
[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = nameof(EventRefactoring))]
16+
[Shared]
17+
internal class EventRefactoring : CodeRefactoringProvider
18+
{
19+
public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
20+
{
21+
var syntaxRoot = await context.Document.GetSyntaxRootAsync(context.CancellationToken)
22+
.ConfigureAwait(false);
23+
var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken)
24+
.ConfigureAwait(false);
25+
26+
if (syntaxRoot?.FindNode(context.Span) is { } node &&
27+
node.FirstAncestorOrSelf<EventFieldDeclarationSyntax>() is { Declaration.Variables.Count: 1, Parent: ClassDeclarationSyntax containingClass } eventDeclaration &&
28+
!eventDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword) &&
29+
eventDeclaration.Modifiers.Any(SyntaxKind.PublicKeyword, SyntaxKind.InternalKeyword) &&
30+
semanticModel?.GetType(containingClass, context.CancellationToken) is { } containingType &&
31+
containingType.IsAssignableTo(KnownSymbols.DependencyObject, semanticModel.Compilation))
32+
{
33+
context.RegisterRefactoring(
34+
CodeAction.Create(
35+
"Change to routed event",
36+
_ => WithRoutedEvent(),
37+
"Change to routed event"));
38+
39+
Task<Document> WithRoutedEvent()
40+
{
41+
var updatedClass = containingClass.ReplaceNode(eventDeclaration, Event(eventDeclaration))
42+
.AddField(Field(eventDeclaration, containingClass));
43+
44+
return Task.FromResult(
45+
context.Document.WithSyntaxRoot(
46+
syntaxRoot.ReplaceNode(containingClass, updatedClass)!));
47+
}
48+
49+
static FieldDeclarationSyntax Field(EventFieldDeclarationSyntax eventDeclaration, ClassDeclarationSyntax containingClass)
50+
{
51+
return SyntaxFactory.FieldDeclaration(
52+
attributeLists: default,
53+
modifiers: SyntaxFactory.TokenList(
54+
SyntaxFactory.Token(SyntaxKind.PublicKeyword),
55+
SyntaxFactory.Token(SyntaxKind.StaticKeyword),
56+
SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword)),
57+
declaration: SyntaxFactory.VariableDeclaration(
58+
type: SyntaxFactory.ParseTypeName("System.Windows.RoutedEvent")
59+
.WithSimplifiedNames(),
60+
variables: SyntaxFactory.SingletonSeparatedList(
61+
SyntaxFactory.VariableDeclarator(
62+
identifier: SyntaxFactory.Identifier(eventDeclaration.Declaration.Variables[0].Identifier.ValueText + "Event"),
63+
argumentList: default,
64+
initializer: SyntaxFactory.EqualsValueClause(
65+
value: Register(eventDeclaration, containingClass))))),
66+
semicolonToken: SyntaxFactory.Token(default, SyntaxKind.SemicolonToken, default));
67+
68+
static InvocationExpressionSyntax Nameof(EventFieldDeclarationSyntax eventDeclaration)
69+
{
70+
return SyntaxFactory.InvocationExpression(
71+
expression: SyntaxFactory.IdentifierName(
72+
SyntaxFactory.Identifier(
73+
SyntaxFactory.TriviaList(
74+
SyntaxFactory.Whitespace(eventDeclaration.LeadingWhitespace() + new string(' ', 4))),
75+
SyntaxKind.NameOfKeyword,
76+
"nameof",
77+
"nameof",
78+
default)),
79+
argumentList: SyntaxFactory.ArgumentList(
80+
arguments: SyntaxFactory.SingletonSeparatedList(
81+
SyntaxFactory.Argument(
82+
SyntaxFactory.IdentifierName(identifier: eventDeclaration.Declaration.Variables[0]
83+
.Identifier)))));
84+
}
85+
86+
static InvocationExpressionSyntax Register(EventFieldDeclarationSyntax eventDeclaration, ClassDeclarationSyntax containingClass)
87+
{
88+
var leadingWhitespace = SyntaxFactory.Whitespace(containingClass.LeadingWhitespace() + new string(' ', 8));
89+
return SyntaxFactory.InvocationExpression(
90+
expression: SyntaxFactory.MemberAccessExpression(
91+
kind: SyntaxKind.SimpleMemberAccessExpression,
92+
expression: SyntaxFactory.ParseTypeName("System.Windows.EventManager")
93+
.WithSimplifiedNames(),
94+
name: SyntaxFactory.IdentifierName("RegisterRoutedEvent")),
95+
argumentList: SyntaxFactory.ArgumentList(
96+
openParenToken: SyntaxFactory.Token(
97+
leading: default,
98+
kind: SyntaxKind.OpenParenToken,
99+
trailing: SyntaxFactory.TriviaList(SyntaxFactory.LineFeed)),
100+
arguments: SyntaxFactory.SeparatedList(
101+
new[]
102+
{
103+
SyntaxFactory.Argument(expression: Nameof(eventDeclaration)
104+
.WithLeadingTrivia(leadingWhitespace)),
105+
SyntaxFactory.Argument(expression: SyntaxFactory.ParseExpression("RoutingStrategy.Direct")
106+
.WithLeadingTrivia(leadingWhitespace)),
107+
SyntaxFactory.Argument(
108+
nameColon: default,
109+
refKindKeyword: default,
110+
expression: SyntaxFactory.TypeOfExpression(
111+
keyword: SyntaxFactory.Token(
112+
leading: SyntaxFactory.TriviaList(leadingWhitespace),
113+
kind: SyntaxKind.TypeOfKeyword,
114+
trailing: default),
115+
openParenToken: SyntaxFactory.Token(SyntaxKind.OpenParenToken),
116+
type: Type(eventDeclaration),
117+
closeParenToken: SyntaxFactory.Token(SyntaxKind.CloseParenToken))),
118+
SyntaxFactory.Argument(
119+
nameColon: default,
120+
refKindKeyword: default,
121+
expression: SyntaxFactory.TypeOfExpression(
122+
keyword: SyntaxFactory.Token(
123+
leading: SyntaxFactory.TriviaList(leadingWhitespace),
124+
kind: SyntaxKind.TypeOfKeyword,
125+
trailing: default),
126+
openParenToken: SyntaxFactory.Token(SyntaxKind.OpenParenToken),
127+
type: SyntaxFactory.IdentifierName(containingClass.Identifier.WithoutTrivia()),
128+
closeParenToken: SyntaxFactory.Token(SyntaxKind.CloseParenToken))),
129+
},
130+
new[]
131+
{
132+
SyntaxFactory.Token(
133+
leading: default,
134+
kind: SyntaxKind.CommaToken,
135+
trailing: SyntaxFactory.TriviaList(SyntaxFactory.LineFeed)),
136+
SyntaxFactory.Token(
137+
leading: default,
138+
kind: SyntaxKind.CommaToken,
139+
trailing: SyntaxFactory.TriviaList(SyntaxFactory.LineFeed)),
140+
SyntaxFactory.Token(
141+
leading: default,
142+
kind: SyntaxKind.CommaToken,
143+
trailing: SyntaxFactory.TriviaList(SyntaxFactory.LineFeed)),
144+
}),
145+
closeParenToken: SyntaxFactory.Token(SyntaxKind.CloseParenToken)));
146+
147+
static TypeSyntax Type(EventFieldDeclarationSyntax eventDeclaration) => eventDeclaration.Declaration.Type switch
148+
{
149+
NullableTypeSyntax { ElementType: { } elementType } => elementType,
150+
{ } type => type,
151+
};
152+
}
153+
}
154+
155+
static EventDeclarationSyntax Event(EventFieldDeclarationSyntax eventDeclaration)
156+
{
157+
return SyntaxFactory.EventDeclaration(
158+
attributeLists: default,
159+
modifiers: eventDeclaration.Modifiers,
160+
eventKeyword: SyntaxFactory.Token(kind: SyntaxKind.EventKeyword),
161+
type: eventDeclaration.Declaration.Type,
162+
explicitInterfaceSpecifier: default,
163+
identifier: SyntaxFactory.Identifier(
164+
leading: default,
165+
text: eventDeclaration.Declaration.Variables[0].Identifier.Text,
166+
trailing: SyntaxFactory.TriviaList(SyntaxFactory.LineFeed)),
167+
accessorList: SyntaxFactory.AccessorList(
168+
openBraceToken: SyntaxFactory.Token(
169+
leading: SyntaxFactory.TriviaList(SyntaxFactory.Whitespace(eventDeclaration.LeadingWhitespace())),
170+
kind: SyntaxKind.OpenBraceToken,
171+
trailing: SyntaxFactory.TriviaList(SyntaxFactory.LineFeed)),
172+
accessors: SyntaxFactory.List(
173+
new AccessorDeclarationSyntax[]
174+
{
175+
SyntaxFactory.AccessorDeclaration(
176+
kind: SyntaxKind.AddAccessorDeclaration,
177+
attributeLists: default,
178+
modifiers: default,
179+
keyword: SyntaxFactory.Token(
180+
leading: SyntaxFactory.TriviaList(SyntaxFactory.Whitespace(eventDeclaration.LeadingWhitespace() + new string(' ', 4))),
181+
kind: SyntaxKind.AddKeyword,
182+
trailing: SyntaxFactory.TriviaList(SyntaxFactory.Space)),
183+
body: default,
184+
expressionBody: SyntaxFactory.ArrowExpressionClause(
185+
arrowToken: SyntaxFactory.Token(
186+
leading: default,
187+
kind: SyntaxKind.EqualsGreaterThanToken,
188+
trailing: SyntaxFactory.TriviaList(SyntaxFactory.Space)),
189+
expression: SyntaxFactory.InvocationExpression(
190+
expression: SyntaxFactory.MemberAccessExpression(
191+
kind: SyntaxKind.SimpleMemberAccessExpression,
192+
expression: SyntaxFactory.ThisExpression(
193+
token: SyntaxFactory.Token(SyntaxKind.ThisKeyword)),
194+
operatorToken: SyntaxFactory.Token(SyntaxKind.DotToken),
195+
name: SyntaxFactory.IdentifierName("AddHandler")),
196+
argumentList: SyntaxFactory.ArgumentList(
197+
openParenToken: SyntaxFactory.Token(SyntaxKind.OpenParenToken),
198+
arguments: SyntaxFactory.SeparatedList(
199+
new ArgumentSyntax[]
200+
{
201+
SyntaxFactory.Argument(expression: SyntaxFactory.IdentifierName($"{eventDeclaration.Declaration.Variables[0].Identifier.Text}Event")),
202+
SyntaxFactory.Argument(expression: SyntaxFactory.IdentifierName("value")),
203+
},
204+
new SyntaxToken[]
205+
{
206+
SyntaxFactory.Token(
207+
leading: default,
208+
kind: SyntaxKind.CommaToken,
209+
trailing: SyntaxFactory.TriviaList(SyntaxFactory.Space)),
210+
}),
211+
closeParenToken: SyntaxFactory.Token(SyntaxKind.CloseParenToken)))),
212+
semicolonToken: SyntaxFactory.Token(
213+
leading: default,
214+
kind: SyntaxKind.SemicolonToken,
215+
trailing: SyntaxFactory.TriviaList(SyntaxFactory.LineFeed))),
216+
SyntaxFactory.AccessorDeclaration(
217+
kind: SyntaxKind.RemoveAccessorDeclaration,
218+
attributeLists: default,
219+
modifiers: default,
220+
keyword: SyntaxFactory.Token(
221+
leading: SyntaxFactory.TriviaList(SyntaxFactory.Whitespace(eventDeclaration.LeadingWhitespace() + new string(' ', 4))),
222+
kind: SyntaxKind.RemoveKeyword,
223+
trailing: SyntaxFactory.TriviaList(SyntaxFactory.Space)),
224+
body: default,
225+
expressionBody: SyntaxFactory.ArrowExpressionClause(
226+
arrowToken: SyntaxFactory.Token(
227+
leading: default,
228+
kind: SyntaxKind.EqualsGreaterThanToken,
229+
trailing: SyntaxFactory.TriviaList(SyntaxFactory.Space)),
230+
expression: SyntaxFactory.InvocationExpression(
231+
expression: SyntaxFactory.MemberAccessExpression(
232+
kind: SyntaxKind.SimpleMemberAccessExpression,
233+
expression: SyntaxFactory.ThisExpression(
234+
token: SyntaxFactory.Token(SyntaxKind.ThisKeyword)),
235+
operatorToken: SyntaxFactory.Token(SyntaxKind.DotToken),
236+
name: SyntaxFactory.IdentifierName("RemoveHandler")),
237+
argumentList: SyntaxFactory.ArgumentList(
238+
openParenToken: SyntaxFactory.Token(SyntaxKind.OpenParenToken),
239+
arguments: SyntaxFactory.SeparatedList(
240+
new ArgumentSyntax[]
241+
{
242+
SyntaxFactory.Argument(expression: SyntaxFactory.IdentifierName($"{eventDeclaration.Declaration.Variables[0].Identifier.Text}Event")),
243+
SyntaxFactory.Argument(expression: SyntaxFactory.IdentifierName("value")),
244+
},
245+
new SyntaxToken[]
246+
{
247+
SyntaxFactory.Token(
248+
leading: default,
249+
kind: SyntaxKind.CommaToken,
250+
trailing: SyntaxFactory.TriviaList(SyntaxFactory.Space)),
251+
}),
252+
closeParenToken: SyntaxFactory.Token(SyntaxKind.CloseParenToken)))),
253+
semicolonToken: SyntaxFactory.Token(
254+
leading: default,
255+
kind: SyntaxKind.SemicolonToken,
256+
trailing: SyntaxFactory.TriviaList(SyntaxFactory.LineFeed))),
257+
}),
258+
closeBraceToken: SyntaxFactory.Token(
259+
leading: SyntaxFactory.TriviaList(SyntaxFactory.Whitespace(eventDeclaration.LeadingWhitespace())),
260+
kind: SyntaxKind.CloseBraceToken,
261+
trailing: SyntaxFactory.TriviaList(SyntaxFactory.LineFeed))),
262+
semicolonToken: default);
263+
}
264+
}
265+
}
266+
}

0 commit comments

Comments
 (0)