c# - 桥接来自两个绑定(bind)源的 DependencyProperty

标签 c# wpf

考虑这个场景,使用 MVVM:

在我的 ModelView 上,我有一个“字符串”类型的属性,它确实通过 INotifyPropertyChanged 通知属性的更改。

在 View 中,有(或没有)一个控件,其 DependencyProperty“Notification”类型不是字符串。该控件可能会或可能不会更改该属性,具体取决于只有该控件知道的事实(ModelView 或 View 都不知道这些)。该控件甚至可能在其他 View 上,而这些 View 可能在也可能不在当前可视化树上。

在 View 中,我需要该控件的 DependencyProperty 和 ViewModel 的属性之间的桥梁,以便更改 View 属性会使控件更改其属性,而更改控件的 DependencyProperty 会使 View 模型的属性更改其值。

我已经让它工作了,但我认为这不是一个优雅的解决方案。这些天我的思路可能有些模糊,所以我想问一下是否有什么明显的事情我可能错过了。

最明显的方法是让 ViewModel 属性成为 DependencyProperty(因此它可以通过两种方式绑定(bind)),但是现在这是不可能的(另外,它会破坏 MVVM 模式,将特定于 View 的实现添加到 View 模型)。

另一种明显的方法是将控件的 DependencyProperty 绑定(bind)到 ViewModel 的属性:这可行,但仅适用于一个 View ......多个属性不能(或者,我不知道如何做)绑定(bind)到同一个DependencyProperty:当我设置一个绑定(bind)时,我会失去另一个。

目前我是这样做的:

public class BaseViewUserControl : UserControl
{
    // Dependency property, bound to the view's 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's 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);
    }
}

注意: 我将其用于多种用途,而不仅仅是用于音频通知(这只是一个示例)。不要依赖名称来提供解决方案(如果有的话),这需要非常通用。此外,任何拼写错误都来自简化问题的代码(为了澄清,我删除了很多代码并更改了一些属性名称)。

正如我所说,它有效......我只是觉得它很不优雅,我相信应该有比这更好的解决方案。

我们非常欢迎任何建议。


更新

基于 Julien 的代码,我做了这个 Behavior,它完全符合我的要求。我使用 Converter 实现了它,但为了清楚起见,我最终在控件本身上进行了转换,并使用 strings 将变量沿 传递(带有未记录的属性在控件中,如果我仍然想使用 native 数据类型)

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();
  }
}

我在我的观点中是这样使用的:

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

<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>

我接受了他的回答,因为这是让我走上正轨的原因,谢谢

最佳答案

如果我没理解错的话,需要将一个DP绑定(bind)到两个source上,一个作为source,一个作为target。我实际上有这样做的行为。

此行为的原理非常简单:它使用两个依赖属性并使一个 (In) 的数据流入另一个 (Out)。使用一种单向绑定(bind)方式绑定(bind) In 并使用一种单向源绑定(bind)方式绑定(bind) Out,您就完成了。

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();
    }

}

此行为需要引用您可能熟悉的 Blend SDK 中的 System.Windows.Interactivity。

假设您删除了 string 属性并只保留一个名为 AudtioNotificationAudioNotificationType,其用法应类似于:

<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>

您可以将行为放在正确名称范围内的任何元素上,以解析元素名称并将 View 模型作为数据上下文。

关于c# - 桥接来自两个绑定(bind)源的 DependencyProperty,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/9623893/

相关文章:

c# - FluentribbonTabItem 在设计器中不可见?

c# - 如何选择将哪个 ObservableCollection 绑定(bind)到列表框?

c# - 从事件触发更改时,MVVM 绑定(bind)不起作用

c# - 需要使用 Azure Ad 进行身份验证将 Blazor Web 应用程序发布到 Azure 的指导

c# - 从 c# 更新 sql 中的一个单元格

c# - 解压缩 PVRTC

c# - 在 xaml 中声明数据模板时如何在代码中创建绑定(bind)

c# - 使用 Enumerable 和 Lambda 过滤文件列表并删除不需要的扩展名

c# - Entity Framework 4 和 Linq : OrderBy a field nested in a Query : refactor my code

c# - Winforms 中的事件和 WPF 中的命令有什么区别?