OneWayToSource binding from readonly property in XAML
Asked Answered
H

9

93

I'm trying to bind to a Readonly property with OneWayToSource as mode, but it seems this cannot be done in XAML:

<controls:FlagThingy IsModified="{Binding FlagIsModified, 
                                          ElementName=container, 
                                          Mode=OneWayToSource}" />

I get:

The property 'FlagThingy.IsModified' cannot be set because it does not have an accessible set accessor.

IsModified is a readonly DependencyProperty on FlagThingy. I want to bind that value to the FlagIsModified property on the container.

To be clear:

FlagThingy.IsModified --> container.FlagIsModified
------ READONLY -----     ----- READWRITE --------

Is this possible using just XAML?


Update: Well, I fixed this case by setting the binding on the container and not on the FlagThingy. But I'd still like to know if this is possible.

Henbane answered 18/3, 2009 at 13:2 Comment(5)
But how can you set value to a read only property?Flooded
You can't. It's also not what I'm trying to achieve. I'm trying to get FROM readonly property IsModified to readwrite property FlagIsModified.Henbane
Good question. Your workaround only works if the container is a DependencyObject and FlagIsModified is a DependencyProperty.Vine
Great question, however I fail to understand the accepted answer. I would appreciate if some WPF guru could enlighten me some more - Is this a bug or per design?Decanter
@Decanter according to this it's a bug. no fix in sight though.Beaconsfield
F
49

Some research results for OneWayToSource...

Option # 1.

// Control definition
public partial class FlagThingy : UserControl
{
    public static readonly DependencyProperty IsModifiedProperty = 
            DependencyProperty.Register("IsModified", typeof(bool), typeof(FlagThingy), new PropertyMetadata());
}
<controls:FlagThingy x:Name="_flagThingy" />
// Binding Code
Binding binding = new Binding();
binding.Path = new PropertyPath("FlagIsModified");
binding.ElementName = "container";
binding.Mode = BindingMode.OneWayToSource;
_flagThingy.SetBinding(FlagThingy.IsModifiedProperty, binding);

Option # 2

// Control definition
public partial class FlagThingy : UserControl
{
    public static readonly DependencyProperty IsModifiedProperty = 
            DependencyProperty.Register("IsModified", typeof(bool), typeof(FlagThingy), new PropertyMetadata());

    public bool IsModified
    {
        get { return (bool)GetValue(IsModifiedProperty); }
        set { throw new Exception("An attempt ot modify Read-Only property"); }
    }
}
<controls:FlagThingy IsModified="{Binding Path=FlagIsModified, 
    ElementName=container, Mode=OneWayToSource}" />

Option # 3 (True read-only dependency property)

System.ArgumentException: 'IsModified' property cannot be data-bound.

// Control definition
public partial class FlagThingy : UserControl
{
    private static readonly DependencyPropertyKey IsModifiedKey =
        DependencyProperty.RegisterReadOnly("IsModified", typeof(bool), typeof(FlagThingy), new PropertyMetadata());

    public static readonly DependencyProperty IsModifiedProperty = 
        IsModifiedKey.DependencyProperty;
}
<controls:FlagThingy x:Name="_flagThingy" />
// Binding Code
Same binding code...

Reflector gives the answer:

