Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

How to bind different UI controls to different objects

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

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

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

>Solution :

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.

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading