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.