0

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?

2
  • Can't you just change the DataContext of the 2 Slider for pointing the right object and property of relative class ? Commented Mar 28, 2022 at 8:08
  • Why not to use _myObject1.MyProperty1 and _myObject2.MyProperty2 for binding? Where _myObject# is a property? Commented Mar 28, 2022 at 8:28

1 Answer 1

2

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.

Sign up to request clarification or add additional context in comments.

1 Comment

@yadda Sorry for the initial edits, it should be consistent now :-D . The combination of a typical monday, daylight savings time and the severe lack of coffee pays its toll.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.