internal static BindingExpression CreateBindingExpression(DependencyObject d, DependencyProperty dp, Binding binding, BindingExpressionBase parent)
{
    FrameworkPropertyMetadata fwMetaData = dp.GetMetadata(d.DependencyObjectType) as FrameworkPropertyMetadata;
    if (((fwMetaData != null) && !fwMetaData.IsDataBindingAllowed) || dp.ReadOnly)
    {
        throw new ArgumentException(System.Windows.SR.Get(System.Windows.SRID.PropertyNotBindable, new object[] { dp.Name }), "dp");
    }
 ....
Fazeli answered 18/3, 2009 at 15:11 Comment(4)
So that's a bug, actually.Henbane
Nice research. If you didn't lay it out so nicely here, I would have walked that same, painful path. Agree with @Inferis.Sterilant
Is this a bug? Why would a OneWayToSource binding not be allowed with a read-only DependencyProperty?Arpent
This is not a bug. It's by design and well-documented. It's because of the way the binding engine works in conjunction with the dependency property system (the binding target must be a DependencyProperty DP). A read-only DP can only modified using the associated DependencyPropertyKey. To register a BindingExpression the engine has to manipulate the meta data of the target DP. Since DependencyPropertyKey is considered private to guarantee public write protection, the engine will have to ignore this key with the result of not being able to register the binding on a read-only DP.Roof
H
27

This is a limitation of WPF and it is by design. It is reported on Connect here:
OneWayToSource binding from a readonly dependency property

I made a solution to dynamically be able to push read-only dependency properties to the source called PushBinding which I blogged about here. The example below does OneWayToSource Bindings from the read-only DP's ActualWidth and ActualHeight to the Width and Height properties of the DataContext

<TextBlock Name="myTextBlock">
    <pb:PushBindingManager.PushBindings>
        <pb:PushBinding TargetProperty="ActualHeight" Path="Height"/>
        <pb:PushBinding TargetProperty="ActualWidth" Path="Width"/>
    </pb:PushBindingManager.PushBindings>
</TextBlock>

PushBinding works by using two Dependency Properties, Listener and Mirror. Listener is bound OneWay to the TargetProperty and in the PropertyChangedCallback it updates the Mirror property which is bound OneWayToSource to whatever was specified in the Binding.

Demo Project can be Downloaded Here.
It contains source code and short sample usage.

Heterocyclic answered 29/8, 2011 at 7:29 Comment(6)
Interesting! I came up with a similar solution and called it a "Conduit" - the Conduit had two dependency properties as per your design and two separate bindings. The use case I had was binding plain old properties to plain old properties in XAML.Undirected
I see that your MS Connect link doesn't work any more. Does it mean MS has fixed it in newer version of .NET or they just deleted it?Cardon
@Cardon Connect seems to have been eventually abandoned, unfortunately. It was linked-to in numerous places. I don't think it specifically implies anything about whether an issue was fixed,.Perrone
I like this solution - very clean and easy to implement. I do have an issue though, and I'm thinking about the cleanest way to solve it. I have the view model properties bound correctly, and when my user control is first constructed, the correct values (ActualWidth and ActualHeight) are set on the vm. But when I set the data context to a different view model, the values of the push binding are not set on the new vm. I could implement a hacky fix to this, but I wonder your thoughts.Bombproof
Solved by listening to the DataContextChanged event, just after the line ((FrameworkElement)targetObject).Loaded += delegate { TargetPropertyValueChanged(); }; and then rebinding on data context changed BindingOperations.SetBinding(this, TargetPropertyMirrorProperty, Binding); TargetPropertyValueChanged(); Assuming I've explained the solution satisfactorily, do you think this is a reasonable change, and can you see any issues with it?Bombproof
@Cardon here is an archived copy of the broken Connect link in the answer above. The archive is from while the issue was still open.Aliquot
C
5

Wrote this:

Usage:

<TextBox Text="{Binding Text}"
         p:OneWayToSource.Bind="{p:Paths From={x:Static Validation.HasErrorProperty},
                                         To=SomeDataContextProperty}" />

Code:

using System;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;

public static class OneWayToSource
{
    public static readonly DependencyProperty BindProperty = DependencyProperty.RegisterAttached(
        "Bind",
        typeof(ProxyBinding),
        typeof(OneWayToSource),
        new PropertyMetadata(default(Paths), OnBindChanged));

    public static void SetBind(this UIElement element, ProxyBinding value)
    {
        element.SetValue(BindProperty, value);
    }

    [AttachedPropertyBrowsableForChildren(IncludeDescendants = false)]
    [AttachedPropertyBrowsableForType(typeof(UIElement))]
    public static ProxyBinding GetBind(this UIElement element)
    {
        return (ProxyBinding)element.GetValue(BindProperty);
    }

    private static void OnBindChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((ProxyBinding)e.OldValue)?.Dispose();
    }

    public class ProxyBinding : DependencyObject, IDisposable
    {
        private static readonly DependencyProperty SourceProxyProperty = DependencyProperty.Register(
            "SourceProxy",
            typeof(object),
            typeof(ProxyBinding),
            new PropertyMetadata(default(object), OnSourceProxyChanged));

        private static readonly DependencyProperty TargetProxyProperty = DependencyProperty.Register(
            "TargetProxy",
            typeof(object),
            typeof(ProxyBinding),
            new PropertyMetadata(default(object)));

        public ProxyBinding(DependencyObject source, DependencyProperty sourceProperty, string targetProperty)
        {
            var sourceBinding = new Binding
            {
                Path = new PropertyPath(sourceProperty),
                Source = source,
                Mode = BindingMode.OneWay,
            };

            BindingOperations.SetBinding(this, SourceProxyProperty, sourceBinding);

            var targetBinding = new Binding()
            {
                Path = new PropertyPath($"{nameof(FrameworkElement.DataContext)}.{targetProperty}"),
                Mode = BindingMode.OneWayToSource,
                Source = source
            };

            BindingOperations.SetBinding(this, TargetProxyProperty, targetBinding);
        }

        public void Dispose()
        {
            BindingOperations.ClearAllBindings(this);
        }

        private static void OnSourceProxyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            d.SetCurrentValue(TargetProxyProperty, e.NewValue);
        }
    }
}

