Bridge DependencyProperty from two binding sources

Consider this scenario using MVVM:

In my ModelView I have one property of type "string", it notifies about property changes via INotifyPropertyChanged.

There is (or not) a single control in the view, with a DependencyProperty of a type that is not a string. This control may or may not change this property depending on facts that only the controls know (neither the ModelView nor the View are aware of this). This control can even be included in another view, which may or may not be in the current visual tree.

In a view, I need a bridge between this DependencyProperty and the ViewModel, so that changing the view property causes the control to change its property, and changing the DependencyProperty makes the viewmodel change its value.

I have a work in progress but I don't think this is an elegant solution. I can think vaguely these days, so I ask if there is something obvious that I might have missed.

The obvious way would be to either have a ViewModel Property DependencyProperty (so it could be linked in two ways), however this is not possible right now (plus it would break the MVVM pattern by adding view specific implementations to the viewmodel).

Another obvious way would be to bind the Control DependencyProperty to the ViewModel: this works, but only for one view ... multiple properties cannot (or, I don't know how to) be bound to the same DependencyProperty: when I set one binding, I lose another.

Currently this is what I am doing:

public class BaseViewUserControl : UserControl
{
    // Dependency property, bound to the view property
    public string AudioNotification
    {
        get { return (string)GetValue(AudioNotificationProperty); }
        set { SetValue(AudioNotificationProperty, value); }
    }
    public static readonly DependencyProperty AudioNotificationProperty = DependencyProperty.Register("AudioNotification", typeof(string), typeof(BaseViewUserControl), new FrameworkPropertyMetadata("None", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnAudioNotificationPropertyChanged));

    // Dependency property, bound to the control dependency property
    public AudioNotificationType AudioNotificationToControl
    {
        get { return (AudioNotificationType)GetValue(AudioNotificationToControlProperty); }
        set { SetValue(AudioNotificationToControlProperty, value); }
    }
    public static readonly DependencyProperty AudioNotificationToControlProperty = DependencyProperty.Register("AudioNotificationToControl", typeof(AudioNotificationType), typeof(BaseViewUserControl), new FrameworkPropertyMetadata(AudioNotificationType.None, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, null, OnAudioNotificationToControlCoerceValue));

    // Converter
    private static IValueConverter _audioNotificationTypeConverter;
    private static IValueConverter AudioNotificationTypeConverter
    {
        get { return _audioNotificationTypeConverter ?? (_audioNotificationTypeConverter = new AudioNotificationConverter()); }
    }

    private Binding _audioNotificationBinding;
    private bool PrepareAudioNotificationControlBinding()
    {
        if (_audioNotificationBinding != null) return true;
        var b = this.FindVisualTreeRoot().TryFindChild<AudioNotification>();
        if (b == null) return false;
        _audioNotificationBinding = new Binding { Source = b, Mode = BindingMode.TwoWay, Path = new PropertyPath("Notification") };
        SetBinding(AudioNotificationToControlProperty, _audioNotificationBinding);
        return true;
    }
    private static void OnAudioNotificationPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
    {
        if (!(source is BaseViewUserControl)) return;

        var src = (BaseViewUserControl)source;
        if(src.PrepareAudioNotificationControlBinding())
        {
            var val = AudioNotificationTypeConverter.ConvertValue<AudioNotificationType>(e.NewValue);
            src.AudioNotificationToControl = val;
        }
    }

    private static object OnAudioNotificationToControlCoerceValue(DependencyObject source, object basevalue)
    {
        if (!(source is BaseViewUserControl)) return basevalue;
        var src = (BaseViewUserControl)source;
        var val = AudioNotificationTypeConverter.ConvertBackValue<string>(basevalue);
        src.AudioNotification = val;
        return basevalue;
    }

    public BaseViewUserControl()
    {
        var ab = new Binding { Path = new PropertyPath("AudibleNotification"), Mode = BindingMode.TwoWay };
        SetBinding(AudibleNotificationProperty, ab);
    }
}

      

NOTE. I use this for a couple of things, not just audio notification (just for example). Don't rely on names to give a solution (if any), it should be pretty general. Also, any typos comes from simplifying the code to a problem (I've removed a lot of the code and changed some of the property names to clarify).

As I said, it works ... I just think it's not entirely elegant and I'm sure there must be a better solution than this.

Any suggestions would be more than welcome.


Update

Based on Julien's code, I made this Behavior which does exactly what I wanted. I implemented it with Converter

, but for the sake of clarity I ended up converting the control itself and used strings

to pass variables (with an undocumented property in the control if I still want to use my own datatype)

