Skip to content

Commit

Permalink
Generated change-handlers can use routed events (#14)
Browse files Browse the repository at this point in the history
If we find a routed event that appears to match the dependency property, then it may be used as the property-changed handler.

In the case of multiple candidates, the priority is
- static methods
- instance methods
- routed events
  • Loading branch information
IGood authored Jul 21, 2021
1 parent 32319d0 commit 8f32853
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 29 deletions.
1 change: 1 addition & 0 deletions Bpz.Test/GeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public class GeneratorTests
[TestCase("PropertyChangedHandlers.cs")]
[TestCase("Coercion.cs")]
[TestCase("FxPropMetadata.cs")]
[TestCase("PropertyChangedEvents.cs")]
public void GenProps(string resourceName)
{
using var source = Resources.GetEmbeddedResource(resourceName);
Expand Down
2 changes: 1 addition & 1 deletion Bpz.Test/GridSnapTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ namespace Bpz.Test
/// Exercises basic attached property behavior.
/// This won't compile if the properties we expect weren't generated.
/// </summary>
[Apartment(ApartmentState.STA)]
public class GridSnapTests
{
[TestCaseSource(nameof(MetadataTestCases))]
Expand Down Expand Up @@ -49,6 +48,7 @@ public static IEnumerable<DependencyPropertyAssert.DependencyPropertyValues> Met
}

[Test(Description = "Change-handers should raise events.")]
[Apartment(ApartmentState.STA)]
public void ExpectEventHandlers()
{
var c = new Canvas();
Expand Down
16 changes: 16 additions & 0 deletions Bpz.Test/NumericUpDowns.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright © Ian Good

using System.Windows;
using System.Windows.Controls;

namespace Bpz.Test
{
public abstract partial class NumericUpDownBase<TValue> : Control
{
public static readonly DependencyProperty ValueProperty = Gen.Value<TValue>(FrameworkPropertyMetadataOptions.BindsTwoWayByDefault);

public static readonly RoutedEvent ValueChangedEvent = Gen.ValueChanged<TValue>();
}

public class DoubleUpDown : NumericUpDownBase<double> { }
}
53 changes: 53 additions & 0 deletions Bpz.Test/NumericUpDownsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright © Ian Good

using NUnit.Framework;
using System.Threading;
using System.Windows;

namespace Bpz.Test
{
/// <summary>
/// Checks behavior of generic base types and raising routed events from generated change-handlers.
/// </summary>
public class NumericUpDownsTests
{
[Test]
public void ExpectMetadata()
{
RoutedEventAssert.Matches(new()
{
OwnerType = typeof(NumericUpDownBase<double>),
Name = "ValueChanged",
HandlerType = typeof(RoutedPropertyChangedEventHandler<double>),
});

DependencyPropertyAssert.Matches(new()
{
OwnerType = typeof(NumericUpDownBase<double>),
Name = "Value",
PropertyType = typeof(double),
});
}

[Test]
[Apartment(ApartmentState.STA)]
public void ExpectEventHandlers()
{
var dud = new DoubleUpDown();

bool eventWasRaised;
dud.ValueChanged += (s, e) =>
{
Assert.AreSame(dud, s);
eventWasRaised = true;
e.Handled = true;
};

{
eventWasRaised = false;
dud.Value = 867.5309;
Assert.IsTrue(eventWasRaised);
}
}
}
}
14 changes: 14 additions & 0 deletions Bpz.Test/SourceText/PropertyChangedEvents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Windows;
using Butt = System.Windows.Controls.Button;

namespace PropChangedEvents
{
public partial class TestMe : Butt
{
public static readonly DependencyProperty AProperty = Gen.A<int>();
public static readonly RoutedEvent AChangedEvent = Gen.AChanged<int>();

public static readonly DependencyProperty BProperty = Gen.B<object>();
public static readonly RoutedEvent BChangedEvent = Gen.BChanged<object>();
}
}
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ namespace Goodies
```
</details>
- <details><summary>detects suitable property-changed handlers</summary>
There are 3 options for property-changed handlers.
There are 4 options for property-changed handlers.
If multiple candidates are found, then they are prioritized (from highest to lowest) as shown below (static methods, instance methods, routed events).

```csharp
// 👩‍💻 user
Expand Down Expand Up @@ -201,6 +202,13 @@ namespace Goodies
// - return type is `void`
// - type of parameter 0 is `DependencyPropertyChangedEventArgs`
}

// Option 4 - routed event, named "SeasonChangedEvent"
public static readonly RoutedEvent SeasonChangedEvent = Gen.SeasonChanged<string>();
// Invoking this event can be used as the property-changed callback during registration!
// It is a candidate because...
// - it is a `static readonly RoutedEvent`
// - it's name is "SeasonChangedEvent"
```
</details>
- <details><summary>detects suitable value coercion handlers</summary>
Expand Down
123 changes: 97 additions & 26 deletions boilerplatezero/Wpf/DependencyPropertyGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ public class DependencyPropertyGenerator : ISourceGenerator
private bool useNullableContext;

// These will be initialized before first use.
private INamedTypeSymbol objTypeSymbol = null!; // System.Object
private INamedTypeSymbol doTypeSymbol = null!; // System.Windows.DependencyObject
private INamedTypeSymbol argsTypeSymbol = null!; // System.Windows.DependencyPropertyChangedEventArgs
private INamedTypeSymbol flagsTypeSymbol = null!;// System.Windows.FrameworkPropertyMetadataOptions
private INamedTypeSymbol objTypeSymbol = null!; // System.Object
private INamedTypeSymbol doTypeSymbol = null!; // System.Windows.DependencyObject
private INamedTypeSymbol argsTypeSymbol = null!;// System.Windows.DependencyPropertyChangedEventArgs
private INamedTypeSymbol? flagsTypeSymbol; // System.Windows.FrameworkPropertyMetadataOptions
private INamedTypeSymbol? reTypeSymbol; // System.Windows.RoutedEvent

public void Initialize(GeneratorInitializationContext context)
{
Expand Down Expand Up @@ -63,7 +64,8 @@ public void Execute(GeneratorExecutionContext context)
this.objTypeSymbol ??= context.Compilation.GetTypeByMetadataName("System.Object")!;
this.doTypeSymbol ??= context.Compilation.GetTypeByMetadataName("System.Windows.DependencyObject")!;
this.argsTypeSymbol ??= context.Compilation.GetTypeByMetadataName("System.Windows.DependencyPropertyChangedEventArgs")!;
this.flagsTypeSymbol ??= context.Compilation.GetTypeByMetadataName("System.Windows.FrameworkPropertyMetadataOptions")!;
this.flagsTypeSymbol ??= context.Compilation.GetTypeByMetadataName("System.Windows.FrameworkPropertyMetadataOptions");
this.reTypeSymbol ??= context.Compilation.GetTypeByMetadataName("System.Windows.RoutedEvent");

string namespaceName = namespaceGroup.Key.ToString();
sourceBuilder.Append($@"
Expand Down Expand Up @@ -312,50 +314,104 @@ private string GetPropertyMetadataInstance(GenerationDetails generateThis, bool
string propertyName = generateThis.MethodNameNode.Identifier.ValueText;
string coerceMethodName = "Coerce" + propertyName;

AssociatedMethods foundMethods = AssociatedMethods.None;

AssociatedHandlers foundAssociates = AssociatedHandlers.None;
ChangeHandlerKind changeHandlerKind = ChangeHandlerKind.None;
string changeHandler = "null";
string coercionHandler = "null";

// Look for associated methods...
// Look for associated handlers...
foreach (ISymbol memberSymbol in ownerType.GetMembers())
{
if (memberSymbol.Kind == SymbolKind.Method)
string maybeChangeHandler;

switch (memberSymbol.Kind)
{
if (!foundMethods.HasFlag(AssociatedMethods.PropertyChanged) && _TryGetChangeHandler((IMethodSymbol)memberSymbol, out changeHandler))
{
foundMethods |= AssociatedMethods.PropertyChanged;
if (foundMethods == AssociatedMethods.All)
case SymbolKind.Field:
// If we haven't found a routed event or better, then check this field.
if (changeHandlerKind < ChangeHandlerKind.RoutedEvent &&
_TryGetChangeHandler2((IFieldSymbol)memberSymbol, out maybeChangeHandler))
{
break;
changeHandlerKind = ChangeHandlerKind.RoutedEvent;
changeHandler = maybeChangeHandler;
}
break;

continue;
}

if (!foundMethods.HasFlag(AssociatedMethods.Coerce) && _TryGetCoercionHandler((IMethodSymbol)memberSymbol, out coercionHandler))
{
foundMethods |= AssociatedMethods.Coerce;
if (foundMethods == AssociatedMethods.All)
case SymbolKind.Method:
// If we haven't found a static property-changed method, then check this method.
if (changeHandlerKind < ChangeHandlerKind.StaticMethod &&
_TryGetChangeHandler((IMethodSymbol)memberSymbol, out maybeChangeHandler, out bool isStatic))
{
if (isStatic)
{
foundAssociates |= AssociatedHandlers.PropertyChanged;
changeHandlerKind = ChangeHandlerKind.StaticMethod;
}
else
{
changeHandlerKind = ChangeHandlerKind.InstanceMethod;
}

changeHandler = maybeChangeHandler;

break;
}
}

// If we haven't found a coercion handler, then check this method.
if (!foundAssociates.HasFlag(AssociatedHandlers.Coerce) &&
_TryGetCoercionHandler((IMethodSymbol)memberSymbol, out coercionHandler))
{
foundAssociates |= AssociatedHandlers.Coerce;
}
break;

default:
continue;
}

if (foundAssociates == AssociatedHandlers.All)
{
break;
}
}

// See if we have any routed events like...
// RoutedEvent FooChangedEvent = Gen.FooChanged<int>();
bool _TryGetChangeHandler2(IFieldSymbol fieldSymbol, out string changeHandler)
{
string fieldName = fieldSymbol.Name;
if (fieldSymbol.IsStatic &&
fieldSymbol.IsReadOnly &&
fieldName == propertyName + "ChangedEvent" &&
fieldSymbol.Type.Equals(this.reTypeSymbol, SymbolEqualityComparer.Default))
{
string? maybeCastArgs = (generateThis.PropertyType?.SpecialType == SpecialType.System_Object)
? null
: $"({generateThis.PropertyTypeName})";

// Something like...
// (d, e) => ((UIElement)d).RaiseEvent(new RoutedPropertyChangedEventArgs<int>((int)e.OldValue, (int)e.NewValue, FooChangedEvent))
changeHandler = $"(d, e) => ((UIElement)d).RaiseEvent(new RoutedPropertyChangedEventArgs<{generateThis.PropertyTypeName}>({maybeCastArgs}e.OldValue, {maybeCastArgs}e.NewValue, {fieldName}))";
return true;
}

changeHandler = "null";
return false;
}

// See if we have any property-changed handlers like...
// static void FooPropertyChanged(Widget self, DependencyPropertyChangedEventArgs e) { ... }
// static void OnFooChanged(Widget self, DependencyPropertyChangedEventArgs e) { ... }
// void FooChanged(DependencyPropertyChangedEventArgs e) { ... }
// void OnFooChanged(string oldFoo, string newFoo) { ... }
bool _TryGetChangeHandler(IMethodSymbol methodSymbol, out string changeHandler)
bool _TryGetChangeHandler(IMethodSymbol methodSymbol, out string changeHandler, out bool isStatic)
{
isStatic = methodSymbol.IsStatic;

if (methodSymbol.ReturnsVoid)
{
string methodName = methodSymbol.Name;

if (methodSymbol.IsStatic)
if (isStatic)
{
if (methodSymbol.Parameters.Length == 2 &&
methodName.EndsWith("Changed", StringComparison.Ordinal) &&
Expand Down Expand Up @@ -564,7 +620,6 @@ private static IEnumerable<GenerationDetails> UpdateAndFilterGenerationRequests(
{
INamedTypeSymbol? dpTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Windows.DependencyProperty");
INamedTypeSymbol? dpkTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Windows.DependencyPropertyKey");

if (dpTypeSymbol == null || dpkTypeSymbol == null)
{
// This probably never happens, but whatevs.
Expand Down Expand Up @@ -634,15 +689,31 @@ private static bool CanCastTo(ITypeSymbol checkThis, ITypeSymbol baseTypeSymbol)
return checkThis.Equals(baseTypeSymbol, SymbolEqualityComparer.Default) || (checkThis.BaseType != null && CanCastTo(checkThis.BaseType, baseTypeSymbol));
}

/// <summary>
/// Specifies potential handler behaviors that are associated with a dependency property.
/// </summary>
[Flags]
private enum AssociatedMethods
private enum AssociatedHandlers
{
None = 0,
PropertyChanged = 1 << 0,
Coerce = 1 << 1,
All = PropertyChanged | Coerce,
}

/// <summary>
/// Specifies the possible kinds of change-handlers.
/// Multiple candidates may be found when looking for associated handlers.
/// Higher values have higher priority.
/// </summary>
private enum ChangeHandlerKind
{
None,
RoutedEvent,
InstanceMethod,
StaticMethod,
}

private class SyntaxReceiver : ISyntaxReceiver
{
public List<GenerationDetails> GenerationRequests { get; } = new();
Expand Down
2 changes: 1 addition & 1 deletion boilerplatezero/boilerplatezero.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<DevelopmentDependency>true</DevelopmentDependency>
<IncludeBuildOutput>false</IncludeBuildOutput>
<PackageId>boilerplatezero</PackageId>
<Version>1.6.0</Version>
<Version>1.7.0</Version>
<Authors>IGood</Authors>
<Company />
<Copyright>Copyright (c) Ian Good</Copyright>
Expand Down

0 comments on commit 8f32853

Please sign in to comment.