[MarkupExtensionReturnType(typeof(OneWayToSource.ProxyBinding))]
public class Paths : MarkupExtension
{
    public DependencyProperty From { get; set; }

    public string To { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        var provideValueTarget = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
        var targetObject = (UIElement)provideValueTarget.TargetObject;
        return new OneWayToSource.ProxyBinding(targetObject, this.From, this.To);
    }
}

Have not tested it in styles and templates yet, guess it needs special casing.

Coryphaeus answered 3/2, 2016 at 11:26 Comment(0)
M
3

Here's another attached property solution based on SizeObserver detailed here Pushing read-only GUI properties back into ViewModel

public static class MouseObserver
{
    public static readonly DependencyProperty ObserveProperty = DependencyProperty.RegisterAttached(
        "Observe",
        typeof(bool),
        typeof(MouseObserver),
        new FrameworkPropertyMetadata(OnObserveChanged));

    public static readonly DependencyProperty ObservedMouseOverProperty = DependencyProperty.RegisterAttached(
        "ObservedMouseOver",
        typeof(bool),
        typeof(MouseObserver));


    public static bool GetObserve(FrameworkElement frameworkElement)
    {
        return (bool)frameworkElement.GetValue(ObserveProperty);
    }

    public static void SetObserve(FrameworkElement frameworkElement, bool observe)
    {
        frameworkElement.SetValue(ObserveProperty, observe);
    }

    public static bool GetObservedMouseOver(FrameworkElement frameworkElement)
    {
        return (bool)frameworkElement.GetValue(ObservedMouseOverProperty);
    }

    public static void SetObservedMouseOver(FrameworkElement frameworkElement, bool observedMouseOver)
    {
        frameworkElement.SetValue(ObservedMouseOverProperty, observedMouseOver);
    }

    private static void OnObserveChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        var frameworkElement = (FrameworkElement)dependencyObject;
        if ((bool)e.NewValue)
        {
            frameworkElement.MouseEnter += OnFrameworkElementMouseOverChanged;
            frameworkElement.MouseLeave += OnFrameworkElementMouseOverChanged;
            UpdateObservedMouseOverForFrameworkElement(frameworkElement);
        }
        else
        {
            frameworkElement.MouseEnter -= OnFrameworkElementMouseOverChanged;
            frameworkElement.MouseLeave -= OnFrameworkElementMouseOverChanged;
        }
    }

    private static void OnFrameworkElementMouseOverChanged(object sender, MouseEventArgs e)
    {
        UpdateObservedMouseOverForFrameworkElement((FrameworkElement)sender);
    }

    private static void UpdateObservedMouseOverForFrameworkElement(FrameworkElement frameworkElement)
    {
        frameworkElement.SetCurrentValue(ObservedMouseOverProperty, frameworkElement.IsMouseOver);
    }
}

Declare attached property in control

<ListView ItemsSource="{Binding SomeGridItems}"                             
     ut:MouseObserver.Observe="True"
     ut:MouseObserver.ObservedMouseOver="{Binding IsMouseOverGrid, Mode=OneWayToSource}">    
Maltese answered 20/6, 2016 at 1:10 Comment(0)
C
2

Here is another implementation for binding to Validation.HasError

public static class OneWayToSource
{
    public static readonly DependencyProperty BindingsProperty = DependencyProperty.RegisterAttached(
        "Bindings",
        typeof(OneWayToSourceBindings),
        typeof(OneWayToSource),
        new PropertyMetadata(default(OneWayToSourceBindings), OnBinidngsChanged));

    public static void SetBindings(this FrameworkElement element, OneWayToSourceBindings value)
    {
        element.SetValue(BindingsProperty, value);
    }

    [AttachedPropertyBrowsableForChildren(IncludeDescendants = false)]
    [AttachedPropertyBrowsableForType(typeof(FrameworkElement))]
    public static OneWayToSourceBindings GetBindings(this FrameworkElement element)
    {
        return (OneWayToSourceBindings)element.GetValue(BindingsProperty);
    }