public class BridgePropertyBinderBehavior : Behavior<DependencyObject>
{
  public static BridgePropertyBinderBehavior PrepareBindingToControl(FrameworkElement sourceView, string viewModelPropertyPath, FrameworkElement targetControl, string controlPropertyPath)
  {
    var b = new BridgePropertyBinderBehavior();
    BindingOperations.SetBinding(b, AProperty, new Binding(viewModelPropertyPath) { Source = sourceView.DataContext, Mode = BindingMode.TwoWay, BindsDirectlyToSource = true, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged });
    BindingOperations.SetBinding(b, BProperty, new Binding(controlPropertyPath) { Source = targetControl, Mode = BindingMode.TwoWay });
    Interaction.GetBehaviors(sourceView).Add(b);
    return b;
  }

  public object A { get { return GetValue(AProperty); } set { SetValue(AProperty, value); } }
  public static readonly DependencyProperty AProperty = DependencyProperty.Register("A", typeof(object), typeof(BridgePropertyBinderBehavior), new FrameworkPropertyMetadata(null, (d, e) => ((BridgePropertyBinderBehavior)d).OnAChanged(e.NewValue)));

  public object B { get { return GetValue(BProperty); } set { SetValue(BProperty, value); } }
  public static readonly DependencyProperty BProperty = DependencyProperty.Register("B", typeof(object), typeof(BridgePropertyBinderBehavior), new FrameworkPropertyMetadata(null, (d, e) => ((BridgePropertyBinderBehavior)d).OnBChanged(e.NewValue)));

  private void OnAChanged(object value) { B = value; }
  private void OnBChanged(object value) { A = value; }

  protected override Freezable CreateInstanceCore()
  {
    return new BridgePropertyBinderBehavior();
  }
}

      

What I am using like this on my view:

var audioNotificationControl = this.FindVisualTreeRoot().TryFindChild<AudioNotification>();
BridgePropertyBinderBehavior.PrepareBindingToControl(this, "AudioNotification", audioNotificationControl, "Notification");

      

or

<AudioNotification x:Name="Control">
  <ia:Interaction.Behaviors>
    <BridgePropertyBinderBehavior
      A="{Binding Path=Notification, ElementName=Control, Mode=TwoWay}"
      B="{Binding Path=AudioNotification, Mode=TwoWay}" />
  </ia:Interaction.Behaviors>
</AudioNotification>

      

I agreed with his answer as it got me on the right track, thanks

+3


source to share


2 answers


If I understand you correctly, you need to link one DP to two sources, one as the source and the other as the target. I do have this behavior.

The principle behind this behavior is quite simple: it uses two dependency properties and forces data from one ( In

) to flow into the other ( Out

). Bind In

with one way binding and Out

one way of binding the source and you're done.

public class BindingBehavior : Behavior<DependencyObject> {

    public static readonly DependencyProperty InProperty = DependencyProperty.Register(
        "In",
        typeof(object),
        typeof(BindingBehavior),
        new FrameworkPropertyMetadata(null, (d, e) => ((BindingBehavior) d).OnInPropertyChanged(e.NewValue)));

    public static readonly DependencyProperty OutProperty = DependencyProperty.Register(
        "Out",
        typeof(object),
        typeof(BindingBehavior),
        new FrameworkPropertyMetadata(null));

    // Bind OneWay
    public object In {
        get { return GetValue(InProperty); }
        set { SetValue(InProperty, value); }
    }

    // Bind OneWayToSource
    public object Out {
        get { return GetValue(OutProperty); }
        set { SetValue(OutProperty, value); }
    }

    private void OnInPropertyChanged(object value) {
        Out = value;
    }

    protected override Freezable CreateInstanceCore() {
        return new BindingBehavior();
    }

}

      

This behavior requires a reference to System.Windows.Interactivity from the Blend SDK, which you can check out.



Suppose you removed a property string

and kept it AudioNotificationType

with name only AudtioNotification

, usage should be like:

<YourView x:Name="View">
  <YourControl x:Name="Control" AudioNotification="{Binding Notification, ElementName=View}>
    <i:Interaction.Behaviors>
      <BindingBehavior
        In="{Binding AudioNotification, ElementName=Control, Mode=OneWay}"
        Out="{Binding YourVmProperty, Mode=OneWayToSource, Converter=YourConverter}" />
    </i:Interaction.Behaviors>
  </YourControl>
</YourView>

      

You can put the behavior of any element in the correct namespace to resolve element names and have the view model as the data context.

+3


source


It looks like it's helpful to add an abstraction layer. I know. Ik. But carry me.

What if you have a bridge object that can communicate with any number of things that handle change notifications. It doesn't even have to be that hard. Just something that implements INotifyPropertyChanged and then has a property (or properties) that emits a change notification. This way your ViewModel, your View and your control can bind to the same property of that bridge object, and when one of these changes changes a property of the bridge object, everyone else will know that the time will change too. As long as all objects are connected to each other, everything should be in sync simply.



This is essentially what you did in your BaseViewUserControl, but encapsulating the behavior in a separate object can give you flexible benefits.

+2


source







All Articles