【问题标题】:Bridge DependencyProperty from two binding sources来自两个绑定源的桥 DependencyProperty
【发布时间】:2012-03-08 19:50:31
【问题描述】:

考虑这种情况,使用 MVVM:

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

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

在视图中,我需要在该控件的 DependencyProperty 和 ViewModel 的属性之间架起一座桥梁,以便更改视图属性使控件更改其属性,更改控件的 DependencyProperty 使视图模型的属性更改其值。

我已经让它工作了,但我认为这不是一个优雅的解决方案。这些天我可能在思考,所以我想问是否有一些明显的东西我可能错过了。

显而易见的方法是让 ViewModel 属性成为一个 DependencyProperty(因此它可以以两种方式绑定),但是现在这是不可能的(另外,它会破坏 MVVM 模式,将特定于视图的实现添加到视图模型)。

另一种明显的方法是将控件的 DependencyProperty 绑定到 ViewModel 的属性:这有效,但仅适用于一个视图...多个属性不能(或者,我不知道该怎么做)绑定到同一个DependencyProperty:当我设置一个绑定时,我会丢失另一个。

目前我是这样做的:

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 沿 传递变量(如果我仍然想使用控件中的未记录属性)本机数据类型)

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>

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

【问题讨论】:

    标签: c# wpf


    【解决方案1】:

    如果我理解正确,您需要将一个 DP 绑定到两个源,一个作为源,另一个作为目标。我其实有这样的行为。

    这种行为的原理很简单:它使用两个依赖属性,并使一个(In)的数据流入另一个(Out)。使用单向绑定绑定In,使用单向绑定绑定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 属性并只保留了一个名为 AudioNotificationType 的 AudtioNotification,其用法应该类似于:

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

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

    【讨论】:

    • 如果我理解正确,这只会在“In”和“Out”之间进行单向绑定(中间有一个 DP)。这里的问题是“Out”永远不会更新,即使“In”改变了,不是吗?我需要在“Out”更改时更新“In”,并在“In”更改时更新“Out”(这就是我在代码上所做的)。
    • Out 在In 更改时更新(请参阅OnInPropertyChanged)。当Out 更改时,您可以轻松修改行为以更改In。
    • 是的,抱歉,我的意思是相反的(In 未在 Out 更改时更新)。我当然可以添加一个OnPropertyChangedCallback 处理程序来更新In,但是一旦我设置了属性值(当它是OneWay 时会发生这种情况),它就会删除绑定。解决方案是绑定两种方式并修改强制回调的属性......但这正是我在我的代码上所做的。无论如何,我稍后会尝试你的行为,看看我是否有错误的概念(这完全有可能,因为我对 WPF 还很陌生)。
    • 被接受为答案,因为它让我走上了解决问题的非常清晰的道路
    【解决方案2】:

    这看起来可能是添加抽象层的有用时机。我知道。伊克。但请耐心等待。

    如果你有一个桥接对象,可以绑定任意数量的东西来处理更改通知。它甚至不需要那么复杂。只是实现 INotifyPropertyChanged 的​​东西,然后有一个(或多个属性)发布更改通知的属性。这样一来,您的 ViewModel、您的 View 和您的控件都可以绑定到此桥对象上的同一属性,并且当其中一个更改桥对象的属性时,所有其他人都会知道该更改也是时候了。只要所有的对象都是双向绑定的,一切都应该同步就好了。

    这基本上就是您在 BaseViewUserControl 上所做的,但将行为封装在单独的对象中可能会为您带来灵活性优势。

    【讨论】:

    • 同意,我一定会为此添加一个新图层。我只是想确保这是在添加该层之前要走的路。似乎是。 WPF 看起来如此半生不熟!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-11-12
    • 2020-01-26
    • 1970-01-01
    相关资源
    最近更新 更多