3

How can I achieve something like following

<CheckBox Content="{Binding Caption}">
    <CheckBox.IsChecked>
        <Binding Path="{Binding PropertyName}"
                 Source="{Binding Source}" />
    </CheckBox.IsChecked>
</CheckBox>

Where

public class ViewModel
{
    public string Caption { get; } = "Test";
    public string PropertyName { get; } = nameof(Test.Property);
    public object Source { get; } = new Test();
}
public class Test
{
    public bool Property { get; set; } = false;
}

Idea is to supply Path and Source (unknown at design time) for the binding via properties.

Currently this throw exception at <Binding Path= line

A 'Binding' cannot be set on the 'Path' property of type 'Binding'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject.

Sinatr
  • 20,892
  • 15
  • 90
  • 319

3 Answers3

4

I'll go with behaviors. Below behavior will get Source and Path and update the binding accordingly for IsChecked Property. You can extend this to meet your need. for now this is limited to IsChecked Property, you can write generic code to support all properties.

public class CheckBoxCustomBindingBehavior : Behavior<CheckBox>
{
    protected override void OnAttached()
    {
        base.OnAttached();
    }

    public object Source
    {
        get
        {
            return (object)GetValue(SourceProperty);
        }
        set
        {
            SetValue(SourceProperty, value);
        }
    }

    // Using a DependencyProperty as the backing store for Source.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty SourceProperty =
        DependencyProperty.Register("Source", typeof(object), typeof(CheckBoxCustomBindingBehavior), new PropertyMetadata(null, OnSourceChanged));

    private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        (d as CheckBoxCustomBindingBehavior).ModifyBinding();
    }

    public string Path
    {
        get
        {
            return (string)GetValue(PathProperty);
        }
        set
        {
            SetValue(PathProperty, value);
        }
    }

    // Using a DependencyProperty as the backing store for Path.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty PathProperty =
        DependencyProperty.Register("Path", typeof(string), typeof(CheckBoxCustomBindingBehavior), new PropertyMetadata(string.Empty, OnPathChanged));

    private static void OnPathChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        (d as CheckBoxCustomBindingBehavior).ModifyBinding();
    }

    private void ModifyBinding()
    {
        var source = Source ?? AssociatedObject.DataContext;
        if (source != null && !string.IsNullOrEmpty(Path))
        {
            Binding b = new Binding(Path);
            b.Source = source;
            AssociatedObject.SetBinding(CheckBox.IsCheckedProperty, b);
        }
    }
}

And Xaml usage,

 <CheckBox xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity">
        <i:Interaction.Behaviors>
            <local:CheckBoxCustomBindingBehavior Path="{Binding SelectedPath}" Source="{Binding}" />
        </i:Interaction.Behaviors>
    </CheckBox>

SelectedPath is from model,and that's where i store the Property Name.

Note: you will need Interactivity assembly.

WPFUser
  • 1,145
  • 7
  • 24
  • Nice answer, It will support MVVM. – Rudra Jan 05 '17 at 13:18
  • Just to clarify; creating a binding to a view model property in the code-behind of the view doesn't break the MVVM pattern. MVVM is about sepearation of concerns and not about eliminating code from the views. The code-behind class is part of the same view as the XAML markup. – mm8 Jan 05 '17 at 15:06
  • 1
    @mm8, your answer is ok, I was using it as workaround until now. However, when you start using data templates extensively (the reason of my question) this code-behind creates huge problems. Hiding it into reusable behaviors will let you to define template purely in the xaml (e.g. resource dictionary). – Sinatr Jan 05 '17 at 15:14
3

The names of the source properties must be known at compile-time for you to be able set up the binding in XAML:

<CheckBox Content="{Binding Caption}">
    <CheckBox.IsChecked>
        <Binding Path="Source.Property" />
    </CheckBox.IsChecked>
</CheckBox>

As the error message tells you, you cannot bind something to the Path property of a Binding.

If you don't know the names of the properties to bind to at design time, you could set up the bindings programmatically:

<CheckBox x:Name="ck" Content="{Binding Caption}" />

ViewModel vm = new ViewModel();
ck.DataContext = vm;
ck.SetBinding(CheckBox.IsCheckedProperty, new Binding(vm.PropertyName) { Source = vm.Source });

There is no way to do this in pure XAML though. Remember that XAML is a markup language.

mm8
  • 163,881
  • 10
  • 57
  • 88
2

A bit late answer after seeing @WPFUser one, but it supports any property and I personally do not like Blend dependencies:

public class DynamicBinding
{
    public static object GetSource(DependencyObject obj) => (object)obj.GetValue(SourceProperty);
    public static void SetSource(DependencyObject obj, object value) => obj.SetValue(SourceProperty, value);
    public static readonly DependencyProperty SourceProperty =
        DependencyProperty.RegisterAttached("Source", typeof(object), typeof(DynamicBinding), new PropertyMetadata(null, (d, e) => SetBinding(d)));

    public static string GetProperty(DependencyObject obj) => (string)obj.GetValue(PropertyProperty);
    public static void SetProperty(DependencyObject obj, string value) => obj.SetValue(PropertyProperty, value);
    public static readonly DependencyProperty PropertyProperty =
        DependencyProperty.RegisterAttached("Property", typeof(string), typeof(DynamicBinding), new PropertyMetadata(null, (d, e) => SetBinding(d)));

    public static string GetTarget(DependencyObject obj) => (string)obj.GetValue(TargetProperty);
    public static void SetTarget(DependencyObject obj, string value) => obj.SetValue(TargetProperty, value);
    public static readonly DependencyProperty TargetProperty =
        DependencyProperty.RegisterAttached("Target", typeof(string), typeof(DynamicBinding), new PropertyMetadata(null, (d, e) => SetBinding(d)));

    static void SetBinding(DependencyObject obj)
    {
        var source = GetSource(obj);
        var property = GetProperty(obj);
        var target = GetTarget(obj);
        // only if all required attached properties values are set
        if (source == null || property == null || target == null)
            return;
        BindingOperations.SetBinding(obj, DependencyPropertyDescriptor.FromName(target, obj.GetType(), obj.GetType()).DependencyProperty,
            new Binding(property) { Source = source });
    }
}

The usage is:

<CheckBox Content="{Binding Caption}"
          local:DynamicBinding.Property="{Binding PropertyName}"
          local:DynamicBinding.Source="{Binding Source}"
          local:DynamicBinding.Target="IsChecked" />

Target can be any dependency property of the control. It's given as a plain string, not sure how I can improve this to get intellisense assistance when entering it.

ToDo: binding is not removed if Target is changed (it will reflect changes made to Source or Property though), no support for multiple dynamic bindings (e.g. to different properties of control).

Sinatr
  • 20,892
  • 15
  • 90
  • 319
  • behavior is a better choice because it will allow to bind *multiple* properties on one element in dynamic way. DP DynamicBinding.Target can be set only *once* – ASh May 26 '20 at 18:22
  • True. But with attached properties idea in mind the next idea will be to create those [dynamically](https://stackoverflow.com/q/42088734/1997232). Then `IsChecked` property is handled by uniquely named attached properties `DynamicBindingTSourceIsChecked`, `DynamicBindingPropertyIsChecked` and `DynamicBindingTargetIsChecked`. – Sinatr May 27 '20 at 06:55