Binding in a Style Setter in WinUI 3
Asked Answered
D

2

3

Does WinUI 3 support binding in a Style Setter? I've defined a Style for a NavigationView and the third line is:

<Setter Property="CompactPaneLength" Value="{Binding CurrentCompactPaneLength}" />

This produces a Specified cast is not valid. exception at run time. The DataContext for the page containing the NavigationView is the ViewModel for the page. Both NavigationView.CompactPaneLength and CurrentCompactPaneLength are double and public and CurrentCompactPaneLength is an ObservableObject (from CommunityToolkit.Mvvm.ComponentModel).

The source code for the WinUI 3 (SDK 1.1.2) includes various Setters, such as

<Setter Target="PaneContentGrid.Width" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=CompactPaneLength}" />

Doing the bindings in code works, if that's what's necessary. But shouldn't the XAML work, too?

Denning answered 7/7, 2022 at 14:46 Comment(2)
You actually found the source code for WinUI 3? Might I ask where? All Microsoft repos are mazes.Yam
I think you might be looking at the WinUI2 code. AFAIK the Winui3 code is still not open source (and at this rate never will be).Yam
D
1

Apparently, general bindings in Setters are not supported yet in WinUI 3, although it is a much-requested feature. A workaround is to create a helper class with a DependencyProperty in it that calls a change handler whenever the property is changed/set. The change handler can then create the required binding in code. Kudos to clemens who suggested something like this ages ago for UWP. Here is an example helper class:

internal class BindingHelper
{
    #region CompactPaneLengthBindingPath
    public static readonly DependencyProperty CompactPaneLengthBindingPathProperty = DependencyProperty.RegisterAttached(
            "CompactPaneLengthBindingPath", typeof(string), typeof(BindingHelper), new PropertyMetadata(null, BindingPathChanged));
            
    public static string GetCompactPaneLengthBindingPath(DependencyObject obj)
    {
        return (string)obj.GetValue(CompactPaneLengthBindingPathProperty);
    }
    
    public static void SetCompactPaneLengthBindingPath(DependencyObject obj, string value)
    {
        obj.SetValue(CompactPaneLengthBindingPathProperty, value);
    }
    #endregion
        
    #region HeightBindingPath
    // another DP is defined here (all of them are strings)
        
    #region ForegroundBindingPath
    // and a third one, etc.
        

    // ===================== Change Handler: Creates the actual binding

    private static void BindingPathChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        if (e.NewValue is string source)                                                                // source property (could add DataContext by setting Value="source@datacontext" for example)
        {
            DependencyProperty target;                                                                  // which property is the target of the binding?
            if (e.Property == CompactPaneLengthBindingPathProperty) target = NavigationView.CompactPaneLengthProperty;
            else if (e.Property == HeightBindingPathProperty) target = FrameworkElement.HeightProperty;
            else if (e.Property == ForegroundBindingPathProperty) target = Control.ForegroundProperty;
            else throw new System.Exception($"BindingHelper: Unknown target ({nameof(e.Property)}");    // don't know this property
        
            obj.ClearValue(target);                                                                     // clear previous bindings (and value)
            BindingOperations.SetBinding(obj, target,                                                   // set new binding (and value)
               new Binding { Path = new PropertyPath(source), Mode = BindingMode.OneWay });
        }
    }

Note that all of the DependencyProperties are of type string and that the target type can be any ancestor type of the control you are working with. For example, the HeightBindingPathProperty binding can be used with any FrameworkElement.

Use the helper in a Style just as you would any Setter, like this:

<Style x:Key="MyNavigationView" TargetType="controls:NavigationView" >
    <Setter Property="local:BindingHelper.CompactPaneLengthBindingPath" Value="CurrentCompactPaneLength" />
</Style>

I hope this helps.

Denning answered 19/7, 2022 at 17:56 Comment(0)
Y
0

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/

Yam answered 30/4 at 12:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.