How to avoid using extra attached property?

55 Views Asked by At

I have attached behavior with 2 properties. Here is what I am trying to do, for this question details are optional.

First one is used to enable/disable behavior:

public static bool GetEnableHasErrors(DependencyObject obj) => (bool)obj.GetValue(EnableHasErrorsProperty);
public static void SetEnableHasErrors(DependencyObject obj, bool value) => obj.SetValue(EnableHasErrorsProperty, value);

public static readonly DependencyProperty EnableHasErrorsProperty =
    DependencyProperty.RegisterAttached("EnableHasErrors", typeof(bool), typeof(Behaviors), new PropertyMetadata((d, e) =>
    {
        var element = d as FrameworkElement;
        if (element == null)
            throw new ArgumentException("Only used with FrameworkElement");
        var handler = new RoutedEventHandler((s, a) => { ... }); // logic to set value of HasErrorsProperty attached property on element
        if ((bool)e.NewValue)
            element.SomeRoutedEvent += handler;
        else
            element.SomeRoutedEvent -= handler;
    }));

Second one is used to pass the result out:

public static bool GetHasErrors(DependencyObject obj) => (bool)obj.GetValue(HasErrorsProperty);
public static void SetHasErrors(DependencyObject obj, bool value) => obj.SetValue(HasErrorsProperty, value);

public static readonly DependencyProperty HasErrorsProperty =
    DependencyProperty.RegisterAttached("HasErrors", typeof(bool), typeof(Behaviors));

And this result can go into view model via normal binding or used in the view, whatever:

<Grid local:Behaviors.EnableHasErrors="True"
      local:Behaviors.HasErrors="{Binding HasErrors, Mode=OneWayToSource}" >

It feels wrong what I need 2 dependency properties for this. Is it possible to use just one? Couldn't I somehow infer inside behavior what I have logic enabled by having binding set? Isn't that enough?

I tried to use single property of BindingBase type, failed, found my own question and this duplicate with not safisfying answer, so BindingBase feels wrong to me.

Ideas?

1

There are 1 best solutions below

6
canton7 On BEST ANSWER

I don't know of any way to avoid this for your specific case.

For more complex behaviors like this, it can be useful to use an attached behavior. Attached behaviors have methods which are called when the behavior is attached or detached, which you can use to subscribe to / unsubscribe from events. These are however significantly more verbose to use.

For example:

public class ErrorsBehavior : Behavior<FrameworkElement>
{
    public bool HasErrors
    {
        get => (bool )this.GetValue(HasErrorsProperty);
        set => this.SetValue(HasErrorsProperty, value);
    }
    public static readonly DependencyProperty HasErrorsProperty =
        DependencyProperty.Register(nameof(HasErrors), typeof(bool), typeof(ErrorsBehavior ), new PropertyMetadata(false));

    protected override void OnAttached()
    {
        AssociatedObject.SomeRoutedEvent += ...
    }

    protected override void OnDetaching()
    {
        AssociatedObject.SomeRoutedEvent -= ...
    }
}

Usage would then be something like:

<Grid>
    <i:Interaction.Behaviors>
        <my:ErrorsBehavior HasErrors="{Binding HasErrors, Mode=OneWayToSource}"/>
    </i:Interaction.Behaviors>
</Grid>

These used to be available only if you specifically installed the "Blend for Visual Studio SDK for .NET" component (Visual Studio 2017), but are now available in the Microsoft.Behaviors.Xaml.Wpf NuGet package.


If you've got a two-way binding which takes a more complex type than bool (such as object), there is a trick you can do:

public static class MyExtensions
{
    private static readonly object initialBindingTarget = new object();

    public static object GetSomething(DependencyObject obj) => obj.GetValue(SomethingProperty);

    public static void SetSomething(DependencyObject obj, object value) => obj.SetValue(SomethingProperty, value);

    public static readonly DependencyProperty SomethingProperty =
        DependencyProperty.RegisterAttached(
            "Something",
            typeof(object),
            typeof(MyExtensions),
            new FrameworkPropertyMetadata(
                initialBindingTarget,
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, (d, e) =>
        {
            // Trick to see if this is the first time we're set
            if (e.OldValue == initialBindingTarget)
            {
                // Do your first-time initialisation here
            }
        }));
}

This uses a sentinel initial value of initialBindingTarget, and checks to see when the binding changes the value away from this.