This morning I redirected my endless frustration with WinUI's senseless limitations into motivational energy to make a general purpose solution. Other answers are great but this is much more scalable:
public class SmartSetter
{
/// <summary>
/// The target <see cref="DependencyProperty"/> name, e.g.,
/// <c>Background</c>. For an attached property, ensure the
/// <see cref="PropertyOwner"/> property is set, since this
/// class doesn't have access to XAML namespaces. I would've
/// loved to subclass Setter, but, sealed. Who'd have guessed.
/// </summary>
public string Property
{
get;
set;
}
/// <summary>
/// References the DependencyProperty owner when attached
/// properties are targeted. Otherwise the owner is assumed
/// to be the type of the FrameworkElement being styled.
/// </summary>
public Type PropertyOwner
{
get;
set;
}
/// <summary>
/// A BindingBase. In XAML, use normal binding notation, e.g.,
/// <c>"{Binding RelativeSource={RelativeSource Mode=Self}, Path=SomeOtherProperty}"</c>
/// </summary>
public BindingBase Binding
{
get;
set;
}
internal DependencyProperty ResolveProperty(Type ownerType)
{
if (_resolvedProperty != null)
return _resolvedProperty;
// The DP for WinUI types is exposed in reflection as a property, but for custom types it's a field.
var dpProp = type.GetProperty(
$"{propertyName}Property",
BindingFlags.Static
| BindingFlags.FlattenHierarchy
| BindingFlags.Public);
if (dpProp != null)
return _resolvedProperty = dpProp.GetValue(null) as DependencyProperty;
var dpField = type.GetField(
$"{propertyName}Property",
BindingFlags.Static
| BindingFlags.FlattenHierarchy
| BindingFlags.Public);
if (dpField != null)
return _resolvedProperty = dpField.GetValue(null) as DependencyProperty;
return null;
}
DependencyProperty _resolvedProperty;
}
/// <summary>
/// Provides a workaround for WinUI 3's omission of a means to
/// set bindings in Style Setters. In the main Style, assign an instance of
/// <see cref="SmartStyle"/>, which includes a collection of <see cref="SmartSetter"/>s.
/// to the <see cref="SmartStyle.StyleProperty"/> attached property.
/// </summary>
[ContentProperty(Name = nameof(Setters))]
public class SmartStyle
{
Collection<SmartSetter> _setters;
public Collection<SmartSetter> Setters => _setters ?? (_setters = new Collection<SmartSetter>());
#region SmartStyle Style attached property
public static readonly DependencyProperty StyleProperty = DependencyProperty.RegisterAttached(
"Style",
typeof(SmartStyle),
typeof(SmartStyle),
new PropertyMetadata(
(SmartStyle)null,
(obj, args) =>
{
if (!(obj is FrameworkElement fe) || !(args.NewValue is SmartStyle style))
return;
foreach (var s in style.Setters)
{
if (string.IsNullOrEmpty(s.Property) || s.Binding == null)
continue;
var dp = s.ResolveProperty(fe.GetType());
if (dp == null)
continue;
BindingOperations.SetBinding(obj, dp, s.Binding);
}
}));
public static SmartStyle GetStyle(DependencyObject obj)
{
return (SmartStyle)obj.GetValue(StyleProperty);
}
public static void SetStyle(DependencyObject obj, SmartStyle style)
{
obj.SetValue(StyleProperty, style);
}
#endregion
}
Usage:
<Style TargetType="Button"
x:Key="SolidBackgroundButtonStyle">
<Setter Property="local:SmartStyle.Style">
<Setter.Value>
<local:SmartStyle>
<local:SmartSetter Property="BorderBrush"
Binding="{Binding
RelativeSource={RelativeSource Mode=Self},
Path=Background}" />
</local:SmartStyle>
</Setter.Value>
</Setter>
</Style>
Incidentally, the fact that this works - SmartSetter.Binding
is given a BindingBase
instance and the binding is not actually evaluated until the style is applied - tells you Microsoft went out of their way to do the opposite for Setter
, and thereby cause this problem, for reasons I cannot begin to fathom.
Update - I decided to turn this and another workaround into a small project. See https://github.com/peter0302/WinUI.Redemption/