    private static void OnBinidngsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((OneWayToSourceBindings)e.OldValue)?.ClearValue(OneWayToSourceBindings.ElementProperty);
        ((OneWayToSourceBindings)e.NewValue)?.SetValue(OneWayToSourceBindings.ElementProperty, d);
    }
}

public class OneWayToSourceBindings : FrameworkElement
{
    private static readonly PropertyPath DataContextPath = new PropertyPath(nameof(DataContext));
    private static readonly PropertyPath HasErrorPath = new PropertyPath($"({typeof(Validation).Name}.{Validation.HasErrorProperty.Name})");
    public static readonly DependencyProperty HasErrorProperty = DependencyProperty.Register(
        nameof(HasError),
        typeof(bool),
        typeof(OneWayToSourceBindings),
        new FrameworkPropertyMetadata(default(bool), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

    internal static readonly DependencyProperty ElementProperty = DependencyProperty.Register(
        "Element",
        typeof(UIElement),
        typeof(OneWayToSourceBindings),
        new PropertyMetadata(default(UIElement), OnElementChanged));

    private static readonly DependencyProperty HasErrorProxyProperty = DependencyProperty.RegisterAttached(
        "HasErrorProxy",
        typeof(bool),
        typeof(OneWayToSourceBindings),
        new PropertyMetadata(default(bool), OnHasErrorProxyChanged));

    public bool HasError
    {
        get { return (bool)this.GetValue(HasErrorProperty); }
        set { this.SetValue(HasErrorProperty, value); }
    }

    private static void OnHasErrorProxyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        d.SetCurrentValue(HasErrorProperty, e.NewValue);
    }

    private static void OnElementChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (e.NewValue == null)
        {
            BindingOperations.ClearBinding(d, DataContextProperty);
            BindingOperations.ClearBinding(d, HasErrorProxyProperty);
        }
        else
        {
            var dataContextBinding = new Binding
                                         {
                                             Path = DataContextPath,
                                             Mode = BindingMode.OneWay,
                                             Source = e.NewValue
                                         };
            BindingOperations.SetBinding(d, DataContextProperty, dataContextBinding);

            var hasErrorBinding = new Binding
                                      {
                                          Path = HasErrorPath,
                                          Mode = BindingMode.OneWay,
                                          Source = e.NewValue
                                      };
            BindingOperations.SetBinding(d, HasErrorProxyProperty, hasErrorBinding);
        }
    }
}

Usage in xaml

<StackPanel>
    <TextBox Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}">
        <local:OneWayToSource.Bindings>
            <local:OneWayToSourceBindings HasError="{Binding HasError}" />
        </local:OneWayToSource.Bindings>
    </TextBox>
    <CheckBox IsChecked="{Binding HasError, Mode=OneWay}" />
</StackPanel>

This implementation is specific to binding Validation.HasError

Coryphaeus answered 8/9, 2016 at 13:22 Comment(0)
F
0

WPF will not use the CLR property setter, but seems it does some odd validation based on it.

May be in your situation this can be ok:

    public bool IsModified
    {
        get { return (bool)GetValue(IsModifiedProperty); }
        set { throw new Exception("An attempt ot modify Read-Only property"); }
    }
Fazeli answered 18/3, 2009 at 13:28 Comment(6)
The CLR property is not used in this case.Henbane
Do you mean that you just defined DependencyProperty and was able to write <controls:FlagThingy IsModified="..." />? For me it say: "The property 'IsModified' does not exist in XML namespace" if I don't add CLR property.Fazeli
I believe design time uses the clr properties where as runtime actually goes directly to the dependency property (if it is one).End
The CLR property is unnecessary in my case (I don't use IsModified from code), but it's there nevertheless (with only a public setter). Both designtime and runtime work fine with just the dependencyproperty registration.Henbane
The binding itself is not using CLR property, but when you define the binding in XAML it have to be translated into code. I guess on this stage XAML parser see that IsModified property is readonly, and throws exception (even before binding created).Fazeli
Too bad this doesn't work for pre-defined properties of BCL classes :-/Passageway
T
0

Hmmm... I'm not sure I agree with any of these solutions. How about specifying a coercion callback in your property registration that ignores external change? For instance, I needed to implement a read-only Position dependency property to get the position of a MediaElement control inside a user control. Here's how I did it:

    public static readonly DependencyProperty PositionProperty = DependencyProperty.Register("Position", typeof(double), typeof(MediaViewer),
        new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault | FrameworkPropertyMetadataOptions.Journal, OnPositionChanged, OnPositionCoerce));

    private static void OnPositionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var ctrl = d as MediaViewer;
    }

    private static object OnPositionCoerce(DependencyObject d, object value)
    {
        var ctrl = d as MediaViewer;
        var position = ctrl.MediaRenderer.Position.TotalSeconds;

        if (ctrl.MediaRenderer.NaturalDuration.HasTimeSpan == false)
            return 0d;
        else
            return Math.Min(position, ctrl.Duration);
    }

    public double Position
    {
        get { return (double)GetValue(PositionProperty); }
        set { SetValue(PositionProperty, value); }
    }

