Home

Awesome

Logo boilerplatezero (BPZ)

NuGet version (boilerplatezero) MIT License

boilerplatezero (BPZ) is a collection of C# source generators that simplify the code required for common C# patterns.

🔗 Jump to...


WPF Dependency Property Generator

Dependency properties in WPF are great! However, they do require quite a bit of ceremony in order to define one.<br> Luckily, dependency properties (and attached properties) always follow the same pattern when implemented.<br> As such, this kind of boilerplate code is the perfect candidate for a source generator.

The dependency property generator in BPZ works by identifying DependencyProperty and DependencyPropertyKey fields that are initialized with calls to appropriately-named Gen or GenAttached methods.<br> When this happens, the source generator adds private static classes as nested types inside your class & implements the dependency property for you.<br> Additionally...

🔗 Jump to...


👩‍💻 Write This, Not That: Property Examples

🔧 Dependency Property

// Write this (using BPZ):
private static readonly DependencyPropertyKey FooPropertyKey = Gen.Foo<string>();

// Not that (idiomatic implementation):
private static readonly DependencyPropertyKey FooPropertyKey = DependencyProperty.RegisterReadOnly(nameof(Foo), typeof(string), typeof(MyClass), null);
public static readonly DependencyProperty FooProperty = FooPropertyKey.DependencyProperty;
public string Foo
{
    get => (string)this.GetValue(FooProperty);
    private set => this.SetValue(FooPropertyKey, value);
}
<details><summary>A more complex example...</summary>
// Write this (using BPZ):
public static readonly DependencyProperty TextProperty = Gen.Text("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault);
protected virtual void OnTextChanged(string oldText, string newText) { ... }

// Not that (idiomatic implementation):
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
    nameof(Text), typeof(string), typeof(MyClass),
    new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, TextPropertyChanged));
public string Text
{
    get => (string)this.GetValue(TextProperty);
    set => this.SetValue(TextProperty, value);
}
private static void TextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    ((MyClass)d).OnTextChanged((string)e.OldValue, (string)e.NewValue);
}
protected virtual void OnTextChanged(string oldText, string newText) { ... }
</details>

🔧 Attached Property

// Write this (using BPZ):
private static readonly DependencyPropertyKey BarPropertyKey = GenAttached.Bar<string>();

// Not that (idiomatic implementation):
private static readonly DependencyPropertyKey BarPropertyKey = DependencyProperty.RegisterAttachedReadOnly("Bar", typeof(string), typeof(MyClass), null);
public static readonly DependencyProperty BarProperty = BarPropertyKey.DependencyProperty;
public static string GetBar(DependencyObject d) => (string)d.GetValue(BarProperty);
private static void SetBar(DependencyObject d, string value) => d.SetValue(BarPropertyKey, value);

🤖 Generated Code

Here's an example of hand-written code & the corresponding generated code.<br> Note that the default value for the dependency property is specified in the user's code & included in the property metadata along with the appropriate property-changed handler.<br> The documentation comment on IdPropertyKey is copied to the generated Id property.

// 👩‍💻 Widget.cs
namespace Goodies
{
    public partial class Widget : DependencyObject
    {
        /// <Summary>Gets the ID of this instance.</Summary>
        protected static readonly DependencyPropertyKey IdPropertyKey = Gen.Id("<unset>");

        private static void IdPropertyChanged(Widget self, DependencyPropertyChangedEventArgs e)
        {
            // This method will be used as the property-changed callback during registration!
            // It was selected because...
            // - its name contains the property name, "Id", & ends with "Changed"
            // - it is `static` with return type `void`
            // - the type of parameter 0 is compatible with the owner type
            // - the type of parameter 1 is `DependencyPropertyChangedEventArgs`
        }
    }
}

// 🤖 Widget.g.cs (actual name may vary)
namespace Goodies
{
    partial class Widget
    {
        public static readonly DependencyProperty IdProperty = IdPropertyKey.DependencyProperty;

        /// <Summary>Gets the ID of this instance.</Summary>
        public string Id
        {
            get => (string)this.GetValue(IdProperty);
            protected set => this.SetValue(IdPropertyKey, value);
        }

        private static partial class Gen
        {
            public static DependencyPropertyKey Id<__T>(__T defaultValue)
            {
                PropertyMetadata metadata = new PropertyMetadata(
                    defaultValue,
                    (d, e) => IdPropertyChanged((Goodies.Widget)d, e),
                    null);
                return DependencyProperty.RegisterReadOnly("Id", typeof(__T), typeof(Widget), metadata);
            }
        }
    }
}

✨ Features


WPF Routed Event Generator

Similar to dependency properties, routed events always use the same pattern when implemented correctly.

The routed event generator in BPZ works by identifying RoutedEvent fields that are initialized with calls to appropriately-named Gen or GenAttached methods.<br> When this happens, the source generator adds private static classes as nested types inside your class & implements the routed event for you.

🔗 Jump to...


👩‍💻 Write This, Not That: Event Examples

⚡ Routed Event

// Write this (using BPZ):
public static readonly RoutedEvent FooChangedEvent = Gen.FooChanged<string>();

// Not that (idiomatic implementation):
public static readonly RoutedEvent FooChangedEvent = EventManager.RegisterRoutedEvent(nameof(FooChanged), RoutingStrategy.Direct, typeof(RoutedPropertyChangedEventHandler<string>), typeof(MyClass));
public event RoutedPropertyChangedEventHandler<string> FooChanged
{
    add => this.AddHandler(FooChangedEvent, value);
    remove => this.RemoveHandler(FooChangedEvent, value);
}

⚡ Attached Event

// Write this (using BPZ):
public static readonly RoutedEvent ThingUpdatedEvent = GenAttached.ThingUpdatedChanged(RoutingStrategy.Bubble);

// Not that (idiomatic implementation):
public static readonly RoutedEvent ThingUpdatedEvent = EventManager.RegisterRoutedEvent(nameof(BarChanged), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(MyClass));
public static void AddThingUpdatedHandler(DependencyObject d, RoutedEventHandler handler) => (d as UIElement)?.AddHandler(BarChangedEvent, handler);
public static void RemoveThingUpdatedHandler(DependencyObject d, RoutedEventHandler handler) => (d as UIElement)?.RemoveHandler(BarChangedEvent, handler);

✨ Features


🐛 Known Issues

related: Source Generator support for WPF project blocked? #3404