1

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?

Sinatr
  • 20,892
  • 15
  • 90
  • 319
  • Using attached behaviors might be an option - they have a hook that's called when they're attached or detached. They are a bit more verbose to use, though. – canton7 Oct 18 '19 at 12:17
  • @canton7, you mean Blend `Interaction.Behaviors` stuff? I've seen it's used in DevExpress too, hmm.. since we have it anyway it could be the answer. – Sinatr Oct 18 '19 at 12:19
  • Yep. It's now released [as its own NuGet package](https://www.nuget.org/packages/Microsoft.Xaml.Behaviors.Wpf/) – canton7 Oct 18 '19 at 12:20
  • 1
    @canton7, work perfectly with [devexpress](https://documentation.devexpress.com/WPF/17458/MVVM-Framework/Behaviors/How-to-Create-a-Custom-Behavior), thanks for tip. Though it's indeed a bit verbose usage in xaml. If you post an answer I'll upvote it, but I am still curious is it achievable in vanila wpf (without nugets). – Sinatr Oct 18 '19 at 13:06

1 Answers1

2

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.

canton7
  • 37,633
  • 3
  • 64
  • 77
  • Cool trick with default value. I have tried single `bool` attached property, but callback wasn't called at all (due to `false = false`, but mostly due to `OneWayToSource`). For e.g. `bool?`, with `null` as default value and binding to normal `bool` that should call callback, though I can't use `OneWayToSource` then. Let me try it. – Sinatr Oct 18 '19 at 13:24
  • Yeah, you do need a two-way binding for that to work, although that isn't always desirable – canton7 Oct 18 '19 at 13:27
  • Another thing - how to disable it later? Though it's rarely needed from my experience. Exposing that initial value as a constant with convenient name to be used to stop behavior? – Sinatr Oct 18 '19 at 13:29
  • I've never had to disable it later, so I've never given that any thought. If you care about being able to do that, I'd definitely go for an explicit `EnableHasErrors` property: make things nice and obvious – canton7 Oct 18 '19 at 13:30
  • It works with `bool?` and `null` perfectly: single time initialization when `e.OldValue == null`, then there is single target update (I don't care about it) and then `false/true` results get synchronized with source. – Sinatr Oct 18 '19 at 13:46
  • Very nice! The only downside is it's now a two-way binding – canton7 Oct 18 '19 at 13:50