In other words, simply ignore the change and return the value backed by a different member that does not have a public modifier. -- In the above example, MediaRenderer is actually the private MediaElement control.

Tricho answered 27/3, 2013 at 22:10 Comment(1)
Too bad this doesn't work for pre-defined properties of BCL classes :-/Passageway
B
0

The way I worked around this limitation was to expose only a Binding property in my class, keeping the DependencyProperty private altogether. I implemented a "PropertyBindingToSource" write-only property (this one not a DependencyProperty) which can be set to a binding value in the xaml. In the setter for this write-only property I call to BindingOperations.SetBinding to link the binding to the DependencyProperty.

For the OP's specific example, it would look like this:

The FlatThingy implementation:

public partial class FlatThingy : UserControl
{
    public FlatThingy()
    {
        InitializeComponent();
    }

    public Binding IsModifiedBindingToSource
    {
        set
        {
            if (value?.Mode != BindingMode.OneWayToSource)
            {
                throw new InvalidOperationException("IsModifiedBindingToSource must be set to a OneWayToSource binding");
            }

            BindingOperations.SetBinding(this, IsModifiedProperty, value);
        }
    }

    public bool IsModified
    {
        get { return (bool)GetValue(IsModifiedProperty); }
        private set { SetValue(IsModifiedProperty, value); }
    }

    private static readonly DependencyProperty IsModifiedProperty =
        DependencyProperty.Register("IsModified", typeof(bool), typeof(FlatThingy), new PropertyMetadata(false));

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        IsModified = !IsModified;
    }
}

Notice that the static readonly DependencyProperty object is private. In the control I added a button whose click is handled by Button_Click. The use of the FlatThingy control in my window.xaml:

<Window x:Class="ReadOnlyBinding.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"
    xmlns:local="clr-namespace:ReadOnlyBinding"
    mc:Ignorable="d"
    DataContext="{x:Static local:ViewModel.Instance}"
    Title="MainWindow" Height="450" Width="800">
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition />
        <RowDefinition />
    </Grid.RowDefinitions>

    <TextBlock Text="{Binding FlagIsModified}" Grid.Row="0" />
    <local:FlatThingy IsModifiedBindingToSource="{Binding FlagIsModified, Mode=OneWayToSource}" Grid.Row="1" />
</Grid>

Note that I've also implemented a ViewModel for binding to that is not shown here. It exposes a DependencyProperty named "FlagIsModified" as you can glean from the source above.

It works great, allowing me to push information back into the ViewModel from the View in a loosely coupled manner, with the direction of that information flow explicitly defined.

Bursiform answered 23/7, 2020 at 20:36 Comment(0)
S
-2

You're doing the binding in the wrong direction right now. OneWayToSource will try and update FlagIsModified on container whenever IsModified changes on the control you are creating. You want the opposite, which is to have IsModified bind to container.FlagIsModified. For that you should use the binding mode OneWay

<controls:FlagThingy IsModified="{Binding FlagIsModified, 
                                          ElementName=container, 
                                          Mode=OneWay}" />

Full list of enumeration members: http://msdn.microsoft.com/en-us/library/system.windows.data.bindingmode.aspx

Swallow answered 18/3, 2009 at 13:45 Comment(3)
No, I want exactly the scenario that you describe that I don't want to do. FlagThingy.IsModified --> container.FlagIsModifiedHenbane
Getting marked down because the questioner had an ambigous question seems a bit overkill.Swallow
@JaredPar: I don't see what is ambiguous about the question. The question states that 1) there is a read-only dependency property IsIsModified, that 2) the OP wants to declare a binding on that property in XAML and that 3) the binding is supposed to work in the OneWayToSource mode. Your solution doesn't work practically because, as described in the question, the compiler won't let you declare a binding on a read-only property, and it doesn't work conceptually because IsModified is read-only and thus its value cannot be changed (by the binding).Passageway

© 2022 - 2024 — McMap. All rights reserved.