如何将不同的 UI 控件绑定到不同的对象

How to bind different UI controls to different objects

我有两个 UI 控件,我想将其属性绑定到两个不同对象的属性。这是我的 XAML 文件:

<Window x:Class="WpfBindingDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d" SizeToContent="WidthAndHeight" ResizeMode="CanMinimize">
    <Canvas Width="300" Height="200">
        <Slider x:Name="_slider1" Canvas.Left="10" Canvas.Top="10" Width="272"/>

        <Slider x:Name="_slider2" Canvas.Left="10" Canvas.Top="36" Width="272"/>
    </Canvas>
</Window>

下面是我的代码:


using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace WpfBindingDemo
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            Binding binding1 = new Binding("MyProperty1");
            binding1.Mode = BindingMode.TwoWay;
            binding1.Source = _myObject1;
            BindingOperations.SetBinding(_slider1, Slider.ValueProperty, binding1);

            Binding binding2 = new Binding("MyProperty2");
            binding2.Mode = BindingMode.TwoWay;
            binding2.Source = _myObject1;
            BindingOperations.SetBinding(_slider2, Slider.ValueProperty, binding2);
        }

        MyClass1 _myObject1 = new MyClass1();
        MyClass2 _myObject2 = new MyClass2();
    }

    public class MyClass1 : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged([CallerMemberName] string name = null)
        { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); }

        public double MyProperty1 {get; set}
    }

    public class MyClass2 : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged([CallerMemberName] string name = null)
        { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); }

        public double MyProperty2 {get; set}
    }
}

如您所见,我将 UI 控件属性(在本例中为 Value)绑定到代码中的不同对象属性(在 window 构造函数中)并且它工作正常,但我发现这个声明太庞大了,我不喜欢它被分成两部分。我想知道在 XAML 中是否有更紧凑的方式来声明这种绑定,比如

<Slider x:Name="_slider1" Value="{Binding MyProperty1, Source=_myObject1}"/>
<Slider x:Name="_slider2" Value="{Binding MyProperty2, Source=_myObject2}"/>

我试过 SourceRelativeSourceElementName 属性,但没能成功。我错过了什么吗?

如果您将 _myObject1_myObject2 声明为属性(pascal 大小写),则可以绑定它们。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    public MyClass1 MyObject1 { get; } = new MyClass1();
    public MyClass2 MyObject2 { get; } = new MyClass2();
}

可以用RelativeSource引用MainWindow来绑定。

<Canvas Width="300" Height="200">
   <Slider Canvas.Left="10" Canvas.Top="10" Width="272" Value="{Binding MyObject1.MyProperty1, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"/>
   <Slider Canvas.Left="10" Canvas.Top="36" Width="272" Value="{Binding MyObject2.MyProperty2, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"/>
</Canvas>

您当然也可以将数据上下文设置为 window 本身,这样您就不必每次都使用 RelativeSource,请参阅@Clemens answer for a code-behind sample .

也可以在 XAML 中设置 DataContext

<Window ...
        DataContext="{Binding RelativeSource={RelativeSource Self}}">

然后绑定将以同样的方式简化。

<Canvas Width="300" Height="200" DataContext="{Binding MainWindowViewModel}">
   <Slider Canvas.Left="10" Canvas.Top="10" Width="272" Value="{Binding MyObject1.MyProperty1}"/>
   <Slider Canvas.Left="10" Canvas.Top="36" Width="272" Value="{Binding MyObject2.MyProperty2}"/>
</Canvas>

虽然这可行,但这是一个糟糕的方法。它将用户界面组件 - MainWindow - 与您的业务数据或逻辑混合在一起。您应该将它们分开以获得更好的可测试性和可维护性。有一种称为 MVVM 的常见模式,它专注于将视图与数据分离。您可以阅读介绍 here.

您应该为主要 window 创建一个通过属性公开数据的视图模型。如果您打算更改属性,您还应该在此处实现 INotifyPropertyChanged

public class MainWindowViewModel
{
   public MainWindowViewModel()
   {
      MyObject1 = new MyClass1();
      MyObject2 = new MyClass2();
   }

   public MyClass1 MyObject1 { get; }
   public MyClass2 MyObject2 { get; }
}

您可以直接创建视图模型并将其分配为 DataContext

<Window x:Class="WpfBindingDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d" SizeToContent="WidthAndHeight" ResizeMode="CanMinimize">
    <Window.DataContext>
       <local:MainWindowViewModel/>
    </Window.DataContext>
    <!-- ...other markup. -->
</Window>

或者,您可以创建一个实例并将其分配到 code-behind。

public MainWindow()
{
    DataContext = this;
    InitializeComponent();
}

然后绑定看起来像这样。

<Canvas Width="300" Height="200">
   <Slider Canvas.Left="10" Canvas.Top="10" Width="272" Value="{Binding MyObject1.MyProperty1}"/>
   <Slider Canvas.Left="10" Canvas.Top="36" Width="272" Value="{Binding MyObject2.MyProperty2}"/>
</Canvas>

附带说明一下,您对 INotifyPropertyChanged 的实现是无用的,除非您实际比较要分配的值是否相等并在 setter 中调用 OnPropertyChanged

public class MyClass1 : INotifyPropertyChanged
{
   private double _myProperty1;

   public event PropertyChangedEventHandler PropertyChanged;
   protected void OnPropertyChanged([CallerMemberName] string name = null)
   { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); }

   public double MyProperty1
   {
      get => _myProperty1;
      set
      {
         if (Math.Abs(_myProperty1 - value) < /* ...your comparison epsilon here. */)
            return;

         _myProperty1 = value;
         OnPropertyChanged();
      }
   }
}

在浮点数的特定情况下,您应该与 epsilon 进行比较,有关详细信息,请参阅 Comparing double values in C#