Home > Enterprise >  How to bind different UI controls to different objects
How to bind different UI controls to different objects

Time:03-28

I have two UI controls whose properties I want to bind to properties of two different objects. Here is my XAML file:

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

And here is my code behind:


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

As you can see, I bind UI control properties (Value in this case) to different objects properties in code (in window constructor) and it works all right, but I find this declaration too bulky and I don't like that it's divided in two parts. I wonder if there is more compact way to declare this kind of binding in XAML, something like

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

I've tried to play with Source, RelativeSource and ElementName properties, but failed to make it work. Am I missing something?

CodePudding user response:

If you declare your _myObject1 and _myObject2 as properties (pascal case), you can bind them.

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

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

You can use RelativeSource to refer to the MainWindow to bind them.

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

You can of course also set the data context to the window itself, so you do not have to use RelativeSource each time, see @Clemens answer for a code-behind sample.

It is also possible to set the DataContext in XAML instead.

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

Then the bindings would be simplified the same way.

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

Although this works, it is a bad approach. It mixes user interface components - the MainWindow - with your business data or logic. You should separate them to achieve better testablility and maintainability. There is a common pattern called MVVM that is focused on separating your view from your data. You can read an introduction here.

You should create a view model for your main window that exposes the data through properties. You should also implement INotifyPropertyChanged here, if you intend to change the properties.

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

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

You can create and assign the view model as DataContext directly.

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

Alternatively, you can create an instance and assign it in code-behind.

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

Then the bindings would look like this.

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

As a side note, your implementation of INotifyPropertyChanged is useless, unless you actually compare values to assign for equality and call OnPropertyChanged in setters.

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

In the specific case of floating point numbers, you should compare against an epsilon, see Comparing double values in C# for more information.

  • Related